Skip to content

Commit daa7eb9

Browse files
committed
refactor(chart): standardize chart mark identity and hover logic
centralize chart mark key generation and hover state handling into shared helpers, replace ad-hoc hover logic across all chart components
1 parent 86423d6 commit daa7eb9

7 files changed

Lines changed: 137 additions & 112 deletions

File tree

agent-html/components/chart/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ Use visx primitives by responsibility:
176176

177177
- `components/ui/chart.tsx` owns generic hover state, presence, opacity, and transition
178178
tokens.
179+
- `components/ui/chart.tsx` owns mark identity helpers. Discrete charts use
180+
`getChartMarkKey`, `getChartMarkPresence`, and `getChartMarkOpacity` instead
181+
of hand-built hover keys or local opacity branches.
179182
- `BarChart`, `BarHChart`, `PieChart`, `HeatmapChart`, `NetworkChart`, and
180183
`SankeyChart` consume the shared hover protocol for highlighted and faded
181184
marks.

agent-html/components/chart/bar-chart.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import {
2121
createBandScale,
2222
createCartesianLayout,
2323
createLinearScale,
24-
getChartHoverOpacity,
25-
getChartHoverPresence,
2624
getChartCssVariable,
25+
getChartMarkKey,
26+
getChartMarkOpacity,
27+
getChartMarkPresence,
2728
getFiniteValues,
2829
getValue,
2930
isFiniteNumber,
@@ -361,15 +362,16 @@ function BarChartCore<T>({
361362
}
362363

363364
const rect = model.getBarRect(datum, value)
364-
const presence = getChartHoverPresence({
365+
const key = getChartMarkKey("bar", category)
366+
const presence = getChartMarkPresence({
365367
hover,
366-
isRelated: hover?.key === category,
368+
key,
367369
})
368-
const opacity = getChartHoverOpacity({ presence })
370+
const opacity = getChartMarkOpacity({ presence })
369371
const showTooltip = (
370372
event: React.PointerEvent<SVGRectElement>
371373
) => {
372-
setHover({ key: category, type: "bar" })
374+
setHover({ key, type: "bar" })
373375
showMarkTooltip(event, {
374376
datum,
375377
})

agent-html/components/chart/heatmap-chart.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
ChartTooltipPanel,
1717
chartMotion,
1818
getChartCssVariable,
19+
getChartMarkKey,
20+
getChartMarkOpacity,
21+
getChartMarkPresence,
1922
getValue,
2023
isFiniteNumber,
2124
useChartMarkTooltip,
@@ -238,9 +241,19 @@ export function HeatmapChart<T>({
238241
<>
239242
{heatmap.flatMap((column) =>
240243
column.map((cell) => {
241-
const key = `${cell.datum.key}-${cell.bin.key}`
242-
const isActive = hover?.key === key
243-
const opacity = isActive ? 1 : cell.opacity
244+
const key = getChartMarkKey(
245+
"cell",
246+
cell.datum.key,
247+
cell.bin.key
248+
)
249+
const presence = getChartMarkPresence({
250+
hover,
251+
key,
252+
})
253+
const opacity = getChartMarkOpacity({
254+
baseOpacity: cell.opacity,
255+
presence,
256+
})
244257

245258
return (
246259
<g key={key}>

agent-html/components/chart/network-chart.tsx

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {
1616
ChartTooltipPanel,
1717
type ChartRenderer,
1818
chartMotion,
19-
getChartHoverOpacity,
20-
getChartHoverPresence,
19+
getChartMarkKey,
20+
getChartMarkOpacity,
21+
getChartMarkPresence,
2122
isFiniteNumber,
2223
useChartMarkTooltip,
2324
} from "../ui/chart"
@@ -223,7 +224,7 @@ function resolveNetworkGraph<
223224

224225
return {
225226
datum: link,
226-
key: `${link.source}-${link.target}-${index}`,
227+
key: getChartMarkKey("link", link.source, link.target, index),
227228
source,
228229
target,
229230
width,
@@ -233,13 +234,6 @@ function resolveNetworkGraph<
233234
return { links, nodes }
234235
}
235236

236-
function isLinkRelatedToNode<
237-
TNode extends NetworkNodeDatum,
238-
TLink extends NetworkLinkDatum,
239-
>(link: PositionedLink<TNode, TLink>, nodeId: string) {
240-
return link.source.id === nodeId || link.target.id === nodeId
241-
}
242-
243237
function isNodeRelatedToLink<
244238
TNode extends NetworkNodeDatum,
245239
TLink extends NetworkLinkDatum,
@@ -335,16 +329,16 @@ function NetworkLinkMark<
335329
showTooltip,
336330
} = useNetworkRenderContext<TNode, TLink>()
337331
const linkIndex = graph.links.indexOf(link)
338-
const presence = getChartHoverPresence({
332+
const presence = getChartMarkPresence({
339333
hover,
334+
key: link.key,
340335
isRelated:
341-
hover?.type === "link"
342-
? hover.key === link.key
343-
: hover?.type === "node"
344-
? isLinkRelatedToNode(link, String(hover.key))
345-
: false,
336+
hover?.type === "node"
337+
? hover.key === getChartMarkKey("node", link.source.id) ||
338+
hover.key === getChartMarkKey("node", link.target.id)
339+
: undefined,
346340
})
347-
const opacity = getChartHoverOpacity({
341+
const opacity = getChartMarkOpacity({
348342
baseOpacity: 0.72,
349343
presence,
350344
})
@@ -411,19 +405,19 @@ function NetworkNodeMark<TNode extends NetworkNodeDatum>({
411405
showTooltip,
412406
} = useNetworkRenderContext<TNode, NetworkLinkDatum>()
413407
const nodeIndex = graph.nodes.indexOf(node)
414-
const presence = getChartHoverPresence({
408+
const nodeKey = getChartMarkKey("node", node.id)
409+
const presence = getChartMarkPresence({
415410
hover,
411+
key: nodeKey,
416412
isRelated:
417-
hover?.type === "node"
418-
? hover.key === node.id
419-
: hover?.type === "link" && activeLink
420-
? isNodeRelatedToLink(node, activeLink)
421-
: false,
413+
hover?.type === "link" && activeLink
414+
? isNodeRelatedToLink(node, activeLink)
415+
: undefined,
422416
})
423-
const opacity = getChartHoverOpacity({ presence })
417+
const opacity = getChartMarkOpacity({ presence })
424418
const color = getNodeColor?.(node, nodeIndex) ?? getDefaultNodeColor(node)
425419
const handlePointerEnter = (event: React.PointerEvent<SVGCircleElement>) => {
426-
setHover({ key: node.id, type: "node" })
420+
setHover({ key: nodeKey, type: "node" })
427421
showTooltip(event, {
428422
index: nodeIndex,
429423
type: "node",

agent-html/components/chart/pie-chart.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
ChartTooltip,
1818
ChartTooltipContent,
1919
chartMotion,
20-
getChartHoverOpacity,
21-
getChartHoverPresence,
2220
getChartCssVariable,
21+
getChartMarkKey,
22+
getChartMarkOpacity,
23+
getChartMarkPresence,
2324
getValue,
2425
isFiniteNumber,
2526
useChartMarkTooltip,
@@ -180,15 +181,16 @@ export function PieChart<T>({
180181
{arcs.map((arc) => {
181182
const d = path(arc) ?? ""
182183
const color = getChartCssVariable(arc.data.key)
183-
const presence = getChartHoverPresence({
184+
const key = getChartMarkKey("slice", arc.data.key)
185+
const presence = getChartMarkPresence({
184186
hover,
185-
isRelated: hover?.key === arc.data.key,
187+
key,
186188
})
187-
const opacity = getChartHoverOpacity({ presence })
189+
const opacity = getChartMarkOpacity({ presence })
188190
const showTooltip = (
189191
event: React.PointerEvent<SVGPathElement>
190192
) => {
191-
setHover({ key: arc.data.key, type: "slice" })
193+
setHover({ key, type: "slice" })
192194
showMarkTooltip(event, {
193195
slice: arc.data,
194196
})

agent-html/components/chart/sankey-chart.tsx

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ import {
2727
ChartTooltipPanel,
2828
chartHoverOpacity,
2929
chartMotion,
30-
getChartHoverOpacity,
31-
getChartHoverPresence,
30+
getChartMarkKey,
31+
getChartMarkOpacity,
32+
getChartMarkPresence,
3233
useChartMarkTooltip,
3334
} from "../ui/chart";
3435
import { RoughPath } from "@/lib/rough-svg";
@@ -119,6 +120,14 @@ type NodeOrIndex = SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum> | number;
119120
const DEFAULT_MARGIN: Margin = { top: 40, right: 180, bottom: 40, left: 180 };
120121
const intFmt = new Intl.NumberFormat("en-US").format;
121122

123+
function getSankeyLinkKey(index: number) {
124+
return getChartMarkKey("link", index);
125+
}
126+
127+
function getSankeyNodeKey(index: number) {
128+
return getChartMarkKey("node", index);
129+
}
130+
122131
function getNodeIndex(nodeOrIndex: NodeOrIndex): number | undefined {
123132
if (typeof nodeOrIndex === "number") {
124133
return nodeOrIndex;
@@ -349,23 +358,26 @@ function SankeyLinks({
349358
const roughPath = roughOptions ? createRibbonPath(link) : undefined;
350359
const sourceIndex = getNodeIndex(link.source as NodeOrIndex) ?? -1;
351360
const targetIndex = getNodeIndex(link.target as NodeOrIndex) ?? -1;
352-
const presence = getChartHoverPresence({
361+
const linkKey = getSankeyLinkKey(index);
362+
const presence = getChartMarkPresence({
353363
hover,
364+
key: linkKey,
354365
isRelated:
355-
hover?.type === "link"
356-
? hover.key === index
357-
: hover?.key === sourceIndex || hover?.key === targetIndex,
366+
hover?.type === "node"
367+
? hover.key === getSankeyNodeKey(sourceIndex) ||
368+
hover.key === getSankeyNodeKey(targetIndex)
369+
: undefined,
358370
});
359371
const stroke = getLinkColor
360372
? getLinkColor(link, index)
361373
: "var(--chart-line-primary)";
362-
const targetOpacity = getChartHoverOpacity({
374+
const targetOpacity = getChartMarkOpacity({
363375
baseOpacity: strokeOpacity,
364376
presence,
365377
});
366378

367379
const handlePointerEnter = (event: React.PointerEvent<Element>) => {
368-
setHover({ key: index, type: "link" });
380+
setHover({ key: linkKey, type: "link" });
369381
showTooltip(event, { type: "link", linkIndex: index });
370382
};
371383
const handlePointerLeave = () => {
@@ -379,7 +391,7 @@ function SankeyLinks({
379391
<ChartMotionGroup
380392
animate={{ opacity: targetOpacity }}
381393
initial={{ opacity: strokeOpacity }}
382-
key={`link-${sourceIndex}-${targetIndex}-${link.width ?? link.value ?? ""}`}
394+
key={linkKey}
383395
onPointerEnter={handlePointerEnter}
384396
onPointerLeave={handlePointerLeave}
385397
style={{ cursor: "pointer" }}
@@ -406,7 +418,7 @@ function SankeyLinks({
406418
d={path}
407419
fill="none"
408420
initial={{ opacity: strokeOpacity }}
409-
key={`link-${sourceIndex}-${targetIndex}-${link.width ?? link.value ?? ""}`}
421+
key={linkKey}
410422
onPointerEnter={handlePointerEnter}
411423
onPointerLeave={handlePointerLeave}
412424
stroke={stroke}
@@ -455,8 +467,10 @@ function SankeyNodes({
455467
const nodeY = node.y0 ?? 0;
456468
const nodeWidth = (node.x1 ?? 0) - nodeX;
457469
const nodeHeight = (node.y1 ?? 0) - nodeY;
458-
const presence = getChartHoverPresence({
470+
const nodeKey = getSankeyNodeKey(index);
471+
const presence = getChartMarkPresence({
459472
hover,
473+
key: nodeKey,
460474
isRelated: isNodeConnected({ hover, links, nodeIndex: index }),
461475
});
462476
const isLeftSide = nodeX < innerWidth / 2;
@@ -468,7 +482,7 @@ function SankeyNodes({
468482
presence === "faded" ? chartHoverOpacity.textFaded : 0.6;
469483

470484
const handlePointerEnter = (event: React.PointerEvent<Element>) => {
471-
setHover({ key: index, type: "node" });
485+
setHover({ key: nodeKey, type: "node" });
472486
showTooltip(event, { type: "node", nodeIndex: index });
473487
};
474488
const handlePointerLeave = () => {
@@ -514,20 +528,24 @@ function isNodeConnected({
514528
}
515529

516530
if (hover.type === "node") {
517-
if (hover.key === nodeIndex) {
531+
if (hover.key === getSankeyNodeKey(nodeIndex)) {
518532
return true;
519533
}
520534
return links.some((link) => {
521535
const sourceIndex = getNodeIndex(link.source as NodeOrIndex);
522536
const targetIndex = getNodeIndex(link.target as NodeOrIndex);
523537
return (
524-
(sourceIndex === hover.key && targetIndex === nodeIndex) ||
525-
(targetIndex === hover.key && sourceIndex === nodeIndex)
538+
(sourceIndex !== undefined &&
539+
getSankeyNodeKey(sourceIndex) === hover.key &&
540+
targetIndex === nodeIndex) ||
541+
(targetIndex !== undefined &&
542+
getSankeyNodeKey(targetIndex) === hover.key &&
543+
sourceIndex === nodeIndex)
526544
);
527545
});
528546
}
529547

530-
const link = links[Number(hover.key)];
548+
const link = links.find((_, index) => getSankeyLinkKey(index) === hover.key);
531549
if (!link) {
532550
return false;
533551
}

0 commit comments

Comments
 (0)