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/area-line-shared-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Area): Apply `mask`, `filter`, and `clip-path` to the `line` path as well as the fill (unless overrideden by `line={{...}}`)
5 changes: 5 additions & 0 deletions .changeset/axis-tick-occlusion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Axis): Add `tickOcclusion` to drop ticks whose labels would overlap, measured at the size they render (including rotation), with `priority` of `'end'` (default), `'start'`, or `'start-end'` to choose which survive
5 changes: 5 additions & 0 deletions .changeset/calendar-month-label-offset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Calendar): Adjust month labels after recent Text verticalAnchor improvements
5 changes: 5 additions & 0 deletions .changeset/calendar-month-path-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Calendar): Size cells to fit `monthPath`, which can extend past the last cell when the range ends mid-month
5 changes: 5 additions & 0 deletions .changeset/chart-single-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

perf(Chart): Measure the container before building the subtree, so marks are only built once
5 changes: 5 additions & 0 deletions .changeset/download-gradient-stops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(downloadImage, downloadSvg): Inline `stop-color`/`stop-opacity` so gradients don't export as solid black
5 changes: 5 additions & 0 deletions .changeset/getter-missing-accessor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Chart): Return `undefined` from `xGet`/`yGet` when that axis has no accessor, instead of throwing `accessor is not a function`
5 changes: 5 additions & 0 deletions .changeset/grouped-path-motion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Spline, Area): Animate each path when the mark is split by `z`, which `motion` previously skipped — segments split by a style function are still redrawn outright
5 changes: 5 additions & 0 deletions .changeset/path-end-content-without-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Path, Spline): Render `endContent` without a `draw` transition
5 changes: 5 additions & 0 deletions .changeset/text-rect-rotate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(utils): Add `rotate` and multiline support to `getTextRect`, so a rotated or stacked `<Text>` can be measured for `occlude` without the caller reproducing `<Text>`'s pivot
26 changes: 26 additions & 0 deletions docs/generated/releases/layerchart-2.3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: "layerchart@2.3.1"
tag: "layerchart@2.3.1"
date: "2026-08-27T11:33:17Z"
url: "https://github.com/techniq/layerchart/releases/tag/layerchart%402.3.1"
draft: false
prerelease: false
author: "github-actions[bot]"
---
### Patch Changes

