From f3e99a14ca599380e196663e65e7c3c40314427e Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 7 Jun 2026 20:07:27 +0000 Subject: [PATCH] fix(graph): stop corpus-graph labels overlapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Labels piled on top of each other (esp. in the neighborhood view) for two reasons, both fixed in graph-canvas.tsx: 1. Font grew as you zoomed OUT (size was divided by sqrt(globalScale) and had a +6 floor), so at overview zoom labels became huge and collided. Now the label font is a ~constant SCREEN size (fontPx / globalScale). 2. Every node drew its label at once. Now labels are zoom-gated: at overview zoom only the active node, the 3 practice-area hubs, and the most-cited precedents (size>=4) are labeled; topic hubs appear at >=1.05 and the rest at >=1.5 — by which point there is pixel room between nodes. Also: a white halo (strokeText) behind each label for legibility over edges and nearby nodes, and stronger d3 forces (charge -220, link distance 60) so nodes spread out and labels have more room. web-ui build passes; /graph in the route table. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-ui/src/components/graph/graph-canvas.tsx | 46 ++++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/web-ui/src/components/graph/graph-canvas.tsx b/web-ui/src/components/graph/graph-canvas.tsx index 7c3374e..ccb7c9f 100644 --- a/web-ui/src/components/graph/graph-canvas.tsx +++ b/web-ui/src/components/graph/graph-canvas.tsx @@ -130,6 +130,17 @@ export function GraphCanvas({ fgRef.current?.zoomToFit?.(400, 60); }, []); + // Spread nodes out so labels have room — stronger repulsion + longer links + // than the d3-force defaults (charge -30, link 30). Re-applied whenever the + // graph data changes (full ↔ neighborhood). + useEffect(() => { + const fg = fgRef.current; + if (!fg?.d3Force) return; + fg.d3Force("charge")?.strength?.(-220); + fg.d3Force("link")?.distance?.(60); + fg.d3ReheatSimulation?.(); + }, [graphData]); + const drawNode = useCallback( (node: FGNode, ctx: CanvasRenderingContext2D, globalScale: number) => { const r = nodeRadius(node); @@ -147,22 +158,39 @@ export function GraphCanvas({ ctx.stroke(); } - // Labels: hubs always; precedents when zoomed in, important, or active. - const isHub = node.type === "topic" || node.type === "practice_area"; + // Labels — gated by zoom so they don't pile up. At overview zoom only a + // few show (active node, the 3 practice-area hubs, the most-cited + // precedents); zooming in reveals the rest, by which point there is pixel + // room between nodes. Font is kept at a ~constant SCREEN size (px / + // globalScale) instead of growing as you zoom out — that growth was what + // made labels collide. + const isTopicHub = node.type === "topic"; + const isAreaHub = node.type === "practice_area"; const showLabel = !dimmed && - (isHub || node.id === activeId || node.size >= 3 || globalScale >= 1.6); + (node.id === activeId || + isAreaHub || + globalScale >= 1.5 || + (isTopicHub && globalScale >= 1.05) || + (!isTopicHub && node.size >= 4 && globalScale >= 0.9)); if (showLabel && node.label) { - const fontSize = Math.max(2.5, (isHub ? 4.5 : 3.6) / Math.sqrt(globalScale)) + - (isHub ? 1 : 0); - ctx.font = `${fontSize + 6}px Heebo, sans-serif`; + const fontPx = isTopicHub || isAreaHub ? 12 : 11; + const fontSize = fontPx / globalScale; // ~constant on screen + ctx.font = `${fontSize}px Heebo, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.direction = "rtl"; - ctx.fillStyle = isHub ? "#7a5a26" : "#1a1a2e"; const label = - node.label.length > 28 ? `${node.label.slice(0, 27)}…` : node.label; - ctx.fillText(label, node.x ?? 0, (node.y ?? 0) + r + 1); + node.label.length > 22 ? `${node.label.slice(0, 21)}…` : node.label; + const lx = node.x ?? 0; + const ly = (node.y ?? 0) + r + 2 / globalScale; + // White halo so the text stays readable over edges and nearby nodes. + ctx.lineWidth = 3 / globalScale; + ctx.strokeStyle = "rgba(255,255,255,0.92)"; + ctx.lineJoin = "round"; + ctx.strokeText(label, lx, ly); + ctx.fillStyle = isTopicHub || isAreaHub ? "#7a5a26" : "#1a1a2e"; + ctx.fillText(label, lx, ly); } ctx.globalAlpha = 1; },