charlesdedampierre/citeo-plastic
1
1import { Backdrop, CircularProgress, Button, Box } from "@mui/material";2import HelpIcon from '@mui/icons-material/Help';3import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';4import Typography from '@mui/material/Typography';5import RepeatIcon from '@mui/icons-material/Repeat';6import { styled } from '@mui/material/styles';7 8import * as d3 from "d3";9import * as d3Contour from "d3-contour";10import React, { useContext, useEffect, useRef, useState } from "react";11 12import TextContainer, { topicsSizeFraction } from "./TextContainer";13import { TopicsContext } from "./UploadFileContext";14import QueryView from "./QueryView";15 16const bunkaDocs = "bunka_docs.json";17const bunkaTopics = "bunka_topics.json";18const { REACT_APP_API_ENDPOINT } = "local";19 20/**21 * Generic tooltip22 */23export const HtmlTooltip = styled(({ className, ...props }) => (24 <Tooltip {...props} classes={{ popper: className }} />25))(({ theme }) => ({26 [`& .${tooltipClasses.popper}`]: {27 backgroundColor: '#fff',28 color: 'rgba(0, 0, 0, 0.87)',29 maxWidth: 220,30 fontSize: theme.typography.pxToRem(12),31 },32}));33 34function MapView() {35 const [selectedDocument, setSelectedDocument] = useState(null);36 const [mapLoading, setMapLoading] = useState(false);37 const [topicsCentroids, setTopicsCentroids] = useState([])38 39 const { data: apiData, isLoading: isFileProcessing } = useContext(TopicsContext);40 41 42 const svgRef = useRef(null);43 const scatterPlotContainerRef = useRef(null);44 const createScatterPlot = (data) => {45 const margin = {46 top: 20,47 right: 20,48 bottom: 50,49 left: 50,50 };51 const plotWidth = window.innerWidth * 0.6;52 const plotHeight = window.innerHeight - document.getElementById("top-banner").clientHeight - 50; // Adjust the height as desired53 54 d3.select(svgRef.current).selectAll("*").remove();55 56 const svg = d357 .select(svgRef.current)58 .attr("width", "100%")59 .attr("height", plotHeight);60 /**61 * SVG canvas group on which transforms apply.62 */63 const g = svg.append("g")64 .classed("canvas", true)65 .attr("transform", `translate(${margin.left}, ${margin.top})`);66 /**67 * TODO Zoom.68 */69 const zoom = d3.zoom()70 .scaleExtent([1, 3])71 .translateExtent([[0, 0], [1000, 1000]])72 .on("zoom", function ({ transform }) {73 g.attr(74 "transform",75 `translate(${transform.x ?? 0}, ${transform.y ?? 0}) scale(${transform.k ?? 1})`76 )77 //positionLabels()78 // props.setTransform?.({79 // x: transform.x,80 // y: transform.y,81 // k: transform.k82 // })83 });84 svg.call(zoom);85 86 const xMin = d3.min(data, (d) => d.x);87 const xMax = d3.max(data, (d) => d.x);88 const yMin = d3.min(data, (d) => d.y);89 const yMax = d3.max(data, (d) => d.y);90 91 const xScale = d392 .scaleLinear()93 .domain([xMin, xMax]) // Use the full range of your data94 .range([0, plotWidth]);95 96 const yScale = d397 .scaleLinear()98 .domain([yMin, yMax]) // Use the full range of your data99 .range([plotHeight, 0]);100 101 // Add contours102 const contourData = d3Contour103 .contourDensity()104 .x((d) => xScale(d.x))105 .y((d) => yScale(d.y))106 .size([plotWidth, plotHeight])107 .bandwidth(5)(108 // Adjust the bandwidth as needed109 data,110 );111 112 // Define a color scale for the contours to add visual depth and appeal113 const colorScale = d3.scaleSequential(d3.interpolateTurbo) // Using d3.interpolateTurbo for vibrant colors114 .domain([0, d3.max(contourData, d => d.value)]); // Dynamically set the domain based on data density115 116 117 // Define a custom color for the contour lines118 119 const contourLineColor = "rgb(94, 163, 252)";120 121 // Append the contour path to the SVG with a custom color122 g123 .selectAll("path.contour")124 .data(contourData)125 .enter()126 .append("path")127 .attr("class", "contour")128 .attr("d", d3.geoPath())129 .style("fill", "lightgreen")130 .attr("fill", d => colorScale(d.value)) // Apply color based on data density131 132 .style("stroke", contourLineColor) // Set the contour line color to the custom color133 .style("stroke-width", 1);134 135 const centroids = data.filter((d) => d.x_centroid && d.y_centroid);136 setTopicsCentroids(centroids);137 138 g139 .selectAll("circle.topic-centroid")140 .data(centroids)141 .enter()142 .append("circle")143 .attr("class", "topic-centroid")144 .attr("cx", (d) => xScale(d.x_centroid))145 .attr("cy", (d) => yScale(d.y_centroid))146 .attr("r", 8) // Adjust the radius as needed147 .style("fill", "red") // Adjust the fill color as needed148 .style("stroke", "black")149 .style("stroke-width", 2)150 .on("click", (event, d) => {151 // Show the content and topic name of the clicked topic centroid in the text container152 setSelectedDocument(d);153 });154 155 156 157 // Add text labels for topic names158 g159 .selectAll("rect.topic-label-background")160 .data(centroids)161 .enter()162 .append("rect")163 .attr("class", "topic-label-background")164 .attr("x", (d) => {165 // Calculate the width of the text166 const first10Words = d.name.split(' ').slice(0, 8).join(' ');167 const textLength = first10Words.length * 8; // Adjust the multiplier for width as needed168 169 // Calculate the x position to center the box170 return xScale(d.x_centroid) - textLength / 2;171 }) // Center the box horizontally172 .attr("y", (d) => yScale(d.y_centroid) - 20) // Adjust the y position173 .attr("width", (d) => {174 // Compute the width based on the text's length175 const first10Words = d.name.split(' ').slice(0, 8).join(' ');176 const textLength = first10Words.length * 8; // Adjust the multiplier for width as needed177 return textLength;178 })179 .attr("height", 30) // Set the height of the white box180 .style("fill", "white") // Set the white fill color181 .style("stroke", "grey") // Set the blue border color182 .style("stroke-width", 2); // Set the border width183 184 // Add text labels in black within the white boxes185 g186 .selectAll("text.topic-label-text")187 .data(centroids)188 .enter()189 .append("text")190 .attr("class", "topic-label-text")191 .attr("x", (d) => xScale(d.x_centroid))192 .attr("y", (d) => yScale(d.y_centroid) + 4) // Adjust the vertical position193 .text((d) => {194 const first10Words = d.name.split(' ').slice(0, 8).join(' ');195 return first10Words;196 }) // Use the first 10 words197 .style("text-anchor", "middle") // Center-align the text198 .style("fill", "black"); // Set the text color199 200 const convexHullData = data.filter((d) => d.convex_hull);201 202 for (const d of convexHullData) {203 const hull = d.convex_hull;204 const hullPoints = hull.x_coordinates.map((x, i) => [xScale(x), yScale(hull.y_coordinates[i])]);205 206 g207 .append("path")208 .datum(d3.polygonHull(hullPoints))209 .attr("class", "convex-hull-polygon")210 .attr("d", (d1) => `M${d1.join("L")}Z`)211 .style("fill", "none")212 .style("stroke", "rgba(255, 255, 255, 0.5)") // White with 50% transparency213 .style("stroke-width", 2);214 }215 216 // Add polygons for topics. Delete if no clicking on polygons217 const topicsPolygons = g218 .selectAll("polygon.topic-polygon")219 .data(centroids)220 .enter()221 .append("polygon")222 .attr("class", "topic-polygon")223 .attr("points", (d) => {224 const hull = d.convex_hull;225 const hullPoints = hull.x_coordinates.map((x, i) => [xScale(x), yScale(hull.y_coordinates[i])]);226 return hullPoints.map((point) => point.join(",")).join(" ");227 })228 .style("fill", "transparent")229 .style("stroke", "transparent")230 .style("stroke-width", 2); // Adjust the border width as needed231 232 let currentlyClickedPolygon = null;233 234 function clickFirstPolygon() {235 // Simulate a click event on the first polygon236 const firstPolygon = d3.select(topicsPolygons.nodes()[0]);237 firstPolygon.node().dispatchEvent(new Event("click"));238 }239 240 241 topicsPolygons.on("click", (event, d) => {242 // Reset the fill color of the previously clicked polygon to transparent light grey243 if (currentlyClickedPolygon !== null) {244 currentlyClickedPolygon.style("fill", "transparent");245 currentlyClickedPolygon.style("stroke", "transparent");246 }247 248 // Set the fill color of the clicked polygon to transparent light grey and add a red border249 const clickedPolygon = d3.select(event.target);250 clickedPolygon.style("fill", "rgba(200, 200, 200, 0.4)");251 clickedPolygon.style("stroke", "red");252 253 currentlyClickedPolygon = clickedPolygon;254 255 256 // Display the topic name and content from top_doc_content with a scroll system257 if (d.top_doc_content) {258 // Render the TextContainer component with topic details259 setSelectedDocument(d);260 }261 });262 263 clickFirstPolygon();264 265 };266 267 useEffect(() => {268 if (REACT_APP_API_ENDPOINT === "local" || apiData === undefined) {269 setMapLoading(true);270 // Fetch the JSON data locally271 fetch(`/${bunkaDocs}`)272 .then((response) => response.json())273 .then((localData) => {274 // Fetch the local topics data and merge it with the existing data275 fetch(`/${bunkaTopics}`)276 .then((response) => response.json())277 .then((topicsData) => {278 // Merge the topics data with the existing data279 const mergedData = localData.concat(topicsData);280 281 // Call the function to create the scatter plot after data is loaded282 createScatterPlot(mergedData);283 })284 .catch((error) => {285 console.error("Error fetching topics data:", error);286 })287 .finally(() => {288 setMapLoading(false);289 });290 })291 .catch((error) => {292 console.error("Error fetching JSON data:", error);293 })294 .finally(() => {295 setMapLoading(false);296 });297 } else {298 // Call the function to create the scatter plot with the data provided by TopicsContext299 createScatterPlot(apiData.docs.concat(apiData.topics));300 }301 302 // After the data is loaded, set the default topic303 if (apiData && apiData.topics && apiData.topics.length > 0) {304 // Set the default topic to the first topic in the list305 setSelectedDocument(apiData.topics[0]);306 }307 }, [apiData]);308 309 310 const mapDescription = "This map is created by embedding documents in a two-dimensional space. Two documents are close to each other if they share similar semantic features, such as vocabulary, expressions, and language. The documents are not directly represented on the map; instead, they are grouped into clusters. A cluster is a set of documents that share similarities. A cluster is automatically described by a few words that best describes it.";311 312 return (313 <div className="json-display">314 {(isFileProcessing || mapLoading) ? (315 <Backdrop open={isFileProcessing || mapLoading} style={{ zIndex: 9999 }}>316 <CircularProgress color="primary" />317 </Backdrop>318 ) : (319 <div className="scatter-plot-and-text-container">320 <div className="scatter-plot-container" ref={scatterPlotContainerRef}>321 <HtmlTooltip322 title={323 <React.Fragment>324 <Typography color="inherit">{mapDescription}</Typography>325 </React.Fragment>326 }327 followCursor328 >329 <HelpIcon style={{330 position: "relative",331 top: 10,332 left: 40,333 border: "none"334 }} />335 </HtmlTooltip>336 <svg ref={svgRef} />337 </div>338 <div className="text-container">339 {selectedDocument ? (340 <TextContainer341 topicName={selectedDocument.name}342 topicSizeFraction={topicsSizeFraction(topicsCentroids, selectedDocument.size)}343 content={selectedDocument.top_doc_content}344 />345 ) : (346 // Display a default view or null if no document is selected347 null348 )}349 </div>350 351 </div>352 )}353 </div>354 );355}356 357export default MapView;358 