- perf(Chart): Resolve a plain-key accessor (`x="date"`) directly instead of re-parsing the path string ([#906](https://github.com/techniq/layerchart/pull/906))

- fix(Chart): Treat an explicit but empty `series` array as authoritative, rather than deriving implicit series from marks ([#906](https://github.com/techniq/layerchart/pull/906))

- perf(Chart): Keep the value domain off the mark registry, so mounting a chart no longer rebuilds its scales — and every path drawn from them — once per mark registered ([#906](https://github.com/techniq/layerchart/pull/906))

- fix(Highlight): Fix crash highlighting `c`-grouped points over nested chart data ([#906](https://github.com/techniq/layerchart/pull/906))

- fix(Highlight): Mark only the hovered row in `quadtree` / `voronoi` tooltips, rather than every row sharing its position ([#906](https://github.com/techniq/layerchart/pull/906))

- feat(Chart): Take the color channel from a mark's own `fill` / `stroke` (`<Circle fill="island">`) when no `c` is declared ([#906](https://github.com/techniq/layerchart/pull/906))

- fix(Tooltip): Center on internal when data snapping (instead of on its leading edge) ([#906](https://github.com/techniq/layerchart/pull/906))

- fix(Tooltip): Fix persistent tooltip when rendered outside Chart container and `pointerEvents` is enabled ([#906](https://github.com/techniq/layerchart/pull/906))
74 changes: 74 additions & 0 deletions docs/src/content/components/Axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,80 @@ Default: `80` for horizontal axes (top/bottom/angle) and `50` for vertical axes
See [time scales](#time-scales) for how tick labels are chosen, and [brush](/docs/components/Axis/time-scale-brush-multiline) for tick spacing while zooming.
::

### tickOcclusion

`tickSpacing` divides the axis length by a pixel budget to pick a tick **count**. It never looks at
the labels, so it is only right when every label is about as wide as the budget assumes.
`tickOcclusion` measures each label at the size it renders and drops the ticks whose labels would
overlap one already kept.

Reach for `tickSpacing` when labels are uniform (a numeric scale, `12:00`-style times) — it is
cheaper and gives evenly spaced ticks. Reach for `tickOcclusion` when they are not:

:example{ name="tick-occlusion" }

`padding` is the minimum gap required between two kept labels — raise it to thin the axis further
than mere non-overlap would.

#### Why not just lower `tickSpacing`?

Because one budget has to serve every label. Set it wide enough for `Customer Success Operations`
and you throw away ticks next to `HR`; set it for `HR` and the long ones still collide.

:example{ name="tick-occlusion-vs-tick-spacing" }

::note
Band scales default `tickSpacing` to `null` and draw a tick per band, so a category axis with long
names overlaps out of the box. This is the case `tickOcclusion` is most useful for.
::

#### Ticks `tickSpacing` cannot thin at all

An explicit `ticks` array or time interval sets the tick values outright — `tickSpacing` and
`count` have no say (see [time scale (explicit)](#time-scale-explicit)). `tickOcclusion` runs after
the values are chosen, so it is the only way to thin them while keeping the interval you asked for.

:example{ name="tick-occlusion-explicit-ticks" }

#### Priority

Which ticks win when labels compete:

- `'end'` (default) — keep the last tick and walk backwards. The newest value on a time axis.
- `'start'` — keep the first and walk forwards.
- `'start-end'` — anchor both ends of the axis, thin what is between them, so the reader always
sees the range the chart spans.

:example{ name="tick-occlusion-priority" }

#### Rotated labels

Rotation is the other way out of a crowded category axis, and the two compose: labels are measured
after rotation, so an angled label — which needs far less horizontal room — keeps neighbours that a
flat one would push out.

:example{ name="tick-occlusion-rotated" }

#### Options

```svelte
<Axis placement="bottom" tickOcclusion={{ priority: 'start-end', padding: 12 }} />
```

- `priority` — `'end'` (default), `'start'`, or `'start-end'`.
- `padding` — minimum gap, in pixels, required between two kept labels. Defaults to `4`.

::note
The whole tick is dropped, not just its label — its tick mark and grid line go with it, the same way
a lower `tickSpacing` would.
::

::note
Labels are tested against each other, not against the plot bounds. A kept label at either end of the
axis is drawn wherever its tick sits, so reserve horizontal `padding` for it — especially with
`priority: 'start-end'`, which anchors both ends by design.
::

### time scales

With no `format`, tick labels are chosen automatically from the duration between ticks — a domain spanning years is labelled with years, one spanning a minute with seconds. Resizing the chart (or changing `tickSpacing`) changes the tick density, and the labels follow.
Expand Down
88 changes: 83 additions & 5 deletions docs/src/examples/catalog/Area.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,43 @@
}
]
},
{
"name": "fade-mask",
"title": "fade mask",
"path": "/docs/components/Area/fade-mask",
"components": [
{
"component": "Chart",
"lineNumber": 14,
"line": "<Chart {data} x=\"date\" y=\"value\" yDomain={[0, null]} yNice padding={20} height={300}>"
},
{
"component": "Layer",
"lineNumber": 16,
"line": "<Layer>"
},
{
"component": "Axis",
"lineNumber": 17,
"line": "<Axis placement=\"left\" grid rule />"
},
{
"component": "LinearGradient",
"lineNumber": 20,
"line": "<LinearGradient stops={['black', 'white']}>"
},
{
"component": "Rect",
"lineNumber": 24,
"line": "<rect width={context.width} height={context.height} fill={gradient} />"
},
{
"component": "Area",
"lineNumber": 28,
"line": "<Area"
}
]
},
{
"name": "gradient",
"title": "gradient",
Expand Down Expand Up @@ -401,6 +438,33 @@
}
]
},
{
"name": "multiple-series-tweened",
"title": "multiple series tweened",
"path": "/docs/components/Area/multiple-series-tweened",
"components": [
{
"component": "Chart",
"lineNumber": 36,
"line": "<Chart"
},
{
"component": "Layer",
"lineNumber": 47,
"line": "<Layer>"
},
{
"component": "Axis",
"lineNumber": 48,
"line": "<Axis placement=\"left\" grid rule />"
},
{
"component": "Area",
"lineNumber": 51,
"line": "<Area fill=\"fruit\" fillOpacity={0.3} line={{ class: 'stroke-2' }} motion=\"tween\" />"
}
]
},
{
"name": "multiple-series-using-overrides",
"title": "multiple series using overrides",
Expand Down Expand Up @@ -963,6 +1027,13 @@
"lineNumber": 18,
"line": "<Area line={{ class: 'stroke-2 stroke-primary' }} class=\"fill-primary/30\" />"
},
{
"example": "fade-mask",
"component": "Area",
"path": "/docs/components/Area/fade-mask",
"lineNumber": 28,
"line": "<Area"
},
{
"example": "gradient",
"component": "Area",
Expand Down Expand Up @@ -1019,6 +1090,13 @@
"lineNumber": 61,
"line": "<Area {data} fill={color} fillOpacity={0.3} line={{ class: 'stroke-2', stroke: color }} />"
},
{
"example": "multiple-series-tweened",
"component": "Area",
"path": "/docs/components/Area/multiple-series-tweened",
"lineNumber": 51,
"line": "<Area fill=\"fruit\" fillOpacity={0.3} line={{ class: 'stroke-2' }} motion=\"tween\" />"
},
{
"example": "multiple-series-using-overrides",
"component": "Area",
Expand Down Expand Up @@ -1394,14 +1472,14 @@
"example": "synced-brushes",
"component": "BrushContext",
"path": "/docs/components/BrushContext/synced-brushes",
"lineNumber": 74,
"lineNumber": 71,
"line": "<Area line={{ class: 'stroke-2 stroke-(--chart-color)' }} fill={gradient} />"
},
{
"example": "synced-brushes",
"component": "BrushContext",
"path": "/docs/components/BrushContext/synced-brushes",
"lineNumber": 93,
"lineNumber": 90,
"line": "<Area"
},
{
Expand Down Expand Up @@ -1443,14 +1521,14 @@
"example": "synced-brushes",
"component": "ChartGroup",
"path": "/docs/components/ChartGroup/synced-brushes",
"lineNumber": 69,
"lineNumber": 66,
"line": "<Area line={{ class: 'stroke-2 stroke-(--chart-color)' }} fill={gradient} />"
},
{
"example": "synced-brushes",
"component": "ChartGroup",
"path": "/docs/components/ChartGroup/synced-brushes",
"lineNumber": 78,
"lineNumber": 75,
"line": "<Area"
},
{
Expand Down Expand Up @@ -1622,5 +1700,5 @@
"line": "<Area"
}
],
"updatedAt": "2026-08-20T17:01:00.930Z"
"updatedAt": "2026-09-03T20:00:05.673Z"
}
Loading
Loading