Skip to content

Commit 07cee8a

Browse files
committed
fix(Highlight): Support stacked series. Remove custom highlight snippet in AreaChart
1 parent 59d9865 commit 07cee8a

File tree

2 files changed

+98
-104
lines changed

2 files changed

+98
-104
lines changed

packages/layerchart/src/lib/components/Highlight.svelte

Lines changed: 84 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,19 @@
157157
const y = $derived(accessor(yProp));
158158
159159
const highlightData = $derived(data ?? ctx.tooltip.data);
160+
160161
const xValue = $derived(x(highlightData));
161162
const xCoord = $derived(
162163
Array.isArray(xValue) ? xValue.map((v) => ctx.xScale(v)) : ctx.xScale(xValue)
163164
);
164165
const xOffset = $derived(isScaleBand(ctx.xScale) && !ctx.radial ? ctx.xScale.bandwidth() / 2 : 0);
166+
165167
const yValue = $derived(y(highlightData));
166168
const yCoord = $derived(
167169
Array.isArray(yValue) ? yValue.map((v) => ctx.yScale(v)) : ctx.yScale(yValue)
168170
);
169171
const yOffset = $derived(isScaleBand(ctx.yScale) && !ctx.radial ? ctx.yScale.bandwidth() / 2 : 0);
172+
170173
const axis = $derived(
171174
axisProp == null ? (isScaleBand(ctx.yScale) || isScaleTime(ctx.yScale) ? 'y' : 'x') : axisProp
172175
);
@@ -324,19 +327,81 @@
324327
return tmpArea;
325328
});
326329
327-
// Helper to find series info from tooltipState.series by key
328-
function getTooltipSeries(key: string) {
329-
return ctx.tooltip.series.find((s) => s.key === key);
330-
}
331-
332330
const _points: HighlightPoint[] = $derived.by(() => {
333331
let tmpPoints: HighlightPoint[] = [];
334332
if (!highlightData) return tmpPoints;
335333
336-
const tooltipSeries = ctx.tooltip.series;
337-
338-
if (Array.isArray(xCoord)) {
339-
// `x` accessor with multiple properties (ex. `x={['start', 'end']}` or `x={[0, 1]}`)
334+
// Use tooltip.series directly when:
335+
// 1. No explicit data prop (using ctx.tooltip.data)
336+
// 2. tooltip.series is populated
337+
// This handles multi-series charts where a single Highlight renders all series points
338+
if (data === undefined && ctx.tooltip.series.length > 0) {
339+
tmpPoints = ctx.tooltip.series
340+
.map((seriesInfo) => {
341+
// Skip if series is not visible
342+
if (!seriesInfo.visible) return null;
343+
344+
let pointX: number;
345+
let pointY: number;
346+
let dataX: any;
347+
let dataY: any;
348+
349+
// For stacked charts, use getStackValue to get the cumulative y1 value
350+
if (ctx.series.isStacked) {
351+
// Find the matching data point in flatData by comparing x values
352+
// This is needed because tooltip.data might be a different object reference
353+
const matchingData = ctx.flatData.find((d) => x(d) === xValue);
354+
const stackValue = matchingData
355+
? ctx.series.getStackValue(seriesInfo.key, matchingData)
356+
: null;
357+
const stackedY1 = stackValue?.[1] ?? 0;
358+
359+
if (ctx.valueAxis === 'x') {
360+
// Horizontal stacked chart
361+
pointX = ctx.xScale(stackedY1) + xOffset;
362+
pointY = (yCoord as number) + yOffset;
363+
dataX = stackedY1;
364+
dataY = yValue;
365+
} else {
366+
// Vertical stacked chart (default)
367+
pointX = (xCoord as number) + xOffset;
368+
pointY = ctx.yScale(stackedY1) + yOffset;
369+
dataX = xValue;
370+
dataY = stackedY1;
371+
}
372+
} else {
373+
// Non-stacked charts - use tooltip.series value directly
374+
const seriesValue = seriesInfo.value;
375+
376+
if (ctx.valueAxis === 'x') {
377+
// Horizontal chart - value is on x-axis
378+
pointX = ctx.xScale(seriesValue) + xOffset;
379+
pointY = (yCoord as number) + yOffset;
380+
dataX = seriesValue;
381+
dataY = yValue;
382+
} else {
383+
// Vertical chart (default) - value is on y-axis
384+
pointX = (xCoord as number) + xOffset;
385+
pointY = ctx.yScale(seriesValue) + yOffset;
386+
dataX = xValue;
387+
dataY = seriesValue;
388+
}
389+
}
390+
391+
return {
392+
x: pointX,
393+
y: pointY,
394+
fill: seriesInfo.color ?? '',
395+
data: {
396+
x: dataX,
397+
y: dataY,
398+
},
399+
seriesKey: seriesInfo.key,
400+
};
401+
})
402+
.filter(notNull);
403+
} else if (Array.isArray(xCoord)) {
404+
// Fallback: `x` accessor with multiple properties (ex. `x={['start', 'end']}` or `x={[0, 1]}`)
340405
341406
if (Array.isArray(highlightData)) {
342407
// Stack series (ex. `y={[['apples', 'bananas', 'oranges']]})`)
@@ -357,13 +422,8 @@
357422
358423
tmpPoints = seriesPointsData
359424
.map((seriesPoint, i) => {
360-
const seriesInfo = tooltipSeries[i];
361-
// Skip if series is not visible
362-
if (seriesInfo && !seriesInfo.visible) return null;
363-
364425
// Use tooltipState.series color if available, fallback to ctx.cGet
365-
const fill =
366-
seriesInfo?.color ?? (ctx.config.c ? ctx.cGet(seriesPoint.series) : null);
426+
const fill = ctx.config.c ? ctx.cGet(seriesPoint.series) : null;
367427
368428
return {
369429
x: ctx.xScale(seriesPoint.point[1]) + xOffset,
@@ -373,7 +433,7 @@
373433
x: seriesPoint.point[1],
374434
y: yValue,
375435
},
376-
seriesKey: seriesInfo?.key,
436+
seriesKey: undefined,
377437
};
378438
})
379439
.filter(notNull);
@@ -385,15 +445,8 @@
385445
if (xItem == null) return null;
386446
// @ts-expect-error - TODO: fix type
387447
const _key = ctx.config.x?.[i];
388-
const seriesInfo = getTooltipSeries(_key);
389448
390-
// Skip if series is not visible
391-
if (seriesInfo && !seriesInfo.visible) return null;
392-
393-
// Use tooltipState.series color if available
394-
const fill =
395-
seriesInfo?.color ??
396-
(ctx.config.c ? ctx.cGet({ ...highlightData, $key: _key }) : null);
449+
const fill = ctx.config.c ? ctx.cGet({ ...highlightData, $key: _key }) : null;
397450
398451
return {
399452
x: xItem + xOffset,
@@ -409,7 +462,7 @@
409462
.filter(notNull);
410463
}
411464
} else if (Array.isArray(yCoord)) {
412-
// `y` accessor with multiple properties (ex. `y={['apples', 'bananas', 'oranges']}` or `y={[0, 1]})
465+
// Fallback: `y` accessor with multiple properties (ex. `y={['apples', 'bananas', 'oranges']}` or `y={[0, 1]})
413466
414467
if (Array.isArray(highlightData)) {
415468
// Stack series (ex. `y={[['apples', 'bananas', 'oranges']]})`)
@@ -430,13 +483,8 @@
430483
431484
tmpPoints = seriesPointsData
432485
.map((seriesPoint, i) => {
433-
const seriesInfo = tooltipSeries[i];
434-
// Skip if series is not visible
435-
if (seriesInfo && !seriesInfo.visible) return null;
436-
437486
// Use tooltipState.series color if available, fallback to ctx.cGet
438-
const fill =
439-
seriesInfo?.color ?? (ctx.config.c ? ctx.cGet(seriesPoint.series) : null);
487+
const fill = ctx.config.c ? ctx.cGet(seriesPoint.series) : null;
440488
441489
return {
442490
x: xCoord + xOffset,
@@ -446,7 +494,7 @@
446494
x: xValue,
447495
y: seriesPoint.point[1],
448496
},
449-
seriesKey: seriesInfo?.key,
497+
seriesKey: undefined,
450498
};
451499
})
452500
.filter(notNull);
@@ -458,15 +506,8 @@
458506
if (yItem == null) return null;
459507
// @ts-expect-error - TODO: fix type
460508
const _key = ctx.config.y[i];
461-
const seriesInfo = getTooltipSeries(_key);
462-
463-
// Skip if series is not visible
464-
if (seriesInfo && !seriesInfo.visible) return null;
465509
466-
// Use tooltipState.series color if available
467-
const fill =
468-
seriesInfo?.color ??
469-
(ctx.config.c ? ctx.cGet({ ...highlightData, $key: _key }) : null);
510+
const fill = ctx.config.c ? ctx.cGet({ ...highlightData, $key: _key }) : null;
470511
471512
return {
472513
x: xCoord + xOffset,
@@ -482,9 +523,8 @@
482523
.filter(notNull);
483524
}
484525
} else if (xCoord != null && yCoord != null) {
485-
// Single point - use tooltipState.series if available (for single-series charts)
486-
const seriesInfo = tooltipSeries.length === 1 ? tooltipSeries[0] : null;
487-
const fill = seriesInfo?.color ?? (ctx.config.c ? ctx.cGet(highlightData) : null);
526+
// Fallback: Single point without tooltip.series
527+
const fill = ctx.config.c ? ctx.cGet(highlightData) : null;
488528
489529
tmpPoints = [
490530
{
@@ -495,7 +535,7 @@
495535
x: xValue,
496536
y: yValue,
497537
},
498-
seriesKey: seriesInfo?.key,
538+
seriesKey: undefined,
499539
},
500540
];
501541
} else {

packages/layerchart/src/lib/components/charts/AreaChart.svelte

Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts" module>
22
import type { ChartProps } from '../Chart.svelte';
3-
import type { HighlightPointData } from '../Highlight.svelte';
3+
import type { HighlightPoint } from '../Highlight.svelte';
44
import type { SeriesData } from './types.js';
55
66
import Area from '../Area.svelte';
@@ -28,12 +28,9 @@
2828
* A callback function called when a point in the chart is clicked.
2929
*
3030
* @param e - the original event that triggered the `onPointClick`
31-
* @param details - an object containing the highlighted point data and series data
31+
* @param details - an object containing the highlighted point and data
3232
*/
33-
onPointClick?: (
34-
e: MouseEvent,
35-
details: { data: HighlightPointData; series: SeriesData<TData, typeof Area> }
36-
) => void;
33+
onPointClick?: (e: MouseEvent, details: { point: HighlightPoint; data: any }) => void;
3734
3835
/**
3936
* Enable profiling to measure render time.
@@ -44,17 +41,11 @@
4441
</script>
4542

4643
<script lang="ts" generics="TData">
47-
import { onMount, type ComponentProps } from 'svelte';
44+
import { onMount } from 'svelte';
4845
4946
import Chart from '../Chart.svelte';
50-
import Highlight from '../Highlight.svelte';
5147
52-
import {
53-
chartDataArray,
54-
defaultChartPadding,
55-
findRelatedData,
56-
type Accessor,
57-
} from '$lib/utils/common.js';
48+
import { chartDataArray, defaultChartPadding, type Accessor } from '$lib/utils/common.js';
5849
import { SeriesState, type StackLayout } from '$lib/states/series.svelte.js';
5950
import type { BrushDomainType } from '../../states/brush.svelte.js';
6051
@@ -71,7 +62,7 @@
7162
grid = true,
7263
legend = false,
7364
tooltipContext = true,
74-
highlight: highlightProp = true,
65+
highlight = { lines: true, points: true },
7566
rule = true,
7667
onTooltipClick = () => {},
7768
onPointClick,
@@ -108,40 +99,6 @@
10899
109100
const brushProps = $derived({ ...(typeof brush === 'object' ? brush : null), ...props.brush });
110101
111-
// Highlight needs per-series props for stacked data
112-
function getHighlightProps(s: SeriesData<TData, typeof Area>): ComponentProps<typeof Highlight> {
113-
if (!context) return {};
114-
const seriesTooltipData =
115-
s.data && context.tooltip.data
116-
? (findRelatedData(s.data, context.tooltip.data, context.x) ?? {})
117-
: null;
118-
const highlightPointsProps =
119-
typeof props.highlight?.points === 'object' ? props.highlight.points : null;
120-
121-
const stackAccessors = seriesState.isStacked ? seriesState.getStackAccessors(s.key) : null;
122-
123-
return {
124-
data: seriesTooltipData,
125-
y: stackAccessors?.y1 ?? s.value ?? (s.data ? undefined : s.key),
126-
lines: seriesState.visibleSeries[0]?.key === s.key,
127-
onPointClick: onPointClick
128-
? (e, detail) => onPointClick(e, { ...detail, series: s })
129-
: undefined,
130-
onPointEnter: () => (seriesState.highlightKey = s.key),
131-
onPointLeave: () => (seriesState.highlightKey = null),
132-
...props.highlight,
133-
...(typeof highlightProp === 'object' ? highlightProp : null),
134-
opacity: seriesState.highlightKey && seriesState.highlightKey !== s.key ? 0.1 : 1,
135-
points:
136-
props.highlight?.points == false
137-
? false
138-
: {
139-
...highlightPointsProps,
140-
fill: s.color,
141-
},
142-
};
143-
}
144-
145102
if (profile) {
146103
console.time('AreaChart render');
147104
onMount(() => {
@@ -204,8 +161,15 @@
204161
{grid}
205162
{rule}
206163
{legend}
164+
{highlight}
207165
tooltip={tooltipProp}
208-
{props}
166+
props={{
167+
...props,
168+
highlight: {
169+
...props.highlight,
170+
onPointClick,
171+
},
172+
}}
209173
>
210174
{#snippet marks(snippetProps)}
211175
{#if typeof marks === 'function'}
@@ -234,14 +198,4 @@
234198
{/each}
235199
{/if}
236200
{/snippet}
237-
238-
{#snippet highlight(snippetProps)}
239-
{#if typeof highlightProp === 'function'}
240-
{@render highlightProp(snippetProps)}
241-
{:else if highlightProp}
242-
{#each seriesState.visibleSeries as s (s.key)}
243-
<Highlight {...getHighlightProps(s)} />
244-
{/each}
245-
{/if}
246-
{/snippet}
247201
</Chart>

0 commit comments

Comments
 (0)