diff --git a/.changeset/bright-axis-titles.md b/.changeset/bright-axis-titles.md
new file mode 100644
index 00000000..2274bf4b
--- /dev/null
+++ b/.changeset/bright-axis-titles.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/charts': minor
+---
+
+Allow Cartesian axis titles to configure font size, font weight, fill, and
+opacity while retaining automatic guide layout.
diff --git a/.changeset/focus-ring-options.md b/.changeset/focus-ring-options.md
new file mode 100644
index 00000000..544922df
--- /dev/null
+++ b/.changeset/focus-ring-options.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/charts': minor
+---
+
+Allow definitions and themes to configure the built-in focus ring's radius,
+stroke width, fill, and stroke while preserving the existing boolean behavior
+and defaults.
diff --git a/.changeset/line-cap-options.md b/.changeset/line-cap-options.md
new file mode 100644
index 00000000..2ea20a1a
--- /dev/null
+++ b/.changeset/line-cap-options.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/charts': minor
+---
+
+Add `lineCap` and `lineJoin` options to `lineY` and `lineX`. Both options use
+the renderer-supported `SceneStyle` values and continue to default to `round`.
diff --git a/.changeset/rtl-axis-side-gutter.md b/.changeset/rtl-axis-side-gutter.md
new file mode 100644
index 00000000..21004beb
--- /dev/null
+++ b/.changeset/rtl-axis-side-gutter.md
@@ -0,0 +1,9 @@
+---
+'@tanstack/charts': patch
+---
+
+Reserve the correct gutter for a positioned axis in a right-to-left container.
+A y scale with `side: 'right'` anchored its tick labels for a left-to-right
+inline direction, so an RTL host painted them across the plot instead of beside
+it. Automatic anchors, fallback measurement, Canvas, native rendering, and SVG
+export now agree on logical `start` and `end` anchors for the host direction.
diff --git a/API-FRICTION.md b/API-FRICTION.md
index 5204435b..0bd2ca5d 100644
--- a/API-FRICTION.md
+++ b/API-FRICTION.md
@@ -5,7 +5,7 @@ observed difficulty from examples, production migrations, tests, and agent
evaluations so later API, documentation, and TanStack Intent skill work is
based on evidence.
-Last updated: 2026-08-26
+Last updated: 2026-09-03
## Triage rule
@@ -331,6 +331,10 @@ Each entry records:
| F-292 | Fixed preview paints ignored the selected site theme | Tooling | resolved |
| F-293 | Root scale slots blocked named axes | API | resolved |
| F-294 | Automatic mark renderers imposed shared host plumbing | API | resolved |
+| F-295 | Line marks forced round endpoints | API | resolved |
+| F-296 | Axis titles could not carry authored typography or paint | API | resolved |
+| F-297 | Focus ring paint required generated SVG selectors | API | resolved |
+| F-298 | Physical axis sides depended on logical text direction | API | resolved |
## Findings
@@ -7195,7 +7199,7 @@ Each entry records:
threshold generators, and consumer-called service methods as classified
exceptions.
- Verification: the public callback inventory follows exported types,
- functions, and values into nested package-owned types. It classifies all 778
+ functions, and values into nested package-owned types. It classifies all 799
reachable callable surfaces, including Alpine's external directive protocol,
Vue's nested tooltip slot, live-chart interaction and presentation service
handles, cursor controllers and host sessions, focus-guide resolvers, the
@@ -7228,6 +7232,10 @@ Each entry records:
supplies one stable context shape and all first-party examples use it.
- Follow-up verification: transform regressions cover datum, index, and complete
data; the public callback inventory and root TypeScript pass.
+- Follow-up verification: narrowing the public composable-responsive chart
+ callback exposed it as a separately reachable surface. It is classified as an
+ application callback with one `ChartBuildContext` object, and the full
+ 799-surface inventory passes.
### F-239 — Example keys collapsed distinct source rows
@@ -8505,3 +8513,101 @@ Each entry records:
React Native Metro gates, and framework package checks pass. Bundle boundary
checks keep SVG-only entries free of Canvas and measure the opt-in mixed
representative and React consumers at 35.68 KiB and 41.63 KiB gzip.
+
+### F-295 - Line marks forced round endpoints
+
+- Status: resolved
+- Severity: medium
+- Owner: API
+- Observed in: migrating Rewardo's composed statistics chart from Recharts
+- Friction: `lineY` and `lineX` exposed stroke width, opacity, dash pattern,
+ and curve options, but hard-coded round caps and joins. Reproducing the
+ previous Recharts line, which inherited SVG's butt cap, required replacing
+ or patching rendered output.
+- Decision: expose `lineCap` and `lineJoin` on the shared line options using
+ the values from `SceneStyle`. Preserve round as the default for both options.
+- Verification: public type tests bind both line APIs to the scene renderer's
+ supported values. Scene, SVG, Canvas, motion, export, and React Native tests
+ preserve configured values. The Rewardo chart can use `lineCap: 'butt'` on
+ its existing pink `lineY` mark.
+
+### F-296 - Axis titles could not carry authored typography or paint
+
+- Status: resolved
+- Severity: medium
+- Owner: API
+- Observed in: migrating Rewardo dashboard charts to TanStack Charts on the
+ `experimental-tanstack-charts` branch and issue #93
+- Friction: tick labels accepted font size, weight, and opacity, but
+ `axis.label` accepted only text, offset, and motion. Rewardo's 14-pixel chart
+ typography and foreground treatment therefore required a generated-node CSS
+ override that Canvas and React Native could not honor.
+- Decision: extend the existing axis-label object with `fontSize`,
+ `fontWeight`, `fill`, and `opacity`. Keep the string form and every omitted
+ object field on the existing defaults. Resolve the options into the shared
+ scene label so automatic guide measurement and every renderer use one
+ contract.
+- Verification: scene layout covers configured typography, paint, default
+ compatibility, automatic margins, and shared facet titles. SVG, Canvas,
+ React Native, and motion tests exercise the same public definition. The
+ composed weather catalog case uses independently styled titles on three y
+ axes without renderer callbacks or CSS selectors. The reviewed shared-path
+ delta is 169 minified bytes and 29 gzip bytes for the locked line plus static
+ SVG consumer. A dedicated styled-title fixture adds 0.06 KiB gzip and no
+ retained modules over that consumer.
+
+### F-297 - Focus ring paint required generated SVG selectors
+
+- Status: resolved
+- Severity: medium
+- Owner: API
+- Observed in: Rewardo's TanStack Charts migration and issue #94
+- Friction: the built-in primary-point ring exposed only a boolean definition
+ option. Rewardo had to target `.ts-chart__focus-layer--default circle` to
+ reduce the fixed 5-pixel radius, tying application styling to generated SVG
+ structure and leaving Canvas and React Native without the same control.
+- Decision: extend `focusRing` to accept a reusable `ChartFocusRingOptions`
+ object with radius, stroke width, fill, and stroke on both chart definitions
+ and themes. Preserve `true`, `false`, and every existing default. An omitted
+ stroke keeps each point's resolved series color. An explicitly supplied
+ definition value takes precedence over the theme default. Composed views and
+ facets take that default from their outer host and reject child ownership
+ instead of silently discarding it while rebuilding the shared focus layer.
+- Verification: scene, SVG, Canvas, and React Native tests cover configured
+ geometry and paint, theme defaults, definition override and disable behavior,
+ invalid theme geometry, outer view propagation, rejected child ownership,
+ series-color fallback, keyboard focus, and hidden accessibility presentation.
+ The catalog's pointer-tooltip case uses the object form, and the reference
+ docs record the complete contract. Against the reviewed feature baseline,
+ theme fallback adds 7 minified bytes and 5 gzip bytes to the locked D3 line
+ scene. The isolated facet entry adds 0.09 KiB gzip for explicit child
+ ownership validation.
+
+### F-298 - Physical axis sides depended on logical text direction
+
+- Status: resolved
+- Severity: high
+- Owner: API
+- Observed in: issue #117, with a right-side y axis inside an RTL host
+- Friction: automatic tick-label anchors encoded a physical outward placement
+ with logical `start` and `end` values that change sides under RTL. Browser
+ layout could therefore measure a right-axis label over the plot and reserve
+ no right gutter. Canvas paint, fallback measurement, exported SVG, and React
+ Native also disagreed about whether those values were logical or physical.
+- Decision: keep scene anchors logical, map every automatic physical text
+ placement through the resolved inline direction, and normalize at renderer
+ boundaries that do not support logical anchors. This covers Cartesian axes,
+ crosshair labels, static legends, and polar outside labels. Carry the
+ resolved direction with the scene so direct static SVG rendering preserves
+ the same layout. Explicit authored anchors retain their SVG meaning.
+- Verification: layout tests cover both y-axis sides, both rotated x-axis
+ directions, crosshair labels, categorical and quantitative legends, polar
+ outside labels, explicit anchors, and direct static SVG. Estimator, DOM
+ fallback, Canvas, SVG export, and React Native tests cover their
+ direction-specific boundaries. A Chromium reproduction with the issue's
+ data measures the same 51.24 px right gutter in LTR and RTL, keeps labels 8
+ px beyond the plot, and produces byte-identical LTR and RTL Canvas output.
+ A second Chromium pass confirms that direct static SVG matches the live
+ mount, the crosshair label stays 8 px left of the plot, quantitative legend
+ endpoints stay inside their physical bounds, and polar outside labels stay
+ 8 px beyond both physical edges.
diff --git a/benchmarks/bundle-size/README.md b/benchmarks/bundle-size/README.md
index bb9c0e54..0b9230aa 100644
--- a/benchmarks/bundle-size/README.md
+++ b/benchmarks/bundle-size/README.md
@@ -37,6 +37,10 @@ gzip ceiling. The locked shared-host entries record the reviewed integration
cost, while noninteractive consumers retain only the small scene-compiler
portion of that contract.
+Cartesian axis-title styling is also part of the shared scene contract. Its
+static SVG fixture adds no retained modules over the ordinary line consumer
+and keeps authored title options under a 0.25 KiB incremental gzip ceiling.
+
Continuous viewports and the controlled interaction controller are also part
of the default scene and host contracts. Default static SVG consumes scene
clips and gradients. Their reviewed shared-path cost is recorded in the locked
diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json
index 486ae8f9..615f0c44 100644
--- a/benchmarks/bundle-size/universal-baseline.json
+++ b/benchmarks/bundle-size/universal-baseline.json
@@ -3,44 +3,44 @@
"policy": "Exact minified and gzip output for entries that optional features must not affect. Review every change before updating.",
"bundles": {
"D3-scale line scene": {
- "bytes": 50813,
- "gzip": 19070
+ "bytes": 51668,
+ "gzip": 19321
},
"D3-scale line + static SVG": {
- "bytes": 55544,
- "gzip": 20776
+ "bytes": 56479,
+ "gzip": 21089
},
"Representative marks": {
- "bytes": 75617,
- "gzip": 27582
+ "bytes": 76547,
+ "gzip": 27858
},
"TanStack DOM host": {
- "bytes": 74947,
- "gzip": 26053
+ "bytes": 75837,
+ "gzip": 26325
},
"React adapter": {
- "bytes": 77130,
- "gzip": 26831
+ "bytes": 78017,
+ "gzip": 27096
},
"React line consumer": {
- "bytes": 100766,
- "gzip": 36222
+ "bytes": 101674,
+ "gzip": 36491
},
"Compact-scale line scene": {
- "bytes": 33265,
- "gzip": 11935
+ "bytes": 34121,
+ "gzip": 12168
},
"React compact-scale line consumer": {
- "bytes": 83266,
- "gzip": 29144
+ "bytes": 84176,
+ "gzip": 29414
},
"Custom-scale line scene": {
- "bytes": 31447,
- "gzip": 11193
+ "bytes": 32303,
+ "gzip": 11426
},
"D3 linear-scale line scene": {
- "bytes": 50745,
- "gzip": 19032
+ "bytes": 51600,
+ "gzip": 19283
}
}
}
diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json
index a22a54c1..5c2b6c41 100644
--- a/benchmarks/comparison/bundle-baseline.json
+++ b/benchmarks/comparison/bundle-baseline.json
@@ -1,8 +1,8 @@
{
"schemaVersion": 4,
- "generatedAt": "2026-08-26T21:51:12.832Z",
+ "generatedAt": "2026-09-03T06:14:17.086Z",
"packageVersions": {
- "tanstack": "0.15.0",
+ "tanstack": "0.16.0",
"chartjs": "4.5.1",
"echarts": "6.1.0",
"recharts": "3.10.1",
@@ -11,8 +11,8 @@
"sources": {
"tanstack": {
"kind": "workspace",
- "revision": "3df87d71f0305e5a450c10b66940f76f3e14259a",
- "inputDigest": "sha256:b1016e599e63d05e1647b9d27adabaaed7a38bb44dac8d6ba0fef06b8b8bd38e"
+ "revision": "699b4be656a22ad99618466a3b46b7966e9fb4ff",
+ "inputDigest": "sha256:8c71a2cb2c02481f11c3c6d23a087af327397726f09ec1fb21b9bf394d458f02"
},
"chartjs": {
"kind": "package",
@@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
- "minifiedBytes": 110431,
- "gzipBytes": 39891,
- "brotliBytes": 35325,
- "incrementalGzipBytes": 39891,
- "incrementalBrotliBytes": 35325
+ "minifiedBytes": 111345,
+ "gzipBytes": 40162,
+ "brotliBytes": 35522,
+ "incrementalGzipBytes": 40162,
+ "incrementalBrotliBytes": 35522
},
"tanstack-line-interactive": {
- "minifiedBytes": 115872,
- "gzipBytes": 41648,
- "brotliBytes": 36746,
- "incrementalGzipBytes": 41648,
- "incrementalBrotliBytes": 36746
+ "minifiedBytes": 116908,
+ "gzipBytes": 41958,
+ "brotliBytes": 36977,
+ "incrementalGzipBytes": 41958,
+ "incrementalBrotliBytes": 36977
},
"tanstack-line-advanced": {
- "minifiedBytes": 123062,
- "gzipBytes": 43974,
- "brotliBytes": 38764,
- "incrementalGzipBytes": 43974,
- "incrementalBrotliBytes": 38764
+ "minifiedBytes": 124100,
+ "gzipBytes": 44318,
+ "brotliBytes": 39054,
+ "incrementalGzipBytes": 44318,
+ "incrementalBrotliBytes": 39054
},
"tanstack-bar-basic": {
- "minifiedBytes": 119225,
- "gzipBytes": 43210,
- "brotliBytes": 38168,
- "incrementalGzipBytes": 43210,
- "incrementalBrotliBytes": 38168
+ "minifiedBytes": 120114,
+ "gzipBytes": 43480,
+ "brotliBytes": 38367,
+ "incrementalGzipBytes": 43480,
+ "incrementalBrotliBytes": 38367
},
"tanstack-bar-interactive": {
- "minifiedBytes": 123521,
- "gzipBytes": 44585,
- "brotliBytes": 39223,
- "incrementalGzipBytes": 44585,
- "incrementalBrotliBytes": 39223
+ "minifiedBytes": 124536,
+ "gzipBytes": 44871,
+ "brotliBytes": 39500,
+ "incrementalGzipBytes": 44871,
+ "incrementalBrotliBytes": 39500
},
"tanstack-bar-advanced": {
- "minifiedBytes": 123860,
- "gzipBytes": 44735,
- "brotliBytes": 39349,
- "incrementalGzipBytes": 44735,
- "incrementalBrotliBytes": 39349
+ "minifiedBytes": 124875,
+ "gzipBytes": 45026,
+ "brotliBytes": 39535,
+ "incrementalGzipBytes": 45026,
+ "incrementalBrotliBytes": 39535
},
"tanstack-area-basic": {
- "minifiedBytes": 115640,
- "gzipBytes": 41867,
- "brotliBytes": 37073,
- "incrementalGzipBytes": 41867,
- "incrementalBrotliBytes": 37073
+ "minifiedBytes": 116525,
+ "gzipBytes": 42109,
+ "brotliBytes": 37343,
+ "incrementalGzipBytes": 42109,
+ "incrementalBrotliBytes": 37343
},
"tanstack-area-interactive": {
- "minifiedBytes": 121085,
- "gzipBytes": 43594,
- "brotliBytes": 38493,
- "incrementalGzipBytes": 43594,
- "incrementalBrotliBytes": 38493
+ "minifiedBytes": 122096,
+ "gzipBytes": 43896,
+ "brotliBytes": 38714,
+ "incrementalGzipBytes": 43896,
+ "incrementalBrotliBytes": 38714
},
"tanstack-area-advanced": {
- "minifiedBytes": 128457,
- "gzipBytes": 46057,
- "brotliBytes": 40527,
- "incrementalGzipBytes": 46057,
- "incrementalBrotliBytes": 40527
+ "minifiedBytes": 129471,
+ "gzipBytes": 46367,
+ "brotliBytes": 40792,
+ "incrementalGzipBytes": 46367,
+ "incrementalBrotliBytes": 40792
},
"tanstack-scatter-basic": {
- "minifiedBytes": 111460,
- "gzipBytes": 40266,
- "brotliBytes": 35596,
- "incrementalGzipBytes": 40266,
- "incrementalBrotliBytes": 35596
+ "minifiedBytes": 112351,
+ "gzipBytes": 40554,
+ "brotliBytes": 35815,
+ "incrementalGzipBytes": 40554,
+ "incrementalBrotliBytes": 35815
},
"tanstack-scatter-interactive": {
- "minifiedBytes": 116901,
- "gzipBytes": 42015,
- "brotliBytes": 36976,
- "incrementalGzipBytes": 42015,
- "incrementalBrotliBytes": 36976
+ "minifiedBytes": 117918,
+ "gzipBytes": 42329,
+ "brotliBytes": 37299,
+ "incrementalGzipBytes": 42329,
+ "incrementalBrotliBytes": 37299
},
"tanstack-scatter-advanced": {
- "minifiedBytes": 116917,
- "gzipBytes": 42020,
- "brotliBytes": 37012,
- "incrementalGzipBytes": 42020,
- "incrementalBrotliBytes": 37012
+ "minifiedBytes": 117934,
+ "gzipBytes": 42335,
+ "brotliBytes": 37379,
+ "incrementalGzipBytes": 42335,
+ "incrementalBrotliBytes": 37379
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
diff --git a/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx b/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx
index 319b366e..8d989259 100644
--- a/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx
+++ b/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx
@@ -69,6 +69,11 @@ export function createExampleChart({ revision = 0 }: ChartOptions = {}) {
},
},
{
+ focusRing: {
+ radius: 4,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ },
keyboard: true,
tooltip: { use: tooltip, ...interactiveTooltip },
},
diff --git a/benchmarks/conformance/cases/70-composed-chart/case.json b/benchmarks/conformance/cases/70-composed-chart/case.json
index 62dac152..bf70b2e6 100644
--- a/benchmarks/conformance/cases/70-composed-chart/case.json
+++ b/benchmarks/conformance/cases/70-composed-chart/case.json
@@ -11,8 +11,10 @@
"mixed marks",
"independent y scales",
"left axis and stacked right axes",
+ "styled axis titles",
"daily observations",
"monotone curves",
+ "butt line cap",
"resolved maximum bar thickness"
],
"geometry": [
diff --git a/benchmarks/conformance/cases/70-composed-chart/example.tsx b/benchmarks/conformance/cases/70-composed-chart/example.tsx
index 02187acb..8490d6d8 100644
--- a/benchmarks/conformance/cases/70-composed-chart/example.tsx
+++ b/benchmarks/conformance/cases/70-composed-chart/example.tsx
@@ -41,6 +41,7 @@ export const createExampleChart = (input: ChartOptions) => {
y: 'temp_min',
stroke: '#ff7300',
strokeWidth: 2,
+ lineCap: 'butt',
curve: monotone,
}),
dot(rows, {
@@ -63,7 +64,13 @@ export const createExampleChart = (input: ChartOptions) => {
scale: scaleLinear,
grid: true,
axis: {
- label: 'Temperature (°C)',
+ label: {
+ text: 'Temperature (°C)',
+ fontSize: 12,
+ fontWeight: 700,
+ fill: '#8884d8',
+ opacity: 0.8,
+ },
ticks: { count: 5 },
},
},
@@ -72,7 +79,13 @@ export const createExampleChart = (input: ChartOptions) => {
side: 'right',
scale: scaleLinear,
axis: {
- label: 'Precipitation (mm)',
+ label: {
+ text: 'Precipitation (mm)',
+ fontSize: 12,
+ fontWeight: 700,
+ fill: '#413ea0',
+ opacity: 0.8,
+ },
ticks: { count: 5 },
},
},
@@ -81,7 +94,13 @@ export const createExampleChart = (input: ChartOptions) => {
side: 'right',
scale: scaleLinear,
axis: {
- label: 'Wind (m/s)',
+ label: {
+ text: 'Wind (m/s)',
+ fontSize: 12,
+ fontWeight: 700,
+ fill: '#ef4444',
+ opacity: 0.8,
+ },
ticks: { count: 5 },
},
},
diff --git a/benchmarks/conformance/cases/70-composed-chart/recharts.ts b/benchmarks/conformance/cases/70-composed-chart/recharts.ts
index 24a7d097..71443f44 100644
--- a/benchmarks/conformance/cases/70-composed-chart/recharts.ts
+++ b/benchmarks/conformance/cases/70-composed-chart/recharts.ts
@@ -55,6 +55,10 @@ function chart(input: ConformanceInput) {
value: 'Temperature (°C)',
angle: -90,
position: 'insideLeft',
+ fill: '#8884d8',
+ fontSize: 12,
+ fontWeight: 700,
+ opacity: 0.8,
},
}),
createElement(YAxis, {
@@ -67,6 +71,10 @@ function chart(input: ConformanceInput) {
value: 'Precipitation (mm)',
angle: 90,
position: 'insideRight',
+ fill: '#413ea0',
+ fontSize: 12,
+ fontWeight: 700,
+ opacity: 0.8,
},
}),
createElement(YAxis, {
@@ -79,6 +87,10 @@ function chart(input: ConformanceInput) {
value: 'Wind (m/s)',
angle: 90,
position: 'insideRight',
+ fill: '#ef4444',
+ fontSize: 12,
+ fontWeight: 700,
+ opacity: 0.8,
},
}),
createElement(Area, {
diff --git a/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts b/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts
index ec59bc74..7df727ab 100644
--- a/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts
+++ b/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts
@@ -2,7 +2,7 @@ import { createChartRuntime } from '@tanstack/charts'
import { describe, expect, it } from 'vitest'
import { loadTanStackSources } from '../../native-catalog'
import { createExampleChart } from './tanstack'
-import type { SceneNode, SceneRule } from '@tanstack/charts'
+import type { SceneLabel, SceneNode, SceneRule } from '@tanstack/charts'
import type { ConformanceInput } from '../../types'
const input = {
@@ -67,6 +67,36 @@ describe('native composed-chart bar sizing', () => {
expect(windAxis?.x1).toBeGreaterThan(precipitationAxis?.x1 ?? Infinity)
})
+ it('styles each axis title through the chart definition', () => {
+ const titles = flatten(render(input).nodes).filter(
+ (node): node is SceneLabel =>
+ node.kind === 'label' && node.key.endsWith('-label'),
+ )
+
+ expect(titles).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ text: 'Temperature (°C)',
+ fontSize: 12,
+ fontWeight: 700,
+ style: { fill: '#8884d8', opacity: 0.8 },
+ }),
+ expect.objectContaining({
+ text: 'Precipitation (mm)',
+ fontSize: 12,
+ fontWeight: 700,
+ style: { fill: '#413ea0', opacity: 0.8 },
+ }),
+ expect.objectContaining({
+ text: 'Wind (m/s)',
+ fontSize: 12,
+ fontWeight: 700,
+ style: { fill: '#ef4444', opacity: 0.8 },
+ }),
+ ]),
+ )
+ })
+
it('does not hide responsive bar geometry outside the definition', async () => {
const closure = await loadTanStackSources('70-composed-chart')
const source = closure.files.map((file) => file.source).join('\n')
diff --git a/benchmarks/conformance/catalog-index.json b/benchmarks/conformance/catalog-index.json
index 9d3c84f0..600b1fca 100644
--- a/benchmarks/conformance/catalog-index.json
+++ b/benchmarks/conformance/catalog-index.json
@@ -2392,8 +2392,10 @@
"mixed marks",
"independent y scales",
"left axis and stacked right axes",
+ "styled axis titles",
"daily observations",
"monotone curves",
+ "butt line cap",
"resolved maximum bar thickness"
],
"geometry": [
diff --git a/benchmarks/conformance/previews/01-line-gaps.svg b/benchmarks/conformance/previews/01-line-gaps.svg
index 09d185e1..0b277690 100644
--- a/benchmarks/conformance/previews/01-line-gaps.svg
+++ b/benchmarks/conformance/previews/01-line-gaps.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/02-multi-line-end-labels.svg b/benchmarks/conformance/previews/02-multi-line-end-labels.svg
index 69a8a44a..5dd2a99e 100644
--- a/benchmarks/conformance/previews/02-multi-line-end-labels.svg
+++ b/benchmarks/conformance/previews/02-multi-line-end-labels.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/03-temperature-range-band.svg b/benchmarks/conformance/previews/03-temperature-range-band.svg
index e152affa..e84a3ff6 100644
--- a/benchmarks/conformance/previews/03-temperature-range-band.svg
+++ b/benchmarks/conformance/previews/03-temperature-range-band.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/04-stacked-time-area.svg b/benchmarks/conformance/previews/04-stacked-time-area.svg
index a01a4faa..552259ab 100644
--- a/benchmarks/conformance/previews/04-stacked-time-area.svg
+++ b/benchmarks/conformance/previews/04-stacked-time-area.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/100-radial-bars.svg b/benchmarks/conformance/previews/100-radial-bars.svg
index 92bdbd04..e13f8b20 100644
--- a/benchmarks/conformance/previews/100-radial-bars.svg
+++ b/benchmarks/conformance/previews/100-radial-bars.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/101-sunburst.svg b/benchmarks/conformance/previews/101-sunburst.svg
index d89af500..f8dd8b39 100644
--- a/benchmarks/conformance/previews/101-sunburst.svg
+++ b/benchmarks/conformance/previews/101-sunburst.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/102-world-choropleth.svg b/benchmarks/conformance/previews/102-world-choropleth.svg
index 8b6ebf5f..ec331dbb 100644
--- a/benchmarks/conformance/previews/102-world-choropleth.svg
+++ b/benchmarks/conformance/previews/102-world-choropleth.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/103-bubble-map.svg b/benchmarks/conformance/previews/103-bubble-map.svg
index 8230330b..ecec392a 100644
--- a/benchmarks/conformance/previews/103-bubble-map.svg
+++ b/benchmarks/conformance/previews/103-bubble-map.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/104-orthographic-globe.svg b/benchmarks/conformance/previews/104-orthographic-globe.svg
index 5110c9dc..0b59316a 100644
--- a/benchmarks/conformance/previews/104-orthographic-globe.svg
+++ b/benchmarks/conformance/previews/104-orthographic-globe.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/105-route-map.svg b/benchmarks/conformance/previews/105-route-map.svg
index b55607a0..f2659e3c 100644
--- a/benchmarks/conformance/previews/105-route-map.svg
+++ b/benchmarks/conformance/previews/105-route-map.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/106-polar-line.svg b/benchmarks/conformance/previews/106-polar-line.svg
index cc0d6cde..a04d292b 100644
--- a/benchmarks/conformance/previews/106-polar-line.svg
+++ b/benchmarks/conformance/previews/106-polar-line.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/107-polar-scatter.svg b/benchmarks/conformance/previews/107-polar-scatter.svg
index 8bbdc5eb..5b43b482 100644
--- a/benchmarks/conformance/previews/107-polar-scatter.svg
+++ b/benchmarks/conformance/previews/107-polar-scatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/108-country-choropleth.svg b/benchmarks/conformance/previews/108-country-choropleth.svg
index 984adb5e..878dd044 100644
--- a/benchmarks/conformance/previews/108-country-choropleth.svg
+++ b/benchmarks/conformance/previews/108-country-choropleth.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/109-us-state-choropleth.svg b/benchmarks/conformance/previews/109-us-state-choropleth.svg
index f02d330f..26c6836c 100644
--- a/benchmarks/conformance/previews/109-us-state-choropleth.svg
+++ b/benchmarks/conformance/previews/109-us-state-choropleth.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/110-projection-gallery.svg b/benchmarks/conformance/previews/110-projection-gallery.svg
index 51c29db4..e199d3ab 100644
--- a/benchmarks/conformance/previews/110-projection-gallery.svg
+++ b/benchmarks/conformance/previews/110-projection-gallery.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/111-basic-sankey.svg b/benchmarks/conformance/previews/111-basic-sankey.svg
index c1a83685..f46aebb6 100644
--- a/benchmarks/conformance/previews/111-basic-sankey.svg
+++ b/benchmarks/conformance/previews/111-basic-sankey.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/111-sankey-flow.svg b/benchmarks/conformance/previews/111-sankey-flow.svg
index d94c0a6f..af3f4f75 100644
--- a/benchmarks/conformance/previews/111-sankey-flow.svg
+++ b/benchmarks/conformance/previews/111-sankey-flow.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/112-motion-entrance.svg b/benchmarks/conformance/previews/112-motion-entrance.svg
index aa6c7162..ecf7cae8 100644
--- a/benchmarks/conformance/previews/112-motion-entrance.svg
+++ b/benchmarks/conformance/previews/112-motion-entrance.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/113-motion-updates.svg b/benchmarks/conformance/previews/113-motion-updates.svg
index 41a62652..79bab72b 100644
--- a/benchmarks/conformance/previews/113-motion-updates.svg
+++ b/benchmarks/conformance/previews/113-motion-updates.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/114-spring-line-motion.svg b/benchmarks/conformance/previews/114-spring-line-motion.svg
index 4c5baa64..2c6c64b5 100644
--- a/benchmarks/conformance/previews/114-spring-line-motion.svg
+++ b/benchmarks/conformance/previews/114-spring-line-motion.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/115-definition-motion.svg b/benchmarks/conformance/previews/115-definition-motion.svg
index df184ba6..5ce964fe 100644
--- a/benchmarks/conformance/previews/115-definition-motion.svg
+++ b/benchmarks/conformance/previews/115-definition-motion.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/116-geometry-morph.svg b/benchmarks/conformance/previews/116-geometry-morph.svg
index e9f4d17a..374023f0 100644
--- a/benchmarks/conformance/previews/116-geometry-morph.svg
+++ b/benchmarks/conformance/previews/116-geometry-morph.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/117-focus-cursor-motion.svg b/benchmarks/conformance/previews/117-focus-cursor-motion.svg
index a1cb7d28..8aa55b65 100644
--- a/benchmarks/conformance/previews/117-focus-cursor-motion.svg
+++ b/benchmarks/conformance/previews/117-focus-cursor-motion.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/118-token-usage-calendar.svg b/benchmarks/conformance/previews/118-token-usage-calendar.svg
index eb4533b3..87e237ca 100644
--- a/benchmarks/conformance/previews/118-token-usage-calendar.svg
+++ b/benchmarks/conformance/previews/118-token-usage-calendar.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg b/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg
index 8171243d..f97f95fc 100644
--- a/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg
+++ b/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/120-themed-interactive-area.svg b/benchmarks/conformance/previews/120-themed-interactive-area.svg
index b1abf55d..2fa649e7 100644
--- a/benchmarks/conformance/previews/120-themed-interactive-area.svg
+++ b/benchmarks/conformance/previews/120-themed-interactive-area.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/121-active-bar-dashboard.svg b/benchmarks/conformance/previews/121-active-bar-dashboard.svg
index 56f5afbf..bab9c428 100644
--- a/benchmarks/conformance/previews/121-active-bar-dashboard.svg
+++ b/benchmarks/conformance/previews/121-active-bar-dashboard.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/122-premium-kpi-sparklines.svg b/benchmarks/conformance/previews/122-premium-kpi-sparklines.svg
index e30a6a89..e6c67b30 100644
--- a/benchmarks/conformance/previews/122-premium-kpi-sparklines.svg
+++ b/benchmarks/conformance/previews/122-premium-kpi-sparklines.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/123-active-donut-metric.svg b/benchmarks/conformance/previews/123-active-donut-metric.svg
index c9252528..4e8d231e 100644
--- a/benchmarks/conformance/previews/123-active-donut-metric.svg
+++ b/benchmarks/conformance/previews/123-active-donut-metric.svg
@@ -1 +1 @@
-275Chrome
+275Chrome
diff --git a/benchmarks/conformance/previews/124-theme-palette-matrix.svg b/benchmarks/conformance/previews/124-theme-palette-matrix.svg
index 2708cdb6..bb9252cb 100644
--- a/benchmarks/conformance/previews/124-theme-palette-matrix.svg
+++ b/benchmarks/conformance/previews/124-theme-palette-matrix.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/125-sales-funnel.svg b/benchmarks/conformance/previews/125-sales-funnel.svg
index a45e54b3..581fa500 100644
--- a/benchmarks/conformance/previews/125-sales-funnel.svg
+++ b/benchmarks/conformance/previews/125-sales-funnel.svg
@@ -1 +1 @@
-Visitors · 12.4KLeads · 6.8KQualified · 3.2KProposals · 1.5KClosed · 620
+Visitors · 12.4KLeads · 6.8KQualified · 3.2KProposals · 1.5KClosed · 620
diff --git a/benchmarks/conformance/previews/126-drillable-sunburst.svg b/benchmarks/conformance/previews/126-drillable-sunburst.svg
index 14373832..efcd9349 100644
--- a/benchmarks/conformance/previews/126-drillable-sunburst.svg
+++ b/benchmarks/conformance/previews/126-drillable-sunburst.svg
@@ -1 +1 @@
-Use arrow keys to inspect segments and Enter or Space to drill into a branch. Use the center button to move up.analytics↑ flare
+Use arrow keys to inspect segments and Enter or Space to drill into a branch. Use the center button to move up.analytics↑ flare
diff --git a/benchmarks/conformance/previews/127-shadcn-dashboard.svg b/benchmarks/conformance/previews/127-shadcn-dashboard.svg
index a9e00819..d0737ee8 100644
--- a/benchmarks/conformance/previews/127-shadcn-dashboard.svg
+++ b/benchmarks/conformance/previews/127-shadcn-dashboard.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg b/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg
index 8d1068e1..7ff0113b 100644
--- a/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg
+++ b/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg b/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg
index d35321f1..36d58c5c 100644
--- a/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg
+++ b/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg
@@ -1 +1 @@
-1,125Visitors
+1,125Visitors
diff --git a/benchmarks/conformance/previews/13-interval-timeline.svg b/benchmarks/conformance/previews/13-interval-timeline.svg
index 642cc785..fd8fde51 100644
--- a/benchmarks/conformance/previews/13-interval-timeline.svg
+++ b/benchmarks/conformance/previews/13-interval-timeline.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg b/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg
index b58ae9d0..19c904f7 100644
--- a/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg
+++ b/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/131-shadcn-radial-text.svg b/benchmarks/conformance/previews/131-shadcn-radial-text.svg
index 528c93fa..43116bb4 100644
--- a/benchmarks/conformance/previews/131-shadcn-radial-text.svg
+++ b/benchmarks/conformance/previews/131-shadcn-radial-text.svg
@@ -1 +1 @@
-200Visitors
+200Visitors
diff --git a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
index b2a90b54..e45a795f 100644
--- a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
+++ b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/133-shadcn-area-axes.svg b/benchmarks/conformance/previews/133-shadcn-area-axes.svg
index 9eec3ad9..b01ac019 100644
--- a/benchmarks/conformance/previews/133-shadcn-area-axes.svg
+++ b/benchmarks/conformance/previews/133-shadcn-area-axes.svg
@@ -1 +1 @@
-JanFebMarAprMayJun0300600
+JanFebMarAprMayJun0300600
diff --git a/benchmarks/conformance/previews/134-shadcn-area-default.svg b/benchmarks/conformance/previews/134-shadcn-area-default.svg
index 8a148ac0..d7f18d96 100644
--- a/benchmarks/conformance/previews/134-shadcn-area-default.svg
+++ b/benchmarks/conformance/previews/134-shadcn-area-default.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/135-shadcn-area-gradient.svg b/benchmarks/conformance/previews/135-shadcn-area-gradient.svg
index 86f7fddc..dd199ece 100644
--- a/benchmarks/conformance/previews/135-shadcn-area-gradient.svg
+++ b/benchmarks/conformance/previews/135-shadcn-area-gradient.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/136-shadcn-area-icons.svg b/benchmarks/conformance/previews/136-shadcn-area-icons.svg
index abd86f10..1e680ca6 100644
--- a/benchmarks/conformance/previews/136-shadcn-area-icons.svg
+++ b/benchmarks/conformance/previews/136-shadcn-area-icons.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/137-shadcn-area-interactive.svg b/benchmarks/conformance/previews/137-shadcn-area-interactive.svg
index d104581b..b8b5524e 100644
--- a/benchmarks/conformance/previews/137-shadcn-area-interactive.svg
+++ b/benchmarks/conformance/previews/137-shadcn-area-interactive.svg
@@ -1 +1 @@
-Apr 10May 2May 25Jun 16Jun 29
+Apr 10May 2May 25Jun 16Jun 29
diff --git a/benchmarks/conformance/previews/138-shadcn-area-legend.svg b/benchmarks/conformance/previews/138-shadcn-area-legend.svg
index 7cc1a8d4..78eeea75 100644
--- a/benchmarks/conformance/previews/138-shadcn-area-legend.svg
+++ b/benchmarks/conformance/previews/138-shadcn-area-legend.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/139-shadcn-area-linear.svg b/benchmarks/conformance/previews/139-shadcn-area-linear.svg
index c79da36c..31ecda7d 100644
--- a/benchmarks/conformance/previews/139-shadcn-area-linear.svg
+++ b/benchmarks/conformance/previews/139-shadcn-area-linear.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/14-error-bars.svg b/benchmarks/conformance/previews/14-error-bars.svg
index 501aa3c4..cfaafba8 100644
--- a/benchmarks/conformance/previews/14-error-bars.svg
+++ b/benchmarks/conformance/previews/14-error-bars.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/140-shadcn-area-stacked-expand.svg b/benchmarks/conformance/previews/140-shadcn-area-stacked-expand.svg
index 3de72562..fe37bfb0 100644
--- a/benchmarks/conformance/previews/140-shadcn-area-stacked-expand.svg
+++ b/benchmarks/conformance/previews/140-shadcn-area-stacked-expand.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/141-shadcn-area-stacked.svg b/benchmarks/conformance/previews/141-shadcn-area-stacked.svg
index 707daf99..3716053a 100644
--- a/benchmarks/conformance/previews/141-shadcn-area-stacked.svg
+++ b/benchmarks/conformance/previews/141-shadcn-area-stacked.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/142-shadcn-area-step.svg b/benchmarks/conformance/previews/142-shadcn-area-step.svg
index 59d3e82c..23ba6e78 100644
--- a/benchmarks/conformance/previews/142-shadcn-area-step.svg
+++ b/benchmarks/conformance/previews/142-shadcn-area-step.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/143-shadcn-bar-active.svg b/benchmarks/conformance/previews/143-shadcn-bar-active.svg
index 6cd8f65c..b4efc083 100644
--- a/benchmarks/conformance/previews/143-shadcn-bar-active.svg
+++ b/benchmarks/conformance/previews/143-shadcn-bar-active.svg
@@ -1 +1 @@
-ChromeSafariFirefoxEdgeOther
+ChromeSafariFirefoxEdgeOther
diff --git a/benchmarks/conformance/previews/144-shadcn-bar-default.svg b/benchmarks/conformance/previews/144-shadcn-bar-default.svg
index ce563605..f6f0a55a 100644
--- a/benchmarks/conformance/previews/144-shadcn-bar-default.svg
+++ b/benchmarks/conformance/previews/144-shadcn-bar-default.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/145-shadcn-bar-horizontal.svg b/benchmarks/conformance/previews/145-shadcn-bar-horizontal.svg
index 4c7a9562..b2b8ec64 100644
--- a/benchmarks/conformance/previews/145-shadcn-bar-horizontal.svg
+++ b/benchmarks/conformance/previews/145-shadcn-bar-horizontal.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/146-shadcn-bar-interactive.svg b/benchmarks/conformance/previews/146-shadcn-bar-interactive.svg
index 64f3a14e..5f637eaf 100644
--- a/benchmarks/conformance/previews/146-shadcn-bar-interactive.svg
+++ b/benchmarks/conformance/previews/146-shadcn-bar-interactive.svg
@@ -1 +1 @@
-Apr 1Apr 22May 14Jun 6Jun 29
+Apr 1Apr 22May 14Jun 6Jun 29
diff --git a/benchmarks/conformance/previews/147-shadcn-bar-label-custom.svg b/benchmarks/conformance/previews/147-shadcn-bar-label-custom.svg
index 70fd82ac..858397cf 100644
--- a/benchmarks/conformance/previews/147-shadcn-bar-label-custom.svg
+++ b/benchmarks/conformance/previews/147-shadcn-bar-label-custom.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune18630523773209214
+JanuaryFebruaryMarchAprilMayJune18630523773209214
diff --git a/benchmarks/conformance/previews/148-shadcn-bar-label.svg b/benchmarks/conformance/previews/148-shadcn-bar-label.svg
index 20809761..1d9fe12e 100644
--- a/benchmarks/conformance/previews/148-shadcn-bar-label.svg
+++ b/benchmarks/conformance/previews/148-shadcn-bar-label.svg
@@ -1 +1 @@
-18630523773209214JanFebMarAprMayJun
+18630523773209214JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/149-shadcn-bar-mixed.svg b/benchmarks/conformance/previews/149-shadcn-bar-mixed.svg
index 71dd0fff..17b58a7e 100644
--- a/benchmarks/conformance/previews/149-shadcn-bar-mixed.svg
+++ b/benchmarks/conformance/previews/149-shadcn-bar-mixed.svg
@@ -1 +1 @@
-ChromeSafariFirefoxEdgeOther
+ChromeSafariFirefoxEdgeOther
diff --git a/benchmarks/conformance/previews/15-boxplot.svg b/benchmarks/conformance/previews/15-boxplot.svg
index 1ba5461a..8021b796 100644
--- a/benchmarks/conformance/previews/15-boxplot.svg
+++ b/benchmarks/conformance/previews/15-boxplot.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/150-shadcn-bar-negative.svg b/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
index 8c11bdf0..35cf1e4e 100644
--- a/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
+++ b/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/151-shadcn-bar-stacked.svg b/benchmarks/conformance/previews/151-shadcn-bar-stacked.svg
index 87aa235c..f70969dc 100644
--- a/benchmarks/conformance/previews/151-shadcn-bar-stacked.svg
+++ b/benchmarks/conformance/previews/151-shadcn-bar-stacked.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/152-shadcn-line-default.svg b/benchmarks/conformance/previews/152-shadcn-line-default.svg
index aa0326d7..db79dbdd 100644
--- a/benchmarks/conformance/previews/152-shadcn-line-default.svg
+++ b/benchmarks/conformance/previews/152-shadcn-line-default.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/153-shadcn-line-dots-colors.svg b/benchmarks/conformance/previews/153-shadcn-line-dots-colors.svg
index 140c1111..b9794e01 100644
--- a/benchmarks/conformance/previews/153-shadcn-line-dots-colors.svg
+++ b/benchmarks/conformance/previews/153-shadcn-line-dots-colors.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/154-shadcn-line-dots-custom.svg b/benchmarks/conformance/previews/154-shadcn-line-dots-custom.svg
index d62d119b..61d6a5b7 100644
--- a/benchmarks/conformance/previews/154-shadcn-line-dots-custom.svg
+++ b/benchmarks/conformance/previews/154-shadcn-line-dots-custom.svg
@@ -1 +1 @@
-││││││JanFebMarAprMayJun
+││││││JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/155-shadcn-line-dots.svg b/benchmarks/conformance/previews/155-shadcn-line-dots.svg
index c8fd0ab1..495fc0fb 100644
--- a/benchmarks/conformance/previews/155-shadcn-line-dots.svg
+++ b/benchmarks/conformance/previews/155-shadcn-line-dots.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/156-shadcn-line-interactive.svg b/benchmarks/conformance/previews/156-shadcn-line-interactive.svg
index 754ac2ee..1e40fbd9 100644
--- a/benchmarks/conformance/previews/156-shadcn-line-interactive.svg
+++ b/benchmarks/conformance/previews/156-shadcn-line-interactive.svg
@@ -1 +1 @@
-Apr 10May 2May 25Jun 16Jun 29
+Apr 10May 2May 25Jun 16Jun 29
diff --git a/benchmarks/conformance/previews/157-shadcn-line-label-custom.svg b/benchmarks/conformance/previews/157-shadcn-line-label-custom.svg
index 50676a5a..3723fd5b 100644
--- a/benchmarks/conformance/previews/157-shadcn-line-label-custom.svg
+++ b/benchmarks/conformance/previews/157-shadcn-line-label-custom.svg
@@ -1 +1 @@
-ChromeSafariFirefoxEdgeOther
+ChromeSafariFirefoxEdgeOther
diff --git a/benchmarks/conformance/previews/158-shadcn-line-label.svg b/benchmarks/conformance/previews/158-shadcn-line-label.svg
index b7961974..95dc05ee 100644
--- a/benchmarks/conformance/previews/158-shadcn-line-label.svg
+++ b/benchmarks/conformance/previews/158-shadcn-line-label.svg
@@ -1 +1 @@
-18630523773209214JanFebMarAprMayJun
+18630523773209214JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/159-shadcn-line-linear.svg b/benchmarks/conformance/previews/159-shadcn-line-linear.svg
index 0cf52e54..e14fd236 100644
--- a/benchmarks/conformance/previews/159-shadcn-line-linear.svg
+++ b/benchmarks/conformance/previews/159-shadcn-line-linear.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/16-lollipop.svg b/benchmarks/conformance/previews/16-lollipop.svg
index 761083d9..33f4c778 100644
--- a/benchmarks/conformance/previews/16-lollipop.svg
+++ b/benchmarks/conformance/previews/16-lollipop.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/160-shadcn-line-multiple.svg b/benchmarks/conformance/previews/160-shadcn-line-multiple.svg
index cb7f3b51..eef91b73 100644
--- a/benchmarks/conformance/previews/160-shadcn-line-multiple.svg
+++ b/benchmarks/conformance/previews/160-shadcn-line-multiple.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/161-shadcn-line-step.svg b/benchmarks/conformance/previews/161-shadcn-line-step.svg
index a78a6ec2..b786c746 100644
--- a/benchmarks/conformance/previews/161-shadcn-line-step.svg
+++ b/benchmarks/conformance/previews/161-shadcn-line-step.svg
@@ -1 +1 @@
-JanFebMarAprMayJun
+JanFebMarAprMayJun
diff --git a/benchmarks/conformance/previews/162-shadcn-pie-donut-active.svg b/benchmarks/conformance/previews/162-shadcn-pie-donut-active.svg
index 36bbfc9c..b225f503 100644
--- a/benchmarks/conformance/previews/162-shadcn-pie-donut-active.svg
+++ b/benchmarks/conformance/previews/162-shadcn-pie-donut-active.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/163-shadcn-pie-donut.svg b/benchmarks/conformance/previews/163-shadcn-pie-donut.svg
index d6c0a3a7..600aa145 100644
--- a/benchmarks/conformance/previews/163-shadcn-pie-donut.svg
+++ b/benchmarks/conformance/previews/163-shadcn-pie-donut.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/164-shadcn-pie-interactive.svg b/benchmarks/conformance/previews/164-shadcn-pie-interactive.svg
index 6ef5221d..8dc34fdc 100644
--- a/benchmarks/conformance/previews/164-shadcn-pie-interactive.svg
+++ b/benchmarks/conformance/previews/164-shadcn-pie-interactive.svg
@@ -1 +1 @@
-186Visitors
+186Visitors
diff --git a/benchmarks/conformance/previews/165-shadcn-pie-label-custom.svg b/benchmarks/conformance/previews/165-shadcn-pie-label-custom.svg
index ee171642..284497ec 100644
--- a/benchmarks/conformance/previews/165-shadcn-pie-label-custom.svg
+++ b/benchmarks/conformance/previews/165-shadcn-pie-label-custom.svg
@@ -1 +1 @@
-27520018717390
+27520018717390
diff --git a/benchmarks/conformance/previews/166-shadcn-pie-label-list.svg b/benchmarks/conformance/previews/166-shadcn-pie-label-list.svg
index 5f7504e3..6470c4fb 100644
--- a/benchmarks/conformance/previews/166-shadcn-pie-label-list.svg
+++ b/benchmarks/conformance/previews/166-shadcn-pie-label-list.svg
@@ -1 +1 @@
-ChromeSafariFirefoxEdgeOther
+ChromeSafariFirefoxEdgeOther
diff --git a/benchmarks/conformance/previews/167-shadcn-pie-label.svg b/benchmarks/conformance/previews/167-shadcn-pie-label.svg
index 07f97928..9eceb000 100644
--- a/benchmarks/conformance/previews/167-shadcn-pie-label.svg
+++ b/benchmarks/conformance/previews/167-shadcn-pie-label.svg
@@ -1 +1 @@
-27520018717390
+27520018717390
diff --git a/benchmarks/conformance/previews/168-shadcn-pie-legend.svg b/benchmarks/conformance/previews/168-shadcn-pie-legend.svg
index 615d8f7c..9d1bae84 100644
--- a/benchmarks/conformance/previews/168-shadcn-pie-legend.svg
+++ b/benchmarks/conformance/previews/168-shadcn-pie-legend.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/169-shadcn-pie-separator-none.svg b/benchmarks/conformance/previews/169-shadcn-pie-separator-none.svg
index 8e3aca63..0303f3ce 100644
--- a/benchmarks/conformance/previews/169-shadcn-pie-separator-none.svg
+++ b/benchmarks/conformance/previews/169-shadcn-pie-separator-none.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/17-dumbbell.svg b/benchmarks/conformance/previews/17-dumbbell.svg
index 14d1e610..1db652a9 100644
--- a/benchmarks/conformance/previews/17-dumbbell.svg
+++ b/benchmarks/conformance/previews/17-dumbbell.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/170-shadcn-pie-simple.svg b/benchmarks/conformance/previews/170-shadcn-pie-simple.svg
index 73fdbafb..d946d3e2 100644
--- a/benchmarks/conformance/previews/170-shadcn-pie-simple.svg
+++ b/benchmarks/conformance/previews/170-shadcn-pie-simple.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/171-shadcn-pie-stacked.svg b/benchmarks/conformance/previews/171-shadcn-pie-stacked.svg
index 697bcc9e..1f0d9dd9 100644
--- a/benchmarks/conformance/previews/171-shadcn-pie-stacked.svg
+++ b/benchmarks/conformance/previews/171-shadcn-pie-stacked.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/172-shadcn-radar-default.svg b/benchmarks/conformance/previews/172-shadcn-radar-default.svg
index 41620652..3f3de3b4 100644
--- a/benchmarks/conformance/previews/172-shadcn-radar-default.svg
+++ b/benchmarks/conformance/previews/172-shadcn-radar-default.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/173-shadcn-radar-dots.svg b/benchmarks/conformance/previews/173-shadcn-radar-dots.svg
index 8072d2f4..f47e27a2 100644
--- a/benchmarks/conformance/previews/173-shadcn-radar-dots.svg
+++ b/benchmarks/conformance/previews/173-shadcn-radar-dots.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/174-shadcn-radar-grid-circle-fill.svg b/benchmarks/conformance/previews/174-shadcn-radar-grid-circle-fill.svg
index b5ca4695..eae15b93 100644
--- a/benchmarks/conformance/previews/174-shadcn-radar-grid-circle-fill.svg
+++ b/benchmarks/conformance/previews/174-shadcn-radar-grid-circle-fill.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/175-shadcn-radar-grid-circle-no-lines.svg b/benchmarks/conformance/previews/175-shadcn-radar-grid-circle-no-lines.svg
index e99ab066..0b9405dc 100644
--- a/benchmarks/conformance/previews/175-shadcn-radar-grid-circle-no-lines.svg
+++ b/benchmarks/conformance/previews/175-shadcn-radar-grid-circle-no-lines.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/176-shadcn-radar-grid-circle.svg b/benchmarks/conformance/previews/176-shadcn-radar-grid-circle.svg
index 175ec845..28928799 100644
--- a/benchmarks/conformance/previews/176-shadcn-radar-grid-circle.svg
+++ b/benchmarks/conformance/previews/176-shadcn-radar-grid-circle.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/177-shadcn-radar-grid-custom.svg b/benchmarks/conformance/previews/177-shadcn-radar-grid-custom.svg
index aa745677..f8d1c314 100644
--- a/benchmarks/conformance/previews/177-shadcn-radar-grid-custom.svg
+++ b/benchmarks/conformance/previews/177-shadcn-radar-grid-custom.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/178-shadcn-radar-grid-fill.svg b/benchmarks/conformance/previews/178-shadcn-radar-grid-fill.svg
index 6e77ccd3..6bdfda1f 100644
--- a/benchmarks/conformance/previews/178-shadcn-radar-grid-fill.svg
+++ b/benchmarks/conformance/previews/178-shadcn-radar-grid-fill.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/179-shadcn-radar-grid-none.svg b/benchmarks/conformance/previews/179-shadcn-radar-grid-none.svg
index 1b02c46c..c6d1ce69 100644
--- a/benchmarks/conformance/previews/179-shadcn-radar-grid-none.svg
+++ b/benchmarks/conformance/previews/179-shadcn-radar-grid-none.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/18-cumulative-histogram.svg b/benchmarks/conformance/previews/18-cumulative-histogram.svg
index 2e5d5a4d..ea517bce 100644
--- a/benchmarks/conformance/previews/18-cumulative-histogram.svg
+++ b/benchmarks/conformance/previews/18-cumulative-histogram.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/180-shadcn-radar-icons.svg b/benchmarks/conformance/previews/180-shadcn-radar-icons.svg
index a519a708..69d97e1e 100644
--- a/benchmarks/conformance/previews/180-shadcn-radar-icons.svg
+++ b/benchmarks/conformance/previews/180-shadcn-radar-icons.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg b/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
index 61b2840e..1b372e88 100644
--- a/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
+++ b/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
@@ -1 +1 @@
-186/80305/200237/12073/190209/130214/140JanuaryFebruaryMarchAprilMayJune
+186/80305/200237/12073/190209/130214/140JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/182-shadcn-radar-legend.svg b/benchmarks/conformance/previews/182-shadcn-radar-legend.svg
index 79bf673d..dff34218 100644
--- a/benchmarks/conformance/previews/182-shadcn-radar-legend.svg
+++ b/benchmarks/conformance/previews/182-shadcn-radar-legend.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/183-shadcn-radar-lines-only.svg b/benchmarks/conformance/previews/183-shadcn-radar-lines-only.svg
index 2dd89904..69dc0695 100644
--- a/benchmarks/conformance/previews/183-shadcn-radar-lines-only.svg
+++ b/benchmarks/conformance/previews/183-shadcn-radar-lines-only.svg
@@ -1 +1 @@
-JanuaryFebruaryMarchAprilMayJune
+JanuaryFebruaryMarchAprilMayJune
diff --git a/benchmarks/conformance/previews/184-shadcn-radar-radius.svg b/benchmarks/conformance/previews/184-shadcn-radar-radius.svg
index d16c873e..efb5db24 100644
--- a/benchmarks/conformance/previews/184-shadcn-radar-radius.svg
+++ b/benchmarks/conformance/previews/184-shadcn-radar-radius.svg
@@ -1 +1 @@
-080160240320
+080160240320
diff --git a/benchmarks/conformance/previews/185-shadcn-radial-grid.svg b/benchmarks/conformance/previews/185-shadcn-radial-grid.svg
index 25224b4f..2969dc75 100644
--- a/benchmarks/conformance/previews/185-shadcn-radial-grid.svg
+++ b/benchmarks/conformance/previews/185-shadcn-radial-grid.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/186-shadcn-radial-label.svg b/benchmarks/conformance/previews/186-shadcn-radial-label.svg
index d3f798f6..d7c38e7b 100644
--- a/benchmarks/conformance/previews/186-shadcn-radial-label.svg
+++ b/benchmarks/conformance/previews/186-shadcn-radial-label.svg
@@ -1 +1 @@
-ChromeSafariFirefoxEdgeOther
+ChromeSafariFirefoxEdgeOther
diff --git a/benchmarks/conformance/previews/187-shadcn-radial-shape.svg b/benchmarks/conformance/previews/187-shadcn-radial-shape.svg
index fbaef355..3ae8e020 100644
--- a/benchmarks/conformance/previews/187-shadcn-radial-shape.svg
+++ b/benchmarks/conformance/previews/187-shadcn-radial-shape.svg
@@ -1 +1 @@
-1,260Visitors
+1,260Visitors
diff --git a/benchmarks/conformance/previews/188-shadcn-radial-simple.svg b/benchmarks/conformance/previews/188-shadcn-radial-simple.svg
index 6330e8f6..c71b221a 100644
--- a/benchmarks/conformance/previews/188-shadcn-radial-simple.svg
+++ b/benchmarks/conformance/previews/188-shadcn-radial-simple.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg b/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
index 3b55c3b4..ed2802ba 100644
--- a/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
+++ b/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
@@ -1 +1 @@
-1,830Visitors
+1,830Visitors
diff --git a/benchmarks/conformance/previews/19-moving-average-line.svg b/benchmarks/conformance/previews/19-moving-average-line.svg
index 173510b5..a6eaf171 100644
--- a/benchmarks/conformance/previews/19-moving-average-line.svg
+++ b/benchmarks/conformance/previews/19-moving-average-line.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg b/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
index d6ca753d..a536d1d6 100644
--- a/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
+++ b/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg b/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
index d49e1a52..3f91ac67 100644
--- a/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
+++ b/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg b/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
index 57a41054..92998466 100644
--- a/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
+++ b/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg b/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
index 93ad3553..f3fa5dcf 100644
--- a/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
+++ b/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg b/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
index 9c5dd3e2..24bc2585 100644
--- a/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
+++ b/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg b/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
index 300d7da4..7d9f4e08 100644
--- a/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
+++ b/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg b/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
index 60706447..003322dd 100644
--- a/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
+++ b/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg b/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
index 33e669ff..941b2616 100644
--- a/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
+++ b/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
@@ -1 +1 @@
-SunMonTueWedThuFri
+SunMonTueWedThuFri
diff --git a/benchmarks/conformance/previews/20-normalized-stacked-area.svg b/benchmarks/conformance/previews/20-normalized-stacked-area.svg
index f7f98eb8..9d8c7496 100644
--- a/benchmarks/conformance/previews/20-normalized-stacked-area.svg
+++ b/benchmarks/conformance/previews/20-normalized-stacked-area.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/21-streamgraph.svg b/benchmarks/conformance/previews/21-streamgraph.svg
index 5cc6f0db..6acd4a45 100644
--- a/benchmarks/conformance/previews/21-streamgraph.svg
+++ b/benchmarks/conformance/previews/21-streamgraph.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/22-bollinger-band.svg b/benchmarks/conformance/previews/22-bollinger-band.svg
index 00564eba..f2d3e256 100644
--- a/benchmarks/conformance/previews/22-bollinger-band.svg
+++ b/benchmarks/conformance/previews/22-bollinger-band.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/24-quantitative-binned-heatmap.svg b/benchmarks/conformance/previews/24-quantitative-binned-heatmap.svg
index 5063cf65..84afc106 100644
--- a/benchmarks/conformance/previews/24-quantitative-binned-heatmap.svg
+++ b/benchmarks/conformance/previews/24-quantitative-binned-heatmap.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/25-calendar-heatmap.svg b/benchmarks/conformance/previews/25-calendar-heatmap.svg
index b273fb56..0111e7df 100644
--- a/benchmarks/conformance/previews/25-calendar-heatmap.svg
+++ b/benchmarks/conformance/previews/25-calendar-heatmap.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/26-diverging-likert.svg b/benchmarks/conformance/previews/26-diverging-likert.svg
index f2c7f6ac..84a44777 100644
--- a/benchmarks/conformance/previews/26-diverging-likert.svg
+++ b/benchmarks/conformance/previews/26-diverging-likert.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/27-parallel-coordinates.svg b/benchmarks/conformance/previews/27-parallel-coordinates.svg
index 4317680f..11d104c9 100644
--- a/benchmarks/conformance/previews/27-parallel-coordinates.svg
+++ b/benchmarks/conformance/previews/27-parallel-coordinates.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/28-candlestick.svg b/benchmarks/conformance/previews/28-candlestick.svg
index d19af45f..eb92030f 100644
--- a/benchmarks/conformance/previews/28-candlestick.svg
+++ b/benchmarks/conformance/previews/28-candlestick.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/29-waterfall.svg b/benchmarks/conformance/previews/29-waterfall.svg
index ce264e98..eb1f6659 100644
--- a/benchmarks/conformance/previews/29-waterfall.svg
+++ b/benchmarks/conformance/previews/29-waterfall.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/30-slopegraph.svg b/benchmarks/conformance/previews/30-slopegraph.svg
index 1b8da897..7ab12b9b 100644
--- a/benchmarks/conformance/previews/30-slopegraph.svg
+++ b/benchmarks/conformance/previews/30-slopegraph.svg
@@ -1 +1 @@
-New YorkLos AngelesChicagoDallasHoustonPhiladelphiaWashington, D.C.Miami
+New YorkLos AngelesChicagoDallasHoustonPhiladelphiaWashington, D.C.Miami
diff --git a/benchmarks/conformance/previews/31-linear-regression.svg b/benchmarks/conformance/previews/31-linear-regression.svg
index 0b736559..6396f1c3 100644
--- a/benchmarks/conformance/previews/31-linear-regression.svg
+++ b/benchmarks/conformance/previews/31-linear-regression.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/32-change-arrows.svg b/benchmarks/conformance/previews/32-change-arrows.svg
index ee79e300..c231c013 100644
--- a/benchmarks/conformance/previews/32-change-arrows.svg
+++ b/benchmarks/conformance/previews/32-change-arrows.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/33-difference-chart.svg b/benchmarks/conformance/previews/33-difference-chart.svg
index fbd62ccf..108051b3 100644
--- a/benchmarks/conformance/previews/33-difference-chart.svg
+++ b/benchmarks/conformance/previews/33-difference-chart.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/34-pointer-tooltip.svg b/benchmarks/conformance/previews/34-pointer-tooltip.svg
index fdb6d046..74411cad 100644
--- a/benchmarks/conformance/previews/34-pointer-tooltip.svg
+++ b/benchmarks/conformance/previews/34-pointer-tooltip.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/35-grouped-tooltip.svg b/benchmarks/conformance/previews/35-grouped-tooltip.svg
index 7cedf5a4..17246717 100644
--- a/benchmarks/conformance/previews/35-grouped-tooltip.svg
+++ b/benchmarks/conformance/previews/35-grouped-tooltip.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/36-hierarchy-tree.svg b/benchmarks/conformance/previews/36-hierarchy-tree.svg
index 3cb7b53a..2aa27944 100644
--- a/benchmarks/conformance/previews/36-hierarchy-tree.svg
+++ b/benchmarks/conformance/previews/36-hierarchy-tree.svg
@@ -1 +1 @@
-analyticsclustergraphAgglomerativeClusterCommunityStructureHierarchicalClusterMergeEdgeBetweennessCentralityLinkDistanceMaxFlowMinCut
+analyticsclustergraphAgglomerativeClusterCommunityStructureHierarchicalClusterMergeEdgeBetweennessCentralityLinkDistanceMaxFlowMinCut
diff --git a/benchmarks/conformance/previews/37-delaunay-network.svg b/benchmarks/conformance/previews/37-delaunay-network.svg
index 9307d34c..c4498bda 100644
--- a/benchmarks/conformance/previews/37-delaunay-network.svg
+++ b/benchmarks/conformance/previews/37-delaunay-network.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/38-contour-topography.svg b/benchmarks/conformance/previews/38-contour-topography.svg
index 43478055..09760f49 100644
--- a/benchmarks/conformance/previews/38-contour-topography.svg
+++ b/benchmarks/conformance/previews/38-contour-topography.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/39-density-contours.svg b/benchmarks/conformance/previews/39-density-contours.svg
index 0b6dd171..c470a6d3 100644
--- a/benchmarks/conformance/previews/39-density-contours.svg
+++ b/benchmarks/conformance/previews/39-density-contours.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/40-force-directed-network.svg b/benchmarks/conformance/previews/40-force-directed-network.svg
index ff1859f7..695d112b 100644
--- a/benchmarks/conformance/previews/40-force-directed-network.svg
+++ b/benchmarks/conformance/previews/40-force-directed-network.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/40-geojson-map.svg b/benchmarks/conformance/previews/40-geojson-map.svg
index ce7843f7..ab88cd84 100644
--- a/benchmarks/conformance/previews/40-geojson-map.svg
+++ b/benchmarks/conformance/previews/40-geojson-map.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/41-waffle-unit-chart.svg b/benchmarks/conformance/previews/41-waffle-unit-chart.svg
index c14125d7..c3ec844b 100644
--- a/benchmarks/conformance/previews/41-waffle-unit-chart.svg
+++ b/benchmarks/conformance/previews/41-waffle-unit-chart.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/42-vector-field.svg b/benchmarks/conformance/previews/42-vector-field.svg
index 763693cb..70a0adde 100644
--- a/benchmarks/conformance/previews/42-vector-field.svg
+++ b/benchmarks/conformance/previews/42-vector-field.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/43-hexbin-density.svg b/benchmarks/conformance/previews/43-hexbin-density.svg
index 62172df3..3c71c8b4 100644
--- a/benchmarks/conformance/previews/43-hexbin-density.svg
+++ b/benchmarks/conformance/previews/43-hexbin-density.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/44-framed-scatter.svg b/benchmarks/conformance/previews/44-framed-scatter.svg
index e3b8d164..a7d80f7c 100644
--- a/benchmarks/conformance/previews/44-framed-scatter.svg
+++ b/benchmarks/conformance/previews/44-framed-scatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/50-empirical-cdf.svg b/benchmarks/conformance/previews/50-empirical-cdf.svg
index e0781c0c..0ead6a7c 100644
--- a/benchmarks/conformance/previews/50-empirical-cdf.svg
+++ b/benchmarks/conformance/previews/50-empirical-cdf.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/51-faceted-distributions.svg b/benchmarks/conformance/previews/51-faceted-distributions.svg
index 4b9e0277..75c0057b 100644
--- a/benchmarks/conformance/previews/51-faceted-distributions.svg
+++ b/benchmarks/conformance/previews/51-faceted-distributions.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/52-beeswarm-dodge.svg b/benchmarks/conformance/previews/52-beeswarm-dodge.svg
index 03ba53fe..4f26b515 100644
--- a/benchmarks/conformance/previews/52-beeswarm-dodge.svg
+++ b/benchmarks/conformance/previews/52-beeswarm-dodge.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/53-log-scale-scatter.svg b/benchmarks/conformance/previews/53-log-scale-scatter.svg
index 8f5b8ace..91e72fcd 100644
--- a/benchmarks/conformance/previews/53-log-scale-scatter.svg
+++ b/benchmarks/conformance/previews/53-log-scale-scatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/54-bump-ranking.svg b/benchmarks/conformance/previews/54-bump-ranking.svg
index 132167e7..75f78d09 100644
--- a/benchmarks/conformance/previews/54-bump-ranking.svg
+++ b/benchmarks/conformance/previews/54-bump-ranking.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/55-indexed-multi-line.svg b/benchmarks/conformance/previews/55-indexed-multi-line.svg
index d548451c..d9329911 100644
--- a/benchmarks/conformance/previews/55-indexed-multi-line.svg
+++ b/benchmarks/conformance/previews/55-indexed-multi-line.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/56-connected-scatter.svg b/benchmarks/conformance/previews/56-connected-scatter.svg
index 942bb57c..de6b62a9 100644
--- a/benchmarks/conformance/previews/56-connected-scatter.svg
+++ b/benchmarks/conformance/previews/56-connected-scatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg b/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg
index d79c6f92..293aa0c4 100644
--- a/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg
+++ b/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/58-select-extrema.svg b/benchmarks/conformance/previews/58-select-extrema.svg
index 1e89e658..172b3679 100644
--- a/benchmarks/conformance/previews/58-select-extrema.svg
+++ b/benchmarks/conformance/previews/58-select-extrema.svg
@@ -1 +1 @@
-Low $56.25High $190.04
+Low $56.25High $190.04
diff --git a/benchmarks/conformance/previews/59-grouped-reducer-bars.svg b/benchmarks/conformance/previews/59-grouped-reducer-bars.svg
index 3c70e8a0..9d7fd668 100644
--- a/benchmarks/conformance/previews/59-grouped-reducer-bars.svg
+++ b/benchmarks/conformance/previews/59-grouped-reducer-bars.svg
@@ -1 +1 @@
-3,700.6623,733.0885,076.016
+3,700.6623,733.0885,076.016
diff --git a/benchmarks/conformance/previews/60-lag-autocorrelation.svg b/benchmarks/conformance/previews/60-lag-autocorrelation.svg
index 943ca1b9..8029d2de 100644
--- a/benchmarks/conformance/previews/60-lag-autocorrelation.svg
+++ b/benchmarks/conformance/previews/60-lag-autocorrelation.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/61-quantile-ribbon.svg b/benchmarks/conformance/previews/61-quantile-ribbon.svg
index d171a68c..769aa51d 100644
--- a/benchmarks/conformance/previews/61-quantile-ribbon.svg
+++ b/benchmarks/conformance/previews/61-quantile-ribbon.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/62-ridgeline-density.svg b/benchmarks/conformance/previews/62-ridgeline-density.svg
index d36e2a00..090b6624 100644
--- a/benchmarks/conformance/previews/62-ridgeline-density.svg
+++ b/benchmarks/conformance/previews/62-ridgeline-density.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/63-violin-distributions.svg b/benchmarks/conformance/previews/63-violin-distributions.svg
index f1fb64af..5a1bc4a0 100644
--- a/benchmarks/conformance/previews/63-violin-distributions.svg
+++ b/benchmarks/conformance/previews/63-violin-distributions.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/64-marimekko-mosaic.svg b/benchmarks/conformance/previews/64-marimekko-mosaic.svg
index c328a8a1..24fb707c 100644
--- a/benchmarks/conformance/previews/64-marimekko-mosaic.svg
+++ b/benchmarks/conformance/previews/64-marimekko-mosaic.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/65-voronoi-nearest-tooltip.svg b/benchmarks/conformance/previews/65-voronoi-nearest-tooltip.svg
index 6efb7a1b..5545bf7a 100644
--- a/benchmarks/conformance/previews/65-voronoi-nearest-tooltip.svg
+++ b/benchmarks/conformance/previews/65-voronoi-nearest-tooltip.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/70-composed-chart.svg b/benchmarks/conformance/previews/70-composed-chart.svg
index 5de7df71..ed1c28c0 100644
--- a/benchmarks/conformance/previews/70-composed-chart.svg
+++ b/benchmarks/conformance/previews/70-composed-chart.svg
@@ -1 +1 @@
-Feb 7Feb 10Feb 1202468101214Temperature (°C)0.00.51.01.52.02.5Precipitation (mm)2345Wind (m/s)
+Feb 7Feb 10Feb 1202468101214Temperature (°C)0.00.51.01.52.02.5Precipitation (mm)2345Wind (m/s)
diff --git a/benchmarks/conformance/previews/71-recharts-population-pyramid.svg b/benchmarks/conformance/previews/71-recharts-population-pyramid.svg
index 73ea2f80..56f637f1 100644
--- a/benchmarks/conformance/previews/71-recharts-population-pyramid.svg
+++ b/benchmarks/conformance/previews/71-recharts-population-pyramid.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/72-recharts-mixed-bars.svg b/benchmarks/conformance/previews/72-recharts-mixed-bars.svg
index 967cf0f0..abc47d53 100644
--- a/benchmarks/conformance/previews/72-recharts-mixed-bars.svg
+++ b/benchmarks/conformance/previews/72-recharts-mixed-bars.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/73-many-point-scatter.svg b/benchmarks/conformance/previews/73-many-point-scatter.svg
index bd7aa70d..8e213d15 100644
--- a/benchmarks/conformance/previews/73-many-point-scatter.svg
+++ b/benchmarks/conformance/previews/73-many-point-scatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/74-recharts-treemap.svg b/benchmarks/conformance/previews/74-recharts-treemap.svg
index b1703830..2e6af9ae 100644
--- a/benchmarks/conformance/previews/74-recharts-treemap.svg
+++ b/benchmarks/conformance/previews/74-recharts-treemap.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/75-radar.svg b/benchmarks/conformance/previews/75-radar.svg
index ff56685b..45e62a8a 100644
--- a/benchmarks/conformance/previews/75-radar.svg
+++ b/benchmarks/conformance/previews/75-radar.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/76-pie.svg b/benchmarks/conformance/previews/76-pie.svg
index a533261e..129bcf7f 100644
--- a/benchmarks/conformance/previews/76-pie.svg
+++ b/benchmarks/conformance/previews/76-pie.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/77-donut.svg b/benchmarks/conformance/previews/77-donut.svg
index aa723867..ad52e085 100644
--- a/benchmarks/conformance/previews/77-donut.svg
+++ b/benchmarks/conformance/previews/77-donut.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/78-gauge.svg b/benchmarks/conformance/previews/78-gauge.svg
index d7ab0e53..e4c753a0 100644
--- a/benchmarks/conformance/previews/78-gauge.svg
+++ b/benchmarks/conformance/previews/78-gauge.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/80-echarts-axis-pointer.svg b/benchmarks/conformance/previews/80-echarts-axis-pointer.svg
index 7b7eab29..b32f7b3c 100644
--- a/benchmarks/conformance/previews/80-echarts-axis-pointer.svg
+++ b/benchmarks/conformance/previews/80-echarts-axis-pointer.svg
@@ -1 +1 @@
-JanAprJul4006008001,000
+JanAprJul4006008001,000
diff --git a/benchmarks/conformance/previews/81-recharts-interactive-legend.svg b/benchmarks/conformance/previews/81-recharts-interactive-legend.svg
index 376082d6..d356b212 100644
--- a/benchmarks/conformance/previews/81-recharts-interactive-legend.svg
+++ b/benchmarks/conformance/previews/81-recharts-interactive-legend.svg
@@ -1 +1 @@
-SeriesManufacturingConstruction
+SeriesManufacturingConstruction
diff --git a/benchmarks/conformance/previews/82-chart-table-selection.svg b/benchmarks/conformance/previews/82-chart-table-selection.svg
index fb7aa686..b120bbd2 100644
--- a/benchmarks/conformance/previews/82-chart-table-selection.svg
+++ b/benchmarks/conformance/previews/82-chart-table-selection.svg
@@ -1 +1 @@
-Use arrow keys to move between observations and Enter or Space to select one. The table below offers the same selections.
+Use arrow keys to move between observations and Enter or Space to select one. The table below offers the same selections.
diff --git a/benchmarks/conformance/previews/83-focus-context-window.svg b/benchmarks/conformance/previews/83-focus-context-window.svg
index c847dfa6..cf534bf1 100644
--- a/benchmarks/conformance/previews/83-focus-context-window.svg
+++ b/benchmarks/conformance/previews/83-focus-context-window.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/84-pinned-nested-chart-tooltip.svg b/benchmarks/conformance/previews/84-pinned-nested-chart-tooltip.svg
index 9bf5e4c1..49c0014e 100644
--- a/benchmarks/conformance/previews/84-pinned-nested-chart-tooltip.svg
+++ b/benchmarks/conformance/previews/84-pinned-nested-chart-tooltip.svg
@@ -1 +1 @@
-A gray area tracks monthly electricity consumption. Stacked gold bars show solar energy used on site and exported.
+A gray area tracks monthly electricity consumption. Stacked gold bars show solar energy used on site and exported.
diff --git a/benchmarks/conformance/previews/85-scrollable-resource-lanes.svg b/benchmarks/conformance/previews/85-scrollable-resource-lanes.svg
index 6030d76d..0d35394b 100644
--- a/benchmarks/conformance/previews/85-scrollable-resource-lanes.svg
+++ b/benchmarks/conformance/previews/85-scrollable-resource-lanes.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/86-streaming-window-preservation.svg b/benchmarks/conformance/previews/86-streaming-window-preservation.svg
index ed99c241..8938c2f4 100644
--- a/benchmarks/conformance/previews/86-streaming-window-preservation.svg
+++ b/benchmarks/conformance/previews/86-streaming-window-preservation.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/87-echarts-synchronized-cursors.svg b/benchmarks/conformance/previews/87-echarts-synchronized-cursors.svg
index 58f9b59f..05582764 100644
--- a/benchmarks/conformance/previews/87-echarts-synchronized-cursors.svg
+++ b/benchmarks/conformance/previews/87-echarts-synchronized-cursors.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/88-echarts-free-cursor.svg b/benchmarks/conformance/previews/88-echarts-free-cursor.svg
index 06e8dd1c..feb6bdaa 100644
--- a/benchmarks/conformance/previews/88-echarts-free-cursor.svg
+++ b/benchmarks/conformance/previews/88-echarts-free-cursor.svg
@@ -1 +1 @@
-HP 101.8MPG 20.8
+HP 101.8MPG 20.8
diff --git a/benchmarks/conformance/previews/89-brush-range-selection.svg b/benchmarks/conformance/previews/89-brush-range-selection.svg
index 7526f0f9..2167d61c 100644
--- a/benchmarks/conformance/previews/89-brush-range-selection.svg
+++ b/benchmarks/conformance/previews/89-brush-range-selection.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/90-zoomable-time-window.svg b/benchmarks/conformance/previews/90-zoomable-time-window.svg
index 405e40a1..c7439934 100644
--- a/benchmarks/conformance/previews/90-zoomable-time-window.svg
+++ b/benchmarks/conformance/previews/90-zoomable-time-window.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/91-timeline-playback-scrubber.svg b/benchmarks/conformance/previews/91-timeline-playback-scrubber.svg
index 0899ae28..602887aa 100644
--- a/benchmarks/conformance/previews/91-timeline-playback-scrubber.svg
+++ b/benchmarks/conformance/previews/91-timeline-playback-scrubber.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/92-editable-event-range.svg b/benchmarks/conformance/previews/92-editable-event-range.svg
index 2fec41b8..4e5aaaea 100644
--- a/benchmarks/conformance/previews/92-editable-event-range.svg
+++ b/benchmarks/conformance/previews/92-editable-event-range.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/93-labeled-pie.svg b/benchmarks/conformance/previews/93-labeled-pie.svg
index 116e099f..f405620c 100644
--- a/benchmarks/conformance/previews/93-labeled-pie.svg
+++ b/benchmarks/conformance/previews/93-labeled-pie.svg
@@ -1 +1 @@
-ETAO
+ETAO
diff --git a/benchmarks/conformance/previews/94-center-donut.svg b/benchmarks/conformance/previews/94-center-donut.svg
index 4966358e..a0495747 100644
--- a/benchmarks/conformance/previews/94-center-donut.svg
+++ b/benchmarks/conformance/previews/94-center-donut.svg
@@ -1 +1 @@
-29.9%
+29.9%
diff --git a/benchmarks/conformance/previews/95-rounded-donut.svg b/benchmarks/conformance/previews/95-rounded-donut.svg
index ee7ce4f8..b5412e87 100644
--- a/benchmarks/conformance/previews/95-rounded-donut.svg
+++ b/benchmarks/conformance/previews/95-rounded-donut.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/96-nested-donut.svg b/benchmarks/conformance/previews/96-nested-donut.svg
index d06987f2..ff7e77d0 100644
--- a/benchmarks/conformance/previews/96-nested-donut.svg
+++ b/benchmarks/conformance/previews/96-nested-donut.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/97-rose.svg b/benchmarks/conformance/previews/97-rose.svg
index 253ca956..d9a93edc 100644
--- a/benchmarks/conformance/previews/97-rose.svg
+++ b/benchmarks/conformance/previews/97-rose.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/98-needle-gauge.svg b/benchmarks/conformance/previews/98-needle-gauge.svg
index 02f91353..98a99d8c 100644
--- a/benchmarks/conformance/previews/98-needle-gauge.svg
+++ b/benchmarks/conformance/previews/98-needle-gauge.svg
@@ -1 +1 @@
-5.1%
+5.1%
diff --git a/benchmarks/conformance/previews/99-comparative-radar.svg b/benchmarks/conformance/previews/99-comparative-radar.svg
index f856962b..3d6290d4 100644
--- a/benchmarks/conformance/previews/99-comparative-radar.svg
+++ b/benchmarks/conformance/previews/99-comparative-radar.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/bar-grouped.svg b/benchmarks/conformance/previews/bar-grouped.svg
index d36e3237..e948e827 100644
--- a/benchmarks/conformance/previews/bar-grouped.svg
+++ b/benchmarks/conformance/previews/bar-grouped.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/bar-horizontal-ranking.svg b/benchmarks/conformance/previews/bar-horizontal-ranking.svg
index a3f2b90a..1847131c 100644
--- a/benchmarks/conformance/previews/bar-horizontal-ranking.svg
+++ b/benchmarks/conformance/previews/bar-horizontal-ranking.svg
@@ -1 +1 @@
-new york-newark-jersey city, ny-nj-palos angeles-long beach-anaheim, cachicago-naperville-elgin, il-in-widallas-fort worth-arlington, txhouston-the woodlands-sugar land, txphiladelphia, pawashington-arlington-alexandria, dc-va-mmiami, fl
+new york-newark-jersey city, ny-nj-palos angeles-long beach-anaheim, cachicago-naperville-elgin, il-in-widallas-fort worth-arlington, txhouston-the woodlands-sugar land, txphiladelphia, pawashington-arlington-alexandria, dc-va-mmiami, fl
diff --git a/benchmarks/conformance/previews/bar-stacked.svg b/benchmarks/conformance/previews/bar-stacked.svg
index 36b101fe..87947f07 100644
--- a/benchmarks/conformance/previews/bar-stacked.svg
+++ b/benchmarks/conformance/previews/bar-stacked.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/bar-vertical-sorted.svg b/benchmarks/conformance/previews/bar-vertical-sorted.svg
index 8c737dbc..850599e5 100644
--- a/benchmarks/conformance/previews/bar-vertical-sorted.svg
+++ b/benchmarks/conformance/previews/bar-vertical-sorted.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/facets-anscombe.svg b/benchmarks/conformance/previews/facets-anscombe.svg
index 2b3fd3ed..244514b3 100644
--- a/benchmarks/conformance/previews/facets-anscombe.svg
+++ b/benchmarks/conformance/previews/facets-anscombe.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/heatmap-labeled.svg b/benchmarks/conformance/previews/heatmap-labeled.svg
index 11478b36..84afb2f9 100644
--- a/benchmarks/conformance/previews/heatmap-labeled.svg
+++ b/benchmarks/conformance/previews/heatmap-labeled.svg
@@ -1 +1 @@
-8.28.18.07.78.77.78.48.27.98.38.27.57.78.27.87.87.68.68.28.58.27.87.88.18.48.19.07.5
+8.28.18.07.78.77.78.48.27.98.38.27.57.78.27.87.87.68.68.28.58.27.87.88.18.48.19.07.5
diff --git a/benchmarks/conformance/previews/histogram.svg b/benchmarks/conformance/previews/histogram.svg
index 7c86d4a5..d0ebc02d 100644
--- a/benchmarks/conformance/previews/histogram.svg
+++ b/benchmarks/conformance/previews/histogram.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json
index e1176205..cf5ab427 100644
--- a/benchmarks/conformance/previews/manifest.json
+++ b/benchmarks/conformance/previews/manifest.json
@@ -2,947 +2,947 @@
"schemaVersion": 1,
"width": 288,
"height": 192,
- "sourceHash": "d3177faf65fca2414b431c195a003cf9afdceb8ae2f1b37e4f8cb81578dc8293",
+ "sourceHash": "4867bae8ac8f2c7eca12d00da48db7acef44832dede91c418498939e2d54b9aa",
"assets": [
{
"id": "01-line-gaps",
- "sha256": "78a0c5fd96c90b3b049f23b0a91f1f04d7636527e07a1f888328e69acc603e5b",
- "bytes": 14974
+ "sha256": "361a4da4acdad9db8af094efff5e4b6f7eb09e062333cc1b02f6c0ca26116716",
+ "bytes": 14990
},
{
"id": "02-multi-line-end-labels",
- "sha256": "d665593cb9a586fb10b68c1fdf1e011181cf2a069d8e6153ea26befade263520",
- "bytes": 4539
+ "sha256": "04b1ef477bd9b892f2b2984bb9cca41e72f682239fdfcfb1bc0cb13c8af62830",
+ "bytes": 4555
},
{
"id": "03-temperature-range-band",
- "sha256": "c163ecbd39d1451b0e302d7c1ba2f59c81483ce0199e05023606c09027068408",
- "bytes": 40075
+ "sha256": "4cf49d2cef353b96b40a9bd59a54134087973ce3d19549a0f9309ab197ffbe3d",
+ "bytes": 40091
},
{
"id": "04-stacked-time-area",
- "sha256": "3a105d8307cb13254d8b434bd712ba50731b5cf765f6580df2cb6a328abf5f52",
- "bytes": 49211
+ "sha256": "583910cb57cbe96e1d49c60e0d0398cdbad81dc7f75a2c2b0e281bca4b14a72c",
+ "bytes": 49227
},
{
"id": "bar-vertical-sorted",
- "sha256": "531a961de019b487765fa061ad31be5c040c6b7ab408356e6707a03befc2cd6f",
- "bytes": 6005
+ "sha256": "868c1e9ccd7cd6f6d55cdf33298666b548eb74710e85742e06a7b37d58038c1d",
+ "bytes": 6021
},
{
"id": "bar-horizontal-ranking",
- "sha256": "062085685118d5154e0b5ab6eee1d695347e63871a5f82e2302e86121ccacc8c",
- "bytes": 6734
+ "sha256": "7c903173f3eee66bd2b3d0f3fc1bd89ad35c59a01bc8a409194ed5647b6a076a",
+ "bytes": 6750
},
{
"id": "bar-grouped",
- "sha256": "29499157e163b7068b1f6b2c47b9e947014ffff695eb94e4ab8b3376e3efdb3b",
- "bytes": 2614
+ "sha256": "05977e545ebc1300878165c162ae20183b5d7d02d59b1af26acdb2e7d8f6bd47",
+ "bytes": 2630
},
{
"id": "bar-stacked",
- "sha256": "10107c15a3cfccc8f4c3a45941737769dd6e9bec3b21bb7d0c31ea10240831ea",
- "bytes": 16464
+ "sha256": "b8913d36eb2b6adf857b5554b08fa4f7ed5d55c174b1a182c5ae95086c251d38",
+ "bytes": 16480
},
{
"id": "scatter-bubble",
- "sha256": "18409ba00bd56ba9b180f0eebcc495803c9a3f01cd27309ec5b7164fd9cdf4ec",
- "bytes": 80411
+ "sha256": "878a6c97b7fa40262131822a62cac0b59b5a9ce2a2d50ba0fd03d59d741f3e57",
+ "bytes": 80427
},
{
"id": "histogram",
- "sha256": "2336fc4104a73940374fb743396b69383700a1fe2b8ccca99dc3b4ea1419ce43",
- "bytes": 3315
+ "sha256": "4970bc0a6ae9a2e326a871035dfa6c761b57fe7da30fd14a3313c34ea6d803c9",
+ "bytes": 3331
},
{
"id": "heatmap-labeled",
- "sha256": "5197df2d338fd535350a937da7a76d894ca130932fc188fc1364eab874da324e",
- "bytes": 12829
+ "sha256": "67cfbb288daab38e52bbe28318ec45962a40298324aed12512b822e80576d844",
+ "bytes": 12845
},
{
"id": "facets-anscombe",
- "sha256": "da4963ab14c5a0206a8d1ddafac5fb16c030f7b6c7451ef39414e4e00f54da56",
- "bytes": 8799
+ "sha256": "20c719db945789d77546980279f1b0fa2eafe10537ee3aebd4e6dfe03d647e60",
+ "bytes": 8815
},
{
"id": "13-interval-timeline",
- "sha256": "de60949c8d0d4c8f68a8a2ebc3761bfb6434013b7b6aa5719c19cfa21aedfa1a",
- "bytes": 3061
+ "sha256": "6e91a97627ce9bc1b86a4d39650f3c9d46a9eccc0b68abd43420fcc7ca291feb",
+ "bytes": 3077
},
{
"id": "14-error-bars",
- "sha256": "c295fe340ce8fbe40697c1377767e0ac56844826452e4d8b8fac5d85ad8dc7cf",
- "bytes": 3730
+ "sha256": "db601bd99de8a260a3085399ac075903178111f51daae476536b0194a86a5f8c",
+ "bytes": 3746
},
{
"id": "15-boxplot",
- "sha256": "40c9001ba5742c7711bf31563f0a802668f2dca056375a808c916bf29115ebd4",
- "bytes": 6141
+ "sha256": "d7357fd95366d3e73bd7d78ec27a4df5359d598ae668a8a34372393b71fcc8d9",
+ "bytes": 6157
},
{
"id": "16-lollipop",
- "sha256": "8bbf4fad276d081e8e4bf16c9787d09c46547ec5cc4f4d8e8d618bf9b41c8b7c",
- "bytes": 10201
+ "sha256": "7284893c1ae919ce1d52f95eb387607d9c78250ae6e0821a9c4b1e595d16b8b6",
+ "bytes": 10217
},
{
"id": "17-dumbbell",
- "sha256": "b046f7e2c2765589281f2a88ac2c435ce2caf30a229cedd553cd587113e25ac5",
- "bytes": 5387
+ "sha256": "4fa7863a5562739bd5c86a5ff7a4df4f61d37eab50697376cce22de3b4894513",
+ "bytes": 5403
},
{
"id": "18-cumulative-histogram",
- "sha256": "60d0dc1a252a726e0fdeae0ca181a461d959c244c23b4925d8dc105e412894eb",
- "bytes": 17758
+ "sha256": "9a126b048c10848913c4d7b902ae252d5be9a164862c4e85d006dac5584f52ac",
+ "bytes": 17774
},
{
"id": "19-moving-average-line",
- "sha256": "fa363928d88be401ef0a91d30d1f106b5e04647e77e7cd17f844bd2ac6079a71",
- "bytes": 20366
+ "sha256": "96774e0a235add53f32516ff31d608c785b2d4293b3a787de2ffba992c2d4c59",
+ "bytes": 20382
},
{
"id": "20-normalized-stacked-area",
- "sha256": "18a084c8d339035cb65aa73937ed1bf581567356ba9c766819359d27de9ef27a",
- "bytes": 46124
+ "sha256": "f31b75a87ccecf74b8fd6a494ae8e464018a83dbefceeb24d8ddd26da1f64da1",
+ "bytes": 46140
},
{
"id": "21-streamgraph",
- "sha256": "cb760551a58cba6002ab11884548d5dda7dd00d780a8f6ff9670abf8684d3535",
- "bytes": 48111
+ "sha256": "56ba2e5a8670995d5ec3599d1fe243f2cccdd6ee12025157a3c8e8bc1e678def",
+ "bytes": 48127
},
{
"id": "22-bollinger-band",
- "sha256": "8d034999ea6109423b45c8e443dc68f8c85e2aa074756cdb1f4c434e834fcebf",
- "bytes": 50229
+ "sha256": "43848eb4c26fdf693d8fb3a69ba85ad4b6aa6b37bedbf0a6d98e05b52018135c",
+ "bytes": 50245
},
{
"id": "24-quantitative-binned-heatmap",
- "sha256": "2c5e024807217b280193372c00ce8eb884151209671581f75576fd58bd027cdb",
- "bytes": 12578
+ "sha256": "add1849eb75614a48a1d45fa56ad5d1db0fa78befddbec204f4742fb2aed574d",
+ "bytes": 12594
},
{
"id": "25-calendar-heatmap",
- "sha256": "c21abf21f1d60ef99f37cb38d7125860e4dce3095dfe4a106e6cdbb60f461d1f",
- "bytes": 28500
+ "sha256": "b3bb4417a0803be9baefdd4ce0f9eadb98cb086218b22a2ccf7e95b5db5ddb91",
+ "bytes": 28516
},
{
"id": "26-diverging-likert",
- "sha256": "22e2d90e9df49afc6277df005f3755837008001f5323d20c7119c54243f51c81",
- "bytes": 7197
+ "sha256": "18b237ed537920be09a0d843ebf52388827ac1a3ef7023f8e032972c7fb79211",
+ "bytes": 7213
},
{
"id": "27-parallel-coordinates",
- "sha256": "f54c2116228dbc3af792e468410c2e7b2318ccc546ebfa1a1a2ccd906f59365a",
- "bytes": 8501
+ "sha256": "dd766613dad6c711b8ab86e3d96796cc1f4ae14f48328dd24693de596874d1de",
+ "bytes": 8517
},
{
"id": "28-candlestick",
- "sha256": "e94fff171160f6a9bdd7103e8916154f82ed33c7ddfe172634b96f1d66c32e06",
- "bytes": 14350
+ "sha256": "495de35eea9d898c13edccc4962b5336491aac69b5104a6e71b89da405ea98aa",
+ "bytes": 14366
},
{
"id": "29-waterfall",
- "sha256": "ba0a9ea33481c51c2eb7dbb4c5697c8318d301ada9ded3e07f97d6bda12e7f68",
- "bytes": 3137
+ "sha256": "1356c6b9cb5d07698dc6c0253b0778914c6ec6d99659ebb596b3b26e38b56779",
+ "bytes": 3153
},
{
"id": "30-slopegraph",
- "sha256": "5b1a1f1bc41393d6207018aabc2f3b4afea022bd2345283ddbf775e7f0b36c00",
- "bytes": 9454
+ "sha256": "f74a29008ef5d2f85c2c8d76af7b4cf576060548d04db3eebf6561442cace21c",
+ "bytes": 9470
},
{
"id": "31-linear-regression",
- "sha256": "5a4d4612197c089d4facf108edba5e1eae86079921707ed5caf97c153b682ed8",
- "bytes": 67630
+ "sha256": "7d3c6a040e0702e1a556e591ff2b3ba11f36287b4cedd40358d0938943f3e126",
+ "bytes": 67646
},
{
"id": "32-change-arrows",
- "sha256": "22821ed5be171c8369d7abefb09ee7e0c6542f4cb1bbcaade6a2c2187c1f144a",
- "bytes": 10679
+ "sha256": "7e9a42b5bed4ca9d46959b871be1e4ebb3217d5aa94ecec38daafb29553b8b5c",
+ "bytes": 10695
},
{
"id": "33-difference-chart",
- "sha256": "10a59544dd243a1f820503a988b40f89ca80f5f321f2b7463003171aa727ccbd",
- "bytes": 11281
+ "sha256": "1472231d7a3740bf711c11e315da8aa033e5a21a398afcc15e380ab97f331745",
+ "bytes": 11297
},
{
"id": "34-pointer-tooltip",
- "sha256": "e4ff132c905893b271d67984435b50620b267447c8081c2ac9f96295d99482e5",
- "bytes": 15814
+ "sha256": "37e1f45b9db3fe069368fbbec62d953aa1a7b2c389f848e6ee63559298b62a62",
+ "bytes": 15796
},
{
"id": "35-grouped-tooltip",
- "sha256": "8a6d29b3745685108a1d2486d18dc7ee62daa6645543740b80c8a0eaf564e1b1",
- "bytes": 56607
+ "sha256": "28ff3021123a3432b3f9446aac8a6f59d87b37ec6b62c3b349c5d24440bdde24",
+ "bytes": 56623
},
{
"id": "36-hierarchy-tree",
- "sha256": "6f1659fec2b14b3d7ab0c45f259ce58695f594fa8feaa965c0b7e236822fb282",
- "bytes": 8271
+ "sha256": "12fecee033795499e60d61bf0a2a802e9385b615c99596a3ad45fcd9341afaf3",
+ "bytes": 8287
},
{
"id": "37-delaunay-network",
- "sha256": "0889b44c69be54d73796de9217eb6f56be417af7af58a9d1121c94a41e8f18f4",
- "bytes": 29129
+ "sha256": "b5401e96b921f21a619f2e2820d24c61040d5f3911fd8a408f1bafea8488622a",
+ "bytes": 29145
},
{
"id": "38-contour-topography",
- "sha256": "09461b319dbd30312dd73debec669f20570ba0359cf406264d0c2178a70afcb3",
- "bytes": 20685
+ "sha256": "a8cd1cc9225f8d56c47000bfb88b081cb2cd705d37933f0f6c24adc895c6bbdd",
+ "bytes": 20701
},
{
"id": "39-density-contours",
- "sha256": "cd88c4a73927f328de96ecfffa909ae7212a8c2d1be55f6c6e847bc593b2459a",
- "bytes": 20325
+ "sha256": "c7f106b309bc66ae0f2474ce3710af57d07e5b26675be9517bcbf8d262ba6a29",
+ "bytes": 20341
},
{
"id": "40-geojson-map",
- "sha256": "b52d1999c5c8576f898056a2a1bd629514279c1ecff85078b21f1f7c34985520",
- "bytes": 39250
+ "sha256": "1e26d1983fc3fbe1506f0f84cc1dac2bb0826301cad4b729bc21d9e151061d1f",
+ "bytes": 39266
},
{
"id": "40-force-directed-network",
- "sha256": "80f5e1c0880ce771298c0bab5636581d8bd3f6f2e24412cb8ecbf0a936603f1e",
- "bytes": 8306
+ "sha256": "7dfbe724861020abb38feb5b0683d37bf4a95fb917fd8f47a1b8589683c15c37",
+ "bytes": 8322
},
{
"id": "41-waffle-unit-chart",
- "sha256": "55139cc3631b3decb1daf4f3980bd06c7669ad3615f9a0753dbd2fe47e4b8946",
- "bytes": 17546
+ "sha256": "b606468770f8b25d561a00a11e83031265836e64dfee9cbdce34107c23cef34c",
+ "bytes": 17562
},
{
"id": "42-vector-field",
- "sha256": "1a61c91001f112afeaf3af9328d80b4dac271e1a5cc4e615e8bfffd45cc3058f",
- "bytes": 25073
+ "sha256": "e0b6475317159a2c5dd33e2451a3be8f462b7e8159d2c33f30a8516d160c74d8",
+ "bytes": 25089
},
{
"id": "43-hexbin-density",
- "sha256": "f744004e1a69c34904939f1a5b350db2072bef8477025219822b709761e00bf3",
- "bytes": 13722
+ "sha256": "af2ddbd4725442ced235d7ec9568e60d5a6348f915984c7190d7083ce4a26ef6",
+ "bytes": 13738
},
{
"id": "44-framed-scatter",
- "sha256": "a12653214d4f29a57c005b3ac21966d53dc719aa8f6e00f3d20d265d99346b59",
- "bytes": 56002
+ "sha256": "56165f9ae5238a5758f8779c1c1c9802f9f64d9ddac92905508403da2b9c7150",
+ "bytes": 56018
},
{
"id": "50-empirical-cdf",
- "sha256": "e7c147a07f71b865b5757692f66fc11b9be15f9059bf32ea4c5f76e341c428ff",
- "bytes": 13298
+ "sha256": "07913c18e9a81437aac15cbda96d6881360298402a032f01952cbfff1feded57",
+ "bytes": 13314
},
{
"id": "51-faceted-distributions",
- "sha256": "43b93993eb1c5745c7dd6a67db01b57a53e3cac300e2a9c78bb64b5f3c913def",
- "bytes": 6748
+ "sha256": "540f6774fadb4083abb7237894309d06910e3ea6e7a113d9634193817d8e062a",
+ "bytes": 6764
},
{
"id": "52-beeswarm-dodge",
- "sha256": "91ac917ba409eba7f43c3eb5c44428681c6fb63843a2108f354f07930af72673",
- "bytes": 16142
+ "sha256": "8fd624ba8d5bbfac733931afd59337c991e2e54e888f06444fa8130f84e606dd",
+ "bytes": 16158
},
{
"id": "53-log-scale-scatter",
- "sha256": "1cd2b67270ac873a4088f4c9c0c98cd889ef41018c3f71f190ae2017a47c90ad",
- "bytes": 45921
+ "sha256": "62c8f1abc7e732679dbba7cf2b8a7d55a0493dce9cd2bd0042f2e628d0e48b57",
+ "bytes": 45937
},
{
"id": "54-bump-ranking",
- "sha256": "ff6ddf1053f5c06ca9dd02ea0eb82544ea326f2ecb1d26686db5d2642a0416f8",
- "bytes": 11508
+ "sha256": "448a7cc25f33f117d5b34f0fd9a8329634f9f86917ae1fd57ab4af93b6646dd7",
+ "bytes": 11524
},
{
"id": "55-indexed-multi-line",
- "sha256": "40e62d6250aec4b755483ca97ce3bdf0817b2c82f32e529789188852529e7ac5",
- "bytes": 4310
+ "sha256": "2093b1d4fbcd50e3339fa11b7cbdb071419550ae5466284d3f1522bed241cad9",
+ "bytes": 4326
},
{
"id": "56-connected-scatter",
- "sha256": "d0849475f9c48598ff60736788402aacf07d06c8e3ecdd7cbe39a46d38a04e08",
- "bytes": 14323
+ "sha256": "a9847c63f8bf1c0495cfe46e106270859f6ae958ce5fecc2b9af6269b921ab48",
+ "bytes": 14339
},
{
"id": "57-scatter-marginal-histograms",
- "sha256": "e0f7f961f06630eced467d48e121793e75211db3651c569cf58477a1a5ad3178",
- "bytes": 84836
+ "sha256": "36f47b62c995502f047697d3206ac230ebc578193e9f068ef344fbd0bf8b086c",
+ "bytes": 84852
},
{
"id": "58-select-extrema",
- "sha256": "ae0d6386349a51cba4284bc1e68bf56c59e7223a4747860af4db59b7ebdf07aa",
- "bytes": 18985
+ "sha256": "f150f229e2aa972358ebe2d392c548aaddd78ee0ed55889378f8a37e834bac07",
+ "bytes": 19001
},
{
"id": "59-grouped-reducer-bars",
- "sha256": "64d82e265b8c99c9e13d7a0c72d5e761ac1531a487f85ba9f368e27430a7996d",
- "bytes": 2630
+ "sha256": "0923bc526e28cdd383b7062be85c6b6cea528740c7f3575f3450df6fcd7b0298",
+ "bytes": 2646
},
{
"id": "60-lag-autocorrelation",
- "sha256": "b2682fbadc7cce9e9e8b555171c611f506b130f02c68af2e7889836906adb85f",
- "bytes": 25808
+ "sha256": "ffead0dd4ced6317301a97177b1a69047500285e09f2418fbaefc4c2f9a2a068",
+ "bytes": 25824
},
{
"id": "61-quantile-ribbon",
- "sha256": "3e97982535d5d84ffd3dfd608c4ddd01d8718290b04e76d0699840f56c9315e5",
- "bytes": 6698
+ "sha256": "e77612d1610f5ebf1b3afb776cc77664650e51b1162e3ea8e99239001d961216",
+ "bytes": 6714
},
{
"id": "62-ridgeline-density",
- "sha256": "0f3946061cdb2166d0fd0ec589bf28229dd64c20f714d274b0156953ab144137",
- "bytes": 7708
+ "sha256": "0c4e31eabde674cb34f64de211f56644cd661fc71f02732a620603378fb4831e",
+ "bytes": 7724
},
{
"id": "63-violin-distributions",
- "sha256": "208a2389c7a98fe23f96b8767babf9cb229bcf14a9485e8ff4116f47ff6bd664",
- "bytes": 7352
+ "sha256": "178eefb2506ffbd7259c770152b9179f5be88f46c462c3a90c4af2c390534e41",
+ "bytes": 7368
},
{
"id": "64-marimekko-mosaic",
- "sha256": "2dbcfe86a88683f3e8e353782b214fac3cddffc94e1d9c848cf740170cd8ef52",
- "bytes": 5659
+ "sha256": "df7fb1d22c8ae4f47bd3a3bdc1f2192ae6115f652e6f38a460771d5c72c68116",
+ "bytes": 5675
},
{
"id": "65-voronoi-nearest-tooltip",
- "sha256": "fbdc48535e8997ef0ccb47ccd85f14c3443c26f869b95e9b79fd317414bb9c88",
- "bytes": 16224
+ "sha256": "134f926141203ed363ef523c698331717cf653d23b0858621d5ab713dc277018",
+ "bytes": 16240
},
{
"id": "70-composed-chart",
- "sha256": "642139631cae72ef76ff81e148e6d40e2ff83225a11b582a32878d0f166beae7",
- "bytes": 14436
+ "sha256": "bde720b0d2cfd752e3a9600f469c2a1ec5a3fdc3232292ad5e4c6c45319994c6",
+ "bytes": 14495
},
{
"id": "71-recharts-population-pyramid",
- "sha256": "2a301e45dba4848b0cc0644cb4eb5acd72966310b124246bf8504d53bdd7486d",
- "bytes": 2637
+ "sha256": "7f66b3792aeebeed1f0fa546b4d8f1769f55f7500228acfa6174674484e19add",
+ "bytes": 2653
},
{
"id": "72-recharts-mixed-bars",
- "sha256": "9f8e93c1a78b6786766c8752b9251757485d34dc27df033c5f329bf1cccfac5b",
- "bytes": 5955
+ "sha256": "5ccdf45b0751d39b53401708d768d7b738a16801c7dba92428ad9e58b92fb07d",
+ "bytes": 5971
},
{
"id": "73-many-point-scatter",
- "sha256": "300438e80ec220a50605cbdb298dbcc8826aa53a5f1147b7857edb77ecc95f89",
- "bytes": 56130
+ "sha256": "7e0f2cbac09823e3672c7e709216bd7dc00e2cc47acfe7307075fc6b91cb26f7",
+ "bytes": 56146
},
{
"id": "74-recharts-treemap",
- "sha256": "24e6e1680ecb558600fdf114d65f1db589bcb30b95b4f653fc1d9481899db723",
- "bytes": 3869
+ "sha256": "7a8d03b2ddb4c3cf9a043e5936676ba0f38b3ac5ac63fcc5c4716b4ed66e67f4",
+ "bytes": 3885
},
{
"id": "75-radar",
- "sha256": "b8ca29100d7bd1dcb9f182bca64cab7f2d1563ba1932ea1b420a1a8fa78a0c5d",
- "bytes": 3489
+ "sha256": "818d77720e57f76464a739a141e73b71a4b73d2f65a64133ccfb7f74b891cc0c",
+ "bytes": 3505
},
{
"id": "76-pie",
- "sha256": "b47572d797a7c673f8683c8f239cae1d8ed4c50f190bf0dd3cff953121e35208",
- "bytes": 2488
+ "sha256": "78f6ab67bac81dec8d39eb77125f60320866018b9db87eec476bd6461b11c21d",
+ "bytes": 2504
},
{
"id": "77-donut",
- "sha256": "6272fc2126f293ea1a35a9812d424fe701c4f84b28c6abac2baa9316c36f064f",
- "bytes": 2988
+ "sha256": "4a91aa414e97ce26110fb16c3436aeb096ba4cfab36fdf44a64996ee3464e7c5",
+ "bytes": 3004
},
{
"id": "78-gauge",
- "sha256": "1fc5a67762dbdd05589b910e9c4642d48057d9f92e849864989d5241514e9f1c",
- "bytes": 2031
+ "sha256": "84de33a695f5038762744e881296dd3910d2f2bb28c02f2d2ed8014b310cc96a",
+ "bytes": 2047
},
{
"id": "80-echarts-axis-pointer",
- "sha256": "ed410449ee25240faf6640bfe12f4968357c68d68bd9b47bd12ffcf79d06ba71",
- "bytes": 11445
+ "sha256": "2501316a8b75b5f54cc07e4ef3ae1e698092b84045b38e4fd35cb539e5165b66",
+ "bytes": 11461
},
{
"id": "81-recharts-interactive-legend",
- "sha256": "101eb6796ec9aa0ec5844e685f1b76c993b38259fbf4e22dffc7dbe9031b944e",
- "bytes": 5840
+ "sha256": "ea05bf6f149a97a9199a99679e972e59665e79c4e3e759c256b7031f203000e0",
+ "bytes": 5916
},
{
"id": "82-chart-table-selection",
- "sha256": "ff310136b7d15c327b6d924c7907793e8c4b291947c8aa1bd744b2a5af334219",
- "bytes": 3857
+ "sha256": "b5b6dc512efc9b1896b5af92166246fce135bc35b61254c4a154d38a456741da",
+ "bytes": 3873
},
{
"id": "83-focus-context-window",
- "sha256": "42cb178cd3c96993548968c7779b71fa54e64141c09a8ea2b2cb6a2aaccdf571",
- "bytes": 9681
+ "sha256": "a12bc78a58208002ed7825ae5420c01da3e37f3f1b21400d6814120f94a87780",
+ "bytes": 9713
},
{
"id": "84-pinned-nested-chart-tooltip",
- "sha256": "e83f4a5f64646c627af03099e0796ddfc0d8699a2ac7b0be848552715ed06dde",
- "bytes": 26174
+ "sha256": "1299d9d5f4c7d04c433f1c2658d00e46660109a20b7acb5d5ee62c37ab50e98e",
+ "bytes": 26206
},
{
"id": "85-scrollable-resource-lanes",
- "sha256": "ef4913fad9e8b92ba255828a1b3b1e89c759a7e948aa30a5ccbd092f1c30bbb2",
- "bytes": 3766
+ "sha256": "4991c55242bcf537967a214e39e068d8f9231fe1744fc4ae5a5eb65d30d02249",
+ "bytes": 3782
},
{
"id": "86-streaming-window-preservation",
- "sha256": "0d45437aed02ed7e219227c5b4ccb638e1d073e680578fd6975c83e520d65440",
- "bytes": 5311
+ "sha256": "e331f854d835e27d886b3e26ac31b8348987315e0f78682114dbcd9651f0ff26",
+ "bytes": 5327
},
{
"id": "87-echarts-synchronized-cursors",
- "sha256": "1d3a678b5cead58648a9837a16dd736eab42b069ce310eebb612fccd7104d586",
- "bytes": 9533
+ "sha256": "fa3b541222d23791cd6f30a3e58acd607827fa2ffbe65a9c032bbb92d9c3aac3",
+ "bytes": 9549
},
{
"id": "88-echarts-free-cursor",
- "sha256": "57d2348aa412d8f8e79c92fd6fa2edb74c5cc129037c03fcccd200d03a475844",
- "bytes": 4994
+ "sha256": "1c13247bae0400063ae43666632c1faf13251031637cb21c97b617324c7ceb76",
+ "bytes": 5010
},
{
"id": "89-brush-range-selection",
- "sha256": "1bad646a0a35650f060215a3f13573c2259d21844c55d070b2a263fd81e24bea",
- "bytes": 5975
+ "sha256": "faedd7a2d8e22f92937328c2c88e30bbcac2fc5c221b9298b0e9a823ee550c8b",
+ "bytes": 5991
},
{
"id": "90-zoomable-time-window",
- "sha256": "0278fb0d29f8cf41fb2f3309b8d5850b525a14b2a701d78824d86afdd526ca5c",
- "bytes": 3875
+ "sha256": "80c67449ee4092a369d75aea9e5eb33f128f4271011e9ef51c427badc5c588b3",
+ "bytes": 3891
},
{
"id": "91-timeline-playback-scrubber",
- "sha256": "97838f8752e739d0f88453253c55142c75306b3b99da721507019d1280ddb34b",
- "bytes": 4196
+ "sha256": "29ac117ef03918b82ede2bd69d486b8087936400dbce3e4c6560231657211817",
+ "bytes": 4212
},
{
"id": "92-editable-event-range",
- "sha256": "5a5c8bfddfc18ef0aa75c772e5f943a52c81487dae21ccbc013f0e40efad00c7",
- "bytes": 3149
+ "sha256": "fdb8f21a901378f058472e1fa5875053db7958241f6bbf81e2894ff1486bbae2",
+ "bytes": 3165
},
{
"id": "93-labeled-pie",
- "sha256": "6f71a585142965c9eb55730fd1ece1cb3d77aeca2f2cd6ad665d6f66b7052258",
- "bytes": 4431
+ "sha256": "1421d5db4def6c252961c94b56c748c07bf1d3693e8e2b2fe101889b1fb98013",
+ "bytes": 4447
},
{
"id": "94-center-donut",
- "sha256": "28623dc05a6ea865284762c3ba189fd5456ffce836ef734b5d9ec96318ebd894",
- "bytes": 2729
+ "sha256": "feeb89d56b9a2e706a589629dbbd32d1c5e0545a0d4778a9c5032e73a60894ae",
+ "bytes": 2745
},
{
"id": "95-rounded-donut",
- "sha256": "9e04bb83eecfc4d92d32ca4a0e4e6190c7f0b427b9e2b512b97d6db618367d2e",
- "bytes": 3487
+ "sha256": "4e31b462ea95c5c47f3eb8a8a8086025ddf6e9f13de432bc211ac354591de531",
+ "bytes": 3503
},
{
"id": "96-nested-donut",
- "sha256": "90539da1b271c47fa1dd7b1ca5a323d7e3907fd945d238be9c035745bf00f191",
- "bytes": 3492
+ "sha256": "64f9a554b23ea80abfc92e2edbeffc0511b93d25a321f10c2edc21df8104020f",
+ "bytes": 3508
},
{
"id": "97-rose",
- "sha256": "f4a7341e1ba7172ef9b7885a8a1890982feadd7bb0f2d0c04401ef7edf7eaa93",
- "bytes": 3560
+ "sha256": "006a91f2aa93a19c5abc1c5d297b4a6b5beade5e468b9c895d27db3685d93053",
+ "bytes": 3576
},
{
"id": "98-needle-gauge",
- "sha256": "cb7cf9b16760931eb62e7d6ca88a6e73964f25671f75856abb066166cb0412d7",
- "bytes": 5530
+ "sha256": "c8bc517c44f473d3397abe00c82d30ccf3e92502673f5f2cc5a0c6ab2202ec6a",
+ "bytes": 5546
},
{
"id": "99-comparative-radar",
- "sha256": "036469db7a38abcd2c5f75e64be3a4e36268aed8267b68452c966c65b620903c",
- "bytes": 3741
+ "sha256": "37ad67a05df8102796cf576c030cb25d528abb4b0aca0a1fb718f345bab180f4",
+ "bytes": 3757
},
{
"id": "100-radial-bars",
- "sha256": "aab758d3f8329bfdde1f0e4afd40acec291c4f815606c8100e951763e80f4e1c",
- "bytes": 3071
+ "sha256": "e04b93235df9fb21ffafa2b4cb01eaf5f74f312b654e723b94e9da91172d6590",
+ "bytes": 3087
},
{
"id": "101-sunburst",
- "sha256": "51a1d6c0a0d5b67cf4a915bc3dd474c0bc3f047f2602f472a6305b6b41935c78",
- "bytes": 5076
+ "sha256": "005504a78661d48b7ed3cf4648e6a36bb32a91f597fc3e4aa2dbf887a4859cb4",
+ "bytes": 5092
},
{
"id": "102-world-choropleth",
- "sha256": "9c55ddeb117e8225cddbf8b5e88e9bd4f6565f6bb2007d0660787eed96879a70",
- "bytes": 63419
+ "sha256": "a0a0bc3347c32e656d86c4811fad6cf693bcc2287f79bbfdf76f7af6aaf890bb",
+ "bytes": 63435
},
{
"id": "103-bubble-map",
- "sha256": "f5619f18fd8ffea5cebdc4bd07e0e6f1de68f94725f0936bc61612fecf7e9d78",
- "bytes": 49104
+ "sha256": "16ec6c79adfb2c9c963c58bd3bfc99cf1d7531dbaaf11e7c3c2126dec40336bd",
+ "bytes": 49120
},
{
"id": "104-orthographic-globe",
- "sha256": "6203a546e0c2eb07aaee46b201b1023f1cc4387179306502f438fb06aa3408e2",
- "bytes": 21647
+ "sha256": "e4caa3113596705cb7bac9e480fb8d6aaf14c6fc9c667291d1032c0686a08341",
+ "bytes": 21663
},
{
"id": "105-route-map",
- "sha256": "9982445ad8a4f2cd46c1b0d95f1b17ca6e8c4eef2c4c5c132e32a75882c83ace",
- "bytes": 35788
+ "sha256": "bbb85ed655645dbc90997c887bc9381120a22f0ddad9e8763f32008db12f105a",
+ "bytes": 35804
},
{
"id": "106-polar-line",
- "sha256": "04b292cc7a427390604d7d21ad85db258d8f8a35d900152cacc3d2da6b28eb2c",
- "bytes": 9610
+ "sha256": "e2a8f8ff1ab563684fcae6a7f4ad1d521db3e8210a0e5055cb393febaace3a37",
+ "bytes": 9626
},
{
"id": "107-polar-scatter",
- "sha256": "1b9c2311d966106e398f8178fceb5e5ca33bf45a076ef6e563b1a9c3bf4e1a0f",
- "bytes": 19659
+ "sha256": "21d619a28500d93f0fae9571fe3c2defa72d16c86d15a8c89ad3b08fbd3059f1",
+ "bytes": 19675
},
{
"id": "108-country-choropleth",
- "sha256": "b2dab3c05a7a682521e7c60877e1bc9daa07efaa9e6c23cb51ea8e1e3e644bfc",
- "bytes": 64626
+ "sha256": "228a41eb6c6010a2a255e0e8e44ab0a05a97d874a0d69c8b1420a1b5bcdb276d",
+ "bytes": 64642
},
{
"id": "109-us-state-choropleth",
- "sha256": "b1fbca0f9413b129fafd9e7365598614320c7eebdf7bced621d42cab79e74461",
- "bytes": 1643004
+ "sha256": "2b32385b66b21d1068ed906324a43081c6a5a1f57f06ecef522f5d9ccc857fd6",
+ "bytes": 1643020
},
{
"id": "110-projection-gallery",
- "sha256": "dff48aa16fc4392971671f6c27dd659a5f077c44c4017ea73b1c7f4409b9efae",
- "bytes": 288504
+ "sha256": "e27afd5a7536035eac914b14ce484caee3a4927f729368eb1546068e69ddb7ff",
+ "bytes": 288520
},
{
"id": "111-basic-sankey",
- "sha256": "e54b0261ddbb7fe1f891a95747d033b98f932b1aff80e1c2b616283722fb3d49",
- "bytes": 3232
+ "sha256": "6a98e28e81e7f8ef0c124e88dba7b1bea3f1a421c3ae6d968e092ade71fc6124",
+ "bytes": 3248
},
{
"id": "111-sankey-flow",
- "sha256": "9b6f8b67dd9168b7e89d92f5d2cbb17aed913fa301af376bdbd9e52b2d8862b1",
- "bytes": 10992
+ "sha256": "ec10d765655eb581177db5b8baf3d1ab5fc51cf7ba4ecfa58876c34a2099e7e1",
+ "bytes": 11008
},
{
"id": "112-motion-entrance",
- "sha256": "69cc8efe0e358c4b08ef58dae8a741c63bf5c425dedd08eb8776bbbda360f107",
- "bytes": 3261
+ "sha256": "a7546fb25b86ba1fc74f15ade08ae85b303d48e4010daf93bd97461af620c57a",
+ "bytes": 3277
},
{
"id": "113-motion-updates",
- "sha256": "e249da4cf2c0e9fa5db6acdcd88364d8786ea87e2edf9781ba58f1e35f61c7f4",
- "bytes": 3259
+ "sha256": "4fa312e18bcac8de4816430c61bf5744aa4443576ad86047d23e548b47138537",
+ "bytes": 3275
},
{
"id": "114-spring-line-motion",
- "sha256": "54046521d92711610e6b6ddfab84f9c44482c9f91b68c9510cc56b625e8b3c30",
- "bytes": 2113
+ "sha256": "805956658ffcb2d17bd64f197bd5cf619ff9e329158b0a9c351450dcc7341039",
+ "bytes": 2129
},
{
"id": "115-definition-motion",
- "sha256": "79c5a21713f55e561938157f728be9a29943f6b495a617c34dcda50e899a075b",
- "bytes": 6708
+ "sha256": "cec9e3316e9cd66de74ad8cc9c6b0f572bfba3f5bfffd318740121c9c314ef05",
+ "bytes": 6724
},
{
"id": "116-geometry-morph",
- "sha256": "03f2531dfc5b664051f4026ed8b4f1d7045869f44971a8f454d203e89aa922ca",
- "bytes": 6537
+ "sha256": "02e24c0c753956d757946e5984d82fa024d4f20e78dec805ce81f23568e284a2",
+ "bytes": 6553
},
{
"id": "117-focus-cursor-motion",
- "sha256": "83bc77c8f5cde5b7ce1cddb1245f26cbfa319fdb2fc423387ac15c89a21d1f6b",
- "bytes": 9468
+ "sha256": "788ab2811fe8c3c0a5dc32f5ece4c59ab69a59228c5a268b548a753c6b1202ec",
+ "bytes": 9484
},
{
"id": "118-token-usage-calendar",
- "sha256": "38a469fba8dfcc2c6eb8d63116481f5cfacfea8bea5450dda2c9c0de9f7eed1a",
- "bytes": 54442
+ "sha256": "a823d473fda26de3817c41551755b78d81ff7e71c4c2b251e8b2bd8e22b29527",
+ "bytes": 54458
},
{
"id": "119-stacked-bar-band-cursor",
- "sha256": "0257543342681ad1161ff6011ebee48abd3dd6b4d2f3ee46b751a4f54fe95407",
- "bytes": 9709
+ "sha256": "6026285d9999725965c969d4865ee16ff9433df25e90ed93f60e2cbcfe6b4519",
+ "bytes": 9725
},
{
"id": "120-themed-interactive-area",
- "sha256": "d6512a266c0234f1743e876b66edd8eff1fac25c4158602224ce4c22e627d652",
- "bytes": 15205
+ "sha256": "3a753cb69b712f5d387d6652a770f7fe549c3617820ca1f41186b643d269681b",
+ "bytes": 15221
},
{
"id": "121-active-bar-dashboard",
- "sha256": "f30ba8a97d5a293637a0a0dc345324bdb3eeec2a391d00d81cd1b6363683469a",
- "bytes": 12508
+ "sha256": "eaae4eae5c94c9c2106fefe7df50f62ce200cbd10d830324f3358a964b574d69",
+ "bytes": 12524
},
{
"id": "122-premium-kpi-sparklines",
- "sha256": "ee9a430fb58aa6f1980866e5c7f47804538133283650dae149c5def1c57316bf",
- "bytes": 8223
+ "sha256": "b3d6a0c9f7f07363ff0d925fffa5510f20eeda937bc35cc372414a61e275d0ae",
+ "bytes": 8271
},
{
"id": "123-active-donut-metric",
- "sha256": "47621d2d1d3361436394442863e8a578369d2f9edd78d8442d92ba04a32e1bec",
- "bytes": 4915
+ "sha256": "22b24eb976ec1fad49832fb2cf01da01370d86d0369e59fa9b94649c0bdc4b25",
+ "bytes": 4931
},
{
"id": "124-theme-palette-matrix",
- "sha256": "1a28df6af085136ad606f445bbf66296ab03d6eccedf31e5197dab8bd1968602",
- "bytes": 9934
+ "sha256": "3ddfc66eadf1d400b321a80cc2f328df7065f2866fa183869e37dd128ffdaa5e",
+ "bytes": 9982
},
{
"id": "125-sales-funnel",
- "sha256": "ac9f936671b1b43bf6b8c5ad2c159aeb410fda69b83fb93e2029a72cae541827",
- "bytes": 3893
+ "sha256": "33f7db88bf672cc67ac2cc17b8ace5ca9c71d057d7d6b61dcfe9888db076de38",
+ "bytes": 3909
},
{
"id": "126-drillable-sunburst",
- "sha256": "a3e94670675af7a48a372df9a94138c34922eb55f2dbf62af1a306d0128a8962",
- "bytes": 9249
+ "sha256": "38972c982cdc75e95215f74c817c075e60a9e74c92c3e6d5b8618d7f746ab8bf",
+ "bytes": 9265
},
{
"id": "127-shadcn-dashboard",
- "sha256": "bbfb55aa262775b1e3cf3ea890cb3200a26b78499d03425efc526c1d9d7e7c85",
- "bytes": 17976
+ "sha256": "ab40ae8ec5fd2838d2631db05d2d1f67945413766ef6a4fdd7fd04e7a910e3ad",
+ "bytes": 17992
},
{
"id": "128-shadcn-bar-multiple",
- "sha256": "97e63f098e3f08b81a8ce91007c480c14b07a1f52d7f6c7dd62b9b15043a7040",
- "bytes": 5352
+ "sha256": "ece980acaebbbcf1e36bae4cd1aa295000393dfeffddf7a9bb204a063ab6d414",
+ "bytes": 5368
},
{
"id": "129-shadcn-pie-donut-text",
- "sha256": "f181e7545d6258011099fe5452554036be1db897f5c320a08336783455fc04ed",
- "bytes": 3474
+ "sha256": "1d61080a0eef84fa06ed82c1102d1bde39c9f9b233bc7faec9f1cb0fa77befa6",
+ "bytes": 3490
},
{
"id": "130-shadcn-radar-multiple",
- "sha256": "7a0779c02233c76836c018c0cebe0eba54270cf6c78d8e4aa08c8220cb7d3e9d",
- "bytes": 5531
+ "sha256": "b0a25db33a560fb216c4fe2743f6ac1b0dc0ec98a3c405d0d784d844feb71207",
+ "bytes": 5547
},
{
"id": "131-shadcn-radial-text",
- "sha256": "99acaaf226fbea7acbc041e7803fb7aa69895bd79213d9c328df25733db2f657",
- "bytes": 2918
+ "sha256": "5fa0d68696a47ef0690be889f87ee91efb30cd48b2237cff39bed0d6d2a2d2c3",
+ "bytes": 2934
},
{
"id": "132-shadcn-tooltip-advanced",
- "sha256": "b653e418fb8dbfe8e0b2a5cbdb6e852bc6dbe907bfa2bba5d897178f65676279",
- "bytes": 5003
+ "sha256": "97738ec4dd4be6c3e40c7b3bae1fa519e809c5f71f451384d2be9b84ef4be79a",
+ "bytes": 5019
},
{
"id": "133-shadcn-area-axes",
- "sha256": "542e34eb4fd56f243ef9685298a12f3b4dbe5aaf79387fa3a4e51aa3a4b81b6a",
- "bytes": 4598
+ "sha256": "2df8a88b2daf1e8457159be6b9f54ab9a0f7eabd1754756ec69f4658e1560df5",
+ "bytes": 4614
},
{
"id": "134-shadcn-area-default",
- "sha256": "e808879e63ade0da237182d3082d67967aaef7edfa75afe92b7732f307af2e12",
- "bytes": 3367
+ "sha256": "bcf17936e81cdb26371b461f69cad3fbaf51a43e3a1c0c99129fc32f771f56d0",
+ "bytes": 3383
},
{
"id": "135-shadcn-area-gradient",
- "sha256": "411d7e51d2372ec4084f5fb901de48a16c04cf0bfcf89e106fdf9c0e1aa7cc9a",
- "bytes": 4761
+ "sha256": "eb23150834e8d8d4a23ff3467b0b3150de56d849381daba3167d19e2237ebf26",
+ "bytes": 4777
},
{
"id": "136-shadcn-area-icons",
- "sha256": "51599128da7ec91ecd34537e08f07ec63a257f78dd23ed768ab367fa71d6ed51",
- "bytes": 3928
+ "sha256": "7a44595cc0b82716448326a34a323f239c41e644db59e03982f7e1443112dd92",
+ "bytes": 3944
},
{
"id": "137-shadcn-area-interactive",
- "sha256": "7a270f4945b32689f9e2ece9dcf0436f5bbbc6afedf6b6dda4ef295f9ac55ca9",
- "bytes": 19509
+ "sha256": "f19a53acc1ad455283fb294a7e4c3c060959351f0b1bdaacfc353aa4018884e2",
+ "bytes": 19525
},
{
"id": "138-shadcn-area-legend",
- "sha256": "d62b6416a44df16c5a1b70121f7fbc126d8f36ef45c73df2096fd769407b8f22",
- "bytes": 3929
+ "sha256": "839c43611f74fab3e0a9145008c6e34cbaf4c83d350197f30f7d59d0a172ff01",
+ "bytes": 3945
},
{
"id": "139-shadcn-area-linear",
- "sha256": "84fc918897879509baf73dcec17e97d91da7c355ce30d50c10f4eee4d17e543a",
- "bytes": 3160
+ "sha256": "703e732227e5a77169c866f0e9a79f4a277e96f99ae8d246d8388c53eec9af85",
+ "bytes": 3176
},
{
"id": "140-shadcn-area-stacked-expand",
- "sha256": "6b8c90b4d05de702ba0e97a15d880b339551e04e30a22433ca1702e22eb5e5f0",
- "bytes": 4579
+ "sha256": "92c67f5aa3057fa857e55c55027cef962eadb5750788fa7be3f935a630c84e2f",
+ "bytes": 4595
},
{
"id": "141-shadcn-area-stacked",
- "sha256": "2c041387a9a4694f536473dd1169210af1ce0be6969ec26774a1944759f267b6",
- "bytes": 3930
+ "sha256": "852a52f3c194a04147576ea99df217aa9ea7f01fa32b500770495293d3299d56",
+ "bytes": 3946
},
{
"id": "142-shadcn-area-step",
- "sha256": "4b0dd63afca3fae361b74e321bc4013f406c6c684a5d59824d57c41718e86ec0",
- "bytes": 3277
+ "sha256": "0dcd92b5f3213f305f7c1acfa30c897e02d25383653ee882c8298a6fc4c9f6a3",
+ "bytes": 3293
},
{
"id": "143-shadcn-bar-active",
- "sha256": "6f5806c2c075dffee8dd4315cdca6d21c8050124f883edbc166b1e056a9b7041",
- "bytes": 3730
+ "sha256": "7da6c2200bd075cdb1022e9d262c1b5a58f253e615f5bf25d568ad082eec1aa2",
+ "bytes": 3746
},
{
"id": "144-shadcn-bar-default",
- "sha256": "f8d84982b4e65699eace793751a5b54c209b6384894103140a17651b3022561c",
- "bytes": 4004
+ "sha256": "7b3c88dba890fae63600b16c9481bb0295f5b44f8801c9bb6b0f0fcec6cfc188",
+ "bytes": 4020
},
{
"id": "145-shadcn-bar-horizontal",
- "sha256": "b5e71b01af8631c0efde5eab8cf9d26826b4e382a95e59b1a278b92cb4ec18c8",
- "bytes": 3724
+ "sha256": "8a76b453a4647ad5e4b85c551a0b5ca27ee860bdb7fbe8e36323c64d0f486ea8",
+ "bytes": 3740
},
{
"id": "146-shadcn-bar-interactive",
- "sha256": "c381cb6116c33c388b1b00437eb98d5bfd33677e88e5a7e2152638e1f4accf79",
- "bytes": 20654
+ "sha256": "e59d7880c6a580984495c2926026a3bcc1afca4770a50dfc6681903bc6e321f4",
+ "bytes": 20670
},
{
"id": "147-shadcn-bar-label-custom",
- "sha256": "c72c269e64025ebbad36f9a502275226e6f83c0e31fe480a9d926f06bbc27dc4",
- "bytes": 5256
+ "sha256": "d3d92faa536fb6a518e0536abdec2c546e60a67da505684ccd204d12bce4f089",
+ "bytes": 5272
},
{
"id": "148-shadcn-bar-label",
- "sha256": "5b30abe83bd7cf32b3aded78f318cc0886b02d39c2d5220c8c134187033ba48b",
- "bytes": 5150
+ "sha256": "2c61d2dc70ace00c7a1697ebf1e457a94b254c7bf5860285a3cca62c75917957",
+ "bytes": 5166
},
{
"id": "149-shadcn-bar-mixed",
- "sha256": "83de97ad7c5dbbe17d31a4ff9a280656832247739a1f9e29683546622f59a254",
- "bytes": 3327
+ "sha256": "01b88eea0805ed92293efb038f930ba76da6a23e2839c2f9192de2a5a16eca2e",
+ "bytes": 3343
},
{
"id": "150-shadcn-bar-negative",
- "sha256": "524a4f51cc5a3e5f787c0c0da2805a64fb15b14229201d2db6fc2342604f98b9",
- "bytes": 4387
+ "sha256": "117285f70baf2b44b96a56023a8b016b4c34ef0b0e44e0f12a23e3311cfbc0d9",
+ "bytes": 4403
},
{
"id": "151-shadcn-bar-stacked",
- "sha256": "481d635d087cd8d651b30fdde2678f658390bf73585642ba534bad5cd8c059bd",
- "bytes": 5276
+ "sha256": "cfa3064537fcdf4ca15d4fcaf6decc6547455af16d9fa69bfb9c4a105e7e2a97",
+ "bytes": 5292
},
{
"id": "152-shadcn-line-default",
- "sha256": "bbb7b127cd0475b8407a90ccbc31025e26e5aa5079b42bc3bf7021f112542571",
- "bytes": 3303
+ "sha256": "1fc4c202fb6393191157d452347efb59d7e96cbab22b346f43f0387d3878703e",
+ "bytes": 3319
},
{
"id": "153-shadcn-line-dots-colors",
- "sha256": "d21c0ef9544a45b8a4150e2a0e4ed251e18f645e72ef921453aca8a20a50ce9e",
- "bytes": 2944
+ "sha256": "b30f2abef04a73e86fd5002084a6e8346c5a3cc6efd6a9c99302a1901c78768e",
+ "bytes": 2960
},
{
"id": "154-shadcn-line-dots-custom",
- "sha256": "d9a0b32b1da0c9674163e0dcf52f7d9642dc2fee0b92210505b0d989b86f93f3",
- "bytes": 5853
+ "sha256": "4ffc3d4c4abf8c4e13a717ef45ce21c9958cc05cecb9935e1be79dc4d09df5fd",
+ "bytes": 5869
},
{
"id": "155-shadcn-line-dots",
- "sha256": "4f3670ad28f6eb33bf38a884e72392d370b1430d4b7cd160336839dfb02addf5",
- "bytes": 4534
+ "sha256": "4b453318da0cca23ee300a3b52f75417f7e1690510ad5cef0c559464321a9149",
+ "bytes": 4550
},
{
"id": "156-shadcn-line-interactive",
- "sha256": "2f29937ee3faa87d3529b6188dc26e675e9f3366d32d8a46a3df20b73e7c124d",
- "bytes": 6741
+ "sha256": "4fa8a838970cd890334bf1f75a1417ae0e1ae1776903e4a49286733c6aa5b107",
+ "bytes": 6757
},
{
"id": "157-shadcn-line-label-custom",
- "sha256": "5cf8c894bf7a245294aa430346f35f784eb31ca851adc20003f3e8e2b0a2dfdf",
- "bytes": 3311
+ "sha256": "482f0889f5d2f9d9e1990b20a037669e685a64b5563a1355dc9b2cb0a864cd44",
+ "bytes": 3327
},
{
"id": "158-shadcn-line-label",
- "sha256": "7c5a761408b8e87f4754fc3c2a2b83405c5a98c0d9b182b3c8248a2165291fe5",
- "bytes": 4599
+ "sha256": "49cb34092b35739f2440d447b300cb1bd4232a66ce25d3dec33a6882da88eca0",
+ "bytes": 4615
},
{
"id": "159-shadcn-line-linear",
- "sha256": "e9587089ec0163c6545b51d9ee95a1b3da10273a6c5f7a9f5f406be7943bb1f7",
- "bytes": 3189
+ "sha256": "0f0b6cc3b0ad04848afc77e6fdafa5ff171d944544ca92cbc822b813fc5f5eb1",
+ "bytes": 3205
},
{
"id": "160-shadcn-line-multiple",
- "sha256": "57b3225b9243f2eb3fd5b11161fb2f1a8633ef14f0d8742a266e36f0022f801c",
- "bytes": 3839
+ "sha256": "2b8203e67c4ae380a933558a3b644d9b162b67932f0b3094470ccac670875cf6",
+ "bytes": 3855
},
{
"id": "161-shadcn-line-step",
- "sha256": "cf59d3d81c032133e4e8baf220c794a307db94f435d30c9c74e9f5bbd2ff1bd7",
- "bytes": 3259
+ "sha256": "4a36ff0ccb69585689d43deae51b9794131b1ec7086b6e3e2cde6117191c513f",
+ "bytes": 3275
},
{
"id": "162-shadcn-pie-donut-active",
- "sha256": "d125c3442aec632ba9a2d134b357e80374d6d3711158f6f0d95801090995e227",
- "bytes": 3231
+ "sha256": "7766f139b2ac4cca286d7be14dd56be707e7d3a1460d5778aff935e721ace554",
+ "bytes": 3247
},
{
"id": "163-shadcn-pie-donut",
- "sha256": "1e11d8f7d142e8a212cd0edda65eda1438d53ebb337fd5ea4ea9921544eef12c",
- "bytes": 2847
+ "sha256": "ea3681b806aec12aa4fa6a131dcf4a3b6ea87766bcef23ea3297cf47ddab791e",
+ "bytes": 2863
},
{
"id": "164-shadcn-pie-interactive",
- "sha256": "64ec9558e3bb84f3c9379fa84ce06374852277107cf0234f3fa041ecb6e44874",
- "bytes": 4203
+ "sha256": "3c10bf06412993f8bb64b77b4f7f7d5d62e24097c58d55d5e03b4ce7c9ded02a",
+ "bytes": 4219
},
{
"id": "165-shadcn-pie-label-custom",
- "sha256": "a0f3095ba32ab14a23d0b194af2f2f362f33cd14751c1ffc221e6dc949d1b865",
- "bytes": 3798
+ "sha256": "fd7ef66385f2b56ca4b88aa1dc7f2433cbbd83603b6bfa708c6dfd4461eaa6c0",
+ "bytes": 3814
},
{
"id": "166-shadcn-pie-label-list",
- "sha256": "b5900b56dca82724c7cf79ab9da5c879c0c8920009c8f7c6a0cab147803072b0",
- "bytes": 3909
+ "sha256": "031cfc2d3ee8754bc3d6053bf5f0659b29656247bc979a3aafe84ac37a96b8b0",
+ "bytes": 3925
},
{
"id": "167-shadcn-pie-label",
- "sha256": "6e03bd9b9b73e7e21cf3cfc6e29b6ae0f2f226616f48e313acd646753880ed6d",
- "bytes": 4765
+ "sha256": "dd1923f46633301045625c06e06e4a1feaaf8e6409c09fd96997ce64171391e6",
+ "bytes": 4781
},
{
"id": "168-shadcn-pie-legend",
- "sha256": "0a1a72cbce6b626cd56cd838ab673431cf1ebbb65f8bc359b464dd5f1a6367c0",
- "bytes": 2686
+ "sha256": "5db75dd923761942ff4327fe88db0f89359ae9146b98abe47ded6fd9ea87d8c7",
+ "bytes": 2702
},
{
"id": "169-shadcn-pie-separator-none",
- "sha256": "66b7baa6996a258b43616df0bb80250b22f403e452bb20598fa151cd8a1464c9",
- "bytes": 2694
+ "sha256": "795c1653d78f6abd95faa2e06d4954b700553e35f9217bb03ad7be2d028d6edb",
+ "bytes": 2710
},
{
"id": "170-shadcn-pie-simple",
- "sha256": "fc94d7c6d4b9b7d5d5e7c2d7509d31e9a45e8952a371ab23652179740ed0ab4f",
- "bytes": 2677
+ "sha256": "8829187c02c3cc68f17576fdfe432d22276d44cc84914f2bdd8d18c41b88e890",
+ "bytes": 2693
},
{
"id": "171-shadcn-pie-stacked",
- "sha256": "a5a3af184ff5fc62f0d8f3ffc02b5c9073fc38c550cb2a798d7e6489f838b6d0",
- "bytes": 4176
+ "sha256": "5ae69c976cfe4f692ac8ebfeeac1cec84e3a16a67e512f1ca832345c6ce045a4",
+ "bytes": 4192
},
{
"id": "172-shadcn-radar-default",
- "sha256": "7c13ad5e33e9969614c79130b678a23e4689c73e902990456056fe6feab12137",
- "bytes": 5091
+ "sha256": "8378b452ee0447ab9d42ddfa366a7df8b64a99b02a65af892130aa3766eebd47",
+ "bytes": 5107
},
{
"id": "173-shadcn-radar-dots",
- "sha256": "32fed94017e38d4a30df7b66d774bf74bf057ee1887856b9e16320f9eb173f83",
- "bytes": 5999
+ "sha256": "5caa44008f965378baa822b94b2e8d0fd2c95162bb8f45c5b8a0567ef6ffeebd",
+ "bytes": 6015
},
{
"id": "174-shadcn-radar-grid-circle-fill",
- "sha256": "da657106ffa05bd2736af664b0fccdc52b3aac1719952b31c0beb0af11886166",
- "bytes": 5244
+ "sha256": "5f5571ae33271a0757d4caa72619a3d24e8368915d6fd170076e686a6158d5d0",
+ "bytes": 5260
},
{
"id": "175-shadcn-radar-grid-circle-no-lines",
- "sha256": "22638099aae601857378a2db16b9f78f35dec1ba5eb4336a7b2d098cc7bdb766",
- "bytes": 5965
+ "sha256": "0efef3a0ff2e1b1a0b4494d2765a466d754ce95a74a9b1b787bb88b857565596",
+ "bytes": 5981
},
{
"id": "176-shadcn-radar-grid-circle",
- "sha256": "c3b6eacfe95022e0cdba6931b2980362d23fcf691629e04c7f2f24277e0c8caa",
- "bytes": 5966
+ "sha256": "912fc166a6b9fc138bee5b3342f9eafcc34cccc6c4afb6267d83b38adebc88f1",
+ "bytes": 5982
},
{
"id": "177-shadcn-radar-grid-custom",
- "sha256": "dc02f8d475f6fb647611a3a9fb0c676825357cc91c541ef20b3bd07594872bd7",
- "bytes": 4438
+ "sha256": "e41a45b7ef82a9593b667b8f0e7e23446ff8e67890a71149fa9168d8520ca2ae",
+ "bytes": 4454
},
{
"id": "178-shadcn-radar-grid-fill",
- "sha256": "3ea97459476e282528d6d3f29628bd038fb484edabfec3f0bf6006fdc122af01",
- "bytes": 5277
+ "sha256": "f6f48b73a27616bb3e1e5c116759943ed20fae3a4ae590508b75b6484e840a4d",
+ "bytes": 5293
},
{
"id": "179-shadcn-radar-grid-none",
- "sha256": "aa750772afe6ddf4eaa2ed2a3d41e836b833866764318cde8404fecd784b4144",
- "bytes": 5017
+ "sha256": "212323aac7bd9361bde297b601dd416dab26b13a32559c828dac44bee1beb6c9",
+ "bytes": 5033
},
{
"id": "180-shadcn-radar-icons",
- "sha256": "229b4e91ee95a4b62b8ce88d69b39f3859e3b19d722200a10a98fe41db6c5b5c",
- "bytes": 5528
+ "sha256": "10a703a323eb10090051560b52f11317468006e9f7d7d18486322c58a0ecd8d4",
+ "bytes": 5544
},
{
"id": "181-shadcn-radar-label-custom",
- "sha256": "cfb621726fb1a43e809e0c3331005b637a0b6f287665b72940e6ab024d3c89f7",
- "bytes": 6893
+ "sha256": "a01a6cb688b465ba300b0b176bce46e2423c76d648358294f6caae6ebb5a199a",
+ "bytes": 6909
},
{
"id": "182-shadcn-radar-legend",
- "sha256": "8b0e06871c7fd91f65fe16ed4fdcbf414e39cad810a00ba3f2496e2c845cd596",
- "bytes": 5529
+ "sha256": "e1b179b9b1d938a1ef7f9c2cdb21ff965391a3c0346928fa53fb1dd708cdd180",
+ "bytes": 5545
},
{
"id": "183-shadcn-radar-lines-only",
- "sha256": "644b114c0f83eaef993a4ca21b6cbdcaf6466943e5ffbbfd4dee4afdd9ff08f8",
- "bytes": 5526
+ "sha256": "8ab56689c65af262672a3d50510446fb380e34ff4d21e6dfa7207d15c7acca73",
+ "bytes": 5542
},
{
"id": "184-shadcn-radar-radius",
- "sha256": "88bccc24fa557d8eb6d9e2f3f79da0030da630fef112300e61bc4cda45202167",
- "bytes": 4520
+ "sha256": "5eaa50e5efb14e29e25c6c5e2967b6a4a72c1e8f1ccc24fb59947a457c1d0570",
+ "bytes": 4536
},
{
"id": "185-shadcn-radial-grid",
- "sha256": "7b19c0c5c92e1619073b98977ccb802e029a1491e2b1dbc4134cf8650baaf804",
- "bytes": 5207
+ "sha256": "f503eb6272c054e951b6c2137454fe1b3671a7182738bd6b7d18a8f262eb4a7c",
+ "bytes": 5223
},
{
"id": "186-shadcn-radial-label",
- "sha256": "d7707821c9e1ee0cddc56c1887fe997336fd28e88cc160cf224d7bd82ab79811",
- "bytes": 5840
+ "sha256": "68f8a11ee336cbe0ef34bdb79bdbd82ebb919794b0fde948e354b0cb02275a86",
+ "bytes": 5856
},
{
"id": "187-shadcn-radial-shape",
- "sha256": "bc48c8ca4252afc7ddd6efefe36d0b96621611b849bebc7d6003453e3365483f",
- "bytes": 2829
+ "sha256": "841af90860d36aa1b3e05860fa0ef893dd1db42793cdb8006020fe125bace66b",
+ "bytes": 2845
},
{
"id": "188-shadcn-radial-simple",
- "sha256": "fb8a00785b0632a4baf7718010820b9b23439ed599960758c831ffecec35cae9",
- "bytes": 4144
+ "sha256": "d50da1169fd4ca661b1d2704e70810087077773f61c80dbf0b7ade8ef2fd8c6a",
+ "bytes": 4160
},
{
"id": "189-shadcn-radial-stacked",
- "sha256": "c2529831101e64b2fe2e8ede8e5f6f562e74fd6987bbe89dd6116fc3ec2f26f2",
- "bytes": 2955
+ "sha256": "b280f74e86ffef5e6865462a9be2d51b4e35479223f49df6fc73c64b063b91dd",
+ "bytes": 2971
},
{
"id": "190-shadcn-tooltip-default",
- "sha256": "397ad7d5f553609bc3546171d6db679cdd46d72daa3b841b1bb33063a4021622",
- "bytes": 5002
+ "sha256": "fe48eaec0c292c1338d653953946483982ef1b23ec356478b24bbf11730fd3f9",
+ "bytes": 5018
},
{
"id": "191-shadcn-tooltip-formatter",
- "sha256": "ce18e3fa959861959737c03962b9ab09935a5bd32c1df60697ad2f34a001771a",
- "bytes": 5004
+ "sha256": "43eb6270aa388f3a3004cec62e53f0fbdb73ff5ddc5c4190649ff64d61730240",
+ "bytes": 5020
},
{
"id": "192-shadcn-tooltip-icons",
- "sha256": "79727762928ce8a1fd484ffd56a47b7e6b6e8ebe9bf43ffdb3522592f26469f4",
- "bytes": 5000
+ "sha256": "bda712a532d9455b3409e7dd4280e1e9262a1bc805fe6b7ffcc237de9693e39e",
+ "bytes": 5016
},
{
"id": "193-shadcn-tooltip-indicator-line",
- "sha256": "a21c33af271d750cf8b7d8b9816c5aae0c5510feebdf25d702201de7f8c0b9f9",
- "bytes": 5009
+ "sha256": "a46a207d4db94529ad698128a0f348300212593c91517fbeb05a4a6e9de07888",
+ "bytes": 5025
},
{
"id": "194-shadcn-tooltip-indicator-none",
- "sha256": "df8dfc52f4dbb10ce2c75477b2f1014b48e71b5ffda81327adff7c794236ccc3",
- "bytes": 5007
+ "sha256": "ac6fe965bea2a6588cc0597a595fa8624c7f6cac3b38a20201ae8807f0ba4514",
+ "bytes": 5023
},
{
"id": "195-shadcn-tooltip-label-custom",
- "sha256": "ca9e23c3c2caad08b2f1b93713d7fe51ec0422912746aff2111cfa3cc335dc70",
- "bytes": 5007
+ "sha256": "b09982f65a0481e1c2efdee73d3132058594acdc9786d8c1f9ad877816752536",
+ "bytes": 5023
},
{
"id": "196-shadcn-tooltip-label-formatter",
- "sha256": "3a1cc49b5994d586fda7d6767d0b09a653aee93fa6b507b00a6f66f6c32c841d",
- "bytes": 5010
+ "sha256": "b18e7ed66963b22aa33bc543ab08d119be452cf7e6f77d485f8756694dd16f2f",
+ "bytes": 5026
},
{
"id": "197-shadcn-tooltip-label-none",
- "sha256": "9eb0e1ea46d89e6752804d62cf5d4fdcb04c3633508895269f28c1d7aa88fc7a",
- "bytes": 5003
+ "sha256": "af6631dd0b4e88a936d9cff1ae28a90b549ef6c2d72513b90e9dadb80d4f437a",
+ "bytes": 5019
}
]
}
diff --git a/benchmarks/conformance/previews/scatter-bubble.svg b/benchmarks/conformance/previews/scatter-bubble.svg
index 5812a7f0..8a354d3b 100644
--- a/benchmarks/conformance/previews/scatter-bubble.svg
+++ b/benchmarks/conformance/previews/scatter-bubble.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/entries/charts-axis-label-styles.ts b/benchmarks/entries/charts-axis-label-styles.ts
new file mode 100644
index 00000000..a0c82ad6
--- /dev/null
+++ b/benchmarks/entries/charts-axis-label-styles.ts
@@ -0,0 +1,32 @@
+import {
+ createChartScene,
+ defineChart,
+ lineY,
+ renderChartSvg,
+} from '@tanstack/charts'
+import { scaleLinear } from 'd3-scale'
+
+const definition = defineChart({
+ marks: [lineY([4, 9, 7])],
+ scales: {
+ x: {
+ scale: scaleLinear().domain([0, 2]),
+ axis: {
+ label: {
+ text: 'Quarter',
+ fontSize: 13,
+ fontWeight: 700,
+ fill: '#2563eb',
+ opacity: 0.8,
+ },
+ },
+ },
+ y: { scale: scaleLinear().domain([0, 10]) },
+ },
+})
+
+export function render(width: number, height: number) {
+ return renderChartSvg(createChartScene(definition, { width, height }), {
+ ariaLabel: 'Styled axis title',
+ })
+}
diff --git a/docs/comparison.md b/docs/comparison.md
index 912adf20..a3d86897 100644
--- a/docs/comparison.md
+++ b/docs/comparison.md
@@ -12,14 +12,14 @@ turning untested behavior into a checkmark.
| Library | Package | Measured source |
| -------------------------------------------------------------------------------------- | -------------------- | ------------------- |
-| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3df87d7` |
+| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `699b4be` |
| [Chart.js](https://www.chartjs.org/docs/latest/) | `chart.js` | npm `4.5.1` |
| [Apache ECharts](https://echarts.apache.org/handbook/en/best-practices/canvas-vs-svg/) | `echarts` | npm `6.1.0` |
| [Recharts](https://recharts.github.io/en-US/) | `recharts` | npm `3.10.1` |
| [Observable Plot](https://observablehq.com/plot/features/plots) | `@observablehq/plot` | npm `0.6.17` |
The competitor versions are exact package pins, not latest versions inferred
-at page render time. The measured TanStack workspace revision is `3df87d7`.
+at page render time. The measured TanStack workspace revision is `699b4be`.
## Capability matrix
@@ -90,7 +90,7 @@ output model.
## Bundle snapshot
-Baseline date: `2026-08-26`.
+Baseline date: `2026-09-03`.
Controlled ranges cover 12 independently built, minified browser consumers:
line, bar, area, and scatter at basic, interactive, and advanced tiers. Only
@@ -106,7 +106,7 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July
| Library | Bundle size | React externalized | Evidence |
| ------------------ | -------------------------------------- | -----------------: | ---------------------------------------------------------- |
-| TanStack Charts | 38.96–44.98 KiB | Not applicable | Controlled suite |
+| TanStack Charts | 39.22–45.28 KiB | Not applicable | Controlled suite |
| D3 | 90 KB gzip | — | External main export |
| Chart.js | 44.70–58.21 KiB | — | Controlled suite |
| Apache ECharts | 153.10–173.18 KiB | — | Controlled suite |
diff --git a/docs/concepts/layout-axes-and-coordinates.md b/docs/concepts/layout-axes-and-coordinates.md
index 9aa67858..ae21a276 100644
--- a/docs/concepts/layout-axes-and-coordinates.md
+++ b/docs/concepts/layout-axes-and-coordinates.md
@@ -152,20 +152,27 @@ const x = {
format: (date: Date) => monthFormatter.format(date),
},
tickLabels: { rotate: -30 },
- label: { text: 'Month', offset: 12 },
+ label: {
+ text: 'Month',
+ offset: 12,
+ fontSize: 13,
+ fontWeight: 600,
+ fill: '#334155',
+ opacity: 0.9,
+ },
},
}
```
-| Option | Purpose |
-| ----------------- | ---------------------------------------------------- |
-| `axis` | Configure the axis or hide it with `false` |
-| `axis.line` | Show or hide the baseline |
-| `axis.ticks` | Configure candidates, stubs, padding, and formatting |
-| `axis.tickLabels` | Configure label rotation and collision thinning |
-| `axis.label` | Configure the axis title and offset |
-| `grid` | Draw grid lines at semantic candidates |
-| `reverse` | Reverse the responsive range |
+| Option | Purpose |
+| ----------------- | -------------------------------------------------------- |
+| `axis` | Configure the axis or hide it with `false` |
+| `axis.line` | Show or hide the baseline |
+| `axis.ticks` | Configure candidates, stubs, padding, and formatting |
+| `axis.tickLabels` | Configure label rotation and collision thinning |
+| `axis.label` | Configure axis title text, typography, paint, and offset |
+| `grid` | Draw grid lines at semantic candidates |
+| `reverse` | Reverse the responsive range |
The y grid defaults to visible and the x grid defaults to hidden when `grid` is omitted.
diff --git a/docs/examples/interactive-charts.md b/docs/examples/interactive-charts.md
index 62f3b914..9a3bef96 100644
--- a/docs/examples/interactive-charts.md
+++ b/docs/examples/interactive-charts.md
@@ -49,6 +49,21 @@ dot(rows, {
})
```
+The same definition can make the built-in indicator smaller while retaining
+its series-colored outline:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ },
+})
+```
+
A focused band emphasizes the shared x value for every series. Its position
diff --git a/docs/examples/lines-and-areas.md b/docs/examples/lines-and-areas.md
index 03e8bd35..d72d0323 100644
--- a/docs/examples/lines-and-areas.md
+++ b/docs/examples/lines-and-areas.md
@@ -48,6 +48,7 @@ const chart = defineChart({
y: 'downloads',
points: true,
stroke: '#2563eb',
+ lineCap: 'butt',
}),
],
scales: {
@@ -64,6 +65,10 @@ const chart = defineChart({
export default chart
```
+`lineCap: 'butt'` ends the stroke at each path endpoint. Omit it to retain the
+default round endpoints. `lineX` accepts the same `lineCap` and `lineJoin`
+options.
+
## Compare several series from a common baseline
Indexing each series to its first observation compares relative change when the
diff --git a/docs/guides/themes-and-styling.md b/docs/guides/themes-and-styling.md
index cbea3392..1cd79e40 100644
--- a/docs/guides/themes-and-styling.md
+++ b/docs/guides/themes-and-styling.md
@@ -93,6 +93,37 @@ Keep these responsibilities separate:
For categorical or quantitative color mapping, use the canonical
[Legends and Color](./legends-and-color.md) guide.
+## Axis title styling
+
+String axis titles use the chart foreground and built-in title typography.
+Use the object form when one title needs an explicit text color, opacity, size,
+weight, or offset:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales: {
+ x: { scale: xScale, axis: { label: 'Month' } },
+ y: {
+ scale: yScale,
+ axis: {
+ label: {
+ text: 'Revenue (USD)',
+ fontSize: 14,
+ fontWeight: 700,
+ fill: '#2563eb',
+ opacity: 0.85,
+ },
+ },
+ },
+ },
+})
+```
+
+`fill` is the title's text color. The layout measures the configured font size
+and weight before resolving automatic guide margins, so larger titles do not
+need matching manual margins.
+
## Canvas styling
The Canvas renderer resolves scene paints such as `currentColor` and CSS
diff --git a/docs/reference/chart-definitions.md b/docs/reference/chart-definitions.md
index 74996619..9257c6cc 100644
--- a/docs/reference/chart-definitions.md
+++ b/docs/reference/chart-definitions.md
@@ -103,6 +103,13 @@ does not return or own them.
belong to both static and responsive definitions. Hosts and framework adapters
do not override them.
+`focusRing` accepts `true`, `false`, or a `ChartFocusRingOptions` object with
+`radius`, `strokeWidth`, `fill`, and `stroke`. Omitted fields keep the built-in
+defaults, including the focused point's resolved color for `stroke`. The same
+value on `theme.focusRing` provides a default, while an explicitly supplied
+definition-level value takes precedence. See
+[Focus and interaction](./focus-and-interaction.md#built-in-focus-ring).
+
Each `ChartControl` resolves after final scales and plot bounds exist. It can
provide renderer-neutral fallback nodes and an optional host control. Control
IDs and host-control identities must be unique. Browser hosts remove a
diff --git a/docs/reference/chart-spec.md b/docs/reference/chart-spec.md
index a8bf4c20..b29735c0 100644
--- a/docs/reference/chart-spec.md
+++ b/docs/reference/chart-spec.md
@@ -178,11 +178,14 @@ interface ChartTheme {
grid: string
background: string
palette: readonly string[]
+ focusRing?: boolean | ChartFocusRingOptions
}
```
`theme` is partial. The palette is replaced as one value rather than merged by
-index. The default palette uses CSS custom-property fallbacks:
+index. `focusRing` supplies the default built-in focus ring presentation for
+every chart using the theme. A definition-level `focusRing` value overrides it.
+The default palette uses CSS custom-property fallbacks:
```css
.dashboard {
diff --git a/docs/reference/dom-host.md b/docs/reference/dom-host.md
index 85633106..e094e536 100644
--- a/docs/reference/dom-host.md
+++ b/docs/reference/dom-host.md
@@ -116,7 +116,7 @@ The definition owns these chart controls:
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `maxFocusDistance` | `48` | Maximum scene-pixel distance for default pointer focus |
| `focus` | Nearest point | Pointer grouping and keyboard navigation strategy; `false` disables chart-owned focus and its generated layer |
-| `focusRing` | `true` | Generated primary-point focus indicator; `false` keeps authored focus layers only |
+| `focusRing` | `true` | Generated primary-point indicator; an options object styles it and `false` keeps authored focus layers only |
| `cursor` | None | Focus-snapped or free application-owned cursor binding |
| `spatialIndex` | Linear nearest-point scan | Dense-data nearest-point index |
| `svgAnimation` | `false` | Keyed attribute, enter, and exit animation |
diff --git a/docs/reference/focus-and-interaction.md b/docs/reference/focus-and-interaction.md
index 96ddf62d..89d4f754 100644
--- a/docs/reference/focus-and-interaction.md
+++ b/docs/reference/focus-and-interaction.md
@@ -36,6 +36,43 @@ Authored `whenFocused` marks compose with the primary-point ring. Set
definition `focusRing: false` only when authored focus geometry replaces that
indicator.
+### Built-in focus ring
+
+Pass an options object to change the built-in ring without targeting generated
+SVG:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ },
+})
+```
+
+| Option | Default |
+| ------------- | ----------------------------------------- |
+| `radius` | `5` |
+| `strokeWidth` | `2.5` |
+| `fill` | `var(--ts-chart-focus-fill, Canvas)` |
+| `stroke` | The focused point's resolved series color |
+
+The object form still enables the ring. `true` keeps every default and `false`
+removes only the built-in ring. Set the same value on `theme.focusRing` to
+provide a theme default. An explicitly supplied definition-level `focusRing`
+value takes precedence, including `true` or `false`. Composed views and facet
+cells share the outer chart's focus layer, so configure their focus ring on
+the outer definition or theme. Their child specs reject both `focusRing` and
+`theme.focusRing`. The definition compiles this presentation into ordinary
+scene dots, so SVG, Canvas, React Native, facets, polar marks, custom mark
+renderers, and the optional motion renderer share the same values. The ring
+remains hidden from the accessibility tree; the chart root continues to report
+focus and values. `radius` and `strokeWidth` accept finite nonnegative numbers,
+including zero. Invalid values use their defaults.
+
Use `focusGuideX` or `focusGuideY` from `@tanstack/charts/focus/guide` for
datum-bound rules, markers, and axis labels. Their active geometry uses stable
structural keys, so the optional motion renderer can animate rapid focus
diff --git a/docs/reference/marks/line-and-area.md b/docs/reference/marks/line-and-area.md
index 2f776f67..a54b015d 100644
--- a/docs/reference/marks/line-and-area.md
+++ b/docs/reference/marks/line-and-area.md
@@ -55,6 +55,8 @@ function lineY(
| `strokeOpacity` | `number` | SVG default | Stroke opacity |
| `strokeWidth` | `number` | `2.25` | Stroke width |
| `strokeDasharray` | `string` | None | SVG dash array |
+| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape |
+| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape |
| `points` | `boolean` | `false` | Draws a radius-`2.5` dot at each valid point |
| `curve` | `ChartCurve` | Straight segments | Optional path generator |
| `states` | `readonly ChartMarkState[]` | None | Focus-driven presentation overrides |
@@ -95,6 +97,13 @@ input order remains path order. Grouping, paint, points, curves, states, and
motion use the same contract. Line interaction follows y affinity so keyboard
and pointer traversal match the longitudinal axis.
+`lineX` inherits these stroke-shape options from the shared line contract:
+
+| Option | Type | Default | Meaning |
+| ---------- | ------------------------ | --------- | --------------------- |
+| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape |
+| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape |
+
## `areaY`
`areaY` fills between a numeric upper channel and a numeric lower baseline
diff --git a/docs/reference/runtime-and-scene.md b/docs/reference/runtime-and-scene.md
index ab5be291..a8836b22 100644
--- a/docs/reference/runtime-and-scene.md
+++ b/docs/reference/runtime-and-scene.md
@@ -114,7 +114,8 @@ The exported theme contains:
- six CSS-variable-backed palette entries
Definitions merge partial overrides into this value. A supplied palette
-replaces the default palette. See [Chart spec](./chart-spec.md#theme).
+replaces the default palette. An omitted focus ring defaults to enabled. See
+[Chart spec](./chart-spec.md#theme).
## `findNearestPoint`
diff --git a/docs/reference/scales-guides-and-color.md b/docs/reference/scales-guides-and-color.md
index 69af1cab..0bfb7097 100644
--- a/docs/reference/scales-guides-and-color.md
+++ b/docs/reference/scales-guides-and-color.md
@@ -228,7 +228,17 @@ interface ChartAxisOptions {
keep?: readonly TValue[]
}
}
- label?: string | { text: string; offset?: number | 'auto' }
+ label?:
+ | string
+ | {
+ text: string
+ offset?: number | 'auto'
+ fontSize?: number
+ fontWeight?: number
+ fill?: string
+ opacity?: number
+ motion?: ChartMotionDefinition
+ }
}
}
```
@@ -343,12 +353,37 @@ const x = {
}
```
-`anchor` defaults to the rotation-derived x anchor or `end` on y. `dx` and
+`anchor` defaults to an outward anchor derived from x rotation or the y-axis
+side. The automatic value accounts for the host's inline direction. `dx` and
`dy` apply after the normal tick position and padding. Resolved font size,
weight, anchor, offset, opacity, and rotation all participate in collision
thinning and automatic margins. Numeric typography follows tick-label motion;
anchor changes snap.
+Axis titles keep the compact string form when only text is needed. Use the
+object form for title typography, paint, offset, or motion:
+
+```ts
+const y = {
+ scale: scaleLinear,
+ axis: {
+ label: {
+ text: 'Average order value (PLN)',
+ fontSize: 14,
+ fontWeight: 500,
+ fill: '#363636',
+ opacity: 0.9,
+ },
+ },
+}
+```
+
+`fill` sets the text color. Omitted presentation fields retain the existing
+title defaults: responsive 10 or 11 pixel sizing on x, 11 pixels on y, weight
+600, theme foreground fill, and 0.76 fill opacity. Configured font size and
+weight participate in text measurement, automatic offsets, and automatic
+margins for SVG, Canvas, and native rendering.
+
## Named scales and multiple axes
The reserved `x` and `y` entries are the default bindings for Cartesian marks.
@@ -396,6 +431,12 @@ mapping should not draw another axis. X scales can use the top or bottom side,
and y scales can use the left or right side. Axes on the same side stack
outward and contribute their measured size to the automatic margin.
+Axis sides stay physical in a right-to-left container, so `side: 'right'`
+still puts the axis on the right. Automatic tick anchors account for the
+container's inline direction and keep labels outside the plot. An explicitly
+authored `start` or `end` anchor remains logical. Text measurement, SVG,
+Canvas, native rendering, and standalone SVG export preserve that direction.
+
## Automatic guide layout
When a margin side is omitted, the scene compiler measures:
diff --git a/docs/reference/types.md b/docs/reference/types.md
index f219b23b..4dd31039 100644
--- a/docs/reference/types.md
+++ b/docs/reference/types.md
@@ -218,7 +218,7 @@ See [Scene nodes](./runtime-and-scene.md#scene-nodes).
| `ChartAxisTickLabelContext` | Semantic value, stable candidate index, pixel position, and bandwidth |
| `ChartAxisTickLabelValue` | Constant or context accessor for one tick-label property |
| `ChartAxisTickLabelThinOptions` | Minimum gap, end priority, and labels that must be kept |
-| `ChartAxisLabelOptions` | Axis title text and explicit or measured offset |
+| `ChartAxisLabelOptions` | Axis title text, typography, paint, motion, and offset |
| `ChartScaleFactory` | Creates a positional scale with a mark-inferred domain |
| `ChartScaleInput` | Factory or configured positional scale instance |
| `InferableScaleLike` | Domain-configurable scale returned by a factory |
@@ -247,7 +247,7 @@ See [Scene nodes](./runtime-and-scene.md#scene-nodes).
| `CrosshairBandOptions` | Categorical cursor-band inset, radius, fill, stroke, and opacity |
| `CrosshairLabelOptions` | Guide label formatting, spacing, text, and halo paint |
| `CrosshairMarkerOptions` | Primary-coordinate marker geometry and paint |
-| `ChartTheme` | Foreground, muted, grid, background, and palette |
+| `ChartTheme` | Foreground, muted, grid, background, palette, and focus ring defaults |
| `ChartLinearGradient` | Named linear-gradient resource |
| `ChartGradientStop` | Gradient offset, color, and optional opacity |
| `ChartCurve` | Line and y-area path generation |
diff --git a/packages/charts-core/docs/comparison.md b/packages/charts-core/docs/comparison.md
index 912adf20..a3d86897 100644
--- a/packages/charts-core/docs/comparison.md
+++ b/packages/charts-core/docs/comparison.md
@@ -12,14 +12,14 @@ turning untested behavior into a checkmark.
| Library | Package | Measured source |
| -------------------------------------------------------------------------------------- | -------------------- | ------------------- |
-| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3df87d7` |
+| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `699b4be` |
| [Chart.js](https://www.chartjs.org/docs/latest/) | `chart.js` | npm `4.5.1` |
| [Apache ECharts](https://echarts.apache.org/handbook/en/best-practices/canvas-vs-svg/) | `echarts` | npm `6.1.0` |
| [Recharts](https://recharts.github.io/en-US/) | `recharts` | npm `3.10.1` |
| [Observable Plot](https://observablehq.com/plot/features/plots) | `@observablehq/plot` | npm `0.6.17` |
The competitor versions are exact package pins, not latest versions inferred
-at page render time. The measured TanStack workspace revision is `3df87d7`.
+at page render time. The measured TanStack workspace revision is `699b4be`.
## Capability matrix
@@ -90,7 +90,7 @@ output model.
## Bundle snapshot
-Baseline date: `2026-08-26`.
+Baseline date: `2026-09-03`.
Controlled ranges cover 12 independently built, minified browser consumers:
line, bar, area, and scatter at basic, interactive, and advanced tiers. Only
@@ -106,7 +106,7 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July
| Library | Bundle size | React externalized | Evidence |
| ------------------ | -------------------------------------- | -----------------: | ---------------------------------------------------------- |
-| TanStack Charts | 38.96–44.98 KiB | Not applicable | Controlled suite |
+| TanStack Charts | 39.22–45.28 KiB | Not applicable | Controlled suite |
| D3 | 90 KB gzip | — | External main export |
| Chart.js | 44.70–58.21 KiB | — | Controlled suite |
| Apache ECharts | 153.10–173.18 KiB | — | Controlled suite |
diff --git a/packages/charts-core/docs/concepts/layout-axes-and-coordinates.md b/packages/charts-core/docs/concepts/layout-axes-and-coordinates.md
index 9aa67858..ae21a276 100644
--- a/packages/charts-core/docs/concepts/layout-axes-and-coordinates.md
+++ b/packages/charts-core/docs/concepts/layout-axes-and-coordinates.md
@@ -152,20 +152,27 @@ const x = {
format: (date: Date) => monthFormatter.format(date),
},
tickLabels: { rotate: -30 },
- label: { text: 'Month', offset: 12 },
+ label: {
+ text: 'Month',
+ offset: 12,
+ fontSize: 13,
+ fontWeight: 600,
+ fill: '#334155',
+ opacity: 0.9,
+ },
},
}
```
-| Option | Purpose |
-| ----------------- | ---------------------------------------------------- |
-| `axis` | Configure the axis or hide it with `false` |
-| `axis.line` | Show or hide the baseline |
-| `axis.ticks` | Configure candidates, stubs, padding, and formatting |
-| `axis.tickLabels` | Configure label rotation and collision thinning |
-| `axis.label` | Configure the axis title and offset |
-| `grid` | Draw grid lines at semantic candidates |
-| `reverse` | Reverse the responsive range |
+| Option | Purpose |
+| ----------------- | -------------------------------------------------------- |
+| `axis` | Configure the axis or hide it with `false` |
+| `axis.line` | Show or hide the baseline |
+| `axis.ticks` | Configure candidates, stubs, padding, and formatting |
+| `axis.tickLabels` | Configure label rotation and collision thinning |
+| `axis.label` | Configure axis title text, typography, paint, and offset |
+| `grid` | Draw grid lines at semantic candidates |
+| `reverse` | Reverse the responsive range |
The y grid defaults to visible and the x grid defaults to hidden when `grid` is omitted.
diff --git a/packages/charts-core/docs/examples/interactive-charts.md b/packages/charts-core/docs/examples/interactive-charts.md
index 62f3b914..9a3bef96 100644
--- a/packages/charts-core/docs/examples/interactive-charts.md
+++ b/packages/charts-core/docs/examples/interactive-charts.md
@@ -49,6 +49,21 @@ dot(rows, {
})
```
+The same definition can make the built-in indicator smaller while retaining
+its series-colored outline:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ },
+})
+```
+
A focused band emphasizes the shared x value for every series. Its position
diff --git a/packages/charts-core/docs/examples/lines-and-areas.md b/packages/charts-core/docs/examples/lines-and-areas.md
index 03e8bd35..d72d0323 100644
--- a/packages/charts-core/docs/examples/lines-and-areas.md
+++ b/packages/charts-core/docs/examples/lines-and-areas.md
@@ -48,6 +48,7 @@ const chart = defineChart({
y: 'downloads',
points: true,
stroke: '#2563eb',
+ lineCap: 'butt',
}),
],
scales: {
@@ -64,6 +65,10 @@ const chart = defineChart({
export default chart
```
+`lineCap: 'butt'` ends the stroke at each path endpoint. Omit it to retain the
+default round endpoints. `lineX` accepts the same `lineCap` and `lineJoin`
+options.
+
## Compare several series from a common baseline
Indexing each series to its first observation compares relative change when the
diff --git a/packages/charts-core/docs/guides/themes-and-styling.md b/packages/charts-core/docs/guides/themes-and-styling.md
index cbea3392..1cd79e40 100644
--- a/packages/charts-core/docs/guides/themes-and-styling.md
+++ b/packages/charts-core/docs/guides/themes-and-styling.md
@@ -93,6 +93,37 @@ Keep these responsibilities separate:
For categorical or quantitative color mapping, use the canonical
[Legends and Color](./legends-and-color.md) guide.
+## Axis title styling
+
+String axis titles use the chart foreground and built-in title typography.
+Use the object form when one title needs an explicit text color, opacity, size,
+weight, or offset:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales: {
+ x: { scale: xScale, axis: { label: 'Month' } },
+ y: {
+ scale: yScale,
+ axis: {
+ label: {
+ text: 'Revenue (USD)',
+ fontSize: 14,
+ fontWeight: 700,
+ fill: '#2563eb',
+ opacity: 0.85,
+ },
+ },
+ },
+ },
+})
+```
+
+`fill` is the title's text color. The layout measures the configured font size
+and weight before resolving automatic guide margins, so larger titles do not
+need matching manual margins.
+
## Canvas styling
The Canvas renderer resolves scene paints such as `currentColor` and CSS
diff --git a/packages/charts-core/docs/reference/chart-definitions.md b/packages/charts-core/docs/reference/chart-definitions.md
index 74996619..9257c6cc 100644
--- a/packages/charts-core/docs/reference/chart-definitions.md
+++ b/packages/charts-core/docs/reference/chart-definitions.md
@@ -103,6 +103,13 @@ does not return or own them.
belong to both static and responsive definitions. Hosts and framework adapters
do not override them.
+`focusRing` accepts `true`, `false`, or a `ChartFocusRingOptions` object with
+`radius`, `strokeWidth`, `fill`, and `stroke`. Omitted fields keep the built-in
+defaults, including the focused point's resolved color for `stroke`. The same
+value on `theme.focusRing` provides a default, while an explicitly supplied
+definition-level value takes precedence. See
+[Focus and interaction](./focus-and-interaction.md#built-in-focus-ring).
+
Each `ChartControl` resolves after final scales and plot bounds exist. It can
provide renderer-neutral fallback nodes and an optional host control. Control
IDs and host-control identities must be unique. Browser hosts remove a
diff --git a/packages/charts-core/docs/reference/chart-spec.md b/packages/charts-core/docs/reference/chart-spec.md
index a8bf4c20..b29735c0 100644
--- a/packages/charts-core/docs/reference/chart-spec.md
+++ b/packages/charts-core/docs/reference/chart-spec.md
@@ -178,11 +178,14 @@ interface ChartTheme {
grid: string
background: string
palette: readonly string[]
+ focusRing?: boolean | ChartFocusRingOptions
}
```
`theme` is partial. The palette is replaced as one value rather than merged by
-index. The default palette uses CSS custom-property fallbacks:
+index. `focusRing` supplies the default built-in focus ring presentation for
+every chart using the theme. A definition-level `focusRing` value overrides it.
+The default palette uses CSS custom-property fallbacks:
```css
.dashboard {
diff --git a/packages/charts-core/docs/reference/dom-host.md b/packages/charts-core/docs/reference/dom-host.md
index 85633106..e094e536 100644
--- a/packages/charts-core/docs/reference/dom-host.md
+++ b/packages/charts-core/docs/reference/dom-host.md
@@ -116,7 +116,7 @@ The definition owns these chart controls:
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `maxFocusDistance` | `48` | Maximum scene-pixel distance for default pointer focus |
| `focus` | Nearest point | Pointer grouping and keyboard navigation strategy; `false` disables chart-owned focus and its generated layer |
-| `focusRing` | `true` | Generated primary-point focus indicator; `false` keeps authored focus layers only |
+| `focusRing` | `true` | Generated primary-point indicator; an options object styles it and `false` keeps authored focus layers only |
| `cursor` | None | Focus-snapped or free application-owned cursor binding |
| `spatialIndex` | Linear nearest-point scan | Dense-data nearest-point index |
| `svgAnimation` | `false` | Keyed attribute, enter, and exit animation |
diff --git a/packages/charts-core/docs/reference/focus-and-interaction.md b/packages/charts-core/docs/reference/focus-and-interaction.md
index 96ddf62d..89d4f754 100644
--- a/packages/charts-core/docs/reference/focus-and-interaction.md
+++ b/packages/charts-core/docs/reference/focus-and-interaction.md
@@ -36,6 +36,43 @@ Authored `whenFocused` marks compose with the primary-point ring. Set
definition `focusRing: false` only when authored focus geometry replaces that
indicator.
+### Built-in focus ring
+
+Pass an options object to change the built-in ring without targeting generated
+SVG:
+
+```ts
+const definition = defineChart({
+ marks,
+ scales,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ },
+})
+```
+
+| Option | Default |
+| ------------- | ----------------------------------------- |
+| `radius` | `5` |
+| `strokeWidth` | `2.5` |
+| `fill` | `var(--ts-chart-focus-fill, Canvas)` |
+| `stroke` | The focused point's resolved series color |
+
+The object form still enables the ring. `true` keeps every default and `false`
+removes only the built-in ring. Set the same value on `theme.focusRing` to
+provide a theme default. An explicitly supplied definition-level `focusRing`
+value takes precedence, including `true` or `false`. Composed views and facet
+cells share the outer chart's focus layer, so configure their focus ring on
+the outer definition or theme. Their child specs reject both `focusRing` and
+`theme.focusRing`. The definition compiles this presentation into ordinary
+scene dots, so SVG, Canvas, React Native, facets, polar marks, custom mark
+renderers, and the optional motion renderer share the same values. The ring
+remains hidden from the accessibility tree; the chart root continues to report
+focus and values. `radius` and `strokeWidth` accept finite nonnegative numbers,
+including zero. Invalid values use their defaults.
+
Use `focusGuideX` or `focusGuideY` from `@tanstack/charts/focus/guide` for
datum-bound rules, markers, and axis labels. Their active geometry uses stable
structural keys, so the optional motion renderer can animate rapid focus
diff --git a/packages/charts-core/docs/reference/marks/line-and-area.md b/packages/charts-core/docs/reference/marks/line-and-area.md
index 2f776f67..a54b015d 100644
--- a/packages/charts-core/docs/reference/marks/line-and-area.md
+++ b/packages/charts-core/docs/reference/marks/line-and-area.md
@@ -55,6 +55,8 @@ function lineY(
| `strokeOpacity` | `number` | SVG default | Stroke opacity |
| `strokeWidth` | `number` | `2.25` | Stroke width |
| `strokeDasharray` | `string` | None | SVG dash array |
+| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape |
+| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape |
| `points` | `boolean` | `false` | Draws a radius-`2.5` dot at each valid point |
| `curve` | `ChartCurve` | Straight segments | Optional path generator |
| `states` | `readonly ChartMarkState[]` | None | Focus-driven presentation overrides |
@@ -95,6 +97,13 @@ input order remains path order. Grouping, paint, points, curves, states, and
motion use the same contract. Line interaction follows y affinity so keyboard
and pointer traversal match the longitudinal axis.
+`lineX` inherits these stroke-shape options from the shared line contract:
+
+| Option | Type | Default | Meaning |
+| ---------- | ------------------------ | --------- | --------------------- |
+| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape |
+| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape |
+
## `areaY`
`areaY` fills between a numeric upper channel and a numeric lower baseline
diff --git a/packages/charts-core/docs/reference/runtime-and-scene.md b/packages/charts-core/docs/reference/runtime-and-scene.md
index ab5be291..a8836b22 100644
--- a/packages/charts-core/docs/reference/runtime-and-scene.md
+++ b/packages/charts-core/docs/reference/runtime-and-scene.md
@@ -114,7 +114,8 @@ The exported theme contains:
- six CSS-variable-backed palette entries
Definitions merge partial overrides into this value. A supplied palette
-replaces the default palette. See [Chart spec](./chart-spec.md#theme).
+replaces the default palette. An omitted focus ring defaults to enabled. See
+[Chart spec](./chart-spec.md#theme).
## `findNearestPoint`
diff --git a/packages/charts-core/docs/reference/scales-guides-and-color.md b/packages/charts-core/docs/reference/scales-guides-and-color.md
index 69af1cab..0bfb7097 100644
--- a/packages/charts-core/docs/reference/scales-guides-and-color.md
+++ b/packages/charts-core/docs/reference/scales-guides-and-color.md
@@ -228,7 +228,17 @@ interface ChartAxisOptions {
keep?: readonly TValue[]
}
}
- label?: string | { text: string; offset?: number | 'auto' }
+ label?:
+ | string
+ | {
+ text: string
+ offset?: number | 'auto'
+ fontSize?: number
+ fontWeight?: number
+ fill?: string
+ opacity?: number
+ motion?: ChartMotionDefinition
+ }
}
}
```
@@ -343,12 +353,37 @@ const x = {
}
```
-`anchor` defaults to the rotation-derived x anchor or `end` on y. `dx` and
+`anchor` defaults to an outward anchor derived from x rotation or the y-axis
+side. The automatic value accounts for the host's inline direction. `dx` and
`dy` apply after the normal tick position and padding. Resolved font size,
weight, anchor, offset, opacity, and rotation all participate in collision
thinning and automatic margins. Numeric typography follows tick-label motion;
anchor changes snap.
+Axis titles keep the compact string form when only text is needed. Use the
+object form for title typography, paint, offset, or motion:
+
+```ts
+const y = {
+ scale: scaleLinear,
+ axis: {
+ label: {
+ text: 'Average order value (PLN)',
+ fontSize: 14,
+ fontWeight: 500,
+ fill: '#363636',
+ opacity: 0.9,
+ },
+ },
+}
+```
+
+`fill` sets the text color. Omitted presentation fields retain the existing
+title defaults: responsive 10 or 11 pixel sizing on x, 11 pixels on y, weight
+600, theme foreground fill, and 0.76 fill opacity. Configured font size and
+weight participate in text measurement, automatic offsets, and automatic
+margins for SVG, Canvas, and native rendering.
+
## Named scales and multiple axes
The reserved `x` and `y` entries are the default bindings for Cartesian marks.
@@ -396,6 +431,12 @@ mapping should not draw another axis. X scales can use the top or bottom side,
and y scales can use the left or right side. Axes on the same side stack
outward and contribute their measured size to the automatic margin.
+Axis sides stay physical in a right-to-left container, so `side: 'right'`
+still puts the axis on the right. Automatic tick anchors account for the
+container's inline direction and keep labels outside the plot. An explicitly
+authored `start` or `end` anchor remains logical. Text measurement, SVG,
+Canvas, native rendering, and standalone SVG export preserve that direction.
+
## Automatic guide layout
When a margin side is omitted, the scene compiler measures:
diff --git a/packages/charts-core/docs/reference/types.md b/packages/charts-core/docs/reference/types.md
index f219b23b..4dd31039 100644
--- a/packages/charts-core/docs/reference/types.md
+++ b/packages/charts-core/docs/reference/types.md
@@ -218,7 +218,7 @@ See [Scene nodes](./runtime-and-scene.md#scene-nodes).
| `ChartAxisTickLabelContext` | Semantic value, stable candidate index, pixel position, and bandwidth |
| `ChartAxisTickLabelValue` | Constant or context accessor for one tick-label property |
| `ChartAxisTickLabelThinOptions` | Minimum gap, end priority, and labels that must be kept |
-| `ChartAxisLabelOptions` | Axis title text and explicit or measured offset |
+| `ChartAxisLabelOptions` | Axis title text, typography, paint, motion, and offset |
| `ChartScaleFactory` | Creates a positional scale with a mark-inferred domain |
| `ChartScaleInput` | Factory or configured positional scale instance |
| `InferableScaleLike` | Domain-configurable scale returned by a factory |
@@ -247,7 +247,7 @@ See [Scene nodes](./runtime-and-scene.md#scene-nodes).
| `CrosshairBandOptions` | Categorical cursor-band inset, radius, fill, stroke, and opacity |
| `CrosshairLabelOptions` | Guide label formatting, spacing, text, and halo paint |
| `CrosshairMarkerOptions` | Primary-coordinate marker geometry and paint |
-| `ChartTheme` | Foreground, muted, grid, background, and palette |
+| `ChartTheme` | Foreground, muted, grid, background, palette, and focus ring defaults |
| `ChartLinearGradient` | Named linear-gradient resource |
| `ChartGradientStop` | Gradient offset, color, and optional opacity |
| `ChartCurve` | Line and y-area path generation |
diff --git a/packages/charts-core/src/canvas.test.ts b/packages/charts-core/src/canvas.test.ts
index 5ce7d73c..55e4019b 100644
--- a/packages/charts-core/src/canvas.test.ts
+++ b/packages/charts-core/src/canvas.test.ts
@@ -37,6 +37,13 @@ import type { ChartScene, SceneNode } from './types'
interface FakeCanvasContext {
operations: string[]
gradientStops: Array<[number, string]>
+ textPaints: Array<{
+ text: string
+ globalAlpha: number
+ font: string
+ direction: CanvasDirection
+ textAlign: CanvasTextAlign
+ }>
context: CanvasRenderingContext2D
}
@@ -77,6 +84,79 @@ afterEach(() => {
})
describe('Canvas renderer', () => {
+ it('paints authored caps and joins for both line directions', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [
+ lineY([4, 9], { lineCap: 'butt', lineJoin: 'bevel' }),
+ lineX([4, 9], { lineCap: 'square', lineJoin: 'miter' }),
+ ],
+ scales: {
+ x: { scale: scaleLinear },
+ y: { scale: scaleLinear },
+ },
+ guides: false,
+ }),
+ { width: 300, height: 180 },
+ )
+ const container = document.createElement('div')
+ const surface = createCanvasChartRenderer().mount(container, () => {})
+
+ surface.render(scene, renderOptions())
+
+ const canvas = container.querySelector(
+ '.ts-chart-canvas__scene',
+ )
+ const painted = canvas ? contexts.get(canvas) : undefined
+ if (!painted) throw new Error('Expected a painted scene canvas')
+ expect(painted.operations).toEqual(
+ expect.arrayContaining([
+ 'lineCap:butt',
+ 'lineJoin:bevel',
+ 'lineCap:square',
+ 'lineJoin:miter',
+ ]),
+ )
+
+ surface.destroy()
+ })
+
+ it('paints round caps and joins by default for both line directions', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [lineY([4, 9]), lineX([4, 9])],
+ scales: {
+ x: { scale: scaleLinear },
+ y: { scale: scaleLinear },
+ },
+ guides: false,
+ }),
+ { width: 300, height: 180 },
+ )
+ const container = document.createElement('div')
+ const surface = createCanvasChartRenderer().mount(container, () => {})
+
+ surface.render(scene, renderOptions())
+
+ const canvas = container.querySelector(
+ '.ts-chart-canvas__scene',
+ )
+ const painted = canvas ? contexts.get(canvas) : undefined
+ if (!painted) throw new Error('Expected a painted scene canvas')
+ expect(
+ painted.operations.filter((operation) =>
+ operation.startsWith('lineCap:'),
+ ),
+ ).toEqual(['lineCap:round', 'lineCap:round'])
+ expect(
+ painted.operations.filter((operation) =>
+ operation.startsWith('lineJoin:'),
+ ),
+ ).toEqual(['lineJoin:round', 'lineJoin:round'])
+
+ surface.destroy()
+ })
+
it('composes renderer-tagged marks in source order', () => {
const data = [
{ id: 'a', category: 'A', value: 3 },
@@ -988,6 +1068,127 @@ describe('Canvas renderer', () => {
surface.destroy()
})
+ it('paints Cartesian axis-title typography and paint', () => {
+ const definition = defineChart({
+ marks: [lineY([1, 2, 3])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 3]),
+ axis: {
+ ticks: false,
+ label: {
+ text: 'Revenue',
+ fontSize: 17,
+ fontWeight: 650,
+ fill: '#0f766e',
+ opacity: 0.6,
+ },
+ },
+ },
+ },
+ })
+ const container = document.createElement('div')
+ const surface = canvasChartRenderer.mount(container, () => {})
+ const generated = createChartScene(definition, {
+ width: 480,
+ height: 260,
+ })
+ const title = generated.nodes
+ .flatMap((node) => (node.kind === 'group' ? node.children : [node]))
+ .find((node) => node.key === 'y-label')
+
+ expect(title?.style?.fill).toBe('#0f766e')
+ surface.render(generated, { ariaLabel: 'Revenue chart' })
+
+ const painted = contexts
+ .get(surface.sceneCanvas)
+ ?.textPaints.find(({ text }) => text === 'Revenue')
+ expect(painted).toMatchObject({
+ globalAlpha: 0.6,
+ })
+ expect(painted?.font).toMatch(/650 17px/)
+ surface.destroy()
+ })
+
+ it('paints start and end as logical anchors in right-to-left hosts', () => {
+ const container = document.createElement('div')
+ const surface = canvasChartRenderer.mount(container, () => {})
+ surface.element.style.direction = 'rtl'
+ surface.render(
+ scene([
+ {
+ kind: 'label',
+ key: 'start',
+ x: 20,
+ y: 20,
+ text: 'Start',
+ anchor: 'start',
+ },
+ {
+ kind: 'label',
+ key: 'end',
+ x: 80,
+ y: 40,
+ text: 'End',
+ anchor: 'end',
+ },
+ ]),
+ renderOptions(),
+ )
+
+ expect(contexts.get(surface.sceneCanvas)?.textPaints).toMatchObject([
+ { text: 'Start', direction: 'rtl', textAlign: 'start' },
+ { text: 'End', direction: 'rtl', textAlign: 'end' },
+ ])
+ surface.destroy()
+ })
+
+ it('paints configured focus-ring geometry and paint on Canvas', () => {
+ const chart = createChartScene(
+ defineChart({
+ marks: [dot([{ x: 1, y: 2 }], { x: 'x', y: 'y' })],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]) },
+ y: { scale: scaleLinear().domain([0, 4]) },
+ },
+ guides: false,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 1.5,
+ fill: '#ffffff',
+ stroke: '#0f172a',
+ },
+ }),
+ { width: 200, height: 120 },
+ )
+ const point = chart.points[0]
+ if (!point) throw new Error('Expected a focus point')
+ const container = document.createElement('div')
+ document.body.append(container)
+ const surface = createCanvasChartRenderer().mount(container, () => {})
+ surface.render(chart, renderOptions())
+ const focus = contexts.get(surface.focusCanvas)
+ if (!focus) throw new Error('Expected a Canvas focus layer')
+
+ surface.paintFocus({
+ primary: point,
+ group: [point],
+ source: 'pointer',
+ pinned: false,
+ })
+
+ expect(focus.operations).toContain(`arc:${point.x},${point.y},4`)
+ expect(focus.operations).toEqual(
+ expect.arrayContaining([expect.stringMatching(/^stroke:.*:1\.5:1$/)]),
+ )
+ expect(focus.operations).toEqual(
+ expect.arrayContaining([expect.stringMatching(/^fill:.*(?:ffffff|255)/)]),
+ )
+ surface.destroy()
+ container.remove()
+ })
+
it('snaps retargeting focus candidates without adding interaction points', () => {
const container = document.createElement('div')
const surface = createCanvasChartRenderer().mount(container, () => {})
@@ -2432,6 +2633,7 @@ function renderOptions(): ChartSurfaceRenderOptions {
function fakeContext(): FakeCanvasContext {
const operations: string[] = []
const gradientStops: Array<[number, string]> = []
+ const textPaints: FakeCanvasContext['textPaints'] = []
let fillStyle: string | CanvasGradient = '#000000'
let strokeStyle: string | CanvasGradient = '#000000'
let lineWidth = 1
@@ -2466,22 +2668,32 @@ function fakeContext(): FakeCanvasContext {
operations.push(`translate:${values.join(',')}`),
rotate: (value: number) => operations.push(`rotate:${value}`),
clip: () => operations.push('clip'),
- fill: (pathOrRule?: Path2D | CanvasFillRule) =>
+ fill: (pathOrRule?: Path2D | CanvasFillRule) => {
operations.push(
pathOrRule === 'evenodd'
? 'fill:evenodd'
: pathOrRule
? 'fill:path'
: 'fill:current',
- ),
+ )
+ operations.push(`fill:${String(fillStyle)}:${globalAlpha}`)
+ },
stroke: (path?: Path2D) => {
operations.push(path ? 'stroke:path' : 'stroke:current')
operations.push(
`stroke:${String(strokeStyle)}:${lineWidth}:${globalAlpha}`,
)
},
- fillText: (text: string, x: number, y: number) =>
- operations.push(`fillText:${text},${x},${y}`),
+ fillText: (text: string, x: number, y: number) => {
+ operations.push(`fillText:${text},${x},${y}`)
+ textPaints.push({
+ text,
+ globalAlpha,
+ font: context.font,
+ direction: context.direction,
+ textAlign: context.textAlign,
+ })
+ },
strokeText: (text: string, x: number, y: number) =>
operations.push(`strokeText:${text},${x},${y}`),
setLineDash: (values: number[]) =>
@@ -2537,5 +2749,5 @@ function fakeContext(): FakeCanvasContext {
textAlign: 'left',
textBaseline: 'alphabetic',
} as unknown as CanvasRenderingContext2D
- return { operations, gradientStops, context }
+ return { operations, gradientStops, textPaints, context }
}
diff --git a/packages/charts-core/src/canvas.ts b/packages/charts-core/src/canvas.ts
index 9c0c06c3..72cac3c0 100644
--- a/packages/charts-core/src/canvas.ts
+++ b/packages/charts-core/src/canvas.ts
@@ -1509,11 +1509,7 @@ function paintLabel(
}
context.direction = painter.font.direction
context.textAlign =
- node.anchor === 'middle'
- ? 'center'
- : node.anchor === 'end'
- ? 'right'
- : 'left'
+ node.anchor === 'middle' ? 'center' : (node.anchor ?? 'start')
context.textBaseline =
node.baseline === 'middle'
? 'middle'
diff --git a/packages/charts-core/src/cartesian-scales.test.ts b/packages/charts-core/src/cartesian-scales.test.ts
index 94f657cb..2fad842a 100644
--- a/packages/charts-core/src/cartesian-scales.test.ts
+++ b/packages/charts-core/src/cartesian-scales.test.ts
@@ -4,7 +4,12 @@ import { crosshair } from './crosshair'
import { lineY } from './line'
import { ruleY } from './rule'
import { createChartScene, defineChart } from './scene'
-import type { SceneNode, SceneRule } from './types'
+import type {
+ ChartTextMeasurer,
+ SceneLabel,
+ SceneNode,
+ SceneRule,
+} from './types'
describe('Cartesian scale registry', () => {
it('binds marks to named y scales and stacks axes sharing the right side', () => {
@@ -67,6 +72,85 @@ describe('Cartesian scale registry', () => {
expect(rules.find((node) => node.key === 'y-axis')?.x1).toBe(scene.chart.x)
})
+ it('reserves the gutter for a right-side axis reading right to left', () => {
+ const leftToRight = createRightAxisScene('ltr')
+ const rightToLeft = createRightAxisScene('rtl')
+
+ expect(rightToLeft.chart.width).toBeCloseTo(leftToRight.chart.width)
+ expect(
+ sceneWidth - rightToLeft.chart.x - rightToLeft.chart.width,
+ ).toBeGreaterThanOrEqual(labelWidth)
+ })
+
+ it('keeps automatic axis labels physically outward in both directions', () => {
+ const leftToRight = createDirectionalAxisScene('ltr')
+ const rightToLeft = createDirectionalAxisScene('rtl')
+ const anchors = (nodes: readonly SceneNode[]) =>
+ Object.fromEntries(
+ flatten(nodes)
+ .filter(
+ (node): node is SceneLabel =>
+ node.kind === 'label' && node.key.includes('-tick-label:'),
+ )
+ .map((node) => [node.key.split('-tick-label:')[0], node.anchor]),
+ )
+
+ expect(anchors(leftToRight.nodes)).toMatchObject({
+ x: 'end',
+ y: 'end',
+ top: 'start',
+ right: 'start',
+ })
+ expect(anchors(rightToLeft.nodes)).toMatchObject({
+ x: 'start',
+ y: 'start',
+ top: 'end',
+ right: 'end',
+ })
+ expect(rightToLeft.chart).toEqual(leftToRight.chart)
+ })
+
+ it('stacks multiple right-side axes outward under RTL', () => {
+ const scene = createNamedScaleScene('rtl')
+ const rules = flatten(scene.nodes).filter(
+ (node): node is SceneRule => node.kind === 'rule',
+ )
+ const inner = rules.find((node) => node.key === 'percent-axis')
+ const outer = rules.find((node) => node.key === 'temperature-axis')
+ if (!inner || !outer) throw new Error('Expected both right-side axes')
+
+ expect(inner.x1).toBe(scene.chart.x + scene.chart.width)
+ expect(outer.x1).toBeGreaterThan(inner.x1)
+ expect(outer.x1).toBeLessThan(scene.width)
+ })
+
+ it('keeps an authored logical anchor unchanged in RTL', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [lineY([1, 2])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 1]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 2]),
+ side: 'right',
+ axis: {
+ ticks: { values: [1] },
+ tickLabels: { anchor: 'start' },
+ },
+ },
+ },
+ }),
+ { width: sceneWidth, height: 260 },
+ { measureText: mirroredText, typography: { direction: 'rtl' } },
+ )
+ const label = flatten(scene.nodes).find(
+ (node): node is SceneLabel =>
+ node.kind === 'label' && node.key.startsWith('y-tick-label:'),
+ )
+
+ expect(label?.anchor).toBe('start')
+ })
+
it('keeps named grids off by default', () => {
const keys = flatten(createNamedScaleScene().nodes).map((node) => node.key)
@@ -238,7 +322,7 @@ describe('Cartesian scale registry', () => {
})
})
-function createNamedScaleScene() {
+function createNamedScaleScene(direction: 'ltr' | 'rtl' = 'ltr') {
return createChartScene(
defineChart({
marks: [
@@ -272,6 +356,80 @@ function createNamedScaleScene() {
},
}),
{ width: 640, height: 320 },
+ { measureText: mirroredText, typography: { direction } },
+ )
+}
+
+const sceneWidth = 480
+const labelWidth = 40
+
+/**
+ * Reports the painted box the way a DOM text measurer does: `anchor` resolves
+ * against inline base direction, so the box sits on the opposite side of the
+ * origin once the container reads right to left.
+ */
+const mirroredText: ChartTextMeasurer = (_text, options) => {
+ const leftwardAnchor = options.direction === 'rtl' ? 'start' : 'end'
+ const x =
+ options.anchor === 'middle'
+ ? -labelWidth / 2
+ : options.anchor === leftwardAnchor
+ ? -labelWidth
+ : 0
+ return { x, y: -8, width: labelWidth, height: 10 }
+}
+
+function createRightAxisScene(direction: 'ltr' | 'rtl') {
+ return createChartScene(
+ defineChart({
+ marks: [lineY([1, 2, 3])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]) },
+ y: { scale: scaleLinear().domain([0, 3]), side: 'right' },
+ },
+ }),
+ { width: sceneWidth, height: 260 },
+ { measureText: mirroredText, typography: { direction } },
+ )
+}
+
+function createDirectionalAxisScene(direction: 'ltr' | 'rtl') {
+ return createChartScene(
+ defineChart({
+ marks: [lineY([1, 2])],
+ scales: {
+ x: {
+ scale: scaleLinear().domain([0, 1]),
+ side: 'bottom',
+ axis: {
+ ticks: { values: [0.5] },
+ tickLabels: { rotate: -30 },
+ },
+ },
+ y: {
+ scale: scaleLinear().domain([0, 2]),
+ side: 'left',
+ axis: { ticks: { values: [1] } },
+ },
+ top: {
+ channel: 'x',
+ side: 'top',
+ scale: scaleLinear().domain([0, 1]),
+ axis: {
+ ticks: { values: [0.5] },
+ tickLabels: { rotate: 30 },
+ },
+ },
+ right: {
+ channel: 'y',
+ side: 'right',
+ scale: scaleLinear().domain([0, 2]),
+ axis: { ticks: { values: [1] } },
+ },
+ },
+ }),
+ { width: sceneWidth, height: 260 },
+ { measureText: mirroredText, typography: { direction } },
)
}
diff --git a/packages/charts-core/src/color-scale.test.ts b/packages/charts-core/src/color-scale.test.ts
index 77b967c7..f6639558 100644
--- a/packages/charts-core/src/color-scale.test.ts
+++ b/packages/charts-core/src/color-scale.test.ts
@@ -330,6 +330,55 @@ describe('continuous color', () => {
)
})
+ it.each([
+ { direction: 'ltr' as const, left: 'start', right: 'end' },
+ { direction: 'rtl' as const, left: 'end', right: 'start' },
+ ])(
+ 'keeps automatic legend labels on their physical sides in $direction',
+ ({ direction, left, right }) => {
+ const categorical = createColorScale(
+ ['Alpha', 'Beta'],
+ {},
+ defaultChartTheme,
+ )
+ const stepped = createColorScale(
+ [0, 12],
+ {
+ scale: scaleQuantize,
+ range: ['#eff6ff', '#93c5fd', '#1d4ed8'],
+ },
+ defaultChartTheme,
+ )
+ const continuous = createColorScale(
+ [10, 20],
+ { scale: scaleLinear, range: ['#eff6ff', '#1d4ed8'] },
+ defaultChartTheme,
+ )
+
+ expect(
+ legendLabelAnchors(
+ colorLegend({ label: 'Category' }),
+ categorical,
+ direction,
+ ),
+ ).toEqual([left, left, left])
+ expect(
+ legendLabelAnchors(
+ colorLegend({ label: 'Intensity' }),
+ stepped,
+ direction,
+ ),
+ ).toEqual([left, left, 'middle', 'middle', right])
+ expect(
+ legendLabelAnchors(
+ colorGradientLegend({ label: 'Intensity', steps: 4 }),
+ continuous,
+ direction,
+ ),
+ ).toEqual([left, left, right])
+ },
+ )
+
it.each([
{
kind: 'quantize',
@@ -403,6 +452,7 @@ describe('continuous color', () => {
function renderLegend(
legend: ReturnType,
colors: ReturnType,
+ direction?: 'ltr' | 'rtl',
) {
const node = legend.render({
colors,
@@ -411,11 +461,22 @@ function renderLegend(
theme: defaultChartTheme,
width: 480,
height: 320,
+ direction,
})
if (node.kind !== 'group') throw new Error('Expected a legend group')
return node
}
+function legendLabelAnchors(
+ legend: ReturnType,
+ colors: ReturnType,
+ direction: 'ltr' | 'rtl',
+) {
+ return renderLegend(legend, colors, direction)
+ .children.filter((child) => child.kind === 'label')
+ .map((child) => child.anchor)
+}
+
function legendRects(
legend: ReturnType,
colors: ReturnType,
diff --git a/packages/charts-core/src/crosshair-resolver.ts b/packages/charts-core/src/crosshair-resolver.ts
index f65ce54a..423292f9 100644
--- a/packages/charts-core/src/crosshair-resolver.ts
+++ b/packages/charts-core/src/crosshair-resolver.ts
@@ -1,4 +1,4 @@
-import { measureSceneLabelBounds } from './guide-layout'
+import { measureSceneLabelBounds, physicalTextAnchor } from './guide-layout'
import { valueKey } from './scales'
import type {
ChartBounds,
@@ -303,7 +303,7 @@ function resolveFocusGuide(
target.yValue,
guide.y.label.format,
),
- anchor: 'end',
+ anchor: physicalTextAnchor('right', guide.direction),
baseline: 'middle',
fontSize: guide.y.label.fontSize,
fontWeight: guide.y.label.fontWeight,
diff --git a/packages/charts-core/src/crosshair.test.ts b/packages/charts-core/src/crosshair.test.ts
index f9b6d6e2..7eaa2990 100644
--- a/packages/charts-core/src/crosshair.test.ts
+++ b/packages/charts-core/src/crosshair.test.ts
@@ -6,6 +6,7 @@ import { dot } from './dot'
import { facet } from './facet'
import { resolveFocusPresentation } from './focus-presentation'
import { group } from './group'
+import { measureSceneLabelBounds } from './guide-layout'
import { createChartScene, defineChart } from './scene'
import { stack } from './stack'
import { linearAxes } from './test-scales'
@@ -283,6 +284,54 @@ describe('crosshair', () => {
).toBe(true)
})
+ it('keeps y labels physically left in both inline directions', () => {
+ const resolved = (direction: 'ltr' | 'rtl') => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [
+ dot([{ x: 1, y: 2 }], { x: 'x', y: 'y' }),
+ crosshair({ x: false, y: { label: true } }),
+ ],
+ ...linearAxes([0, 2], [0, 4]),
+ guides: false,
+ focusRing: false,
+ margin: { top: 20, right: 20, bottom: 20, left: 80 },
+ }),
+ { width: 240, height: 160 },
+ { typography: { direction } },
+ )
+ const presentation = resolveFocusPresentation(
+ scene,
+ focus(scene.points[0]!),
+ )
+ const label = findNode(
+ presentation.over,
+ 'crosshair-1:y-label:text',
+ ) as SceneLabel
+ const guide = scene.focusGuides![0]!
+ const bounds = measureSceneLabelBounds(label, guide.measureText)
+ return { scene, label, bounds }
+ }
+
+ const leftToRight = resolved('ltr')
+ const rightToLeft = resolved('rtl')
+
+ expect(leftToRight.label.anchor).toBe('end')
+ expect(rightToLeft.label.anchor).toBe('start')
+ expect(leftToRight.bounds.x + leftToRight.bounds.width).toBeCloseTo(
+ leftToRight.label.x,
+ )
+ expect(rightToLeft.bounds.x + rightToLeft.bounds.width).toBeCloseTo(
+ rightToLeft.label.x,
+ )
+ expect(leftToRight.bounds.x + leftToRight.bounds.width).toBeLessThan(
+ leftToRight.scene.chart.x,
+ )
+ expect(rightToLeft.bounds.x + rightToLeft.bounds.width).toBeLessThan(
+ rightToLeft.scene.chart.x,
+ )
+ })
+
it('labels stacked difference bars with their plotted endpoint', () => {
const rows = [
{ id: 'first', category: 'A', series: 'first', value: 40 },
diff --git a/packages/charts-core/src/crosshair.ts b/packages/charts-core/src/crosshair.ts
index 5f1e9969..1dfe9b02 100644
--- a/packages/charts-core/src/crosshair.ts
+++ b/packages/charts-core/src/crosshair.ts
@@ -135,6 +135,7 @@ export function crosshair<
projectY: projector(scales[yScale]),
motion: options.motion,
measureText: layout.measureText,
+ direction: layout.typography?.direction,
resolve: resolveCrosshairGuide,
},
],
diff --git a/packages/charts-core/src/dom-text.test.ts b/packages/charts-core/src/dom-text.test.ts
index 8586fe73..e397516d 100644
--- a/packages/charts-core/src/dom-text.test.ts
+++ b/packages/charts-core/src/dom-text.test.ts
@@ -78,4 +78,56 @@ describe('DOM text measurement', () => {
value: previousCanvasContext,
})
})
+
+ it('mirrors logical anchors when Canvas omits exact painted bounds', () => {
+ const context = {
+ direction: 'inherit',
+ font: '',
+ fontStretch: 'normal',
+ letterSpacing: '0px',
+ textAlign: 'start',
+ textBaseline: 'alphabetic',
+ measureText: vi.fn(() => ({ width: 32 })),
+ } as unknown as CanvasRenderingContext2D
+ const previousCanvasContext = window.CanvasRenderingContext2D
+ Object.defineProperty(window, 'CanvasRenderingContext2D', {
+ configurable: true,
+ value: class TestCanvasContext {},
+ })
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context)
+
+ try {
+ const measureText = createDomTextMeasurer(
+ document.createElement('div'),
+ ).measureText
+ const options = {
+ fontSize: 10,
+ fontFamily: 'sans-serif',
+ fontStyle: 'normal',
+ fontStretch: 'normal',
+ letterSpacing: 0,
+ direction: 'rtl' as const,
+ fontScale: 1,
+ baseline: 'auto' as const,
+ }
+
+ expect(measureText('Start', { ...options, anchor: 'start' })).toEqual({
+ x: -32,
+ y: -8,
+ width: 32,
+ height: 10,
+ })
+ expect(measureText('End', { ...options, anchor: 'end' })).toEqual({
+ x: 0,
+ y: -8,
+ width: 32,
+ height: 10,
+ })
+ } finally {
+ Object.defineProperty(window, 'CanvasRenderingContext2D', {
+ configurable: true,
+ value: previousCanvasContext,
+ })
+ }
+ })
})
diff --git a/packages/charts-core/src/dom-text.ts b/packages/charts-core/src/dom-text.ts
index a09f9a52..ccfb6660 100644
--- a/packages/charts-core/src/dom-text.ts
+++ b/packages/charts-core/src/dom-text.ts
@@ -1,4 +1,4 @@
-import { estimateSceneText } from './guide-layout'
+import { estimateSceneText, logicalTextAnchorOffset } from './guide-layout'
import type {
ChartTextMeasurer,
ChartTextMeasureOptions,
@@ -137,12 +137,7 @@ function paintedBounds(
const width = Number.isFinite(measured.width)
? Math.max(0, measured.width)
: 0
- const x =
- options.anchor === 'middle'
- ? -width / 2
- : options.anchor === 'end'
- ? -width
- : 0
+ const x = logicalTextAnchorOffset(width, options.anchor, options.direction)
const y =
options.baseline === 'middle'
? -fontSize / 2
diff --git a/packages/charts-core/src/export.test.ts b/packages/charts-core/src/export.test.ts
index 0e64ed83..a8296e6a 100644
--- a/packages/charts-core/src/export.test.ts
+++ b/packages/charts-core/src/export.test.ts
@@ -18,6 +18,8 @@ describe('optional export', () => {
defineChart({
marks: [
lineY([1, 3, 2], {
+ lineCap: 'butt',
+ lineJoin: 'bevel',
stroke: 'currentColor',
}),
],
@@ -34,6 +36,8 @@ describe('optional export', () => {
expect(result).toContain('width="480"')
expect(result).toContain('height="260"')
expect(result).toContain('aria-label="Exported chart"')
+ expect(result).toContain('stroke-linecap="butt"')
+ expect(result).toContain('stroke-linejoin="bevel"')
expect(result).not.toContain('data-ts-focus-layer')
})
@@ -89,6 +93,37 @@ describe('optional export', () => {
readStyle.mockRestore()
})
+ it('preserves the computed text direction in standalone SVG', () => {
+ const container = document.createElement('div')
+ container.innerHTML = renderChartSvg(
+ createChartScene(
+ defineChart({
+ marks: [lineY([1, 3, 2])],
+ ...linearAxes([0, 2], [0, 3]),
+ }),
+ { width: 480, height: 260 },
+ ),
+ { ariaLabel: 'RTL export' },
+ )
+ const svg = container.querySelector('svg')
+ if (!svg) throw new Error('Expected an SVG chart')
+ const readStyle = vi
+ .spyOn(window, 'getComputedStyle')
+ .mockImplementation((element) => {
+ return {
+ direction: element === svg ? 'rtl' : '',
+ getPropertyValue() {
+ return ''
+ },
+ } as unknown as CSSStyleDeclaration
+ })
+
+ const result = serializeChartSvg(container)
+
+ expect(result).toContain('direction="rtl"')
+ readStyle.mockRestore()
+ })
+
it('includes the currently painted crosshair only when requested', () => {
const scene = createChartScene(
defineChart({
diff --git a/packages/charts-core/src/export.ts b/packages/charts-core/src/export.ts
index 09a8b16f..3e6e03e2 100644
--- a/packages/charts-core/src/export.ts
+++ b/packages/charts-core/src/export.ts
@@ -34,6 +34,7 @@ export function serializeChartSvg(
const svg = resolveSvg(target)
const clone = svg.cloneNode(true) as SVGSVGElement
inlinePresentation(svg, clone)
+ inlineComputedDirection(svg, clone)
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
if (!options.includeFocus) {
clone
@@ -50,6 +51,14 @@ export function serializeChartSvg(
return new Serializer().serializeToString(clone)
}
+function inlineComputedDirection(source: SVGSVGElement, clone: SVGSVGElement) {
+ const direction =
+ source.ownerDocument.defaultView?.getComputedStyle(source).direction
+ if (direction === 'ltr' || direction === 'rtl') {
+ clone.setAttribute('direction', direction)
+ }
+}
+
export function downloadChartSvg(
target: Element,
filename = 'chart.svg',
diff --git a/packages/charts-core/src/facet.test.ts b/packages/charts-core/src/facet.test.ts
index e1d0fd43..40ba07ba 100644
--- a/packages/charts-core/src/facet.test.ts
+++ b/packages/charts-core/src/facet.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'
import { scaleBand, scaleLinear } from 'd3-scale'
import { bandX, bandY } from './band'
import { barY } from './bar'
-import { facet, facetChart } from './facet'
+import { facet, facetChart, type FacetOptions } from './facet'
import { whenFocused } from './focus-mark'
import { measureSceneLabelBounds } from './guide-layout'
import { lineY } from './line'
@@ -115,6 +115,74 @@ describe('facets', () => {
expect(svg).toContain('Beta')
})
+ it('rejects focus ring options owned by a facet cell', () => {
+ type Row = { group: string; value: number }
+ const data: Row[] = [{ group: 'Alpha', value: 2 }]
+ const definition = (
+ childOptions: { focusRing: false } | { theme: { focusRing: false } },
+ ) => {
+ const options = {
+ by: 'group',
+ chart: (rows: readonly [Row, ...Row[]]) => ({
+ marks: [lineY(rows, { y: 'value' })],
+ ...linearAxes([0, 1], [0, 2]),
+ ...childOptions,
+ }),
+ } as unknown as FacetOptions
+ return facetChart(data, options)
+ }
+
+ expect(() =>
+ createChartScene(definition({ focusRing: false }), {
+ width: 320,
+ height: 180,
+ }),
+ ).toThrow(/host option "focusRing"/)
+ expect(() =>
+ createChartScene(definition({ theme: { focusRing: false } }), {
+ width: 320,
+ height: 180,
+ }),
+ ).toThrow(/cannot own a focus ring theme/)
+ })
+
+ it('uses the outer focus ring theme for faceted points', () => {
+ const data = [{ id: 'a', group: 'Alpha', x: 0, y: 2 }]
+ const definition = facetChart(data, {
+ by: 'group',
+ label: false,
+ chart: (rows) => ({
+ marks: [lineY(rows, { x: 'x', y: 'y', key: 'id' })],
+ ...linearAxes([0, 1], [0, 2]),
+ }),
+ })
+ const scene = createChartScene(
+ {
+ ...definition,
+ theme: {
+ focusRing: {
+ radius: 9,
+ strokeWidth: 1,
+ fill: '#f8fafc',
+ stroke: '#0f172a',
+ },
+ },
+ },
+ { width: 320, height: 180 },
+ )
+ const focusDots = flatten(scene.nodes).filter(
+ (node) =>
+ node.kind === 'dot' &&
+ node.radius === 9 &&
+ scene.points.some((point) => point.key === node.key),
+ )
+
+ expect(focusDots).toHaveLength(1)
+ expect(focusDots[0]).toMatchObject({
+ style: { fill: '#f8fafc', stroke: '#0f172a', strokeWidth: 1 },
+ })
+ })
+
it('stacks panels when the available width is narrow', () => {
const data = [
{ group: 'A', value: 1 },
@@ -178,7 +246,15 @@ describe('facets', () => {
},
y: {
scale: scaleLinear().domain([0, 6]),
- axis: { label: 'Vertical' },
+ axis: {
+ label: {
+ text: 'Vertical',
+ fontSize: 18,
+ fontWeight: 700,
+ fill: '#7c3aed',
+ opacity: 0.5,
+ },
+ },
},
},
}),
@@ -201,6 +277,10 @@ describe('facets', () => {
)
const aPoints = scene.points.filter((point) => point.datum.group === 'A')
const bPoints = scene.points.filter((point) => point.datum.group === 'B')
+ const yTitle = nodes.find(
+ (node): node is Extract =>
+ node.kind === 'label' && node.key === 'facet-0:y-label',
+ )
expect(yAxes).toHaveLength(1)
expect(xAxes).toHaveLength(2)
@@ -210,6 +290,11 @@ describe('facets', () => {
expect(nodes.filter((node) => node.key === 'facet-0:y-label')).toHaveLength(
1,
)
+ expect(yTitle).toMatchObject({
+ fontSize: 18,
+ fontWeight: 700,
+ style: { fill: '#7c3aed', opacity: 0.5 },
+ })
expect(yLabels).toHaveLength(
new Set(yLabels.map((node) => (node.kind === 'label' ? node.text : '')))
.size,
diff --git a/packages/charts-core/src/facet.ts b/packages/charts-core/src/facet.ts
index 0d2963f1..fb5e6ba2 100644
--- a/packages/charts-core/src/facet.ts
+++ b/packages/charts-core/src/facet.ts
@@ -59,12 +59,32 @@ type FacetOptionsInput> = Omit<
FacetOptions,
'chart' | 'motion'
> & {
- chart: TChart
+ chart: TChart & ValidFacetChartFunction>
motion?: ChartMotionDefinition<
ChartSpecDatum, ChartSpec>>
>
} & ([ReturnType] extends [ChartSpec] ? unknown : never)
+type InvalidFacetChartResult = TResult extends unknown
+ ? 'focusRing' extends keyof TResult
+ ? StaticChartDefinition extends TResult
+ ? never
+ : TResult
+ : TResult extends { theme: infer TTheme }
+ ? [TTheme] extends [never]
+ ? never
+ : 'focusRing' extends keyof TTheme
+ ? TResult
+ : never
+ : never
+ : never
+
+type ValidFacetChartFunction> = [
+ InvalidFacetChartResult>,
+] extends [never]
+ ? unknown
+ : never
+
interface FacetEntry {
key: ChartKey
data: [TDatum, ...TDatum[]]
@@ -134,9 +154,11 @@ export function facet(
const cellHeight = cellSize(chart.height, gap, rows)
const showLabel = options.label !== false
const labelHeight = showLabel ? 22 : 0
- const definitions = entries.map((entry) =>
- mergeTheme(options.chart(entry.data, { key: entry.key }), theme),
- )
+ const definitions = entries.map((entry) => {
+ const definition = options.chart(entry.data, { key: entry.key })
+ assertFacetChildDefinition(id, entry.key, definition)
+ return mergeTheme(definition, theme)
+ })
if (
options.axes === 'cell' ||
@@ -642,6 +664,24 @@ function createFacetScene(
}
}
+function assertFacetChildDefinition(
+ id: string,
+ key: ChartKey,
+ definition: ChartSpec,
+): void {
+ const child = definition as ChartSpec & { focusRing?: unknown }
+ if (child.focusRing !== undefined) {
+ throw new TypeError(
+ `Facet "${id}" cell "${String(key)}" cannot own chart host option "focusRing"; configure it on the outer definition`,
+ )
+ }
+ if (child.theme?.focusRing !== undefined) {
+ throw new TypeError(
+ `Facet "${id}" cell "${String(key)}" cannot own a focus ring theme; configure theme.focusRing on the outer definition`,
+ )
+ }
+}
+
function withoutMarkRendering(
mark: ChartMark,
): ChartMark {
diff --git a/packages/charts-core/src/guide-layout.test.ts b/packages/charts-core/src/guide-layout.test.ts
index dcb9c997..ee2df832 100644
--- a/packages/charts-core/src/guide-layout.test.ts
+++ b/packages/charts-core/src/guide-layout.test.ts
@@ -1,7 +1,9 @@
import { describe, expect, it, vi } from 'vitest'
import {
estimateSceneText,
+ logicalTextAnchorOffset,
measureSceneLabelBounds,
+ physicalTextAnchor,
resolveGuideMargins,
} from './guide-layout'
import type { ChartTextMeasurer, SceneGroup, SceneLabel } from './types'
@@ -106,6 +108,43 @@ describe('guide layout', () => {
})
})
+ it('mirrors the estimated painted box in a right-to-left direction', () => {
+ const options = {
+ ...typography,
+ direction: 'rtl' as const,
+ fontSize: 10,
+ fontWeight: 400,
+ baseline: 'auto' as const,
+ }
+ const start = estimateSceneText('Margin 100', {
+ ...options,
+ anchor: 'start',
+ })
+ const end = estimateSceneText('Margin 100', { ...options, anchor: 'end' })
+
+ expect(start.x).toBeCloseTo(-start.width)
+ expect(end.x).toBe(0)
+ })
+
+ it('resolves logical anchors against the inline direction', () => {
+ expect(logicalTextAnchorOffset(40, 'start', 'ltr')).toBe(0)
+ expect(logicalTextAnchorOffset(40, 'end', 'ltr')).toBe(-40)
+ expect(logicalTextAnchorOffset(40, 'start', 'rtl')).toBe(-40)
+ expect(logicalTextAnchorOffset(40, 'end', 'rtl')).toBe(0)
+ expect(logicalTextAnchorOffset(40, 'middle', 'rtl')).toBe(-20)
+ expect(logicalTextAnchorOffset(40, 'start', 'inherit')).toBe(0)
+ })
+
+ it('maps physical text sides to logical anchors', () => {
+ expect(physicalTextAnchor('left', 'ltr')).toBe('start')
+ expect(physicalTextAnchor('right', 'ltr')).toBe('end')
+ expect(physicalTextAnchor('left', 'rtl')).toBe('end')
+ expect(physicalTextAnchor('right', 'rtl')).toBe('start')
+ expect(physicalTextAnchor('left', 'inherit')).toBe('start')
+ expect(physicalTextAnchor('right', undefined)).toBe('end')
+ expect(physicalTextAnchor('middle', 'rtl')).toBe('middle')
+ })
+
it('rotates the measured rectangle around the label position', () => {
const bounds = measureSceneLabelBounds(
label({
diff --git a/packages/charts-core/src/guide-layout.ts b/packages/charts-core/src/guide-layout.ts
index ac1f6b6b..22ec3599 100644
--- a/packages/charts-core/src/guide-layout.ts
+++ b/packages/charts-core/src/guide-layout.ts
@@ -27,6 +27,26 @@ export interface GuideMarginOptions {
measureText?: ChartTextMeasurer
}
+export function physicalTextAnchor(
+ side: 'left' | 'middle' | 'right',
+ direction: ChartTextMeasureOptions['direction'] | undefined,
+): ChartTextMeasureOptions['anchor'] {
+ if (side === 'middle') return 'middle'
+ const startsAtLeft = direction !== 'rtl'
+ if (side === 'left') return startsAtLeft ? 'start' : 'end'
+ return startsAtLeft ? 'end' : 'start'
+}
+
+export function logicalTextAnchorOffset(
+ width: number,
+ anchor: ChartTextMeasureOptions['anchor'],
+ direction: ChartTextMeasureOptions['direction'],
+): number {
+ if (anchor === 'middle') return -width / 2
+ const startsAtLeft = direction !== 'rtl'
+ return (anchor === 'start') === startsAtLeft ? 0 : -width
+}
+
export function estimateSceneText(
text: string,
style: ChartTextMeasureOptions,
@@ -55,8 +75,11 @@ export function estimateSceneText(
Math.max(0, Array.from(text).length - 1) * letterSpacing,
)
const height = fontSize
- const x =
- style.anchor === 'middle' ? -width / 2 : style.anchor === 'end' ? -width : 0
+ // `anchor` resolves against inline base direction, so which side of the
+ // origin the painted box occupies mirrors with that direction. The DOM
+ // measurer already reports the mirrored origin; match it here so a host
+ // without one lays out the same chart.
+ const x = logicalTextAnchorOffset(width, style.anchor, style.direction)
const y =
style.baseline === 'middle'
? -height / 2
diff --git a/packages/charts-core/src/index.ts b/packages/charts-core/src/index.ts
index 192eb680..f8b24647 100644
--- a/packages/charts-core/src/index.ts
+++ b/packages/charts-core/src/index.ts
@@ -336,6 +336,7 @@ export type {
ChartFocusAnchor,
ChartFocusCursorBinding,
ChartFocusPresentation,
+ ChartFocusRingOptions,
ChartFocusMode,
ChartFocusFilter,
ChartFocusAffinity,
diff --git a/packages/charts-core/src/legend-static.ts b/packages/charts-core/src/legend-static.ts
index cf7e2e6f..6f2f682d 100644
--- a/packages/charts-core/src/legend-static.ts
+++ b/packages/charts-core/src/legend-static.ts
@@ -2,6 +2,7 @@ import {
layoutCategoricalLegendItems,
resolveCategoricalLegendItems,
} from './legend-layout-internal'
+import { physicalTextAnchor } from './guide-layout'
import type {
ChartColorLegend,
ChartLegendPlacement,
@@ -56,7 +57,7 @@ export function colorLegend(
if (isSteppedLegend(context.colors.kind)) {
return renderSteppedLegend(options, context)
}
- const { colors, bounds, theme } = context
+ const { colors, bounds, theme, direction } = context
const items = resolveCategoricalLegendItems(colors)
const layout = layoutCategoricalLegendItems(
items.length,
@@ -71,6 +72,7 @@ export function colorLegend(
x: bounds.x,
y: bounds.y + 11,
text: options.label,
+ anchor: physicalTextAnchor('left', direction),
fontSize: 11,
fontWeight: 600,
style: { fill: theme.foreground, fillOpacity: 0.78 },
@@ -96,6 +98,7 @@ export function colorLegend(
x: x + 13,
y,
text: item.label,
+ anchor: physicalTextAnchor('left', direction),
baseline: 'middle',
fontSize: 11,
style: { fill: theme.foreground, fillOpacity: 0.76 },
@@ -130,7 +133,12 @@ function isQuantitativeLegend(
function renderSteppedLegend(
options: ColorLegendOptions,
- { colors, bounds, theme }: Parameters[0],
+ {
+ colors,
+ bounds,
+ theme,
+ direction,
+ }: Parameters[0],
): SceneNode {
const width = Math.min(bounds.width, Math.max(80, options.width ?? 240))
const x = bounds.x
@@ -146,6 +154,7 @@ function renderSteppedLegend(
x,
y: bounds.y + 10,
text: options.label,
+ anchor: physicalTextAnchor('left', direction),
fontSize: 11,
fontWeight: 600,
style: { fill: theme.foreground, fillOpacity: 0.78 },
@@ -172,36 +181,36 @@ function renderSteppedLegend(
? thresholds.map((value, index) => ({
value,
index: index + 1,
- anchor: 'middle' as const,
+ side: 'middle' as const,
}))
: [
...(typeof first === 'number'
- ? [{ value: first, index: 0, anchor: 'start' as const }]
+ ? [{ value: first, index: 0, side: 'left' as const }]
: []),
...thresholds.map((value, index) => ({
value,
index: index + 1,
- anchor: 'middle' as const,
+ side: 'middle' as const,
})),
...(typeof last === 'number'
? [
{
value: last,
index: colors.range.length,
- anchor: 'end' as const,
+ side: 'right' as const,
},
]
: []),
]
- boundaries.forEach(({ value, index, anchor }) => {
+ boundaries.forEach(({ value, index, side }) => {
children.push({
kind: 'label',
key: `legend-step-label:${index}:${value}`,
x: x + index * itemWidth,
y: y + 21,
text: format(value),
- anchor,
+ anchor: physicalTextAnchor(side, direction),
fontSize: 10,
style: { fill: theme.muted, fillOpacity: 0.72 },
})
@@ -266,7 +275,7 @@ export function colorGradientLegend(
height() {
return options.label ? 55 : 42
},
- render({ colors, bounds, theme }) {
+ render({ colors, bounds, theme, direction }) {
const first = colors.domain[0]
const last = colors.domain.at(-1)
if (typeof first !== 'number' || typeof last !== 'number') {
@@ -289,6 +298,7 @@ export function colorGradientLegend(
x,
y: bounds.y + 10,
text: options.label,
+ anchor: physicalTextAnchor('left', direction),
fontSize: 11,
fontWeight: 600,
style: { fill: theme.foreground, fillOpacity: 0.78 },
@@ -314,7 +324,7 @@ export function colorGradientLegend(
x,
y: y + 21,
text: format(first),
- anchor: 'start',
+ anchor: physicalTextAnchor('left', direction),
fontSize: 10,
style: { fill: theme.muted, fillOpacity: 0.72 },
},
@@ -324,7 +334,7 @@ export function colorGradientLegend(
x: x + width,
y: y + 21,
text: format(last),
- anchor: 'end',
+ anchor: physicalTextAnchor('right', direction),
fontSize: 10,
style: { fill: theme.muted, fillOpacity: 0.72 },
},
diff --git a/packages/charts-core/src/line.test.ts b/packages/charts-core/src/line.test.ts
index 7d897ad3..3b67d052 100644
--- a/packages/charts-core/src/line.test.ts
+++ b/packages/charts-core/src/line.test.ts
@@ -5,11 +5,13 @@ import { d3Curve } from './d3-shape'
import { lineX, lineY } from './line'
import { createChartScene, defineChart } from './scene'
import { renderChartSvg } from './svg'
+import type { LineXOptions, LineYOptions } from './line'
import type {
ChartDefinition,
ChartMark,
SceneNode,
ScenePolyline,
+ SceneStyle,
} from './types'
interface Row {
@@ -40,6 +42,75 @@ const rows: readonly Row[] = [
},
]
+describe('line presentation', () => {
+ it('applies authored caps and joins to vertical and horizontal lines', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [
+ lineY([4, 9], {
+ id: 'vertical',
+ lineCap: 'butt',
+ lineJoin: 'bevel',
+ }),
+ lineX([4, 9], {
+ id: 'horizontal',
+ lineCap: 'square',
+ lineJoin: 'miter-clip',
+ }),
+ ],
+ scales: {
+ x: { scale: scaleLinear },
+ y: { scale: scaleLinear },
+ },
+ }),
+ { width: 480, height: 260 },
+ )
+ const lines = flatten(scene.nodes).filter(
+ (node): node is ScenePolyline => node.kind === 'polyline',
+ )
+ const svg = renderChartSvg(scene, { ariaLabel: 'Line presentation' })
+
+ expect(lines.map(({ style }) => style)).toEqual([
+ expect.objectContaining({ lineCap: 'butt', lineJoin: 'bevel' }),
+ expect.objectContaining({ lineCap: 'square', lineJoin: 'miter-clip' }),
+ ])
+ expect(svg).toContain('stroke-linecap="butt" stroke-linejoin="bevel"')
+ expect(svg).toContain(
+ 'stroke-linecap="square" stroke-linejoin="miter-clip"',
+ )
+ })
+
+ it('keeps round caps and joins as the default', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [
+ lineY([4, 9], { id: 'vertical' }),
+ lineX([4, 9], { id: 'horizontal' }),
+ ],
+ scales: {
+ x: { scale: scaleLinear },
+ y: { scale: scaleLinear },
+ },
+ }),
+ { width: 480, height: 260 },
+ )
+ const lines = flatten(scene.nodes).filter(
+ (node): node is ScenePolyline => node.kind === 'polyline',
+ )
+ const svg = renderChartSvg(scene, {
+ ariaLabel: 'Default line presentation',
+ })
+
+ expect(lines.map(({ style }) => style)).toEqual([
+ expect.objectContaining({ lineCap: 'round', lineJoin: 'round' }),
+ expect.objectContaining({ lineCap: 'round', lineJoin: 'round' }),
+ ])
+ expect(
+ svg.match(/stroke-linecap="round" stroke-linejoin="round"/g),
+ ).toHaveLength(2)
+ })
+})
+
describe('lineX mark', () => {
it('transposes the implicit value and index channels from lineY', () => {
const values = [4, 9, 7]
@@ -232,6 +303,24 @@ if (false) {
expectTypeOf(numeric).toEqualTypeOf>()
expectTypeOf(temporal).toEqualTypeOf>()
+ expectTypeOf['lineCap']>().toEqualTypeOf<
+ SceneStyle['lineCap']
+ >()
+ expectTypeOf['lineJoin']>().toEqualTypeOf<
+ SceneStyle['lineJoin']
+ >()
+ expectTypeOf['lineCap']>().toEqualTypeOf<
+ SceneStyle['lineCap']
+ >()
+ expectTypeOf['lineJoin']>().toEqualTypeOf<
+ SceneStyle['lineJoin']
+ >()
+
+ // @ts-expect-error Line caps must use a renderer-supported scene value.
+ lineY(rows, { x: 'at', y: 'value', lineCap: 'flat' })
+
+ // @ts-expect-error Line joins must use a renderer-supported scene value.
+ lineX(rows, { x: 'value', y: 'at', lineJoin: 'sharp' })
// @ts-expect-error Horizontal line values must be numeric.
lineX(rows, { x: 'category', y: 'at' })
diff --git a/packages/charts-core/src/line.ts b/packages/charts-core/src/line.ts
index 1734f929..937d7e52 100644
--- a/packages/charts-core/src/line.ts
+++ b/packages/charts-core/src/line.ts
@@ -13,6 +13,7 @@ import type {
Channel,
CartesianChartMark,
CartesianScaleBindings,
+ ChartCurve,
ChartKey,
ChartMark,
ChartMarkMotionOptions,
@@ -24,7 +25,7 @@ import type {
MarkChannelOutput,
MarkScaleBindings,
SceneNode,
- ChartCurve,
+ SceneStyle,
VisualChannel,
} from './types'
@@ -38,6 +39,8 @@ interface LineOptions
strokeOpacity?: number
strokeWidth?: number
strokeDasharray?: string
+ lineCap?: SceneStyle['lineCap']
+ lineJoin?: SceneStyle['lineJoin']
points?: boolean
curve?: ChartCurve
states?: readonly ChartMarkState>[]
@@ -299,8 +302,8 @@ function createLineMark(
strokeOpacity: options.strokeOpacity,
strokeWidth: options.strokeWidth ?? 2.25,
strokeDasharray: options.strokeDasharray,
- lineCap: 'round',
- lineJoin: 'round',
+ lineCap: options.lineCap || 'round',
+ lineJoin: options.lineJoin || 'round',
},
})
segment = []
diff --git a/packages/charts-core/src/motion.test.ts b/packages/charts-core/src/motion.test.ts
index a8f24f91..5a9a6772 100644
--- a/packages/charts-core/src/motion.test.ts
+++ b/packages/charts-core/src/motion.test.ts
@@ -28,6 +28,52 @@ const rows = [
]
describe('SVG motion', () => {
+ it('applies discrete line cap and join revisions synchronously', () => {
+ const definition = (
+ lineCap: 'butt' | 'round',
+ lineJoin: 'bevel' | 'round',
+ ) =>
+ defineChart({
+ motion: {
+ transition: { type: 'tween', duration: 100, easing: 'linear' },
+ },
+ marks: [
+ lineY(rows, {
+ id: 'series',
+ x: 'category',
+ y: 'value',
+ key: 'id',
+ lineCap,
+ lineJoin,
+ }),
+ ],
+ scales: {
+ x: { scale: scaleBand().domain(['A', 'B']) },
+ y: { scale: scaleLinear().domain([0, 100]) },
+ },
+ guides: false,
+ })
+ const first = createChartScene(definition('round', 'round'), {
+ width: 300,
+ height: 200,
+ })
+ const next = createChartScene(definition('butt', 'bevel'), {
+ width: 300,
+ height: 200,
+ })
+ const container = document.createElement('div')
+ const surface = motion({ initial: false }).mount(container, () => {})
+ surface.render(first, { ariaLabel: 'Line style motion' })
+ const line = container.querySelector('g.ts-chart__line > path')
+
+ surface.render(next, { ariaLabel: 'Line style motion' })
+
+ expect(container.querySelector('g.ts-chart__line > path')).toBe(line)
+ expect(line?.getAttribute('stroke-linecap')).toBe('butt')
+ expect(line?.getAttribute('stroke-linejoin')).toBe('bevel')
+ surface.destroy()
+ })
+
it('maps client coordinates through the rendered SVG transform', () => {
const scene = createChartScene(
defineChart({
@@ -2106,6 +2152,72 @@ describe('SVG motion', () => {
frames.restore()
})
+ it('applies axis-title motion to configured typography and opacity', () => {
+ const definition = (
+ fontSize: number,
+ fontWeight: number,
+ opacity: number,
+ ) =>
+ defineChart({
+ marks: [lineY([0, 1])],
+ margin: 0,
+ scales: {
+ x: { scale: scaleLinear().domain([0, 1]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 1]),
+ axis: {
+ ticks: false,
+ label: {
+ text: 'Revenue',
+ fontSize,
+ fontWeight,
+ opacity,
+ motion: {
+ transition: {
+ type: 'tween',
+ duration: 100,
+ easing: 'linear',
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ const first = createChartScene(definition(10, 400, 0.4), {
+ width: 300,
+ height: 200,
+ })
+ const next = createChartScene(definition(20, 700, 0.8), {
+ width: 300,
+ height: 200,
+ })
+ const container = document.createElement('div')
+ const surface = motion({ initial: false }).mount(container, () => {})
+ surface.render(first, { ariaLabel: 'Axis title typography' })
+ const frames = installManagedFrames()
+ surface.render(next, { ariaLabel: 'Axis title typography' })
+ const label = container.querySelector(
+ '[data-ts-key="y-label"]',
+ )
+
+ expect(label?.getAttribute('font-size')).toBe('10')
+ expect(label?.getAttribute('font-weight')).toBe('400')
+ expect(label?.getAttribute('opacity')).toBe('0.4')
+ frames.run(0)
+ frames.run(50)
+ expect(Number(label?.getAttribute('font-size'))).toBeCloseTo(15)
+ expect(Number(label?.getAttribute('font-weight'))).toBeCloseTo(550)
+ expect(Number(label?.getAttribute('opacity'))).toBeCloseTo(0.6)
+ frames.run(100)
+ expect(label?.getAttribute('font-size')).toBe('20')
+ expect(label?.getAttribute('font-weight')).toBe('700')
+ expect(label?.getAttribute('opacity')).toBe('0.8')
+
+ surface.destroy()
+ frames.restore()
+ })
+
it('reports radial-bar paths with the semantic bar motion role', () => {
const roles: string[] = []
const definition = (value: number) =>
diff --git a/packages/charts-core/src/polar-mark-internal.ts b/packages/charts-core/src/polar-mark-internal.ts
index 54fa2211..673ca8a1 100644
--- a/packages/charts-core/src/polar-mark-internal.ts
+++ b/packages/charts-core/src/polar-mark-internal.ts
@@ -4,6 +4,7 @@ import type {
ChartMarkRenderer,
ChartMotionDefinition,
ChartTheme,
+ ChartTextTypography,
ChartValue,
MarkScene,
} from './types'
@@ -25,6 +26,7 @@ export interface PolarLayoutContext {
radius: number
startAngle: number
endAngle: number
+ direction?: ChartTextTypography['direction']
scales: Readonly>
}
diff --git a/packages/charts-core/src/polar.test.ts b/packages/charts-core/src/polar.test.ts
index 3f1d68a8..58a577b4 100644
--- a/packages/charts-core/src/polar.test.ts
+++ b/packages/charts-core/src/polar.test.ts
@@ -687,40 +687,46 @@ describe('polar marks', () => {
angle,
radius: 1,
}))
- const scene = createChartScene(
- defineChart({
- scales: { x: null, y: null },
- marks: [
- polar({
- scales: {
- angle: { scale: scaleLinear().domain([0, tau]) },
- radius: { scale: scaleLinear().domain([0, 1]) },
- },
- guides: [
- angleGrid({ values }),
- angleGrid({
- id: 'authored-anchor',
- values: [Math.PI / 2],
- labelAnchor: 'end',
- }),
- ],
- marks: [
- radialText(rows, {
- id: 'outside-labels',
- angle: 'angle',
- radius: 'radius',
- text: 'id',
- key: 'id',
- radiusOffset: 8,
- anchor: 'outside',
- }),
- ],
- }),
- ],
- margin: 0,
- }),
- { width: 200, height: 200 },
- )
+ const definition = defineChart({
+ scales: { x: null, y: null },
+ marks: [
+ polar({
+ scales: {
+ angle: { scale: scaleLinear().domain([0, tau]) },
+ radius: { scale: scaleLinear().domain([0, 1]) },
+ },
+ guides: [
+ angleGrid({ values }),
+ angleGrid({
+ id: 'authored-anchor',
+ values: [Math.PI / 2],
+ labelAnchor: 'end',
+ }),
+ ],
+ marks: [
+ radialText(rows, {
+ id: 'outside-labels',
+ angle: 'angle',
+ radius: 'radius',
+ text: 'id',
+ key: 'id',
+ radiusOffset: 8,
+ anchor: 'outside',
+ }),
+ radialText([{ id: 'authored', angle: Math.PI / 2, radius: 1 }], {
+ id: 'authored-text-anchor',
+ angle: 'angle',
+ radius: 'radius',
+ text: 'id',
+ key: 'id',
+ anchor: 'start',
+ }),
+ ],
+ }),
+ ],
+ margin: 0,
+ })
+ const scene = createChartScene(definition, { width: 200, height: 200 })
const labels = flatten(scene.nodes).filter((node) => node.kind === 'label')
const textAnchors = rows.map((row) =>
labels.find((node) =>
@@ -743,6 +749,45 @@ describe('polar marks', () => {
expect(labels.every((node) => node.anchor !== ('outside' as never))).toBe(
true,
)
+
+ const rtlScene = createChartScene(
+ definition,
+ { width: 200, height: 200 },
+ { typography: { direction: 'rtl' } },
+ )
+ const rtlLabels = flatten(rtlScene.nodes).filter(
+ (node) => node.kind === 'label',
+ )
+ const rtlExpected = expected.map((anchor) =>
+ anchor === 'start' ? 'end' : anchor === 'end' ? 'start' : anchor,
+ )
+ expect(
+ rows.map(
+ (row) =>
+ rtlLabels.find((node) =>
+ node.key.endsWith(`string:${row.id.length}:${row.id}`),
+ )?.anchor,
+ ),
+ ).toEqual(rtlExpected)
+ expect(
+ values.map(
+ (value) =>
+ rtlLabels.find((node) => node.key === `angle-label:number:${value}`)
+ ?.anchor,
+ ),
+ ).toEqual(rtlExpected)
+ const authoredGroup = flatten(rtlScene.nodes).find(
+ (node) => node.kind === 'group' && node.key === 'authored-anchor:labels',
+ )
+ expect(
+ authoredGroup?.kind === 'group'
+ ? authoredGroup.children.find((node) => node.kind === 'label')?.anchor
+ : undefined,
+ ).toBe('end')
+ expect(
+ rtlLabels.find((node) => node.key.startsWith('authored-text-anchor:'))
+ ?.anchor,
+ ).toBe('start')
})
it('omits every nonfinite radial offset before paint callbacks', () => {
diff --git a/packages/charts-core/src/polar.ts b/packages/charts-core/src/polar.ts
index 9f21807c..7008c67b 100644
--- a/packages/charts-core/src/polar.ts
+++ b/packages/charts-core/src/polar.ts
@@ -24,6 +24,7 @@ import {
} from './polar-sector-internal'
import { resolveNumericScale, resolveScaleInput } from './scale-input'
import { valueKey } from './scales'
+import { physicalTextAnchor } from './guide-layout'
import type { Arc, CurveFactory, CurveFactoryLineOnly } from 'd3-shape'
import type {
Channel,
@@ -280,8 +281,13 @@ export function polar(
values: marks.flatMap((mark) => mark.colorValues),
},
},
- render: ({ chart, color, theme }) => {
- const layout = resolvePolarLayout(options, chart, marks)
+ render: ({ chart, color, theme, layout: chartLayout }) => {
+ const layout = resolvePolarLayout(
+ options,
+ chart,
+ marks,
+ chartLayout.typography?.direction,
+ )
for (const mark of marks) {
if (mark.angleScale) {
requiredPolarScale(
@@ -1720,7 +1726,7 @@ export function radialText(
text: String(textValue),
anchor:
authoredAnchor === 'outside'
- ? outsideRadialAnchor(anglePosition)
+ ? outsideRadialAnchor(anglePosition, layout.direction)
: authoredAnchor,
baseline: visualValue(
options.baseline,
@@ -2408,7 +2414,7 @@ export function angleGrid(options: AngleGridOptions = {}): PolarGuide {
anchor: guideLabelOption(
options.labelAnchor,
labelContext,
- outsideRadialAnchor(position),
+ outsideRadialAnchor(position, layout.direction),
),
baseline: guideLabelOption(
options.labelBaseline,
@@ -2452,6 +2458,7 @@ function resolvePolarLayout(
options: PolarOptions,
chart: ChartBounds,
marks: readonly InitializedPolarMark[],
+ direction: PolarLayoutContext['direction'],
): PolarLayoutContext {
const startAngle = finite(options.startAngle, 0)
const endAngle = finite(options.endAngle, tau)
@@ -2468,6 +2475,7 @@ function resolvePolarLayout(
radius,
startAngle,
endAngle,
+ direction,
scales,
}
@@ -2794,13 +2802,13 @@ function isCompleteRevolution(startAngle: number, endAngle: number): boolean {
return Math.abs(Math.abs(endAngle - startAngle) - tau) <= 1e-12
}
-function outsideRadialAnchor(angle: number): 'start' | 'middle' | 'end' {
+function outsideRadialAnchor(
+ angle: number,
+ direction: PolarLayoutContext['direction'],
+): 'start' | 'middle' | 'end' {
const horizontal = Math.sin(angle)
- return Math.abs(horizontal) <= 1e-6
- ? 'middle'
- : horizontal < 0
- ? 'end'
- : 'start'
+ if (Math.abs(horizontal) <= 1e-6) return 'middle'
+ return physicalTextAnchor(horizontal < 0 ? 'right' : 'left', direction)
}
function guideLabelOption(
diff --git a/packages/charts-core/src/scene-layout.test.ts b/packages/charts-core/src/scene-layout.test.ts
index 8d2d8492..c393a186 100644
--- a/packages/charts-core/src/scene-layout.test.ts
+++ b/packages/charts-core/src/scene-layout.test.ts
@@ -6,6 +6,7 @@ import { createMark } from './mark'
import { createChartScene, defineChart } from './scene'
import { text } from './text'
import type {
+ ChartAxisLabelOptions,
ChartAxisTickLabelContext,
ChartAxisTickLabelOptions,
ChartAxisTickLabelValue,
@@ -240,6 +241,140 @@ describe('automatic scene guide layout', () => {
expect(yLabels.every(({ anchor }) => anchor === 'end')).toBe(true)
})
+ it('applies axis-title typography and paint while preserving string-label defaults', () => {
+ const xLabel = {
+ text: 'Styled horizontal title',
+ fontSize: 18,
+ fontWeight: 700,
+ fill: '#7c3aed',
+ opacity: 0.45,
+ } satisfies ChartAxisLabelOptions
+ const scene = createChartScene(
+ defineChart({
+ marks: [lineY([0, 1])],
+ margin: 0,
+ scales: {
+ x: {
+ scale: scaleLinear().domain([0, 1]),
+ axis: { label: xLabel },
+ },
+ y: {
+ scale: scaleLinear().domain([0, 1]),
+ axis: { label: 'Default vertical title' },
+ },
+ },
+ }),
+ { width: 480, height: 240 },
+ { measureText },
+ )
+ const labels = flatten(scene.nodes).filter(
+ (node): node is SceneLabel => node.kind === 'label',
+ )
+ const styled = labels.find(({ key }) => key === 'x-label')
+ const defaulted = labels.find(({ key }) => key === 'y-label')
+
+ expect(styled).toMatchObject({
+ text: xLabel.text,
+ fontSize: 18,
+ fontWeight: 700,
+ style: { fill: '#7c3aed', opacity: 0.45 },
+ })
+ expect(defaulted).toMatchObject({
+ text: 'Default vertical title',
+ fontSize: 11,
+ fontWeight: 600,
+ style: { fill: 'currentColor', fillOpacity: 0.76 },
+ })
+ expect(styled?.style).toStrictEqual({
+ fill: '#7c3aed',
+ opacity: 0.45,
+ })
+ expect(defaulted?.style).toStrictEqual({
+ fill: 'currentColor',
+ fillOpacity: 0.76,
+ })
+
+ const xTitle = (label: string | ChartAxisLabelOptions) => {
+ const titleScene = createChartScene(
+ defineChart({
+ marks: [lineY([0, 1])],
+ scales: {
+ x: {
+ scale: scaleLinear().domain([0, 1]),
+ axis: { label },
+ },
+ y: { scale: scaleLinear().domain([0, 1]), axis: false },
+ },
+ }),
+ { width: 480, height: 240 },
+ { measureText },
+ )
+ const title = flatten(titleScene.nodes).find(
+ (node): node is SceneLabel =>
+ node.kind === 'label' && node.key === 'x-label',
+ )
+ if (!title) throw new Error('Expected an x-axis title')
+ return title
+ }
+ const defaultStringTitle = xTitle('Default horizontal title')
+ const defaultObjectTitle = xTitle({ text: 'Default horizontal title' })
+ const transparentTitle = xTitle({
+ text: 'Transparent horizontal title',
+ opacity: 0,
+ })
+
+ expect(defaultObjectTitle).toStrictEqual(defaultStringTitle)
+ expect(transparentTitle.style).toStrictEqual({
+ fill: 'currentColor',
+ opacity: 0,
+ })
+ })
+
+ it('measures configured axis-title typography into automatic margins', () => {
+ const definition = (fontSize: number, fontWeight: number) =>
+ defineChart({
+ marks: [lineY([0, 1])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 1]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 1]),
+ axis: {
+ ticks: false,
+ label: {
+ text: 'Average order value',
+ fontSize,
+ fontWeight,
+ },
+ },
+ },
+ },
+ })
+ const measured = vi.fn(measureText)
+ const regular = createChartScene(
+ definition(11, 600),
+ { width: 480, height: 240 },
+ { measureText: measured },
+ )
+ const enlarged = createChartScene(
+ definition(24, 750),
+ { width: 480, height: 240 },
+ { measureText: measured },
+ )
+
+ expect(enlarged.margin.left).toBeGreaterThan(regular.margin.left)
+ expect(measured).toHaveBeenCalledWith(
+ 'Average order value',
+ expect.objectContaining({ fontSize: 24, fontWeight: 750 }),
+ )
+ const title = flatten(enlarged.nodes).find(
+ (node): node is SceneLabel =>
+ node.kind === 'label' && node.key === 'y-label',
+ )
+ if (!title) throw new Error('Expected a y-axis title')
+ const bounds = measureSceneLabelBounds(title, measureText)
+ expect(bounds.x).toBeGreaterThanOrEqual(3.99)
+ })
+
it('measures accessor-resolved tick-label bounds into automatic margins', () => {
const definition = (fontSize: number, dx: number) =>
defineChart({
diff --git a/packages/charts-core/src/scene.test.ts b/packages/charts-core/src/scene.test.ts
index 77158588..3022a2f7 100644
--- a/packages/charts-core/src/scene.test.ts
+++ b/packages/charts-core/src/scene.test.ts
@@ -19,6 +19,7 @@ import type {
ChartDefinition,
ChartPoint,
ChartScene,
+ SceneDot,
SceneNode,
} from './types'
@@ -173,6 +174,335 @@ describe('native mark and channel scene', () => {
)
})
+ it('configures the built-in focus ring while retaining point-color defaults', () => {
+ const rows = [
+ { id: 'alpha', series: 'Alpha', x: 0, y: 1 },
+ { id: 'beta', series: 'Beta', x: 1, y: 2 },
+ ]
+ const spec = {
+ marks: [
+ lineY(rows, {
+ id: 'series',
+ x: 'x',
+ y: 'y',
+ z: 'series',
+ key: 'id',
+ }),
+ ],
+ color: {
+ domain: ['Alpha', 'Beta'],
+ range: ['#2563eb', '#f97316'],
+ },
+ theme: { background: '#f8fafc', foreground: '#0f172a' },
+ ...linearAxes([0, 1], [0, 2]),
+ } as const
+ const scene = createChartScene(
+ defineChart({
+ ...spec,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 1.5,
+ fill: '#ffffff',
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ const focusDots = flatten(scene.nodes).filter(
+ (node) =>
+ node.kind === 'dot' &&
+ scene.points.some((point) => point.key === node.key) &&
+ node.radius === 4,
+ )
+
+ expect(scene.theme).toMatchObject({
+ background: '#f8fafc',
+ foreground: '#0f172a',
+ })
+ expect(focusDots.map((node) => node.style)).toEqual([
+ { fill: '#ffffff', stroke: '#2563eb', strokeWidth: 1.5 },
+ { fill: '#ffffff', stroke: '#f97316', strokeWidth: 1.5 },
+ ])
+
+ const fixedStroke = createChartScene(
+ defineChart({
+ ...spec,
+ focusRing: {
+ radius: 3,
+ strokeWidth: 2,
+ fill: '#ffffff',
+ stroke: '#0f172a',
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ const fixedDots = flatten(fixedStroke.nodes).filter(
+ (node) => node.kind === 'dot' && node.radius === 3,
+ )
+
+ expect(fixedDots.map((node) => node.style)).toEqual([
+ { fill: '#ffffff', stroke: '#0f172a', strokeWidth: 2 },
+ { fill: '#ffffff', stroke: '#0f172a', strokeWidth: 2 },
+ ])
+
+ const booleanRing = createChartScene(
+ defineChart({ ...spec, focusRing: true }),
+ { width: 320, height: 180 },
+ )
+ const booleanDots = flatten(booleanRing.nodes).filter(
+ (node): node is SceneDot => node.kind === 'dot' && node.radius === 5,
+ )
+
+ expect(booleanDots.map((node) => node.style)).toEqual([
+ {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: '#2563eb',
+ strokeWidth: 2.5,
+ },
+ {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: '#f97316',
+ strokeWidth: 2.5,
+ },
+ ])
+
+ const undefinedFields = createChartScene(
+ defineChart({
+ ...spec,
+ focusRing: {
+ radius: undefined,
+ fill: undefined,
+ stroke: undefined,
+ strokeWidth: undefined,
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ const undefinedDots = flatten(undefinedFields.nodes).filter(
+ (node) => node.kind === 'dot' && node.radius === 5,
+ )
+
+ expect(undefinedDots.map((node) => node.style)).toEqual(
+ booleanDots.map((node) => node.style),
+ )
+
+ const zeroFields = createChartScene(
+ defineChart({
+ ...spec,
+ focusRing: {
+ radius: 0,
+ strokeWidth: 0,
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ const zeroDots = flatten(zeroFields.nodes).filter(
+ (node): node is SceneDot =>
+ node.kind === 'dot' &&
+ zeroFields.points.some((point) => point.key === node.key),
+ )
+
+ expect(
+ zeroDots.map((node) => ({ radius: node.radius, style: node.style })),
+ ).toEqual([
+ {
+ radius: 0,
+ style: {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: '#2563eb',
+ strokeWidth: 0,
+ },
+ },
+ {
+ radius: 0,
+ style: {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: '#f97316',
+ strokeWidth: 0,
+ },
+ },
+ ])
+
+ for (const invalid of [
+ -1,
+ Number.NaN,
+ Number.POSITIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ ]) {
+ const invalidFields = createChartScene(
+ defineChart({
+ ...spec,
+ focusRing: {
+ radius: invalid,
+ strokeWidth: invalid,
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ const invalidDots = flatten(invalidFields.nodes).filter(
+ (node): node is SceneDot =>
+ node.kind === 'dot' &&
+ invalidFields.points.some((point) => point.key === node.key),
+ )
+
+ expect(
+ invalidDots.map((node) => ({
+ radius: node.radius,
+ style: node.style,
+ })),
+ ).toEqual(
+ booleanDots.map((node) => ({
+ radius: node.radius,
+ style: node.style,
+ })),
+ )
+ }
+ expectTypeOf(scene).toMatchTypeOf>()
+ })
+
+ it('uses the theme focus ring unless the definition overrides it', () => {
+ const spec = {
+ marks: [lineY([1, 2])],
+ ...linearAxes([0, 1], [0, 2]),
+ } as const
+ const focusDots = (scene: ChartScene) =>
+ flatten(scene.nodes).filter(
+ (node): node is SceneDot =>
+ node.kind === 'dot' &&
+ scene.points.some((point) => point.key === node.key),
+ )
+
+ const themed = createChartScene(
+ defineChart({
+ ...spec,
+ theme: {
+ focusRing: {
+ radius: 8,
+ strokeWidth: 4,
+ fill: '#f8fafc',
+ stroke: '#0f172a',
+ },
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+
+ expect(themed.theme.focusRing).toEqual({
+ radius: 8,
+ strokeWidth: 4,
+ fill: '#f8fafc',
+ stroke: '#0f172a',
+ })
+ expect(
+ focusDots(themed).map((node) => ({
+ radius: node.radius,
+ style: node.style,
+ })),
+ ).toEqual([
+ {
+ radius: 8,
+ style: {
+ fill: '#f8fafc',
+ stroke: '#0f172a',
+ strokeWidth: 4,
+ },
+ },
+ {
+ radius: 8,
+ style: {
+ fill: '#f8fafc',
+ stroke: '#0f172a',
+ strokeWidth: 4,
+ },
+ },
+ ])
+
+ const themeDisabled = createChartScene(
+ defineChart({ ...spec, theme: { focusRing: false } }),
+ { width: 320, height: 180 },
+ )
+ expect(focusDots(themeDisabled)).toHaveLength(0)
+
+ const definitionOptions = createChartScene(
+ defineChart({
+ ...spec,
+ theme: { focusRing: false },
+ focusRing: { radius: 3, strokeWidth: 1 },
+ }),
+ { width: 320, height: 180 },
+ )
+ expect(
+ focusDots(definitionOptions).map((node) => ({
+ radius: node.radius,
+ style: node.style,
+ })),
+ ).toEqual([
+ {
+ radius: 3,
+ style: {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: 'var(--ts-chart-1, #2563eb)',
+ strokeWidth: 1,
+ },
+ },
+ {
+ radius: 3,
+ style: {
+ fill: 'var(--ts-chart-focus-fill, Canvas)',
+ stroke: 'var(--ts-chart-1, #2563eb)',
+ strokeWidth: 1,
+ },
+ },
+ ])
+
+ const definitionDisabled = createChartScene(
+ defineChart({
+ ...spec,
+ theme: { focusRing: { radius: 8 } },
+ focusRing: false,
+ }),
+ { width: 320, height: 180 },
+ )
+ expect(focusDots(definitionDisabled)).toHaveLength(0)
+
+ const definitionEnabled = createChartScene(
+ defineChart({
+ ...spec,
+ theme: { focusRing: false },
+ focusRing: true,
+ }),
+ { width: 320, height: 180 },
+ )
+ expect(focusDots(definitionEnabled).map((node) => node.radius)).toEqual([
+ 5, 5,
+ ])
+
+ for (const invalid of [
+ -1,
+ Number.NaN,
+ Number.POSITIVE_INFINITY,
+ Number.NEGATIVE_INFINITY,
+ ]) {
+ const invalidTheme = createChartScene(
+ defineChart({
+ ...spec,
+ theme: {
+ focusRing: { radius: invalid, strokeWidth: invalid },
+ },
+ }),
+ { width: 320, height: 180 },
+ )
+ expect(
+ focusDots(invalidTheme).map((node) => ({
+ radius: node.radius,
+ strokeWidth: node.style?.strokeWidth,
+ })),
+ ).toEqual([
+ { radius: 5, strokeWidth: 2.5 },
+ { radius: 5, strokeWidth: 2.5 },
+ ])
+ }
+ })
+
it('rejects duplicate control ids', () => {
const behavior = { id: 'duplicate', resolve: () => ({}) }
diff --git a/packages/charts-core/src/scene.ts b/packages/charts-core/src/scene.ts
index 5f465fd4..02a16ca1 100644
--- a/packages/charts-core/src/scene.ts
+++ b/packages/charts-core/src/scene.ts
@@ -2,6 +2,7 @@ import { createColorScale, valueKey } from './scales'
import { resolveConfiguredScale } from './configured-scale'
import {
measureSceneLabelBounds,
+ physicalTextAnchor,
withChartTextTypography,
} from './guide-layout'
import { nearestScenePoint } from './nearest'
@@ -118,17 +119,29 @@ type ChartDefinitionWithOptions = Omit<
> &
TOptions
+declare const definedResponsiveChart: unique symbol
+
type DefinedResponsiveChart = Omit<
ResponsiveChartDefinition<
ChartSpecDatum,
ChartSpecXValue,
ChartSpecYValue
>,
- keyof ChartDefinitionOptions
+ keyof ChartDefinitionOptions | 'chart'
> & {
- chart: (context: ChartBuildContext) => CheckedChartSpec
+ readonly [definedResponsiveChart]: TSpec
+ chart: (context: ChartBuildContext) => TSpec
}
+type ValidResponsiveDefinitionInput<
+ TDefinition extends ResponsiveChartDefinition,
+> =
+ ErasedChartSpec extends ReturnType
+ ? unknown
+ : typeof definedResponsiveChart extends keyof TDefinition
+ ? unknown
+ : never
+
export function defineChart<
const TMarks extends readonly ChartMark[],
const TSpec extends ChartSpec,
@@ -141,18 +154,11 @@ export function defineChart<
): DefinedStaticChart
export function defineChart<
const TSpec extends ErasedChartSpec,
- TTooltipHost extends string = never,
+ TTooltipHost extends string,
+ const TConfig extends ResponsiveChartConfig,
>(
- config: ResponsiveChartConfig,
-): Omit<
- ResponsiveChartDefinition<
- ChartSpecDatum,
- ChartSpecXValue,
- ChartSpecYValue
- >,
- keyof ChartDefinitionOptions
-> &
- ResponsiveChartConfig
+ config: ResponsiveChartConfig & TConfig,
+): DefinedResponsiveChart> & Omit
export function defineChart(
chart: (context: ChartBuildContext) => CheckedChartSpec,
): DefinedResponsiveChart
@@ -231,9 +237,7 @@ export function defineChart<
>,
>(
definition: TDefinition &
- (ErasedChartSpec extends ReturnType
- ? unknown
- : never),
+ ValidResponsiveDefinitionInput>,
options: TOptions,
): ChartDefinitionWithOptions, NoInfer>
export function defineChart(definition?: any, options?: any): any {
@@ -512,6 +516,7 @@ function createChartSceneWithScaleResolver<
theme,
width,
height,
+ direction: layoutOptions.typography?.direction,
}
nodes.push(legend.render(legendContext))
if (legend.control) controls.push(legend.control(legendContext))
@@ -524,11 +529,14 @@ function createChartSceneWithScaleResolver<
}
hostControlIds.add(identity)
}
- if (
- definition.focus !== false &&
- definition.focusRing !== false &&
- points.length
- ) {
+ const focusRing = definition.focusRing ?? theme.focusRing
+ if (definition.focus !== false && focusRing !== false && points.length) {
+ const focusRingOptions =
+ typeof focusRing === 'object' ? focusRing : undefined
+ const radius = finiteNonNegative(focusRingOptions?.radius, 5)
+ const fill = focusRingOptions?.fill ?? 'var(--ts-chart-focus-fill, Canvas)'
+ const stroke = focusRingOptions?.stroke
+ const strokeWidth = finiteNonNegative(focusRingOptions?.strokeWidth, 2.5)
for (const entry of defaultFocusEntries) {
nodes.push({
kind: 'group',
@@ -547,11 +555,11 @@ function createChartSceneWithScaleResolver<
key: point.key,
x: point.x,
y: point.y,
- radius: 5,
+ radius,
style: {
- fill: 'var(--ts-chart-focus-fill, Canvas)',
- stroke: point.color,
- strokeWidth: 2.5,
+ fill,
+ stroke: stroke ?? point.color,
+ strokeWidth,
},
})),
})
@@ -569,6 +577,9 @@ function createChartSceneWithScaleResolver<
colors,
gradients: definition.gradients ?? [],
theme,
+ ...(layoutOptions.typography?.direction === undefined
+ ? {}
+ : { direction: layoutOptions.typography.direction }),
...(controls.length ? { controls } : {}),
...(focusGuides.length ? { focusGuides } : {}),
[chartSceneSource]: [definition, initialized],
@@ -1077,6 +1088,7 @@ function resolveSceneLayout(
theme,
width,
height,
+ direction: layout.typography?.direction,
})
const legendBounds =
legend && legendHeight !== undefined
@@ -1097,6 +1109,7 @@ function resolveSceneLayout(
theme,
width,
layout.measureText,
+ layout.typography?.direction === 'rtl',
)
return {
margin,
@@ -1131,6 +1144,7 @@ function resolveSceneLayout(
theme,
width,
height,
+ direction: layout.typography?.direction,
},
)
if (resolved.legend.placement === 'bottom') {
@@ -1274,6 +1288,15 @@ function finiteMargin(value: number | undefined): number {
return value !== undefined && Number.isFinite(value) ? Math.max(0, value) : 0
}
+function finiteNonNegative(
+ value: number | undefined,
+ fallback: number,
+): number {
+ return value !== undefined && Number.isFinite(value) && value >= 0
+ ? value
+ : fallback
+}
+
function uniformMargin(value: number): ChartMargin {
return { top: value, right: value, bottom: value, left: value }
}
@@ -1355,6 +1378,7 @@ function createAxes(
theme: ChartTheme,
width: number,
measureText?: ChartTextMeasurer,
+ rightToLeft = false,
): ResolvedAxes {
const children: SceneNode[] = []
const inset = guides.length ? automaticGuideInset : 0
@@ -1469,6 +1493,7 @@ function createAxes(
width,
theme,
measureText,
+ rightToLeft,
)
const visibleLabels =
tickLabels === false
@@ -1501,9 +1526,12 @@ function createAxes(
children.push(candidate.label)
}
- const labelText = axisLabelText(presentation)
+ const axisLabel = presentation?.label
+ const labelText =
+ typeof axisLabel === 'string' ? axisLabel : axisLabel?.text
if (!labelText) return
- const labelOffset = axisLabelOffset(presentation)
+ const labelOptions = typeof axisLabel === 'object' ? axisLabel : undefined
+ const labelOffset = labelOptions?.offset ?? 'auto'
const explicitOffset = labelOffset !== 'auto'
const label: SceneLabel = {
kind: 'label',
@@ -1515,9 +1543,14 @@ function createAxes(
text: labelText,
anchor: 'middle',
baseline: bottom && !explicitOffset ? 'hanging' : 'auto',
- fontSize: width < 360 ? 10 : 11,
- fontWeight: 600,
- style: { fill: theme.foreground, fillOpacity: 0.76 },
+ fontSize: labelOptions?.fontSize ?? (width < 360 ? 10 : 11),
+ fontWeight: labelOptions?.fontWeight ?? 600,
+ style: {
+ fill: labelOptions?.fill ?? theme.foreground,
+ ...(labelOptions?.opacity === undefined
+ ? { fillOpacity: 0.76 }
+ : { opacity: labelOptions.opacity }),
+ },
}
includeOutward(measureSceneLabelBounds(label, measureText))
children.push(label)
@@ -1564,6 +1597,7 @@ function createAxes(
width,
theme,
measureText,
+ rightToLeft,
)
const visibleLabels =
tickLabels === false ? [] : thinTickLabels(candidates, tickLabels, false)
@@ -1594,8 +1628,11 @@ function createAxes(
children.push(candidate.label)
}
- const labelText = axisLabelText(presentation)
+ const axisLabel = presentation?.label
+ const labelText =
+ typeof axisLabel === 'string' ? axisLabel : axisLabel?.text
if (!labelText) return
+ const labelOptions = typeof axisLabel === 'object' ? axisLabel : undefined
const label: SceneLabel = {
kind: 'label',
key: `${guide.id}-label`,
@@ -1605,11 +1642,16 @@ function createAxes(
anchor: 'middle',
baseline: 'middle',
rotate: right ? 90 : -90,
- fontSize: 11,
- fontWeight: 600,
- style: { fill: theme.foreground, fillOpacity: 0.76 },
+ fontSize: labelOptions?.fontSize ?? 11,
+ fontWeight: labelOptions?.fontWeight ?? 600,
+ style: {
+ fill: labelOptions?.fill ?? theme.foreground,
+ ...(labelOptions?.opacity === undefined
+ ? { fillOpacity: 0.76 }
+ : { opacity: labelOptions.opacity }),
+ },
}
- const labelOffset = axisLabelOffset(presentation)
+ const labelOffset = labelOptions?.offset ?? 'auto'
if (labelOffset !== 'auto') {
label.x = axisX + direction * Math.max(0, finiteMargin(labelOffset))
} else {
@@ -1676,20 +1718,6 @@ function tickLabelPresentation(
return axis?.tickLabels ?? {}
}
-function axisLabelText(
- axis: ChartAxisPresentationOptions | undefined,
-): string | undefined {
- return typeof axis?.label === 'string' ? axis.label : axis?.label?.text
-}
-
-function axisLabelOffset(
- axis: ChartAxisPresentationOptions | undefined,
-): number | 'auto' {
- return typeof axis?.label === 'object'
- ? (axis.label.offset ?? 'auto')
- : 'auto'
-}
-
interface TickLabelCandidate {
value: ChartValue
label: SceneLabel
@@ -1737,6 +1765,7 @@ function createTickLabelCandidates(
width: number,
theme: ChartTheme,
measureText: ChartTextMeasurer | undefined,
+ rightToLeft = false,
): TickLabelCandidate[] {
const defaultFontSize = width < 360 ? 10 : 11
const positiveSide = guide.side === 'bottom' || guide.side === 'right'
@@ -1755,18 +1784,20 @@ function createTickLabelCandidates(
const opacity = resolveTickLabelValue(options.opacity, context)
const dx = resolveTickLabelValue(options.dx, context) ?? 0
const dy = resolveTickLabelValue(options.dy, context) ?? 0
- const defaultAnchor =
+ // Automatic anchors preserve a physical placement outside the plot.
+ // Authored anchors remain logical SVG start/end values.
+ const automaticAnchor: NonNullable =
guide.channel === 'y'
? positiveSide
- ? 'start'
- : 'end'
+ ? physicalTextAnchor('left', rightToLeft ? 'rtl' : 'ltr')
+ : physicalTextAnchor('right', rightToLeft ? 'rtl' : 'ltr')
: (rotate ?? 0) < 0
- ? 'end'
+ ? physicalTextAnchor('right', rightToLeft ? 'rtl' : 'ltr')
: (rotate ?? 0) > 0
- ? 'start'
+ ? physicalTextAnchor('left', rightToLeft ? 'rtl' : 'ltr')
: 'middle'
const anchor =
- resolveTickLabelValue(options.anchor, context) ?? defaultAnchor
+ resolveTickLabelValue(options.anchor, context) ?? automaticAnchor
const label: SceneLabel =
guide.channel === 'x'
? {
diff --git a/packages/charts-core/src/svg-renderer.test.ts b/packages/charts-core/src/svg-renderer.test.ts
index af0d7c39..36e18e9e 100644
--- a/packages/charts-core/src/svg-renderer.test.ts
+++ b/packages/charts-core/src/svg-renderer.test.ts
@@ -1,4 +1,7 @@
+import { scaleLinear } from 'd3-scale'
import { describe, expect, it } from 'vitest'
+import { lineY } from './line'
+import { createChartScene, defineChart } from './scene'
import { renderChartSvg } from './svg'
import type { ChartScene } from './types'
@@ -13,6 +16,61 @@ describe('SVG scene renderer', () => {
)
expect(svg).not.toContain('M99,99Z')
})
+
+ it('serializes Cartesian axis-title typography and paint', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [lineY([1, 2, 3])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 3]),
+ axis: {
+ ticks: false,
+ label: {
+ text: 'Revenue',
+ fontSize: 17,
+ fontWeight: 650,
+ fill: '#0f766e',
+ opacity: 0.6,
+ },
+ },
+ },
+ },
+ }),
+ { width: 480, height: 260 },
+ )
+ const svg = renderChartSvg(scene, { ariaLabel: 'Revenue chart' })
+
+ expect(svg).toMatch(
+ /]* fill="#0f766e" opacity="0\.6"[^>]* font-size="17" font-weight="650"/,
+ )
+ })
+
+ it('serializes right-to-left scene direction on the root SVG', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [lineY([1, 2, 3])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 3]),
+ side: 'right',
+ },
+ },
+ }),
+ { width: 480, height: 260 },
+ { typography: { direction: 'rtl' } },
+ )
+
+ const svg = renderChartSvg(scene, { ariaLabel: 'RTL chart' })
+
+ expect(scene.direction).toBe('rtl')
+ expect(svg).toMatch(/^]* direction="rtl"[^>]*>/)
+ expect(svg).toMatch(
+ /data-ts-key="y-tick-label:[^"]+"[^>]*text-anchor="end"/,
+ )
+ })
})
function testScene(): ChartScene {
diff --git a/packages/charts-core/src/svg-renderer.ts b/packages/charts-core/src/svg-renderer.ts
index aa191bf5..4e09f45a 100644
--- a/packages/charts-core/src/svg-renderer.ts
+++ b/packages/charts-core/src/svg-renderer.ts
@@ -29,6 +29,10 @@ export function renderChartSvgWithHooks(
? `${escapeText(options.ariaDescription)}`
: ''
const definitions = hooks?.renderDefinitions?.(scene, idPrefix) ?? ''
+ const direction =
+ scene.direction === 'ltr' || scene.direction === 'rtl'
+ ? ` direction="${scene.direction}"`
+ : ''
const background =
scene.theme.background === 'transparent'
? ''
@@ -46,7 +50,7 @@ export function renderChartSvgWithHooks(
idPrefix,
)
- return `${description}${definitions}${background}${renderSceneNodes(scene.nodes, idPrefix, hooks)}`
+ return `${description}${definitions}${background}${renderSceneNodes(scene.nodes, idPrefix, hooks)}`
}
export function renderSceneNodes(
diff --git a/packages/charts-core/src/svg-surface.test.ts b/packages/charts-core/src/svg-surface.test.ts
index 0f23946f..847e9f3d 100644
--- a/packages/charts-core/src/svg-surface.test.ts
+++ b/packages/charts-core/src/svg-surface.test.ts
@@ -9,6 +9,62 @@ import { renderChartSvgWithResources } from './svg-resources'
import { createSvgChartRenderer, svgChartRenderer } from './svg-surface'
describe('SVG surface coordinates', () => {
+ it('paints a configured focus ring without exposing it to accessibility', () => {
+ const scene = createChartScene(
+ defineChart({
+ marks: [
+ lineY([{ x: 1, y: 2 }], {
+ id: 'trend',
+ x: 'x',
+ y: 'y',
+ }),
+ ],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]) },
+ y: { scale: scaleLinear().domain([0, 4]) },
+ },
+ guides: false,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 1.5,
+ fill: '#ffffff',
+ stroke: '#0f172a',
+ },
+ }),
+ { width: 200, height: 120 },
+ )
+ const point = scene.points[0]
+ if (!point) throw new Error('Expected a focus point')
+ const container = document.createElement('div')
+ const surface = svgChartRenderer.mount(container, () => {})
+ surface.render(scene, { ariaLabel: 'Configured focus ring' })
+ const svg = container.querySelector('svg')
+ const layer = container.querySelector(
+ '.ts-chart__focus-layer--default',
+ )
+ const ring = layer?.querySelector('circle')
+
+ expect(svg?.getAttribute('role')).toBe('img')
+ expect(svg?.getAttribute('aria-label')).toBe('Configured focus ring')
+ expect(layer?.getAttribute('aria-hidden')).toBe('true')
+ expect(layer?.getAttribute('visibility')).toBe('hidden')
+ expect(ring?.getAttribute('r')).toBe('4')
+ expect(ring?.getAttribute('fill')).toBe('#ffffff')
+ expect(ring?.getAttribute('stroke')).toBe('#0f172a')
+ expect(ring?.getAttribute('stroke-width')).toBe('1.5')
+
+ surface.paintFocus({
+ primary: point,
+ group: [point],
+ source: 'keyboard',
+ pinned: false,
+ })
+
+ expect(layer?.getAttribute('visibility')).toBe('visible')
+ expect(ring?.getAttribute('visibility')).toBe('visible')
+ surface.destroy()
+ })
+
it('mounts viewport content with the fixed authored plot clip', () => {
const scene = createChartScene(
defineChart({
diff --git a/packages/charts-core/src/type-contract.test.ts b/packages/charts-core/src/type-contract.test.ts
index 0c1223c6..dfdc7317 100644
--- a/packages/charts-core/src/type-contract.test.ts
+++ b/packages/charts-core/src/type-contract.test.ts
@@ -418,6 +418,39 @@ const facetedMark = facet(rows, {
return categoricalSpec
},
})
+if (false) {
+ facet(rows, {
+ by: 'category',
+ // @ts-expect-error Facet cell focus rings belong to the outer theme.
+ chart: () => ({ ...categoricalSpec, theme: { focusRing: false } }),
+ })
+ facet(rows, {
+ by: 'category',
+ // @ts-expect-error Facet cells cannot override the outer focus ring.
+ chart: () => ({ ...categoricalSpec, focusRing: false }),
+ })
+ const storedFocusRingChild = defineChart({
+ ...categoricalSpec,
+ focusRing: false,
+ })
+ facet(rows, {
+ by: 'category',
+ // @ts-expect-error Stored facet children cannot own the outer focus ring.
+ chart: () => storedFocusRingChild,
+ })
+ const mixedFocusRingChild = Math.random()
+ ? { ...categoricalSpec, kind: 'valid' as const }
+ : {
+ ...categoricalSpec,
+ kind: 'invalid' as const,
+ theme: { focusRing: false as const },
+ }
+ facet(rows, {
+ by: 'category',
+ // @ts-expect-error Every possible facet child must leave the focus ring to the outer chart.
+ chart: () => mixedFocusRingChild,
+ })
+}
const customMark = createMark(() => ({
id: 'custom',
channels: {},
diff --git a/packages/charts-core/src/types.ts b/packages/charts-core/src/types.ts
index 3ed32bb9..77056344 100644
--- a/packages/charts-core/src/types.ts
+++ b/packages/charts-core/src/types.ts
@@ -381,6 +381,11 @@ export interface ChartAxisTickLabelOptions {
export interface ChartAxisLabelOptions {
text: string
offset?: number | 'auto'
+ fontSize?: number
+ fontWeight?: number
+ /** Text fill color. Defaults to the theme foreground. */
+ fill?: string
+ opacity?: number
motion?: ChartMotionDefinition
}
@@ -519,6 +524,7 @@ export interface ChartColorLegendContext {
theme: ChartTheme
width: number
height: number
+ direction?: ChartTextTypography['direction']
}
export type ChartLegendPlacement = 'top' | 'bottom'
@@ -579,6 +585,17 @@ export interface ChartTheme {
grid: string
background: string
palette: readonly string[]
+ /** Default presentation for the built-in primary-point focus ring. */
+ focusRing?: boolean | ChartFocusRingOptions
+}
+
+/** Paint and geometry for the built-in primary-point focus indicator. */
+export interface ChartFocusRingOptions {
+ radius?: number
+ fill?: string
+ /** Defaults to the focused point's resolved color. */
+ stroke?: string
+ strokeWidth?: number
}
export interface ChartGradientStop {
@@ -879,8 +896,8 @@ export interface ChartDefinitionOptions<
> {
maxFocusDistance?: number
focus?: ChartFocusMode, NoInfer, NoInfer>
- /** Shows the built-in primary-point focus ring. Defaults to true. */
- focusRing?: boolean
+ /** Shows and optionally styles the built-in primary-point focus ring. Overrides the theme default. */
+ focusRing?: boolean | ChartFocusRingOptions
/** Optional app-owned cursor shared by one or more chart definitions. */
cursor?: ChartCursorBinding<
NoInfer,
@@ -913,7 +930,7 @@ export interface ChartDefinitionOptions<
interface StoredChartDefinitionOptions {
maxFocusDistance?: number
focus?: ChartFocusMode
- focusRing?: boolean
+ focusRing?: boolean | ChartFocusRingOptions
cursor?: ChartCursorBinding
spatialIndex?: ChartSpatialIndexFactory
svgAnimation?: boolean | ChartAnimationOptions
@@ -1384,6 +1401,7 @@ export interface SceneFocusGuide {
projectY?: (value: ChartValue) => number | undefined
motion?: ChartMotionDefinition
measureText?: ChartTextMeasurer
+ direction?: ChartTextTypography['direction']
/** Facet-owned point-key prefix. Omitted for a top-level guide. */
scope?: string
/** Resolves this guide's dynamic presentation without retaining its implementation in every renderer bundle. */
@@ -1527,6 +1545,7 @@ export interface ChartScene<
colors: ResolvedColorScale
gradients: readonly ChartLinearGradient[]
theme: ChartTheme
+ direction?: ChartTextTypography['direction']
controls?: readonly ChartHostControl[]
focusGuides?: readonly SceneFocusGuide[]
}
diff --git a/packages/charts-core/src/universal-types.ts b/packages/charts-core/src/universal-types.ts
index 3aef282c..8aed7dbe 100644
--- a/packages/charts-core/src/universal-types.ts
+++ b/packages/charts-core/src/universal-types.ts
@@ -225,6 +225,7 @@ export type {
ChartFocusAnchor,
ChartFocusCursorBinding,
ChartFocusPresentation,
+ ChartFocusRingOptions,
ChartFocusFilter,
ChartFocusAffinity,
ChartFocusGroupContext,
diff --git a/packages/charts-core/src/view-compose.test.ts b/packages/charts-core/src/view-compose.test.ts
index 49ce0986..fadd4ff1 100644
--- a/packages/charts-core/src/view-compose.test.ts
+++ b/packages/charts-core/src/view-compose.test.ts
@@ -27,7 +27,11 @@ import type {
SceneNode,
StaticChartDefinition,
} from './types'
-import type { ComposeViewsOptions, ViewScaleLink } from './view'
+import type {
+ ComposableResponsiveChartDefinition,
+ ComposeViewsOptions,
+ ViewScaleLink,
+} from './view'
import type { PieDatum } from './polar'
interface MainRow {
@@ -71,26 +75,34 @@ describe('composed views', () => {
height: number
foreground: string
}> = []
- const responsive = defineChart(({ width, height, defaultTheme }) => {
- contexts.push({
- width,
- height,
- foreground: defaultTheme.foreground,
- })
- return {
- marks: [dot([{ x: width, y: height }], { x: 'x', y: 'y' })],
- scales: {
- x: { scale: scaleLinear().domain([0, width]) },
- y: { scale: scaleLinear().domain([0, height]) },
- },
- guides: false,
- margin: 0,
- }
+ const responsive = defineChart({
+ chart: ({ width, height, defaultTheme }) => {
+ contexts.push({
+ width,
+ height,
+ foreground: defaultTheme.foreground,
+ })
+ return {
+ marks: [dot([{ x: width, y: height }], { x: 'x', y: 'y' })],
+ scales: {
+ x: { scale: scaleLinear().domain([0, width]) },
+ y: { scale: scaleLinear().domain([0, height]) },
+ },
+ guides: false,
+ margin: 0,
+ }
+ },
})
const definition = composeViews({
views: { responsive },
layout: fill('responsive'),
})
+ expectTypeOf>().toEqualTypeOf<{
+ x: number
+ y: number
+ }>()
+ expectTypeOf>().toEqualTypeOf()
+ expectTypeOf>().toEqualTypeOf()
const scene = createChartScene(
{ ...definition, theme: { foreground: '#123456' } },
@@ -573,6 +585,20 @@ describe('composed views', () => {
...child,
theme: { background: '#fff' },
} as unknown as StaticChartDefinition
+ const focusThemeChild = {
+ ...child,
+ theme: { focusRing: false },
+ } as unknown as StaticChartDefinition
+ const responsiveFocusThemeChild = defineChart(() => ({
+ marks: [dot([{ id: 'point', x: 0, y: 1 }], { x: 'x', y: 'y' })],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 10]) },
+ y: { scale: scaleLinear().domain([0, 2]) },
+ },
+ guides: false,
+ margin: 0,
+ theme: { focusRing: false },
+ }))
expect(() =>
unsafeCompose({
@@ -604,6 +630,21 @@ describe('composed views', () => {
layout: fill('child'),
}),
).toThrow(/cannot own a scene background/)
+ expect(() =>
+ unsafeCompose({
+ views: { child: focusThemeChild },
+ layout: fill('child'),
+ }),
+ ).toThrow(/cannot own a focus ring theme/)
+ expect(() =>
+ createChartScene(
+ unsafeCompose({
+ views: { child: responsiveFocusThemeChild },
+ layout: fill('child'),
+ }),
+ { width: 300, height: 200 },
+ ),
+ ).toThrow(/cannot own a focus ring theme/)
const controlled = composeViews({
views: { child: controlledLegendChild() },
@@ -620,6 +661,38 @@ describe('composed views', () => {
summary: linearChild([0, 10]),
}
if (false) {
+ const focusThemeChild = {
+ ...linearChild([0, 10]),
+ theme: { focusRing: false as const },
+ }
+ const responsiveFocusThemeChild = defineChart(() => ({
+ ...linearChild([0, 10]),
+ theme: { focusRing: false as const },
+ }))
+ const annotatedResponsiveFocusThemeChild: ComposableResponsiveChartDefinition =
+ {
+ chart: () => ({
+ marks: [],
+ scales: { x: null, y: null },
+ theme: {
+ // @ts-expect-error A composable responsive child cannot resolve an outer focus ring theme.
+ focusRing: false,
+ },
+ }),
+ }
+ void annotatedResponsiveFocusThemeChild
+ composeViews({
+ // @ts-expect-error A composed child cannot own the outer focus ring theme.
+ views: { child: focusThemeChild },
+ // @ts-expect-error A composed child cannot own the outer focus ring theme.
+ layout: fill('child'),
+ })
+ composeViews({
+ // @ts-expect-error A responsive child cannot return the outer focus ring theme.
+ views: { child: responsiveFocusThemeChild },
+ // @ts-expect-error The rejected responsive child cannot be placed.
+ layout: fill('child'),
+ })
composeViews({
views,
// @ts-expect-error "ghost" is not a named view
@@ -640,6 +713,22 @@ describe('composed views', () => {
expect(Object.keys(views)).toEqual(['main', 'summary'])
})
+
+ it('uses the outer focus ring theme for composed points', () => {
+ const definition = composeViews({
+ views: { child: linearChild([0, 10]) },
+ layout: fill('child'),
+ })
+ const scene = createChartScene(
+ { ...definition, theme: { focusRing: { radius: 9 } } },
+ { width: 300, height: 200 },
+ )
+ const focusDots = flatten(scene.nodes).filter(
+ (node) => node.kind === 'dot' && node.radius === 9,
+ )
+
+ expect(focusDots).toHaveLength(1)
+ })
})
function mixedDefinition(rows: readonly MainRow[] = mainRows) {
diff --git a/packages/charts-core/src/view.ts b/packages/charts-core/src/view.ts
index e02e122f..81c40ab2 100644
--- a/packages/charts-core/src/view.ts
+++ b/packages/charts-core/src/view.ts
@@ -13,6 +13,7 @@ import {
resolveViewLayoutInternal,
} from './view-layout'
import type {
+ ChartBuildContext,
ChartMargin,
ChartDefinition,
ChartMotionDefinition,
@@ -73,6 +74,14 @@ type WithoutEmbeddedHostOptions = Omit<
[TOption in EmbeddedHostOption]?: never
}
+type ComposableChartTheme = Omit<
+ Partial,
+ 'background' | 'focusRing'
+> & {
+ background?: never
+ focusRing?: never
+}
+
/** A chart definition that can be embedded without creating a second host. */
export type ComposableStaticChartDefinition<
TDatum = unknown,
@@ -83,7 +92,7 @@ export type ComposableStaticChartDefinition<
'gradients' | 'theme'
> & {
gradients?: readonly []
- theme?: Omit, 'background'> & { background?: never }
+ theme?: ComposableChartTheme
}
/** A responsive chart definition whose resolved spec can be embedded in a view. */
@@ -91,9 +100,16 @@ export type ComposableResponsiveChartDefinition<
TDatum = unknown,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
-> = WithoutEmbeddedHostOptions<
- ResponsiveChartDefinition
->
+> = Omit<
+ WithoutEmbeddedHostOptions<
+ ResponsiveChartDefinition
+ >,
+ 'chart'
+> & {
+ chart: (
+ context: ChartBuildContext,
+ ) => ComposableStaticChartDefinition
+}
export type ComposableChartDefinition<
TDatum = unknown,
@@ -204,6 +220,23 @@ type NamedViewYValue = DefinitionYValue<
NamedViewDefinition
>
+type InvalidResponsiveViewDefinition =
+ TDefinition extends ResponsiveChartDefinition
+ ? ReturnType extends infer TResult
+ ? TResult extends { theme: infer TTheme }
+ ? 'focusRing' extends keyof TTheme
+ ? TResult
+ : never
+ : never
+ : never
+ : never
+
+type ValidResponsiveViewDefinitions = [
+ InvalidResponsiveViewDefinition,
+] extends [never]
+ ? unknown
+ : never
+
type ValidViewLayout<
TViews extends ViewDefinitions,
TLayout extends ViewLayout,
@@ -318,9 +351,10 @@ export function composeViews<
const TViews extends ViewDefinitions,
const TLayout extends ViewLayout,
>(
- options: ComposeViewsOptions & {
- layout: TLayout & ValidViewLayout, TLayout>
- },
+ options: ComposeViewsOptions &
+ ValidResponsiveViewDefinitions> & {
+ layout: TLayout & ValidViewLayout, TLayout>
+ },
): StaticChartDefinition<
NamedViewDatum,
NamedViewXValue,
@@ -355,7 +389,8 @@ export function viewGrid<
TrackId
>[],
>(
- options: ViewGridOptions,
+ options: ViewGridOptions &
+ ValidResponsiveViewDefinitions>,
): StaticChartDefinition<
ViewDatum,
ViewXValue,
@@ -758,6 +793,11 @@ function assertChildDefinition(
`View "${id}" cannot own a scene background; use an ordinary background mark inside the child definition`,
)
}
+ if (definition.theme?.focusRing !== undefined) {
+ throw new TypeError(
+ `View "${id}" cannot own a focus ring theme; configure theme.focusRing on the outer definition`,
+ )
+ }
for (const axis of ['x', 'y'] as const) {
const configured = definition.scales[axis]
const presentation =
diff --git a/packages/react-native-charts/README.md b/packages/react-native-charts/README.md
index 9d751d8b..6f10ec3e 100644
--- a/packages/react-native-charts/README.md
+++ b/packages/react-native-charts/README.md
@@ -59,14 +59,19 @@ export function RevenueChart() {
`fontFamily`, `fontStyle`, `fontStretch`, `letterSpacing`, `direction`,
`locale`, and `fontScale` are passed to the synchronous `measureText` contract.
The SVG painter applies the corresponding family, style, stretch, spacing, and
-font scale. If native font metrics become available asynchronously, keep them
-in application state and render the chart again with an updated `measureText`
-function or typography prop.
+font scale. Set `direction="rtl"` explicitly when the chart reads right to left;
+the native painter preserves logical `start` and `end` anchors. If native font
+metrics become available asynchronously, keep them in application state and
+render the chart again with an updated `measureText` function or typography
+prop.
Exact core subpaths keep Metro from retaining unrelated universal-entry
exports. The `/universal` barrel remains valid when portability matters more
than the native bundle floor.
+`focusFill` supplies the native fallback for the built-in focus ring's default
+CSS-variable fill. A literal `definition.focusRing.fill` takes precedence.
+
The bare fixture uses React Native 0.86.2 with `react-native-svg` 15.15.5. The
Expo 57 fixture uses `react-native-svg` 15.15.4 and renders in Expo Go on an iOS
simulator. It remains experimental: bare-native and Android simulators,
diff --git a/packages/react-native-charts/src/Chart.test.tsx b/packages/react-native-charts/src/Chart.test.tsx
index 4c62d636..64f7143f 100644
--- a/packages/react-native-charts/src/Chart.test.tsx
+++ b/packages/react-native-charts/src/Chart.test.tsx
@@ -69,6 +69,7 @@ const nativeViewState = vi.hoisted(() => ({
vi.mock('react-native', async () => {
const ReactModule = await import('react')
return {
+ Platform: { OS: 'ios' },
Text: 'span',
View: ReactModule.forwardRef(
function MockView(props, ref) {
@@ -878,6 +879,54 @@ describe('React Native Chart', () => {
}
})
+ it('paints configured focus-ring geometry and paint in native SVG', async () => {
+ const configuredDefinition = defineChart({
+ marks: [lineY(data, { x: 'month', y: 'value' })],
+ scales: {
+ x: { scale: scaleLinear().domain([1, 2]) },
+ y: { scale: scaleLinear().domain([8, 12]) },
+ },
+ guides: false,
+ focusRing: {
+ radius: 4,
+ strokeWidth: 1.5,
+ fill: '#ffffff',
+ stroke: '#0f172a',
+ },
+ })
+ const container = document.createElement('div')
+ const root = createRoot(container)
+
+ try {
+ await React.act(() => {
+ root.render(
+ ,
+ )
+ })
+ await React.act(() => {
+ container.firstElementChild?.dispatchEvent(
+ new FocusEvent('focusin', { bubbles: true }),
+ )
+ })
+
+ const ring = container.querySelector('circle')
+ expect(ring?.getAttribute('r')).toBe('4')
+ expect(ring?.getAttribute('fill')).toBe('#ffffff')
+ expect(ring?.getAttribute('stroke')).toBe('#0f172a')
+ expect(ring?.getAttribute('stroke-width')).toBe('1.5')
+ expect(
+ container.firstElementChild?.getAttribute('data-accessibility-role'),
+ ).toBe('adjustable')
+ } finally {
+ await React.act(() => root.unmount())
+ }
+ })
+
it('renders a clipped two-axis crosshair, marker, labels, and focus fill', async () => {
const crosshairDefinition = defineChart({
marks: [
diff --git a/packages/react-native-charts/src/FocusOverlay.tsx b/packages/react-native-charts/src/FocusOverlay.tsx
index 657369af..e5200976 100644
--- a/packages/react-native-charts/src/FocusOverlay.tsx
+++ b/packages/react-native-charts/src/FocusOverlay.tsx
@@ -9,6 +9,7 @@ import type {
ChartFocusSource,
ChartPoint,
ChartScene,
+ ChartTextTypography,
ChartValue,
} from '@tanstack/charts/types'
import { resolveNativeSolidPaint, type NativePaintResolver } from './paint'
@@ -30,6 +31,7 @@ export interface NativeChartFocusOverlayProps<
color: ColorValue
fill: ColorValue
fontFamily?: string
+ direction?: ChartTextTypography['direction']
idPrefix: string
resolvePaint: NativePaintResolver
}
@@ -50,6 +52,7 @@ export function NativeChartFocusOverlay<
color,
fill,
fontFamily,
+ direction,
idPrefix,
resolvePaint,
}: NativeChartFocusOverlayProps) {
@@ -81,6 +84,7 @@ export function NativeChartFocusOverlay<
nodes={nodes}
color={color}
fontFamily={fontFamily}
+ direction={direction}
idPrefix={idPrefix}
resolvePaint={resolvePaint}
/>
diff --git a/packages/react-native-charts/src/SvgScene.test.tsx b/packages/react-native-charts/src/SvgScene.test.tsx
index 272eab7a..2a290be4 100644
--- a/packages/react-native-charts/src/SvgScene.test.tsx
+++ b/packages/react-native-charts/src/SvgScene.test.tsx
@@ -1,10 +1,20 @@
import * as React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { createChartScene, defineChart, lineY } from '@tanstack/charts'
+import { scaleLinear } from '@tanstack/charts/scales/linear'
import type { ChartScene, SceneNode } from '@tanstack/charts/types'
import { NativeChartFocusOverlay } from './FocusOverlay'
import { resolveNativePaint } from './paint'
-import { NativeChartScene, resolveNativeLineJoin } from './SvgScene'
+import {
+ NativeChartScene,
+ NativeChartSceneNodes,
+ resolveNativeLineJoin,
+} from './SvgScene'
+
+const platform = vi.hoisted(() => ({ OS: 'ios' }))
+
+vi.mock('react-native', () => ({ Platform: platform }))
vi.mock('react-native-svg', () => ({
Circle: 'circle',
@@ -20,6 +30,10 @@ vi.mock('react-native-svg', () => ({
Text: 'text',
}))
+afterEach(() => {
+ platform.OS = 'ios'
+})
+
describe('React Native SVG scene renderer', () => {
it('maps every scene primitive, gradients, clipping, and authored paths', () => {
const markup = renderToStaticMarkup(
@@ -37,6 +51,8 @@ describe('React Native SVG scene renderer', () => {
expect(markup).toContain(' {
expect(markup).toContain('font-size="24"')
})
+ it('converts logical RTL anchors for native labels in every scene layer', () => {
+ const rtlScene = scene()
+ rtlScene.nodes = [
+ label('start', 'start'),
+ label('middle', 'middle'),
+ label('end', 'end'),
+ label('undefined'),
+ {
+ kind: 'group',
+ key: 'nested',
+ children: [label('nested-start', 'start')],
+ },
+ ]
+
+ const markup = renderToStaticMarkup(
+ ,
+ )
+
+ expect(renderedTextAnchor(markup, 'start')).toBe('end')
+ expect(renderedTextAnchor(markup, 'middle')).toBe('middle')
+ expect(renderedTextAnchor(markup, 'end')).toBe('start')
+ expect(renderedTextAnchor(markup, 'undefined')).toBe('end')
+ expect(renderedTextAnchor(markup, 'nested-start')).toBe('end')
+ expect(renderedTextAnchor(markup, 'under-end')).toBe('start')
+ expect(renderedTextAnchor(markup, 'over-start')).toBe('end')
+ })
+
+ it('keeps logical anchors unchanged outside native RTL rendering', () => {
+ const anchorScene = scene()
+ anchorScene.nodes = [
+ label('start', 'start'),
+ label('middle', 'middle'),
+ label('end', 'end'),
+ label('undefined'),
+ ]
+
+ const ltrMarkup = renderToStaticMarkup(
+ ,
+ )
+ expect(renderedTextAnchor(ltrMarkup, 'start')).toBe('start')
+ expect(renderedTextAnchor(ltrMarkup, 'middle')).toBe('middle')
+ expect(renderedTextAnchor(ltrMarkup, 'end')).toBe('end')
+ expect(renderedTextAnchor(ltrMarkup, 'undefined')).toBeUndefined()
+
+ platform.OS = 'web'
+ const webMarkup = renderToStaticMarkup(
+ ,
+ )
+ expect(renderedTextAnchor(webMarkup, 'start')).toBe('start')
+ expect(renderedTextAnchor(webMarkup, 'middle')).toBe('middle')
+ expect(renderedTextAnchor(webMarkup, 'end')).toBe('end')
+ expect(renderedTextAnchor(webMarkup, 'undefined')).toBeUndefined()
+ expect(renderedTextAttributes(webMarkup, 'start')).toContain(
+ 'direction="rtl"',
+ )
+ })
+
+ it('passes direction through NativeChartSceneNodes', () => {
+ const base = scene()
+ const markup = renderToStaticMarkup(
+ ,
+ )
+
+ expect(renderedTextAnchor(markup, 'standalone-start')).toBe('end')
+ })
+
+ it('renders Cartesian axis-title typography and paint', () => {
+ const generated = createChartScene(
+ defineChart({
+ marks: [lineY([1, 2, 3])],
+ scales: {
+ x: { scale: scaleLinear().domain([0, 2]), axis: false },
+ y: {
+ scale: scaleLinear().domain([0, 3]),
+ axis: {
+ ticks: false,
+ label: {
+ text: 'Revenue',
+ fontSize: 17,
+ fontWeight: 650,
+ fill: '#0f766e',
+ opacity: 0.6,
+ },
+ },
+ },
+ },
+ }),
+ { width: 480, height: 260 },
+ )
+ const markup = renderToStaticMarkup(
+ ,
+ )
+
+ expect(markup).toMatch(
+ /]*fill="#0f766e"[^>]*opacity="0\.6"[^>]*font-size="17" font-weight="650"[^>]*>Revenue<\/text>/,
+ )
+ })
+
it('makes the two unsupported SVG joins an explicit lossy mapping', () => {
expect(resolveNativeLineJoin('arcs')).toBe('round')
expect(resolveNativeLineJoin('miter-clip')).toBe('miter')
@@ -251,6 +399,55 @@ describe('React Native SVG scene renderer', () => {
expect(render([second])).not.toContain('x1="10"')
})
+ it('passes RTL direction through the native focus overlay', () => {
+ const focusScene = scene()
+ const point = {
+ key: 'focused-point',
+ markId: 'focused-mark',
+ group: null,
+ groupLabel: 'focused-mark',
+ datum: { id: 'focused-point' },
+ datumIndex: 0,
+ xValue: 1,
+ yValue: 2,
+ x: 40,
+ y: 30,
+ color: '#2563eb',
+ }
+ focusScene.nodes = [
+ {
+ kind: 'group',
+ key: 'focus:label',
+ children: [label(point.key, 'start')],
+ focus: {
+ match: 'primary',
+ points: [point],
+ placement: 'over',
+ },
+ },
+ ]
+
+ const markup = renderToStaticMarkup(
+ ,
+ )
+
+ expect(renderedTextAnchor(markup, point.key)).toBe('end')
+ })
+
it('paints focus underlays and overlays around the base scene in one SVG', () => {
const base = scene()
base.nodes = [
@@ -338,7 +535,12 @@ function scene(): ChartScene {
[2, 4],
[20, 30],
],
- style: { fill: 'none', stroke: '#abcdef', lineJoin: 'arcs' },
+ style: {
+ fill: 'none',
+ stroke: '#abcdef',
+ lineCap: 'square',
+ lineJoin: 'bevel',
+ },
},
{
kind: 'polyline',
@@ -440,3 +642,29 @@ function scene(): ChartScene {
},
}
}
+
+function label(
+ key: string,
+ anchor?: Extract['anchor'],
+): Extract {
+ return {
+ kind: 'label',
+ key,
+ x: 10,
+ y: 10,
+ text: key,
+ anchor,
+ baseline: 'middle',
+ }
+}
+
+function renderedTextAttributes(markup: string, text: string) {
+ return new RegExp(`]*)>${text}`).exec(markup)?.[1]
+}
+
+function renderedTextAnchor(markup: string, text: string) {
+ const attributes = renderedTextAttributes(markup, text)
+ return attributes === undefined
+ ? undefined
+ : /text-anchor="([^"]+)"/.exec(attributes)?.[1]
+}
diff --git a/packages/react-native-charts/src/SvgScene.tsx b/packages/react-native-charts/src/SvgScene.tsx
index aa6c4cba..97e3c9a8 100644
--- a/packages/react-native-charts/src/SvgScene.tsx
+++ b/packages/react-native-charts/src/SvgScene.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import type { ColorValue } from 'react-native'
+import { Platform, type ColorValue } from 'react-native'
import {
Circle,
ClipPath,
@@ -48,6 +48,7 @@ export function NativeChartSceneNodes({
nodes,
color,
focusFill,
+ direction,
fontScale,
idPrefix,
resolvePaint,
@@ -71,7 +72,13 @@ export function NativeChartSceneNodes({
return (
<>
{nodes.map((node) =>
- renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1)),
+ renderSceneNode(
+ node,
+ idPrefix,
+ paint,
+ positiveFinite(fontScale, 1),
+ direction,
+ ),
)}
>
)
@@ -84,6 +91,7 @@ export const NativeChartScene = React.memo(function NativeChartScene({
fontStyle,
fontStretch,
letterSpacing,
+ direction,
fontScale,
idPrefix,
resolvePaint,
@@ -157,13 +165,31 @@ export const NativeChartScene = React.memo(function NativeChartScene({
/>
)}
{focusPresentation?.under.map((node) =>
- renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1)),
+ renderSceneNode(
+ node,
+ idPrefix,
+ paint,
+ positiveFinite(fontScale, 1),
+ direction,
+ ),
)}
{scene.nodes.map((node) =>
- renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1)),
+ renderSceneNode(
+ node,
+ idPrefix,
+ paint,
+ positiveFinite(fontScale, 1),
+ direction,
+ ),
)}
{focusPresentation?.over.map((node) =>
- renderSceneNode(node, idPrefix, paint, positiveFinite(fontScale, 1)),
+ renderSceneNode(
+ node,
+ idPrefix,
+ paint,
+ positiveFinite(fontScale, 1),
+ direction,
+ ),
)}
)
@@ -174,13 +200,14 @@ function renderSceneNode(
idPrefix: string,
paint: (value: string) => ColorValue,
fontScale: number,
+ direction: ChartTextTypography['direction'],
): React.ReactNode {
if (node.kind === 'group' && node.focus) return null
const style = nativeSceneStyle(node.style, paint)
switch (node.kind) {
case 'group':
- return renderGroup(node, idPrefix, paint, style, fontScale)
+ return renderGroup(node, idPrefix, paint, style, fontScale, direction)
case 'rule':
return (
ColorValue,
style: ReturnType,
fontScale: number,
+ direction: ChartTextTypography['direction'],
) {
const clipId = node.clip
? scopedId(idPrefix, `clip-${stableId(node.key)}`)
@@ -297,7 +326,7 @@ function renderGroup(
) : null}
{node.children.map((child) =>
- renderSceneNode(child, idPrefix, paint, fontScale),
+ renderSceneNode(child, idPrefix, paint, fontScale, direction),
)}
)
@@ -329,6 +358,19 @@ export function resolveNativeLineJoin(
return lineJoin
}
+function resolveNativeTextAnchor(
+ anchor: Extract['anchor'],
+ direction: ChartTextTypography['direction'],
+) {
+ if (Platform.OS === 'web' || direction !== 'rtl') return anchor
+ if (anchor === 'end') return 'start'
+ return anchor === 'middle' ? 'middle' : 'end'
+}
+
+function webTextDirection(direction: ChartTextTypography['direction']) {
+ return Platform.OS === 'web' && direction !== undefined ? { direction } : {}
+}
+
function resolveScenePaint(
value: string,
gradientIds: ReadonlySet,
diff --git a/scripts/measure-bundles.mjs b/scripts/measure-bundles.mjs
index bb05b66a..be6907d4 100644
--- a/scripts/measure-bundles.mjs
+++ b/scripts/measure-bundles.mjs
@@ -552,7 +552,7 @@ const entries = [
'Hierarchy treemap mark',
'benchmarks/entries/charts-hierarchy-treemap.ts',
'D3 hierarchy treemap kernel',
- 3.74,
+ 3.77,
{
inputBoundary: {
require: ['hierarchyFlat', 'hierarchyTreemap', 'd3Hierarchy'],
@@ -671,37 +671,37 @@ const entries = [
budgeted(
'D3-scale lineX + static SVG',
'benchmarks/entries/charts-line-x-svg.ts',
- 20.24,
+ 20.51,
),
budgeted(
'D3-scale UTC line + static SVG',
'benchmarks/entries/charts-time-svg.ts',
- 24.9,
+ 25.14,
),
budgeted(
'D3-scale histogram + static SVG',
'benchmarks/entries/charts-histogram-svg.ts',
- 22.78,
+ 23.02,
),
budgeted(
'D3-scale facets + static SVG',
'benchmarks/entries/charts-facet-svg.ts',
- 23.94,
+ 24.26,
),
budgeted(
'D3-scale arrows + static SVG',
'benchmarks/entries/charts-arrow-svg.ts',
- 20.2,
+ 20.45,
),
budgeted(
'D3-scale areaX + static SVG',
'benchmarks/entries/charts-area-x-svg.ts',
- 24.25,
+ 24.52,
),
budgeted(
'D3-scale dots + static SVG',
'benchmarks/entries/charts-dot-svg.ts',
- 20.67,
+ 20.9,
{
inputBoundary: {
forbid: [
@@ -761,10 +761,31 @@ const entries = [
},
},
),
+ incrementalBudgeted(
+ 'Axis title styles + static SVG',
+ 'benchmarks/entries/charts-axis-label-styles.ts',
+ 'D3-scale line + static SVG',
+ 0.25,
+ {
+ inputBoundary: {
+ forbid: [
+ 'focusGuide',
+ 'focusMark',
+ 'motionRuntime',
+ 'springRuntime',
+ 'tooltipRuntime',
+ 'tooltipPortal',
+ 'd3GeometryRuntime',
+ ],
+ addedFrom: 'D3-scale line + static SVG',
+ allowAdded: [],
+ },
+ },
+ ),
budgeted(
'Frame + static SVG',
'benchmarks/entries/charts-frame-svg.ts',
- 11.63,
+ 11.86,
),
incrementalBudgeted(
'Spatial density contours + static SVG',
@@ -903,7 +924,7 @@ const entries = [
budgeted(
'Composite mark + static SVG',
'benchmarks/entries/charts-composite-mark.ts',
- 27.75,
+ 27.98,
{
inputBoundary: {
require: [
@@ -943,7 +964,7 @@ const entries = [
'Linear regression + static SVG',
'benchmarks/entries/charts-regression-svg.ts',
'D3-scale line + static SVG',
- 6.59,
+ 6.6,
{
inputBoundary: {
require: [
@@ -977,7 +998,7 @@ const entries = [
'Difference mark + static SVG',
'benchmarks/entries/charts-difference-svg.ts',
'D3-scale line + static SVG',
- 7.03,
+ 7.06,
{
inputBoundary: {
require: [
@@ -1072,7 +1093,7 @@ const entries = [
budgeted(
'D3-scale hexagons + static SVG',
'benchmarks/entries/charts-hexagon-svg.ts',
- 20.11,
+ 20.37,
{ inputBoundary: { forbid: ['d3Hexbin'] } },
),
incrementalBudgeted(
@@ -1102,7 +1123,7 @@ const entries = [
budgeted(
'D3-scale link + static SVG',
'benchmarks/entries/charts-link-svg.ts',
- 20.1,
+ 20.34,
{
inputBoundary: {
forbid: [
@@ -1142,22 +1163,22 @@ const entries = [
budgeted(
'D3-scale ticks + static SVG',
'benchmarks/entries/charts-tick-svg.ts',
- 21.13,
+ 21.37,
),
budgeted(
'D3-scale vectors + static SVG',
'benchmarks/entries/charts-vector-svg.ts',
- 20.31,
+ 20.54,
),
budgeted(
'D3 geo shape + static SVG',
'benchmarks/entries/charts-geo-svg.ts',
- 17.58,
+ 17.81,
),
budgeted(
'Polar arc + static SVG',
'benchmarks/entries/charts-polar-arc-svg.ts',
- 16.56,
+ 16.79,
{ inputBoundary: { forbid: ['polarPie'] } },
),
incrementalBudgeted(
@@ -1176,18 +1197,18 @@ const entries = [
budgeted(
'Polar gauge composition + static SVG',
'benchmarks/entries/charts-polar-gauge-svg.ts',
- 25.69,
+ 25.93,
),
budgeted(
'Radial labels + static SVG',
'benchmarks/entries/charts-radial-label-svg.ts',
- 22.2,
+ 22.43,
{ inputBoundary: { forbid: ['polarPie'] } },
),
budgeted(
'Polar radial bars + static SVG',
'benchmarks/entries/charts-radial-bar-svg.ts',
- 25.7,
+ 25.96,
{
inputBoundary: {
require: ['polarMarks', 'd3ScaleRuntime', 'd3Shape'],
@@ -1198,7 +1219,7 @@ const entries = [
budgeted(
'Polar line + scatter composition + static SVG',
'benchmarks/entries/charts-polar-line-scatter-svg.ts',
- 26.8,
+ 27.05,
),
locked(
'Representative marks',
@@ -1225,7 +1246,7 @@ const entries = [
budgeted(
'Representative marks + mark Canvas renderer',
'benchmarks/entries/charts-representative-mark-canvas.ts',
- 35.8,
+ 35.98,
{
rendererBoundary: 'mixed',
inputBoundary: {
@@ -1562,7 +1583,7 @@ const entries = [
budgeted(
'React line consumer + mark Canvas renderer',
'benchmarks/entries/charts-react-line-mark-canvas.ts',
- 41.8,
+ 41.95,
{
external: ['react', 'react/jsx-runtime', 'react-dom'],
rendererBoundary: 'mixed',
@@ -1642,7 +1663,7 @@ const entries = [
lockedBudgeted(
'Compact-scale line scene',
'benchmarks/entries/charts-compact-linear-scene.ts',
- 11.68,
+ 11.89,
{
inputBoundary: {
require: ['compactLinear'],
@@ -1661,7 +1682,7 @@ const entries = [
lockedBudgeted(
'React compact-scale line consumer',
'benchmarks/entries/charts-react-compact-line.ts',
- 28.47,
+ 28.73,
{
external: ['react', 'react/jsx-runtime', 'react-dom'],
rendererBoundary: 'svg',
@@ -1723,7 +1744,7 @@ const entries = [
budgeted(
'Motion SVG renderer',
'benchmarks/entries/charts-motion-svg-renderer.ts',
- 20.91,
+ 20.94,
{
rendererBoundary: 'svg',
inputBoundary: {
@@ -1829,7 +1850,7 @@ const entries = [
budgeted(
'Stats parity surface',
'benchmarks/entries/charts-stats-parity.ts',
- 52.82,
+ 53.1,
),
locked(
'Custom-scale line scene',
@@ -1855,37 +1876,37 @@ const entries = [
budgeted(
'D3 curved line scene',
'benchmarks/entries/charts-d3-curved-line-scene.ts',
- 20.9,
+ 21.12,
),
budgeted(
'D3 time-scale line scene',
'benchmarks/entries/charts-d3-time-scene.ts',
- 23.21,
+ 23.42,
),
budgeted(
'Direct D3 monotone + TanStack SVG',
'benchmarks/entries/charts-d3-curve-svg.ts',
- 22.61,
+ 22.85,
),
budgeted(
'Direct D3 transforms + TanStack histogram',
'benchmarks/entries/charts-d3-transform-histogram.ts',
- 21.5,
+ 21.77,
),
budgeted(
'Direct D3 time + TanStack UTC line',
'benchmarks/entries/charts-d3-time-svg.ts',
- 24.9,
+ 25.14,
),
budgeted(
'Direct D3 quadtree + TanStack DOM host',
'benchmarks/entries/charts-d3-quadtree-dom.ts',
- 36.84,
+ 37.11,
),
budgeted(
'Direct D3 Delaunay + TanStack DOM host',
'benchmarks/entries/charts-d3-delaunay-dom.ts',
- 42.07,
+ 42.32,
),
measured('D3 array numeric kernel', 'benchmarks/entries/d3-array-kernel.ts'),
measured(
@@ -2089,7 +2110,7 @@ const entries = [
budgeted(
'React Stats parity surface',
'benchmarks/entries/charts-react-stats-parity.tsx',
- 53.72,
+ 54,
{ external: ['react', 'react/jsx-runtime', 'react-dom'] },
),
measured('Plot renderer integration', 'benchmarks/entries/plot-renderer.ts'),
diff --git a/scripts/public-callback-contract.mjs b/scripts/public-callback-contract.mjs
index 3d47bf56..0896e460 100644
--- a/scripts/public-callback-contract.mjs
+++ b/scripts/public-callback-contract.mjs
@@ -371,6 +371,10 @@ const callbackInventory = {
['@tanstack/charts:src/types.ts:ResponsiveChartConfig', 'chart'],
['@tanstack/charts:src/types.ts:ResponsiveChartDefinition', 'chart'],
['@tanstack/charts:src/scene.ts:DefinedResponsiveChart', 'chart'],
+ [
+ '@tanstack/charts:src/view.ts:ComposableResponsiveChartDefinition',
+ 'chart',
+ ],
['@tanstack/charts:src/types.ts:InitializedMark', 'render resolveLayout'],
[
'@tanstack/charts:src/types.ts:InitializedMarkBase',