Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/axis-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Axis): Add `domain` to label the chart's scale in other units, such as a secondary axis
5 changes: 5 additions & 0 deletions .changeset/axis-interval-last-tick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Axis): Only drop the last tick of an `xInterval` / `yInterval` axis when it falls on the domain's end
5 changes: 5 additions & 0 deletions .changeset/axis-utc-auto-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Axis): Label `scaleUtc()` ticks in UTC when no `format` is given
5 changes: 5 additions & 0 deletions .changeset/grouped-series-value-sub-band.md
Original file line number Diff line number Diff line change
@@ -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`
5 changes: 5 additions & 0 deletions .changeset/tooltip-item-series-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(TooltipItem): Add `seriesKey` to simplify setting the label, color, and value from a series
10 changes: 10 additions & 0 deletions docs/src/content/components/Axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ Since band padding is not available when not using a band scale, you can leverag

:example{ name="barchart-xinterval-xinset" }

### secondary axes

`domain` ticks a different domain across the chart's own scale, keeping its range and type, so a second axis can label the same space in other units. See [multiple scales](/docs/guides/multiple-scales) for plotting a second measure against it.

```svelte
<Axis placement="right" domain={[32, 212]} label="Fahrenheit" />
```

:example{ name="multiple-axis-same-placement-right" }

<!-- ## Examples

### Placement (bottom / left)
Expand Down
121 changes: 17 additions & 104 deletions docs/src/content/components/Chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
<Bars seriesKey={s.key} />
{/each}
<Spline y="target" />
```

: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
<Chart view={isometric}>
```

: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

Expand All @@ -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" }
118 changes: 118 additions & 0 deletions docs/src/content/guides/multiple-scales.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
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 an axis `domain` | 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
<Chart {data} x="month" series={[{ key: 'apples' }, { key: 'bananas' }, { key: 'cherries' }]}>
{#snippet children({ context })}
<Layer>
{#each context.series.visibleSeries as s (s.key)}
<Bars seriesKey={s.key} />
{/each}
<Spline y="target" />
</Layer>
{/snippet}
</Chart>
```

: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" }

## Remapped onto one scale

Map the second measure into the first measure's domain, so it's drawn against the same `y` scale, then give its axis the second measure's `domain`. 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
<Chart {data} x="year" y="sales" y1="efficiency" y1Range={({ yScale }) => yScale.domain()}>
{#snippet children({ context })}
<Layer>
<Axis placement="left" />
<Axis placement="right" domain={context.y1Scale.domain()} />
<Spline />
<Spline y={(d) => context.y1Scale(d.efficiency)} />
</Layer>
{/snippet}
</Chart>
```

: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
<script>
const yScale = scaleLinear()
.domain([0, max(data, (d) => d.energy)])
.nice();
const efficiencyScale = scaleLinear()
.domain([0, max(data, (d) => d.efficiency)])
.nice();
const efficiencyToY = scaleLinear().domain(efficiencyScale.domain()).range(yScale.domain());
</script>

<BarChart
{data}
x="month"
yDomain={yScale.domain()}
series={[{ key: 'energy' }, { key: 'efficiency', value: (d) => 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
<ChartGroup pointer={{ tooltip: false }}>
<div class="grid grid-stack">
<Chart {data} x="year" y="sales" {padding} height={300}>...</Chart>
<Chart {data} x="year" y="efficiency" {padding} height={300}>...</Chart>
</div>
</ChartGroup>
```

: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, while `pointer={{ tooltip: false }}` leaves the tooltip to the top one.

: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.
18 changes: 13 additions & 5 deletions docs/src/content/guides/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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
<Tooltip.Root>
{#snippet children({ data, series })}
{#snippet children({ data })}
<Tooltip.Header value={data.date} format="day" />
<Tooltip.List>
{#each series as s}
<Tooltip.Item label={s.label} value={s.value} color={s.color} />
{/each}
<Tooltip.Item seriesKey="apples" />
<Tooltip.Item seriesKey="bananas" format="integer" />
</Tooltip.List>
{/snippet}
</Tooltip.Root>
```

Or iterate over the series in your snippet:

```svelte
{#each context.series.series as s (s.key)}
<Tooltip.Item seriesKey={s.key} />
{/each}
```

:example{ component="Tooltip" name="stacked-area" }

## Default tooltip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,7 @@
{#each context.series.visibleSeries as s}
{@const valueAccessor = accessor(s.value ?? s.key)}
{@const value = valueAccessor(data)}
<Tooltip.Item
label={s.key}
color={s.color}
onpointerenter={() => (context.series.highlightKey = s.key)}
onpointerleave={() => (context.series.highlightKey = null)}
>
<Tooltip.Item seriesKey={s.key}>
{format(value)}
</Tooltip.Item>
{/each}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@
{#each context.series.visibleSeries as s}
{@const valueAccessor = accessor(s.value ?? s.key)}
{@const value = Math.abs(valueAccessor(data))}
<Tooltip.Item
label={s.key}
color={s.color}
onpointerenter={() => (context.series.highlightKey = s.key)}
onpointerleave={() => (context.series.highlightKey = null)}
>
<Tooltip.Item seriesKey={s.key}>
{format(value)}

<Button
Expand Down
Loading
Loading