import React, { useEffect, useMemo, useRef, useState } from "react"; import ForceGraph2D from "react-force-graph-2d"; import * as d3 from "d3-force-3d"; import data from "./data/data.json"; import titlesData from "./data/titles.json"; import HomeButton from "./utils/HomeButton"; import { FloatingPanel } from "./utils/FloatingPanel"; import { DetailsPanel } from "./utils/DetailsPanel"; import { FloatingPanelStack } from "./utils/FloatingPanelStack"; import { drawRoundedRect, getConnectedComponents } from "./graph/common"; export function buildGraph(data) { const nodes = []; const links = []; // Create a lookup map for quick access const titleMap = new Map(titlesData.map(t => [t.cluster_id, t.title])); data.claim_clusters.forEach((cluster) => { nodes.push({ id: cluster.cluster_id, label: titleMap.get(cluster.cluster_id) || cluster.title || "Unnamed Claim Cluster", type: "claim_cluster", type_nice: "Claim", members: cluster.members }); }); data.event_clusters.forEach((cluster) => { nodes.push({ id: cluster.cluster_id, label: titleMap.get(cluster.cluster_id) || cluster.title || "Unnamed Event Cluster", type: "event_cluster", type_nice: "Event", members: cluster.members }); }); data.cluster_links.forEach((link) => { links.push({ source: link.claim_cluster_id, target: link.event_cluster_id }); }); return { nodes, links }; } export function VizSmallConnected() { const fgRef = useRef(); const [selectedNode, setSelectedNode] = useState(null); const [minGraphSize, setMinGraphSize] = useState(10); const graphData = useMemo(() => { const full = buildGraph(data); const components = getConnectedComponents(full.nodes, full.links); // keep only components large enough const validIds = new Set( components .filter(comp => comp.length >= minGraphSize && comp.length < 50) .flat() ); const filteredNodes = full.nodes.filter(n => validIds.has(n.id)); const filteredLinks = full.links.filter( l => validIds.has(l.source) && validIds.has(l.target) ); return { nodes: filteredNodes, links: filteredLinks }; }, [minGraphSize]); useEffect(() => { if (!fgRef.current) return; // Stronger repulsion fgRef.current.d3Force( "charge", d3.forceManyBody().strength(-10000) ); // Link distance fgRef.current.d3Force( "link", d3.forceLink().distance(140) ); // Collision based on dynamic box size fgRef.current.d3Force( "collision", d3.forceCollide((node) => { const dims = node.__bckgDimensions; return dims ? Math.max(dims[0], dims[1]) / 2 + 32 : 40; }) ); fgRef.current.d3ReheatSimulation(); }, [graphData]); useEffect(() => { if (fgRef.current) { fgRef.current.zoom(0.05, 0); } }, []); return (
Trigger Event Cluster
Claim Cluster