fix(graph): stop corpus-graph labels overlapping
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) <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,17 @@ export function GraphCanvas({
|
|||||||
fgRef.current?.zoomToFit?.(400, 60);
|
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(
|
const drawNode = useCallback(
|
||||||
(node: FGNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
(node: FGNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||||
const r = nodeRadius(node);
|
const r = nodeRadius(node);
|
||||||
@@ -147,22 +158,39 @@ export function GraphCanvas({
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels: hubs always; precedents when zoomed in, important, or active.
|
// Labels — gated by zoom so they don't pile up. At overview zoom only a
|
||||||
const isHub = node.type === "topic" || node.type === "practice_area";
|
// 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 =
|
const showLabel =
|
||||||
!dimmed &&
|
!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) {
|
if (showLabel && node.label) {
|
||||||
const fontSize = Math.max(2.5, (isHub ? 4.5 : 3.6) / Math.sqrt(globalScale)) +
|
const fontPx = isTopicHub || isAreaHub ? 12 : 11;
|
||||||
(isHub ? 1 : 0);
|
const fontSize = fontPx / globalScale; // ~constant on screen
|
||||||
ctx.font = `${fontSize + 6}px Heebo, sans-serif`;
|
ctx.font = `${fontSize}px Heebo, sans-serif`;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
ctx.direction = "rtl";
|
ctx.direction = "rtl";
|
||||||
ctx.fillStyle = isHub ? "#7a5a26" : "#1a1a2e";
|
|
||||||
const label =
|
const label =
|
||||||
node.label.length > 28 ? `${node.label.slice(0, 27)}…` : node.label;
|
node.label.length > 22 ? `${node.label.slice(0, 21)}…` : node.label;
|
||||||
ctx.fillText(label, node.x ?? 0, (node.y ?? 0) + r + 1);
|
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;
|
ctx.globalAlpha = 1;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user