bunkalab/Argilla-Prompt-Exploration
5
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 } = process.env;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 const svgRef = useRef(null);42 const scatterPlotContainerRef = useRef(null);43 const createScatterPlot = (data) => {44 const margin = {45 top: 20,46 right: 20,47 bottom: 50,48 left: 50,49 };50 const plotWidth = window.innerWidth * 0.6;51 const plotHeight = window.innerHeight - document.getElementById("top-banner").clientHeight - 50; // Adjust the height as desired52 53 d3.select(svgRef.current).selectAll("*").remove();54 55 const svg = d356 .select(svgRef.current)57 .attr("width", "100%")58 .attr("height", plotHeight);59 /**60 * SVG canvas group on which transforms apply.61 */62 const g = svg.append("g")63 .classed("canvas", true)64 .attr("transform", `translate(${margin.left}, ${margin.top})`);65 /**66 * TODO Zoom.67 */68 const zoom = d3.zoom()69 .scaleExtent([1, 3])70 .translateExtent([[0, 0], [1000, 1000]])71 .on("zoom", function ({ transform }) {72 g.attr(73 "transform",74 `translate(${transform.x ?? 0}, ${transform.y ?? 0}) scale(${transform.k ?? 1})`75 )76 //positionLabels()77 // props.setTransform?.({78 // x: transform.x,79 // y: transform.y,80 // k: transform.k81 // })82 });83 svg.call(zoom);84 85 /**86 * Initial zoom.87 */88 // const defaultTransform = { k: 1 };89 // const initialTransform = defaultTransform?.k != null90 // ? new ZoomTransform(91 // defaultTransform.k ?? 1,92 // defaultTransform.x ?? 0,93 // defaultTransform.y ?? 094 // )95 // : d3.zoomIdentity;96 // svg.call(zoom.transform, initialTransform);97 98 const xMin = d3.min(data, (d) => d.x);99 const xMax = d3.max(data, (d) => d.x);100 const yMin = d3.min(data, (d) => d.y);101 const yMax = d3.max(data, (d) => d.y);102 103 const xScale = d3104 .scaleLinear()105 .domain([xMin, xMax]) // Use the full range of your data106 .range([0, plotWidth]);107 108 const yScale = d3109 .scaleLinear()110 .domain([yMin, yMax]) // Use the full range of your data111 .range([plotHeight, 0]);112 113 // Add contours114 const contourData = d3Contour115 .contourDensity()116 .x((d) => xScale(d.x))117 .y((d) => yScale(d.y))118 .size([plotWidth, plotHeight])119 .bandwidth(30)(120 // Adjust the bandwidth as needed121 data,122 );123 124 // Define a custom color for the contour lines125 126 const contourLineColor = "rgb(94, 163, 252)";127 128 // Append the contour path to the SVG with a custom color129 g130 .selectAll("path.contour")131 .data(contourData)132 .enter()133 .append("path")134 .attr("class", "contour")135 .attr("d", d3.geoPath())136 .style("fill", "none")137 .style("stroke", contourLineColor) // Set the contour line color to the custom color138 .style("stroke-width", 1);139 140 /*141 const circles = svg.selectAll('circle')142 .data(data)143 .enter()144 .append('circle')145 .attr('cx', (d) => xScale(d.x))146 .attr('cy', (d) => yScale(d.y))147 .attr('r', 5)148 .style('fill', 'lightblue')149 .on('click', (event, d) => {150 // Show the content and topic name of the clicked point in the text container151 setSelectedDocument(d);152 // Change the color to pink on click153 circles.style('fill', (pointData) => (pointData === d) ? 'pink' : 'lightblue');154 });155 */156 157 const centroids = data.filter((d) => d.x_centroid && d.y_centroid);158 setTopicsCentroids(centroids);159 160 g161 .selectAll("circle.topic-centroid")162 .data(centroids)163 .enter()164 .append("circle")165 .attr("class", "topic-centroid")166 .attr("cx", (d) => xScale(d.x_centroid))167 .attr("cy", (d) => yScale(d.y_centroid))168 .attr("r", 8) // Adjust the radius as needed169 .style("fill", "red") // Adjust the fill color as needed170 .style("stroke", "black")171 .style("stroke-width", 2)172 .on("click", (event, d) => {173 // Show the content and topic name of the clicked topic centroid in the text container174 setSelectedDocument(d);175 });176 177 // Add text labels for topic names178 g179 .selectAll("text.topic-label")180 .data(centroids)181 .enter()182 .append("text")183 .attr("class", "topic-label")184 .attr("x", (d) => xScale(d.x_centroid))185 .attr("y", (d) => yScale(d.y_centroid) - 12) // Adjust the vertical position186 .text((d) => d.name) // Use the 'name' property for topic names187 .style("text-anchor", "middle"); // Center-align the text188 189 const convexHullData = data.filter((d) => d.convex_hull);190 191 for (const d of convexHullData) {192 const hull = d.convex_hull;193 const hullPoints = hull.x_coordinates.map((x, i) => [xScale(x), yScale(hull.y_coordinates[i])]);194 195 g196 .append("path")197 .datum(d3.polygonHull(hullPoints))198 .attr("class", "convex-hull-polygon")199 .attr("d", (d1) => `M${d1.join("L")}Z`)200 .style("fill", "none")201 .style("stroke", "rgba(255, 255, 255, 0.5)") // White with 50% transparency202 .style("stroke-width", 2);203 }204 205 // Add polygons for topics. Delete if no clicking on polygons206 const topicsPolygons = g207 .selectAll("polygon.topic-polygon")208 .data(centroids)209 .enter()210 .append("polygon")211 .attr("class", "topic-polygon")212 .attr("points", (d) => {213 const hull = d.convex_hull;214 const hullPoints = hull.x_coordinates.map((x, i) => [xScale(x), yScale(hull.y_coordinates[i])]);215 return hullPoints.map((point) => point.join(",")).join(" ");216 })217 .style("fill", "transparent")218 .style("stroke", "transparent")219 .style("stroke-width", 2); // Adjust the border width as needed220 221 let currentlyClickedPolygon = null;222 223 topicsPolygons.on("click", (event, d) => {224 // Reset the fill color of the previously clicked polygon to transparent light grey225 if (currentlyClickedPolygon !== null) {226 currentlyClickedPolygon.style("fill", "transparent");227 currentlyClickedPolygon.style("stroke", "transparent");228 }229 230 // Set the fill color of the clicked polygon to transparent light grey and add a red border231 const clickedPolygon = d3.select(event.target);232 clickedPolygon.style("fill", "rgba(200, 200, 200, 0.4)");233 clickedPolygon.style("stroke", "red");234 235 currentlyClickedPolygon = clickedPolygon;236 237 // Display the topic name and content from top_doc_content with a scroll system238 if (d.top_doc_content) {239 // Render the TextContainer component with topic details240 setSelectedDocument(d);241 }242 });243 };244 245 useEffect(() => {246 if (REACT_APP_API_ENDPOINT === "local" || apiData === undefined) {247 setMapLoading(true);248 // Fetch the JSON data locally249 fetch(`/${bunkaDocs}`)250 .then((response) => response.json())251 .then((localData) => {252 // Fetch the local topics data and merge it with the existing data253 fetch(`/${bunkaTopics}`)254 .then((response) => response.json())255 .then((topicsData) => {256 // Merge the topics data with the existing data257 const mergedData = localData.concat(topicsData);258 259 // Call the function to create the scatter plot after data is loaded260 createScatterPlot(mergedData);261 })262 .catch((error) => {263 console.error("Error fetching topics data:", error);264 })265 .finally(() => {266 setMapLoading(false);267 });268 })269 .catch((error) => {270 console.error("Error fetching JSON data:", error);271 })272 .finally(() => {273 setMapLoading(false);274 });275 } else {276 // Call the function to create the scatter plot with the data provided by TopicsContext277 createScatterPlot(apiData.docs.concat(apiData.topics));278 }279 280 // After the data is loaded, set the default topic281 if (apiData && apiData.topics && apiData.topics.length > 0) {282 // Set the default topic to the first topic in the list283 setSelectedDocument(apiData.topics[0]);284 }285 }, [apiData]);286 287 288 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.";289 290 return (291 <div className="json-display">292 {(isFileProcessing || mapLoading) ? (293 <Backdrop open={isFileProcessing || mapLoading} style={{ zIndex: 9999 }}>294 <CircularProgress color="primary" />295 </Backdrop>296 ) : (297 <div className="scatter-plot-and-text-container">298 <div className="scatter-plot-container" ref={scatterPlotContainerRef}>299 <HtmlTooltip300 title={301 <React.Fragment>302 <Typography color="inherit">{mapDescription}</Typography>303 </React.Fragment>304 }305 followCursor306 >307 <HelpIcon style={{308 position: "relative",309 top: 10,310 left: 40,311 border: "none"312 }} />313 </HtmlTooltip>314 <svg ref={svgRef} />315 </div>316 <div className="text-container" >317 {selectedDocument !== null ? (318 <>319 {/* <Box sx={{ marginBottom: "1em" }}>320 <Button sx={{ width: "100%" }} component="label" variant="outlined" startIcon={<RepeatIcon />} onClick={() => setSelectedDocument(null)}>321 Upload another CSV file322 </Button>323 </Box> */}324 <TextContainer topicName={selectedDocument.name} topicSizeFraction={topicsSizeFraction(topicsCentroids, selectedDocument.size)} content={selectedDocument.top_doc_content} />325 </>326 ) : <QueryView />}327 </div>328 </div>329 )}330 </div>331 );332}333 334export default MapView;335 