From eb5ce3fb38534c78556ca88201b2c80aeee1cf85 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 13:12:00 -0400 Subject: [PATCH 1/8] fix(BarChart): allocate sub-bands by series key for grouped values --- .changeset/grouped-series-value-sub-band.md | 5 + .../BarChart/group-series-dual-axis.svelte | 87 ++++++++++++++++++ .../components/charts/BarChart.svelte.test.ts | 50 ++++++++++ .../charts/BarChart/BarChart.base.svelte | 4 +- ...-a--value--accessor-its-own-sub-band-1.png | Bin 0 -> 5579 bytes ...-a--value--accessor-its-own-sub-band-2.png | Bin 0 -> 5579 bytes 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 .changeset/grouped-series-value-sub-band.md create mode 100644 docs/src/examples/components/BarChart/group-series-dual-axis.svelte create mode 100644 packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-1.png create mode 100644 packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-2.png diff --git a/.changeset/grouped-series-value-sub-band.md b/.changeset/grouped-series-value-sub-band.md new file mode 100644 index 000000000..9b281b8c8 --- /dev/null +++ b/.changeset/grouped-series-value-sub-band.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(BarChart): Place a grouped series in its own sub-band when its `value` is an accessor or a field other than its `key` diff --git a/docs/src/examples/components/BarChart/group-series-dual-axis.svelte b/docs/src/examples/components/BarChart/group-series-dual-axis.svelte new file mode 100644 index 000000000..972ab108a --- /dev/null +++ b/docs/src/examples/components/BarChart/group-series-dual-axis.svelte @@ -0,0 +1,87 @@ + + + efficiencyToY(d.efficiency), + color: 'var(--color-success)' + } + ]} + seriesLayout="group" + legend + padding={defaultChartPadding({ top: 8, left: 48, right: 48, legend: true })} + height={300} +> + {#snippet axis({ context })} + + `${v}%`} + rule + label="Efficiency" + labelPlacement="middle" + /> + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + + + + + + + {/snippet} + + {/snippet} + diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts b/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts index eed5c41a5..aafec7df6 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts @@ -381,6 +381,56 @@ describe('BarChart', () => { expect(barsGroups.length).toBe(4); }); + it('should give a grouped series with a `value` accessor its own sub-band', async () => { + const { container } = render(BarChart, { + data: wideData, + x: 'year', + series: [ + { key: 'apples' }, + { key: 'scaledBananas', value: (d: (typeof wideData)[number]) => d.bananas * 2 }, + ], + seriesLayout: 'group', + height: 300, + width: 400, + }); + + // The first bar of each series, left to right + const firstBarX = () => + Array.from(container.querySelectorAll('.lc-bars')).map((g) => { + const bar = g.querySelector('.lc-bar') as SVGGraphicsElement | null; + return bar ? bar.getBBox().x : NaN; + }); + + await vi.waitFor(() => { + const [apples, scaledBananas] = firstBarX(); + expect(Number.isFinite(apples)).toBe(true); + expect(scaledBananas).toBeGreaterThan(apples); + }); + }); + + it('should give a grouped series with a string `value` its own sub-band', async () => { + const { container } = render(BarChart, { + data: wideData, + x: 'year', + series: [{ key: 'apples' }, { key: 'yellowFruit', value: 'bananas' }], + seriesLayout: 'group', + height: 300, + width: 400, + }); + + const firstBarX = () => + Array.from(container.querySelectorAll('.lc-bars')).map((g) => { + const bar = g.querySelector('.lc-bar') as SVGGraphicsElement | null; + return bar ? bar.getBBox().x : NaN; + }); + + await vi.waitFor(() => { + const [apples, yellowFruit] = firstBarX(); + expect(Number.isFinite(apples)).toBe(true); + expect(yellowFruit).toBeGreaterThan(apples); + }); + }); + it('should render stackExpand series', async () => { const { container } = render(BarChart, { data: wideData, diff --git a/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte index 06b2458a8..c3f637a3b 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte @@ -124,10 +124,10 @@ s.value ?? s.key + ? (d: any) => s.key : undefined} y1={valueAxis === 'x' && isGroupSeries && restProps.y1 == null - ? (d: any) => s.value ?? s.key + ? (d: any) => s.key : undefined} rounded={context.series.stackLayout != null ? // Per row rather than per series: a sub-band or a gap in the data can leave the diff --git a/packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-1.png b/packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5e6e0f07980f578e76bf5d55642f8d6488d890f7 GIT binary patch literal 5579 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 literal 0 HcmV?d00001 diff --git a/packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-2.png b/packages/layerchart/src/lib/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-series-should-give-a-grouped-series-with-a--value--accessor-its-own-sub-band-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5e6e0f07980f578e76bf5d55642f8d6488d890f7 GIT binary patch literal 5579 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 literal 0 HcmV?d00001 From ad528fe196cc583e0d465c0c3845176499bd6cca Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 19:32:52 -0400 Subject: [PATCH 2/8] Improve dual-axis chart tooltip and highlight examples --- .../BarChart/group-series-dual-axis.svelte | 22 ++-- ...pound-dual-axis-with-stacked-charts.svelte | 116 +++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/docs/src/examples/components/BarChart/group-series-dual-axis.svelte b/docs/src/examples/components/BarChart/group-series-dual-axis.svelte index 972ab108a..cd81bab74 100644 --- a/docs/src/examples/components/BarChart/group-series-dual-axis.svelte +++ b/docs/src/examples/components/BarChart/group-series-dual-axis.svelte @@ -68,18 +68,16 @@ {#snippet children({ data })} - - - + {#each context.series.visibleSeries as s (s.key)} + + + {/each} {/snippet} diff --git a/docs/src/examples/components/Chart/compound-dual-axis-with-stacked-charts.svelte b/docs/src/examples/components/Chart/compound-dual-axis-with-stacked-charts.svelte index 116aeeb9d..5959567c6 100644 --- a/docs/src/examples/components/Chart/compound-dual-axis-with-stacked-charts.svelte +++ b/docs/src/examples/components/Chart/compound-dual-axis-with-stacked-charts.svelte @@ -4,66 +4,68 @@ -
- - - - - - - - - + + +
+ + + + + + + + + - - - - - - - - + + + + + + + - - {#snippet children({ data })} - {data.year} - - - - - {/snippet} - - -
+ + {#snippet children({ data })} + {data.year} + + + + + {/snippet} + + +
+ From 22087335248cea93769ad2a0b50ba3d55255bcfc Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 19:33:35 -0400 Subject: [PATCH 3/8] Document approaches for multiple chart scales --- docs/src/content/guides/multiple-scales.md | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/src/content/guides/multiple-scales.md diff --git a/docs/src/content/guides/multiple-scales.md b/docs/src/content/guides/multiple-scales.md new file mode 100644 index 000000000..edd4760d6 --- /dev/null +++ b/docs/src/content/guides/multiple-scales.md @@ -0,0 +1,112 @@ +--- +title: Multiple Scales (Dual axis) +category: state +--- + +A chart has one `y` scale, so every mark is measured against the same axis. Plotting two measures on one chart — sales and fuel efficiency, energy and a percentage — takes one of a few approaches, depending on whether the measures share units and how much of the chart they need to share. + +## Choosing an approach + +| | Scales | Tooltip & highlight | Use when | +| --------------------------------------------------- | --------------------------- | ------------------------------ | ------------------------------------------------------------------- | +| [One scale, extra marks](#one-scale-extra-marks) | one | one, covers every mark | the measures share units | +| [Remapped onto one scale](#remapped-onto-one-scale) | one, plus a hand-built axis | one, covers every mark | different units, one chart's interactions | +| [Overlaid charts](#overlaid-charts) | one per chart | per chart, synced with a group | different units, and each measure needs its own range or chart type | +| [Side by side](#side-by-side) | one per chart | per chart, synced with a group | the measures don't need to share a plot | + +A second axis invites readers to compare where two lines cross or which bar is taller, though both depend on the domains you picked. When the measures don't need to share a plot, [side by side](#side-by-side) avoids that. + +## One scale, extra marks + +When the measures share units — a stack of sales and a sales target — there is no second scale. Draw the extra mark from its own accessor and the domain covers it: + +```svelte + + {#snippet children({ context })} + + {#each context.series.visibleSeries as s (s.key)} + + {/each} + + + {/snippet} + +``` + +:example{ component="Chart" name="mixed-marks-with-stack" } + +Simplified charts take extra marks through `aboveMarks` / `belowMarks`: + +:example{ component="Chart" name="compound-common-scale-with-extra-marks" } + +## Remapped onto one scale + +Map the second measure into the first measure's domain, so it's drawn against the same `y` scale, then draw its axis with a scale of its own. One chart means one tooltip, one highlight, and one legend. + +On `Chart`, `y1` with `y1Range={({ yScale }) => yScale.domain()}` does the mapping — `context.y1Scale` turns an efficiency into its equivalent in sales: + +```svelte + yScale.domain()}> + {#snippet children({ context })} + + + + + context.y1Scale(d.efficiency)} /> + + {/snippet} + +``` + +:example{ component="Chart" name="compound-dual-axis-with-single-chart-using-remapped-scale" } + +`y1` is also the sub-band of horizontal grouped bars, so on a simplified chart build the mapping as a standalone scale and use it as a series `value`. Set `yDomain` from the first measure alone, or the remapped series widens it: + +```svelte + + + efficiencyToY(d.efficiency) }]} + seriesLayout="group" +/> +``` + +:example{ component="BarChart" name="group-series-dual-axis" } + +The tooltip reads each series' `value`, so it would show the remapped number — write a `tooltip` snippet that reads the original field. + +## Overlaid charts + +Stack two charts in the same grid cell, each with its own scales, marks, and axis. Each measure can have its own range — rainfall hanging from the top while flows rise from the bottom — or its own chart type. + +```svelte + +
+ ... + ... +
+
+``` + +:example{ component="Chart" name="compound-dual-axis-with-stacked-charts" } + +The plot areas only line up if the charts share `height`, `padding`, and `x` domain, so set them on both. The top chart receives the pointer; wrap them in a [`ChartGroup`](/docs/guides/chart-group) and the charts underneath show their highlight too. + +:example{ component="Chart" name="compound-separate-scales-with-stacked-charts-and-overridden-marks" } + +:example{ component="Chart" name="compound-separate-scales-with-stacked-charts-with-inverted-range-top-down" } + +## Side by side + +When the measures don't need to share a plot, give each its own chart. A [`ChartGroup`](/docs/guides/chart-group#group-or-facets) keeps the pointer, brush, and zoom in step, and each chart keeps an axis that only it is read against. From 831573add125846bd817e0f04f61df808fb37487 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 19:37:09 -0400 Subject: [PATCH 4/8] Organize Chart documentation around configuration guides --- docs/src/content/components/Chart.md | 121 +++------------------ docs/src/content/guides/multiple-scales.md | 6 + 2 files changed, 23 insertions(+), 104 deletions(-) diff --git a/docs/src/content/components/Chart.md b/docs/src/content/components/Chart.md index f2256ccee..8c515f898 100644 --- a/docs/src/content/components/Chart.md +++ b/docs/src/content/components/Chart.md @@ -11,54 +11,23 @@ order: 1 :example{ component="Area" name="basic" showCode } -::note -Features: Adds support for x and y baselines (always show 0, etc) -:: - -## Mixed marks - -Marks compose: each reads the chart's scales but decides for itself what to draw. Here the `Bars` stack among themselves while the `Spline` draws a target from its own values, and the scale covers both — so the line stays on the chart even where it runs above the tallest stack. - -```svelte -{#each context.series.visibleSeries as s (s.key)} - -{/each} - -``` - -:example{ name="mixed-marks-with-stack" } - -A `Spline` given a `seriesKey` instead follows that series' stacked top, which is how a line is drawn along the edge of a stacked `Area`. - -The same chart from long data needs no `series` at all — `c` names the layers, so one `Bars` stacks them: - -:example{ name="mixed-marks-with-stack-long-data" } - -## Isometric - -`view={isometric}` draws the plot area as a floor seen from above at an angle, for a faux 3D look in any layer. See the [Isometric guide](/docs/guides/isometric) for the view options, height, text, and tooltips. - -```svelte - -``` - -:example{ name="isometric-heatmap" } - -:example{ name="isometric-columns" } - -:example{ name="isometric-stacked-columns" } - -:example{ name="isometric-areas" } - -:example{ name="isometric-scatter-3d" } - -:example{ name="isometric-treemap" } - -:example{ name="isometric-scatter" } - -:example{ name="isometric-playground" } - -:example{ name="isometric-pan-zoom" } +## Guides + +Most of what a chart can do is configured on `Chart`, and each part has its own guide: + +| Guide | Covers | +| ----------------------------------------------- | -------------------------------------------- | +| [Scales](/docs/guides/scales) | `x`, `y`, `xDomain`, `xScale`, `xRange`, ... | +| [Structure](/docs/guides/structure) | snippets and the order layers are drawn in | +| [Series](/docs/guides/series) | `series`, `seriesLayout`, `c` | +| [Multiple scales](/docs/guides/multiple-scales) | mixed marks, dual axes | +| [Facets](/docs/guides/facets) | `fx`, `fy` | +| [Tooltip](/docs/guides/tooltip) | `tooltipContext` | +| [Brush](/docs/guides/brush) | `brush` | +| [Transform](/docs/guides/transform) | `transform` | +| [Chart Group](/docs/guides/chart-group) | `group` | +| [Geo](/docs/guides/geo) | `geo` | +| [Isometric](/docs/guides/isometric) | `view` | ## Text selection @@ -74,59 +43,3 @@ To re-enable selection where you need it, set the `--lc-user-select` custom prop ``` An individual selectable region can also just set `user-select: text` on itself (e.g. Tailwind's `select-text`), which overrides the inherited value. - -### Facet (fx) - -:example{ name="facet-x" } - -### Facet (fy) - -:example{ name="facet-y" } - -### Facet grid (fx + fy) - -:example{ name="facet-grid" } - -### Faceted lines - -:example{ name="facet-lines" } - -### Facet with composed children - -:example{ name="facet-composed" } - -### Two-dimensional faceting - -:example{ name="facet-two-dimensional" } - -### Non-faceted marks - -:example{ name="facet-non-faceted-marks" } - -### Facet wrap - -:example{ name="facet-wrap" } - -### Annotated facets - -:example{ name="facet-annotation" } - -### Panel colours - -:example{ name="facet-color" } - -### Panel spacing - -:example{ name="facet-spacing" } - -### Facet tooltips - -:example{ name="facet-tooltip" } - -### Facet brushing - -:example{ name="facet-brush" } - -### Facet brush-to-zoom - -:example{ name="facet-brush-zoom" } diff --git a/docs/src/content/guides/multiple-scales.md b/docs/src/content/guides/multiple-scales.md index edd4760d6..902600393 100644 --- a/docs/src/content/guides/multiple-scales.md +++ b/docs/src/content/guides/multiple-scales.md @@ -35,6 +35,12 @@ When the measures share units — a stack of sales and a sales target — there :example{ component="Chart" name="mixed-marks-with-stack" } +A `Spline` given a `seriesKey` instead follows that series' stacked top, which is how a line is drawn along the edge of a stacked `Area`. + +The same chart from long data needs no `series` at all — `c` names the layers, so one `Bars` stacks them: + +:example{ component="Chart" name="mixed-marks-with-stack-long-data" } + Simplified charts take extra marks through `aboveMarks` / `belowMarks`: :example{ component="Chart" name="compound-common-scale-with-extra-marks" } From 25a08dcfcf0964581af97a66e228c41d0b6b7950 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 20:21:10 -0400 Subject: [PATCH 5/8] feat(TooltipItem): add seriesKey support for series-aware rows --- .changeset/tooltip-item-series-key.md | 5 + docs/src/content/guides/tooltip.md | 18 ++- .../tooltip-fixed-with-hide-delay.svelte | 7 +- .../AreaChart/tooltip-locking.svelte | 7 +- .../BarChart/group-series-dual-axis.svelte | 14 +- ...ies-horizontal-diverging-as-percent.svelte | 2 +- .../series-horizontal-diverging.svelte | 2 +- .../Chart/mixed-marks-with-stack.svelte | 8 +- .../components/Waffle/custom-symbol.svelte | 8 +- .../Waffle/olympians-weight-by-sex.svelte | 8 +- .../components/Waffle/penguins.svelte | 8 +- .../examples/components/Waffle/stacked.svelte | 8 +- .../components/charts/BarChart.svelte.test.ts | 2 +- .../components/charts/DefaultTooltip.svelte | 6 +- .../lib/components/tooltip/TooltipItem.svelte | 121 ++++++++++++------ .../tooltip/TooltipItem.svelte.test.ts | 85 ++++++++++++ .../lib/tests/TooltipItemSeriesHarness.svelte | 34 +++++ 17 files changed, 234 insertions(+), 109 deletions(-) create mode 100644 .changeset/tooltip-item-series-key.md create mode 100644 packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte.test.ts create mode 100644 packages/layerchart/src/lib/tests/TooltipItemSeriesHarness.svelte diff --git a/.changeset/tooltip-item-series-key.md b/.changeset/tooltip-item-series-key.md new file mode 100644 index 000000000..e46b18a68 --- /dev/null +++ b/.changeset/tooltip-item-series-key.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(TooltipItem): Add `seriesKey` to read the label, color, and value from a series diff --git a/docs/src/content/guides/tooltip.md b/docs/src/content/guides/tooltip.md index 2ad35d689..e530329de 100644 --- a/docs/src/content/guides/tooltip.md +++ b/docs/src/content/guides/tooltip.md @@ -234,6 +234,7 @@ A single label-value row. The `color` prop shows a color indicator: | `format` | `FormatType \| FormatConfig` | Value formatting | | `valueAlign` | `'left' \| 'right' \| 'center'` | Alignment of value column | | `color` | `string` | Color dot indicator | +| `seriesKey` | `string` | Series the row shows | ### `Tooltip.Separator` @@ -308,21 +309,28 @@ When using multi-series charts, the tooltip state includes a `series` array with /> ``` -For custom tooltips with series data, iterate over the series in your snippet: +For custom tooltips, give each `Tooltip.Item` a `seriesKey`. It takes the series' label, color, and value at the hovered point, highlights the series while hovered, and isn't rendered while the series is hidden from the legend. Any of `label`, `value`, or `color` passed explicitly wins: ```svelte - {#snippet children({ data, series })} + {#snippet children({ data })} - {#each series as s} - - {/each} + + {/snippet} ``` +Or iterate over the series in your snippet: + +```svelte +{#each context.series.series as s (s.key)} + +{/each} +``` + :example{ component="Tooltip" name="stacked-area" } ## Default tooltip diff --git a/docs/src/examples/components/AreaChart/tooltip-fixed-with-hide-delay.svelte b/docs/src/examples/components/AreaChart/tooltip-fixed-with-hide-delay.svelte index 6dbe34a20..d1f705d64 100644 --- a/docs/src/examples/components/AreaChart/tooltip-fixed-with-hide-delay.svelte +++ b/docs/src/examples/components/AreaChart/tooltip-fixed-with-hide-delay.svelte @@ -36,12 +36,7 @@ {#each context.series.visibleSeries as s} {@const valueAccessor = accessor(s.value ?? s.key)} {@const value = valueAccessor(data)} - (context.series.highlightKey = s.key)} - onpointerleave={() => (context.series.highlightKey = null)} - > + {format(value)} {/each} diff --git a/docs/src/examples/components/AreaChart/tooltip-locking.svelte b/docs/src/examples/components/AreaChart/tooltip-locking.svelte index 23a33d38a..059738a8b 100644 --- a/docs/src/examples/components/AreaChart/tooltip-locking.svelte +++ b/docs/src/examples/components/AreaChart/tooltip-locking.svelte @@ -45,12 +45,7 @@ {#each context.series.visibleSeries as s} {@const valueAccessor = accessor(s.value ?? s.key)} {@const value = Math.abs(valueAccessor(data))} - (context.series.highlightKey = s.key)} - onpointerleave={() => (context.series.highlightKey = null)} - > + {format(value)}