From 942865947e02dae3318cebb4f4bb4908a5951731 Mon Sep 17 00:00:00 2001 From: William Jeynes Date: Thu, 9 Apr 2026 12:56:07 +0100 Subject: [PATCH] Finalise text view, increase repulsion --- graphviz/frontend/src/App.tsx | 190 ++++++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 56 deletions(-) diff --git a/graphviz/frontend/src/App.tsx b/graphviz/frontend/src/App.tsx index 39f9967..6e406ce 100644 --- a/graphviz/frontend/src/App.tsx +++ b/graphviz/frontend/src/App.tsx @@ -1,114 +1,192 @@ -import React, { useEffect, useMemo, useState } from "react"; +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.json"; +function drawRoundedRect(ctx, x, y, width, height, radius) { + const r = Math.min(radius, width / 2, height / 2); + + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + width - r, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + r); + ctx.lineTo(x + width, y + height - r); + ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); + ctx.lineTo(x + r, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); +} + function buildGraph(data) { const nodes = []; const links = []; - const claimClusterMap = new Map(); - const eventClusterMap = new Map(); - - // Build cluster nodes data.claim_clusters.forEach((cluster) => { - const clusterNode = { + nodes.push({ id: cluster.cluster_id, label: cluster.title || "Unnamed Claim Cluster", type: "claim_cluster", members: cluster.members - }; - nodes.push(clusterNode); - claimClusterMap.set(cluster.cluster_id, clusterNode); + }); }); data.event_clusters.forEach((cluster) => { - const clusterNode = { + nodes.push({ id: cluster.cluster_id, label: cluster.title || "Unnamed Event Cluster", type: "event_cluster", members: cluster.members - }; - nodes.push(clusterNode); - eventClusterMap.set(cluster.cluster_id, clusterNode); + }); }); - // Build links between clusters data.cluster_links.forEach((link) => { - links.push({ source: link.claim_cluster_id, target: link.event_cluster_id }); + links.push({ + source: link.claim_cluster_id, + target: link.event_cluster_id + }); }); return { nodes, links }; } export function App() { + const fgRef = useRef(); const [selectedNode, setSelectedNode] = useState(null); const graphData = useMemo(() => buildGraph(data), []); - function setNode(node) { - console.log(node) - setSelectedNode(node) - } + + useEffect(() => { + if (!fgRef.current) return; + + // Stronger repulsion + fgRef.current.d3Force( + "charge", + d3.forceManyBody().strength(-3000) + ); + + // Link distance + fgRef.current.d3Force( + "link", + d3.forceLink().distance(240) + ); + + // 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 + 16 : 20; + }) + ); + + fgRef.current.d3ReheatSimulation(); + }, []); + return (
-
- node.label} - nodeAutoColorBy="type" - //linkDirectionalParticles={1} - //linkDirectionalParticleSpeed={0.002} - onNodeRightClick={(node) => setNode(node)} - nodeCanvasObject={(node, ctx, globalScale) => { - const label = node.label; - const fontSize = 6 + 2*node.members.length; - ctx.font = `${fontSize}px Sans-Serif`; - const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding + node.label} + nodeAutoColorBy="type" + linkDirectionalParticles={1} + linkDirectionalParticleSpeed={0.002} + onNodeRightClick={(node) => setSelectedNode(node)} - ctx.fillStyle = node.type.includes('claim') ? "blue" : "green"; - ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions); + nodeCanvasObject={(node, ctx, globalScale) => { + const label = node.label; + const fontSize = 16 + 32 * node.members.length; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = 'white'; - ctx.fillText(label, node.x, node.y); + ctx.font = `${fontSize}px Sans-Serif`; - node.__bckgDimensions = bckgDimensions; - - }} - nodePointerAreaPaint={(node, color, ctx) => { - ctx.fillStyle = color; - const bckgDimensions = node.__bckgDimensions; - bckgDimensions && ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions); - }} - - /> -
+ const textWidth = ctx.measureText(label).width; + const padding = fontSize * 0.6; -
+ const width = textWidth + padding; + const height = fontSize + padding; + + const x = node.x - width / 2; + const y = node.y - height / 2; + + const radius = Math.min(10, fontSize * 0.6); + + // background + ctx.fillStyle = node.type.includes("claim") ? "blue" : "green"; + drawRoundedRect(ctx, x, y, width, height, radius); + ctx.fill(); + + // optional border + ctx.strokeStyle = "white"; + ctx.lineWidth = 1; + ctx.stroke(); + + // text + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "white"; + ctx.fillText(label, node.x, node.y); + + // store dimensions for collision + pointer + node.__bckgDimensions = [width, height]; + node.__bckgPos = { x, y }; + }} + + nodePointerAreaPaint={(node, color, ctx) => { + const dims = node.__bckgDimensions; + const pos = node.__bckgPos; + + if (!dims || !pos) return; + + ctx.fillStyle = color; + drawRoundedRect(ctx, pos.x, pos.y, dims[0], dims[1], 6); + ctx.fill(); + }} + /> + +

Details

{selectedNode ? (

ID: {selectedNode.id}

Type: {selectedNode.type}

-

Title / Label: {selectedNode.label}

+

Title: {selectedNode.label}

+ {selectedNode.members && (

Members:

    {selectedNode.members.map((m) => { - const memberData = data.claims.find(c => c.id === m) || data.events.find(e => e.id === m); - return
  • {memberData ? memberData.text : m}
  • ; + const memberData = + data.claims.find((c) => c.id === m) || + data.events.find((e) => e.id === m); + + return ( +
  • + {memberData ? memberData.text : m} +
  • + ); })}
)}
) : ( -

Click a cluster node to see its members

+

Right-click a cluster node to see its members

)}
); -} +} \ No newline at end of file