CoolFace
Apppublic

charlesdedampierre/citeo-plastic

sourceHugging Faceupdated 3y agoView on Hugging Face
1likes
Map.jsx358 linesDownload Raw Back to src
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