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 @@ - + 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 @@ - + 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. +Use arrow keys to inspect segments and Enter or Space to drill into a branch. Use the center button to move up. 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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 @@ - + 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',