From 3df87d71f0305e5a450c10b66940f76f3e14259a Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 26 Aug 2026 15:50:34 -0600 Subject: [PATCH 1/3] Prepare TanStack Charts for official Alpha --- .changeset/README.md | 2 +- .changeset/official-alpha.md | 27 ++ API-FRICTION.md | 77 ++-- MARKETING.md | 30 +- PLAN.md | 24 +- README.md | 10 +- .../bundle-size/universal-baseline.json | 40 +- .../162-shadcn-pie-donut-active/example.tsx | 5 +- .../cases/163-shadcn-pie-donut/example.tsx | 5 +- .../cases/168-shadcn-pie-legend/example.tsx | 5 +- .../169-shadcn-pie-separator-none/example.tsx | 5 +- .../cases/170-shadcn-pie-simple/example.tsx | 5 +- .../cases/171-shadcn-pie-stacked/example.tsx | 5 +- .../previews/119-stacked-bar-band-cursor.svg | 2 +- .../conformance/previews/bar-stacked.svg | 2 +- benchmarks/conformance/previews/manifest.json | 10 +- docs/config.json | 4 + docs/guides/ai-authoring.md | 10 +- docs/guides/migrating.md | 45 +-- docs/installation.md | 9 +- docs/overview.md | 6 +- docs/reference/focus-and-interaction.md | 6 +- docs/reference/types.md | 3 - docs/stability.md | 49 +++ llms.txt | 207 +++++----- packages/charts-core/README.md | 42 +- packages/charts-core/docs/config.json | 4 + .../charts-core/docs/guides/ai-authoring.md | 10 +- packages/charts-core/docs/guides/migrating.md | 45 +-- packages/charts-core/docs/installation.md | 9 +- packages/charts-core/docs/overview.md | 6 +- .../docs/reference/focus-and-interaction.md | 6 +- packages/charts-core/docs/reference/types.md | 3 - packages/charts-core/docs/stability.md | 49 +++ packages/charts-core/llms.txt | 207 +++++----- .../skills/build-chart-interactions/SKILL.md | 14 +- .../skills/compose-marks-and-views/SKILL.md | 8 +- .../configure-scales-guides-color/SKILL.md | 16 +- .../coordinate-charts-with-tanstack/SKILL.md | 8 +- .../skills/debug-and-verify-charts/SKILL.md | 8 +- .../skills/design-a-chart/SKILL.md | 8 +- .../skills/design-responsive-charts/SKILL.md | 23 +- .../skills/extend-tanstack-charts/SKILL.md | 7 +- .../migrate-to-tanstack-charts/SKILL.md | 2 +- .../skills/prepare-chart-data/SKILL.md | 2 +- .../skills/ship-accessible-charts/SKILL.md | 8 +- .../skills/update-and-animate-charts/SKILL.md | 8 +- packages/charts-core/src/adapter.test.ts | 12 +- packages/charts-core/src/area-x.ts | 32 +- packages/charts-core/src/area.ts | 32 +- packages/charts-core/src/arrow.ts | 71 +++- packages/charts-core/src/band.ts | 154 +++++--- packages/charts-core/src/bar.ts | 58 ++- packages/charts-core/src/box.test.ts | 48 ++- packages/charts-core/src/canvas.test.ts | 62 ++- .../src/cartesian-scale-types.test.ts | 79 ++++ .../charts-core/src/cartesian-scales.test.ts | 111 +----- .../charts-core/src/configured-scale.test.ts | 280 ++++++++----- packages/charts-core/src/crosshair.test.ts | 68 ++-- packages/charts-core/src/cursor.test.ts | 62 +-- packages/charts-core/src/difference.test.ts | 78 ++-- packages/charts-core/src/dodge.test.ts | 46 ++- packages/charts-core/src/dot.ts | 95 +++-- packages/charts-core/src/export.test.ts | 6 +- packages/charts-core/src/facet.test.ts | 120 +++--- packages/charts-core/src/facet.ts | 13 +- packages/charts-core/src/focus-guide.test.ts | 18 +- packages/charts-core/src/focus-layer.ts | 3 - packages/charts-core/src/focus-mark.test.ts | 134 +++++-- packages/charts-core/src/frame.test.ts | 6 +- packages/charts-core/src/geo.test.ts | 8 + packages/charts-core/src/hexagon.ts | 41 +- .../src/hierarchy-sunburst.test.ts | 6 +- .../charts-core/src/hierarchy-sunburst.ts | 6 +- .../charts-core/src/hierarchy-treemap.test.ts | 8 + packages/charts-core/src/index.ts | 2 - .../charts-core/src/interaction-brush.test.ts | 6 +- .../src/interaction-cursor.test.ts | 18 +- .../src/interaction-handle.test.ts | 12 +- .../charts-core/src/interaction-zoom.test.ts | 16 +- .../src/interactive-legend.test.ts | 18 +- packages/charts-core/src/line.test.ts | 36 +- packages/charts-core/src/line.ts | 60 ++- packages/charts-core/src/link.ts | 71 +++- .../charts-core/src/mark-composite.test.ts | 12 +- .../charts-core/src/mark-decorative.test.ts | 24 +- packages/charts-core/src/motion.test.ts | 338 ++++++++++------ packages/charts-core/src/motion.ts | 5 +- packages/charts-core/src/nearest.test.ts | 12 +- .../charts-core/src/network-sankey.test.ts | 44 ++- packages/charts-core/src/point-color.test.ts | 5 +- .../charts-core/src/polar-mark-internal.ts | 2 - packages/charts-core/src/polar-scales.test.ts | 103 ++--- packages/charts-core/src/polar.test.ts | 33 +- packages/charts-core/src/polar.ts | 372 +++++++++++++----- packages/charts-core/src/rect.ts | 139 ++++--- packages/charts-core/src/regression.test.ts | 66 ++-- packages/charts-core/src/renderer.test.ts | 253 ++++++++++-- packages/charts-core/src/renderer.ts | 33 ++ .../charts-core/src/resolved-layout.test.ts | 12 +- packages/charts-core/src/ridgeline.test.ts | 78 ++-- packages/charts-core/src/rule.ts | 46 ++- packages/charts-core/src/runtime.test.ts | 78 ++-- .../src/scene-embed-internal.test.ts | 6 +- packages/charts-core/src/scene-layout.test.ts | 277 ++++++++----- packages/charts-core/src/scene.test.ts | 108 ++--- packages/charts-core/src/scene.ts | 90 ++--- packages/charts-core/src/selection.test.ts | 67 ++-- .../charts-core/src/spatial-contour.test.ts | 2 + .../charts-core/src/spatial-delaunay.test.ts | 48 ++- packages/charts-core/src/spatial-delaunay.ts | 57 ++- .../charts-core/src/spatial-density.test.ts | 30 +- packages/charts-core/src/spatial-density.ts | 49 ++- .../charts-core/src/spatial-hexbin.test.ts | 18 +- .../charts-core/src/spatial-voronoi.test.ts | 54 ++- packages/charts-core/src/spatial-voronoi.ts | 49 ++- .../charts-core/src/stack-internal.test.ts | 100 +++++ packages/charts-core/src/stack-internal.ts | 53 ++- packages/charts-core/src/svg-surface.test.ts | 62 +-- packages/charts-core/src/test-scales.ts | 36 +- packages/charts-core/src/text.ts | 41 +- packages/charts-core/src/tick.test.ts | 24 +- packages/charts-core/src/tick.ts | 78 +++- .../charts-core/src/type-contract.test.ts | 310 +++++++++------ packages/charts-core/src/types.ts | 69 ++-- packages/charts-core/src/universal-types.ts | 2 - packages/charts-core/src/vector.ts | 49 ++- packages/charts-core/src/view-compose.test.ts | 46 ++- packages/charts-core/src/view.test.ts | 68 ++-- packages/charts-core/src/view.ts | 8 +- packages/charts-core/src/violin.test.ts | 72 ++-- packages/charts-core/src/waffle.test.ts | 10 + packages/charts-scales/README.md | 6 +- packages/react-native-charts/README.md | 6 +- .../src/interaction.test.ts | 2 +- scripts/check-packed-consumers.mjs | 5 - scripts/measure-bundles.mjs | 22 +- scripts/sync-package-docs.mjs | 6 +- scripts/sync-release-version.mjs | 50 ++- scripts/sync-release-version.test.mjs | 41 +- 140 files changed, 4416 insertions(+), 2242 deletions(-) create mode 100644 .changeset/official-alpha.md create mode 100644 docs/stability.md create mode 100644 packages/charts-core/docs/stability.md diff --git a/.changeset/README.md b/.changeset/README.md index 45eea806..9ab5c86d 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -6,6 +6,6 @@ Add a changeset for every user-visible package change: pnpm changeset ``` -All thirteen public Charts packages release as one fixed version. Documentation, +All twelve public Charts packages release as one fixed version. Documentation, tests, benchmarks, and build-only changes do not need a changeset unless they change the published package contract. diff --git a/.changeset/official-alpha.md b/.changeset/official-alpha.md new file mode 100644 index 00000000..f99c7f7a --- /dev/null +++ b/.changeset/official-alpha.md @@ -0,0 +1,27 @@ +--- +'@tanstack/charts': minor +--- + +Move TanStack Charts into official Alpha on the regular `0.x` release line. + +Chart definitions now require Cartesian scales under `scales.x` and `scales.y`. +Polar definitions require `scales.angle` and `scales.radius`. The temporary +pre-Alpha root properties, runtime adapters, and development warnings have been +removed. Custom mark types should replace `ChartMarkX` and `ChartMarkY` with +`ChartMarkPointX` and `ChartMarkPointY`. Polar layout callbacks should read the +reserved mappings from `layout.scales`. + +Reserved scale entries are checked against the marks that use them at the +precise authored type boundary. A materialized dimension requires a configured +scale, while an unused dimension requires an explicit `null` entry. Runtime +validation still checks required reserved entries and channels that actually +materialize, but type-only scale ownership cannot be recovered after a custom +mark or stored definition has been erased. + +Fresh built-in mark option literals now reject unsupported properties while +preserving typed channels, named scale IDs, and generic mark wrappers. + +Exact-zero cells in diverging stacks now stay on their running positive or +negative baseline instead of drawing false spikes through lower area layers. +Pinned tooltips now dismiss when the pointer is pressed outside the chart and +tooltip, so pointer inspection resumes after focus leaves the chart. diff --git a/API-FRICTION.md b/API-FRICTION.md index 358e85da..5204435b 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -52,7 +52,7 @@ Each entry records: | F-013 | Bar series identity also changed bar geometry | API | resolved | | F-014 | Responsive nicing duplicates layout calculations | API | resolved | | F-015 | Legacy scale helpers compete with the D3-first API | API | resolved | -| F-016 | Stats animated export still renders through Plot | Integration/API | resolved | +| F-016 | Stats animated export still renders through Plot | Integration/API | monitoring | | F-017 | React migration rebuilt a static definition | Documentation | resolved | | F-018 | Stats derivations still invalidate dynamic input | Application | resolved | | F-019 | Custom tooltip formatting leaked float artifacts | Application | resolved | @@ -298,7 +298,7 @@ Each entry records: | F-259 | Chart resources cannot declare patterns | API | open | | F-260 | Static guides cannot express stroke treatment | API | open | | F-261 | Cartesian bars cannot round only exposed corners | API | open | -| F-262 | Mark inference accepted an unsupported style option | API | open | +| F-262 | Mark inference accepted an unsupported style option | API | resolved | | F-263 | Chromium transport suspension interrupted catalog previews | Tooling | resolved | | F-264 | Drillable sunbursts required rebuilding hierarchy rows | API/Documentation | resolved | | F-265 | Sunburst motion lost hierarchy across enter and exit | API | resolved | @@ -376,14 +376,18 @@ Each entry records: had to describe an axis it could never materialize. - Decision: derive each axis requirement from the marks' scale phantoms. A materialized dimension requires a configured scale. A dimension whose marks - all expose `never` may be omitted or explicitly set to `null`, and rejects a - configured phantom axis. Mixed charts require every dimension materialized - by any constituent mark. The runtime mirrors the type contract and renders - guides only for configured axes. + all expose `never` requires an explicit `null` reserved entry and rejects a + configured phantom axis at the precise authored type boundary. Every + definition explicitly declares both reserved entries. Mixed charts require + every dimension materialized by any constituent mark. Runtime validation + requires both reserved entries, rejects `null` for channels that materialize, + and renders guides only for configured axes. It cannot recover type-only + scale phantoms after a custom mark or stored definition has been erased. - Verification: focused type-contract and configured-scale tests cover - required Cartesian axes, mixed charts, omitted positionless axes, rejected - phantom axes, one-dimensional rules, runtime guards, and guide suppression. - Root typecheck passes. + required Cartesian axes, mixed charts, explicit null positionless axes, + rejected configured phantom axes, one-dimensional rules, deferred custom + marks, erased definitions, runtime guards, and guide suppression. Root + typecheck passes. ### F-004 — A radius channel silently imported continuous D3 @@ -659,7 +663,7 @@ Each entry records: ### F-016 — Stats animated export still renders through Plot -- Status: resolved +- Status: monitoring - Severity: high - Owner: Integration/API - Observed in: TanStack Stats default-renderer cutover @@ -1051,9 +1055,8 @@ Each entry records: rectangle endpoints remain inferred, widened rectangle options remain compatible, and custom marks and `ChartScale` retain the unchecked path. Public `ChartMarkPointX`/`ChartMarkPointY` helpers expose interaction values - separately from `ChartMarkScaleX`/`ChartMarkScaleY`. The pre-existing - `ChartMarkX`/`ChartMarkY` names remain point aliases rather than silently - changing meaning. The explicit helpers ship from the advanced + separately from `ChartMarkScaleX`/`ChartMarkScaleY`. The explicit helpers + ship from the advanced `mark/scale-values` subpath: exporting them from the ergonomic root changed esbuild symbol ordering by 1–5 gzip bytes despite erasing at runtime, so the exact ordinary-bundle gate rejected that shape. @@ -4469,6 +4472,14 @@ Each entry records: `tree/v` and `blob/v` links as release references, validates their exact count, and advances them with the package version. The focused regression counts only the matching version tag. +- `0.15.0` follow-up: all twelve shipped Intent skills still targeted 0.9.0. + Intent defines `metadata.library_version` as the source library version the + skill targets and reports drift against the currently published version. The + release synchronizer now includes every shipped `SKILL.md` with one exact + version reference. A focused regression discovers the shipped skill + directories and rejects any skill missing from the allowlist, then the normal + reference test checks that every tracked skill matches the current package + version. ### F-154 — Root barrels crossed the browser host boundary @@ -7819,7 +7830,7 @@ Each entry records: ### F-262 — Mark inference accepted an unsupported style option -- Status: open +- Status: resolved - Severity: medium - Owner: API - Observed in: hiding the unfocused dots in the themed area case 120 @@ -7827,10 +7838,20 @@ Each entry records: preserving generic options overload accepts extra keys, but `DotOptions` does not own base `opacity` and the renderer silently ignored it. State styles do accept `opacity`, so the boundary was especially easy to misread. -- Current decision: case 120 sets both `fillOpacity` and `strokeOpacity` to - zero, then restores them in focused states. Keep an exact-options check or a - consistent base-opacity contract open for the mark family; do not add a - runtime-only special case for dots. +- Decision: keep the distinct base style contracts and give public mark + factories fixed-key contextual option signatures. Per-property const + generics retain direct channel and named-scale inference, while fresh object + literals receive normal TypeScript excess-property checks. Generic + `Options` and `Options | undefined` forwarding wrappers remain assignable. + Predeclared structural supersets remain accepted, which matches standard + TypeScript behavior. The helpers are type-only and add no runtime code or + bundle weight. +- Verification: `cartesian-scale-types.test.ts` infers named scale IDs across + every Cartesian built-in factory and a polar mark, keeps reserved scale IDs + for explicit `undefined`, and compiles whole-options and optional forwarding + wrappers. Negative type checks reject fresh dot `opacity` and unknown keys + on Cartesian, composite, and polar factories. The focused strict TypeScript + compile and the full workspace `pnpm typecheck` pass. ### F-263 — Chromium transport and context churn interrupted catalog previews @@ -8448,17 +8469,17 @@ Each entry records: - Decision: make `scales` the canonical Cartesian and polar registry, reserve `x`, `y`, `angle`, and `radius` as readable defaults, and let marks bind to named entries. Each named scale declares its positional channel and guide - side. Axes support all four sides and stack when they share one side. Keep the - old root options only as temporary compatibility, warn once with exact - migration instructions, and remove the warning text from production bundles. - Preserve positional identity through composite channel namespacing and expose - the public resolved polar registry to length callbacks. + side. Axes support all four sides and stack when they share one side. The + pre-Alpha release kept the old root options temporarily and warned with exact + migration instructions. The Alpha API removes those options, their runtime + adapters, and their deprecated aliases. Preserve positional identity through + composite channel namespacing and expose the public resolved polar registry + to length callbacks. - Verification: named scale, axis, grid, focus, facet, motion, composite, box, - regression, polar, and type-contract regressions pass. All authored examples, - benchmarks, fixtures, adapters, and docs use the canonical registry, while - focused compatibility tests are the only intentional legacy syntax. The full - 1,920-test suite, workspace typecheck, documentation contract, and packed - package gate pass. + regression, polar, and type-contract regressions pass. Every TanStack Charts + definition in examples, benchmarks, adapters, tests, and docs uses the + canonical registry. Alpha residue checks find no root scale compatibility + types, runtime fallback, warning, or deprecated public alias. ### F-294 - Automatic mark renderers imposed shared host plumbing diff --git a/MARKETING.md b/MARKETING.md index d7d6e6e5..4061d92f 100644 --- a/MARKETING.md +++ b/MARKETING.md @@ -1,14 +1,15 @@ # TanStack Charts Marketing Strategy -Last updated: 2026-08-09 +Last updated: 2026-08-26 ## Status -The latest public pre-alpha release is TanStack Charts `0.15.0`. Repository docs, -examples, and the catalog follow unreleased `main` and may include contracts not -available in `0.15.0`; documentation at the verified release source revision is -the release record. Marketing must keep that distinction and the pre-alpha -status visible until the gates in [`PLAN.md`](./PLAN.md) are complete. +TanStack Charts is in official Alpha on `main`. The latest public release is +`0.15.0`. Repository docs, examples, and the catalog may include contracts not +available in that release, so documentation at the verified release source +revision remains the release record. Alpha is not a production-readiness claim, +and marketing must keep the compatibility limits in +[Alpha stability](./docs/stability.md) visible. ## Executive summary @@ -44,7 +45,7 @@ responsive, accessible, server-rendered application charts. library. **Product type:** MIT-licensed open-source developer library. The current -package line is pre-alpha. +package line is Alpha. **Core model:** Marks consume application data directly. Channels describe visual encodings. Compact TanStack primitives and native D3 callables compose @@ -461,7 +462,7 @@ about being "AI-native." | The chart catalog is smaller than AG Charts, ECharts, or Nivo | Also correct. The product thesis is a composable grammar with interoperable primitives, not first-party ownership of every specialized chart type. | | Why not use D3 or visx directly? | They provide the algorithms or primitives. TanStack supplies the application runtime: responsive layout, guides, scene compilation, lifecycle, interaction, accessibility, SSR, hydration, animation, and export. | | Why not use Observable Plot? | Plot is the closest API inspiration and remains an excellent choice for concise exploratory visualization. TanStack is an independent implementation focused on typed application integration, composable capability-level imports, framework lifecycle, and stable interactive scenes. | -| Is it ready for production? | Not yet. `0.15.0` is a public pre-alpha release. Marketing must keep the documented release gates visible until they close. | +| Is it ready for production? | It is Alpha. Teams can evaluate it or adopt it with exact version pins and upgrade tests, but minor releases may contain breaking changes. Do not market Alpha as stable or production-ready. | | Can it handle millions of live points? | Canvas is an explicit opt-in and keeps the same definition and interaction API while removing per-mark DOM cost. It still creates scene nodes and interaction points, default focus is linear without a spatial index, and overplotting does not become useful because the pixels are cheaper. Treat million-point streaming as a measured representation problem, not a renderer claim. | ## Anti-personas @@ -587,17 +588,18 @@ production case study. ## Launch sequence -### Current proof phase +### Official Alpha -- Conversion: Install `0.15.0`, read its release-source docs, explore the catalog, - and report friction. +- Conversion: Install `0.15.0`, read its release-source docs, explore the + catalog, and report friction. - Publish architecture, benchmarks, and working examples with limitations. - Recruit a small number of chart-heavy TanStack users. +- Keep the migration guide and Alpha stability policy current with every + breaking minor release. -### Public alpha or beta +### Public beta - Conversion: Install the prerelease and report friction. -- Publish migration guides and an API stability policy. - Add packed-consumer, visual regression, accessibility, and browser benchmark evidence. @@ -618,7 +620,7 @@ tracked in [`PLAN.md`](./PLAN.md): - Longitudinal, release-linked bundle-comparison results. - Remaining Plot-backed animated export migration. - Accessibility, locale, and RTL release gates. -- Release and compatibility policy. +- Stable-release compatibility policy. ## Goals diff --git a/PLAN.md b/PLAN.md index 7eb9d881..e5f1f3b1 100644 --- a/PLAN.md +++ b/PLAN.md @@ -6,7 +6,7 @@ Observed difficulty from using the API is tracked separately in [`API-FRICTION.md`](./API-FRICTION.md). Production migrations, examples, and agent evaluations must update that log when they expose a repeatable problem. -Last updated: 2026-08-06 +Last updated: 2026-08-26 ## Working thesis @@ -380,7 +380,7 @@ Remaining production gaps are tracked by the open and monitoring entries in Plot-backed GIF/WebM frame generator after the TanStack.com default-renderer cutover. -`pnpm package:check` now builds real package artifacts, packs all three public +`pnpm package:check` now builds real package artifacts, packs all twelve public packages, installs their tarballs into a disposable offline fixture, and gates every ESM subpath, strict declaration inference, required dynamic input, React/Octane runtime loading, and minimal production bundles. Every TanStack @@ -1553,10 +1553,12 @@ export const downloadsChart = defineChart({ key: 'id', }), ], - x: { scale: scaleUtc().domain(dateDomain) }, - y: { - scale: scaleLinear().domain(downloadDomain).nice(), - label: 'Weekly downloads', + scales: { + x: { scale: scaleUtc().domain(dateDomain) }, + y: { + scale: scaleLinear().domain(downloadDomain).nice(), + axis: { label: 'Weekly downloads' }, + }, }, }) ``` @@ -3140,11 +3142,11 @@ Exit criteria: ### Phase 2: minimal production core -Status: in progress. Package names, capability boundaries, packed-consumer -artifacts, automated visual and interaction conformance, production-browser -stress evidence, and bundle CI are established. Remaining productization gates -include public diagnostics, release compatibility policy, and published bundle -budgets. +Status: complete for Alpha and ongoing for production readiness. Package names, +capability boundaries, packed-consumer artifacts, automated visual and +interaction conformance, production-browser stress evidence, bundle CI, locked +budgets, and the Alpha compatibility policy are established. Public diagnostics +and stable-release policy remain productization work. - Finalize public package and entry-point structure. - Implement host lifecycle and diagnostics. diff --git a/README.md b/README.md index 7acc991e..2c867eb5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@
- Status - PRE-ALPHA + Status - ALPHA Follow @TanStack @@ -39,10 +39,12 @@ server-rendered application charts. > [!IMPORTANT] -> This README follows unreleased `main`. The latest published release is -> TanStack Charts `0.15.0`; use its +> This README follows unreleased `main`, the official Alpha line. The latest +> published release is TanStack Charts `0.15.0`; use its > [release-source documentation](https://github.com/TanStack/charts/tree/v0.15.0/docs). -> It is pre-alpha and not ready for production use. +> Alpha releases use regular `0.x` versions and may contain breaking changes +> between minor releases. Pin an exact version for production use and read the +> [Alpha stability policy](./docs/stability.md) before upgrading. Most chart libraries are easy until the chart stops being standard. TanStack Charts gives you one typed grammar that can grow from a familiar line or bar diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json index f4e885f6..486ae8f9 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": 50871, - "gzip": 19096 + "bytes": 50813, + "gzip": 19070 }, "D3-scale line + static SVG": { - "bytes": 55602, - "gzip": 20795 + "bytes": 55544, + "gzip": 20776 }, "Representative marks": { - "bytes": 75552, - "gzip": 27551 + "bytes": 75617, + "gzip": 27582 }, "TanStack DOM host": { - "bytes": 74740, - "gzip": 26000 + "bytes": 74947, + "gzip": 26053 }, "React adapter": { - "bytes": 76923, - "gzip": 26780 + "bytes": 77130, + "gzip": 26831 }, "React line consumer": { - "bytes": 100559, - "gzip": 36186 + "bytes": 100766, + "gzip": 36222 }, "Compact-scale line scene": { - "bytes": 33323, - "gzip": 11958 + "bytes": 33265, + "gzip": 11935 }, "React compact-scale line consumer": { - "bytes": 83059, - "gzip": 29082 + "bytes": 83266, + "gzip": 29144 }, "Custom-scale line scene": { - "bytes": 31505, - "gzip": 11222 + "bytes": 31447, + "gzip": 11193 }, "D3 linear-scale line scene": { - "bytes": 50803, - "gzip": 19059 + "bytes": 50745, + "gzip": 19032 } } } diff --git a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx index 2ad3d3d3..f573d6e1 100644 --- a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx +++ b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnBrowsers, shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' const browserNames = shadcnBrowsers.map((row) => row.browser) @@ -39,8 +38,8 @@ export function createExampleChart() { polar({ radiusRatio: 0.78, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks, diff --git a/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx b/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx index 56feb8b1..6a4460b4 100644 --- a/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx +++ b/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnBrowsers, shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' const browserNames = shadcnBrowsers.map((row) => row.browser) @@ -30,8 +29,8 @@ export function createExampleChart() { polar({ radiusRatio: 0.78, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks, diff --git a/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx b/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx index 3970dc55..cb193587 100644 --- a/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx +++ b/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnBrowsers, shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' const browserNames = shadcnBrowsers.map((row) => row.browser) @@ -30,8 +29,8 @@ export function createExampleChart() { polar({ radiusRatio: 0.78, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks, diff --git a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx index 1cb2e506..e23b14bf 100644 --- a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx +++ b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnBrowsers, shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' const browserNames = shadcnBrowsers.map((row) => row.browser) @@ -30,8 +29,8 @@ export function createExampleChart() { polar({ radiusRatio: 0.78, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks, diff --git a/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx b/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx index d64e7bce..2036ae00 100644 --- a/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx +++ b/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnBrowsers, shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' const browserNames = shadcnBrowsers.map((row) => row.browser) @@ -30,8 +29,8 @@ export function createExampleChart() { polar({ radiusRatio: 0.78, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks, diff --git a/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx b/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx index 4c0f7b40..5ac63dc4 100644 --- a/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx +++ b/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx @@ -3,7 +3,6 @@ import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' import { RendererChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from 'd3-scale' import { shadcnColors } from '@tanstack/charts-data/shadcn' import './styles.css' export function createExampleChart() { @@ -32,8 +31,8 @@ export function createExampleChart() { polar({ radiusRatio: 1, scales: { - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, + angle: null, + radius: null, }, marks: [ diff --git a/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg b/benchmarks/conformance/previews/119-stacked-bar-band-cursor.svg index 0fc5ffd6..8171243d 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/bar-stacked.svg b/benchmarks/conformance/previews/bar-stacked.svg index 4ef03375..36b101fe 100644 --- a/benchmarks/conformance/previews/bar-stacked.svg +++ b/benchmarks/conformance/previews/bar-stacked.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json index 0dc401fb..e1176205 100644 --- a/benchmarks/conformance/previews/manifest.json +++ b/benchmarks/conformance/previews/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "width": 288, "height": 192, - "sourceHash": "77c9a85664a8a74ac8d3d00ebbdc9ff1be08ecd5859b962d16077cef8d996da0", + "sourceHash": "d3177faf65fca2414b431c195a003cf9afdceb8ae2f1b37e4f8cb81578dc8293", "assets": [ { "id": "01-line-gaps", @@ -41,8 +41,8 @@ }, { "id": "bar-stacked", - "sha256": "a63c40af079eebd20474ae240798dc3da1bca1bee3fe48f753ebacedfa45e4ed", - "bytes": 16447 + "sha256": "10107c15a3cfccc8f4c3a45941737769dd6e9bec3b21bb7d0c31ea10240831ea", + "bytes": 16464 }, { "id": "scatter-bubble", @@ -551,8 +551,8 @@ }, { "id": "119-stacked-bar-band-cursor", - "sha256": "28f6ebbfb34bd586336035fac604e7bc04f04685d6599cb6c90e049731cae2ee", - "bytes": 9706 + "sha256": "0257543342681ad1161ff6011ebee48abd3dd6b4d2f3ee46b751a4f54fe95407", + "bytes": 9709 }, { "id": "120-themed-interactive-area", diff --git a/docs/config.json b/docs/config.json index 48e92d3f..34c4418a 100644 --- a/docs/config.json +++ b/docs/config.json @@ -21,6 +21,10 @@ "label": "Installation", "to": "installation" }, + { + "label": "Alpha Stability", + "to": "stability" + }, { "label": "Quick Start", "to": "quick-start" diff --git a/docs/guides/ai-authoring.md b/docs/guides/ai-authoring.md index b25f3110..23a67dc6 100644 --- a/docs/guides/ai-authoring.md +++ b/docs/guides/ai-authoring.md @@ -22,11 +22,11 @@ path is explicit and repeatable. Start at [Choosing a Chart](./choosing-a-chart.md), then use the relevant [example family](../examples/index.md). -Put Cartesian scale and axis options under `scales.x` and `scales.y`. Never -generate the deprecated root `x` or `y` form. Inside `polar()`, put position -scales under `scales.angle` and `scales.radius`. Use named registry entries and -mark-level `xScale`, `yScale`, `angleScale`, or `radiusScale` selectors when a -chart needs more than one mapping on the same channel. +Put Cartesian scale and axis options under `scales.x` and `scales.y`. The +pre-Alpha root `x` and `y` form is not part of the Alpha API. Inside `polar()`, +put position scales under `scales.angle` and `scales.radius`. Use named registry +entries and mark-level `xScale`, `yScale`, `angleScale`, or `radiusScale` +selectors when a chart needs more than one mapping on the same channel. Keep the default SVG renderer unless a measured paint-heavy mark benefits from Canvas. To mix surfaces, import `canvasChartRenderer` from the exact diff --git a/docs/guides/migrating.md b/docs/guides/migrating.md index 4b2f535b..c53020f4 100644 --- a/docs/guides/migrating.md +++ b/docs/guides/migrating.md @@ -7,9 +7,9 @@ Migration is a semantic exercise, not a component-name translation. First describe what the existing chart means and how users operate it. Then express that behavior with data preparation, scales, marks, and host options. -## Move root scales into the registry +## Move pre-Alpha root scales into the registry -Current definitions put Cartesian scale and axis options under `scales`: +Alpha definitions require Cartesian scale and axis options under `scales`: ```ts const definition = defineChart({ @@ -21,37 +21,20 @@ const definition = defineChart({ }) ``` -The earlier root properties still work temporarily: +When updating from a pre-Alpha release, move root `x` to `scales.x` and root +`y` to `scales.y`. Every definition must provide both reserved entries. Use +`null` when the chart does not use one of the Cartesian dimensions. -```ts -const legacyDefinition = defineChart({ - marks, - x: { scale: xScale }, - y: { scale: yScale, grid: true }, -}) -``` - -Development builds warn with the exact replacement. Move `x` to `scales.x` -and `y` to `scales.y`. Root `x` and `y` will be removed when TanStack Charts -enters Alpha. New code and generated code should only use the `scales` form. +Polar definitions follow the same migration. Move pre-Alpha root `angle` and +`radius` options into `polar({ scales: { angle, radius } })`. Every polar +definition must provide both reserved entries. If neither positional scale is +used, write `scales: { angle: null, radius: null }`. -Polar definitions follow the same migration. Move `polar({ angle, radius })` -to `polar({ scales: { angle, radius } })`. If neither positional scale is -used, write `scales: { angle: null, radius: null }`. The old polar properties -also work temporarily, warn in development, and will be removed when TanStack -Charts enters Alpha. - -During this compatibility window, `ChartSpec` is a union of the canonical and -legacy shapes. If application code extended the old interface, replace that -extension with an intersection: - -```ts -import type { ChartSpec } from '@tanstack/charts' - -type ApplicationChartSpec = ChartSpec & { - applicationTag: string -} -``` +Custom mark type code should replace `ChartMarkX` and `ChartMarkY` with +`ChartMarkPointX` and `ChartMarkPointY` from +`@tanstack/charts/mark/scale-values`. A polar length callback should replace +`layout.angle` with `layout.scales.angle` and `layout.radiusScale` with +`layout.scales.radius`. ## Inventory the current contract diff --git a/docs/installation.md b/docs/installation.md index abd54437..d2a0d1b3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,11 +3,12 @@ title: Installation description: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. --- -These docs follow unreleased `main`. The latest published pre-alpha is TanStack -Charts `0.15.0`; use its +These docs follow unreleased `main`, the official Alpha line. The latest +published release is TanStack Charts `0.15.0`; use its [release-source docs](https://github.com/TanStack/charts/tree/v0.15.0/docs) -for the exact surface. Install TanStack Charts in each application that authors -chart definitions: +for the exact surface. Alpha releases use regular `0.x` versions and may break +APIs between minor releases. Install TanStack Charts in each application that +authors chart definitions: ```sh pnpm add @tanstack/charts diff --git a/docs/overview.md b/docs/overview.md index 582303ba..faba9a1a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -3,8 +3,10 @@ title: Overview description: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. --- -These docs follow unreleased `main`. The latest published TanStack Charts -release is `0.15.0`; it is pre-alpha and its API may change between releases. +These docs follow unreleased `main`, the official Alpha line. The latest +published TanStack Charts release is `0.15.0`. Alpha uses regular `0.x` +versions, and APIs may change between minor releases. See +[Alpha stability](./stability.md) for the release contract. TanStack Charts is a small, framework-agnostic chart grammar for TypeScript and JavaScript. Give each mark its natural data, map fields or accessors to visual diff --git a/docs/reference/focus-and-interaction.md b/docs/reference/focus-and-interaction.md index 301d6cc7..a1f189aa 100644 --- a/docs/reference/focus-and-interaction.md +++ b/docs/reference/focus-and-interaction.md @@ -22,6 +22,7 @@ With no custom focus strategy: - `Home` and `End` move to the first and last point - `Enter` and Space toggle an enabled sticky tooltip and call `onSelect` for the focused point +- pressing the pointer outside the chart or tooltip dismisses a pinned tooltip - a configured selection controller receives the same focused point before `onSelect` - a click focuses and selects the nearest point, or selects `null` on the @@ -898,8 +899,9 @@ resize, and content resize, and collide against the viewport instead of the chart box. Clicking, Enter, or Space pins the tooltip. The next activation unpins it. -`Escape` unpins and clears focus. Set `sticky: false` to disable pinning. A -display-only tooltip has `role="status"` and `aria-live="polite"`. +Pressing the pointer outside the chart and tooltip, or pressing `Escape`, +unpins and clears focus. Set `sticky: false` to disable pinning. A display-only +tooltip has `role="status"` and `aria-live="polite"`. Set `visibility: 'pinned'` for click-or-keyboard detail that should not paint a transient shell. Focus and inline mark states still update before activation; diff --git a/docs/reference/types.md b/docs/reference/types.md index 4fd27c07..f219b23b 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -114,9 +114,6 @@ are available from the root entry point. The four `ChartMarkPoint*` and | `ChartSpecXValue` | Interaction x union across marks | | `ChartSpecYValue` | Interaction y union across marks | -`ChartMarkX` and `ChartMarkY` remain exported as deprecated aliases of the -point extractors. New code should use the explicit names. - Stateful mark presentation uses `ChartMarkStateContext` as one object bag for the datum, index, data, point, focus, pointer, and matching helper. A `ChartMarkStateSelector` handles the common declarative cases, while callbacks diff --git a/docs/stability.md b/docs/stability.md new file mode 100644 index 00000000..bb41c758 --- /dev/null +++ b/docs/stability.md @@ -0,0 +1,49 @@ +--- +title: Alpha Stability +description: Understand what TanStack Charts Alpha versions promise, what may change, and how releases communicate breaking changes. +--- + +TanStack Charts is in Alpha. Packages use regular `0.x` versions on the normal +`latest` npm tag, without an `-alpha` suffix or separate release channel. + +Alpha is ready for evaluation and early application integration. It is not a +stable API promise. Pin an exact version in production applications and test an +upgrade before changing that pin. + +## Version contract + +All public TanStack Charts packages move together as one fixed release group. + +- Patch releases fix defects and do not intentionally remove or rename public + APIs. A fix may correct rendering or interaction that was observably wrong. +- Minor releases may add features and may contain breaking API changes while + the package major remains `0`. +- The project will publish a stable-release compatibility policy before `1.0`. + +## Public surface + +The public surface is the package export map and the APIs documented on this +site. Source files, internal modules, unexported types, generated scene details, +and undocumented behavior may change without a migration path. + +Framework adapters share the same chart definition and release version. Their +runtime support and peer ranges are listed in +[Installation](./installation.md#framework-compatibility). + +## Breaking changes + +A breaking Alpha release must include a changeset, changelog entry, and concrete +migration instructions. We will use a development warning ahead of removal when +that warning is practical and useful, but Alpha does not promise a minimum +deprecation window. + +Production bundles do not retain development migration warnings. Removed APIs +fail through TypeScript or an actionable runtime error instead of silently +falling back to an older interpretation. + +## Report a regression + +[Open a GitHub issue](https://github.com/TanStack/charts/issues/new/choose) with +the exact package version, framework, browser or native runtime, a minimal +reproduction, and the expected and actual result. A regression in a patch +release is treated as a defect against this policy. diff --git a/llms.txt b/llms.txt index 10b9a3bf..07d1e4fc 100644 --- a/llms.txt +++ b/llms.txt @@ -4,107 +4,108 @@ TanStack Charts is a framework-agnostic, type-safe visualization grammar with th Read the canonical pages below. Each concept is documented once; guides and examples link back to its owner page. -- docs/overview.md — Overview: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. -- docs/comparison.md — Compare Libraries: Compare TanStack Charts with measured and documentation-reviewed charting libraries. -- docs/installation.md — Installation: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. -- docs/quick-start.md — Quick Start: Build, mount, update, and clean up a responsive TanStack Charts line chart with fully inferred types. -- docs/framework/react/quick-start.md — React Quick Start: Install the React adapter, define a typed chart, render responsive SVG, and add native interaction. -- docs/framework/react/adapter.md — React Adapter: Understand the thin React lifecycle, SSR, hydration, update, sizing, class, and style behavior around the shared chart host. -- docs/framework/preact/adapter.md — Preact Adapter: Render and hydrate TanStack Charts with Preact. -- docs/framework/vue/adapter.md — Vue Adapter: Render and hydrate TanStack Charts with Vue 3. -- docs/framework/solid/adapter.md — Solid Adapter: Render and hydrate TanStack Charts with Solid. -- docs/framework/svelte/adapter.md — Svelte Adapter: Render and hydrate TanStack Charts with Svelte 5. -- docs/framework/angular/adapter.md — Angular Adapter: Render TanStack Charts with an Angular standalone component. -- docs/framework/lit/adapter.md — Lit Adapter: Render TanStack Charts with a light-DOM Lit custom element. -- docs/framework/alpine/adapter.md — Alpine Adapter: Mount TanStack Charts with an Alpine directive. -- docs/framework/octane/quick-start.md — Octane Quick Start: Install the Octane adapter, define a typed chart, render responsive SSR-safe SVG, and add native interaction. -- docs/framework/octane/adapter.md — Octane Adapter: Understand the native TSRX lifecycle, conditional SSR target, hydration, sizing, class, and style behavior around the shared chart host. -- docs/concepts/grammar-of-graphics.md — Grammar of Graphics: Understand TanStack Charts as a composition of data, marks, channels, scales, guides, and layers. -- docs/concepts/chart-definitions.md — Chart Definitions: Capture application values in memoized definitions and use responsive builders for surface-dependent choices. -- docs/concepts/data-and-channels.md — Data and Channels: Map typed application data to positions, grouping, color, radius, and stable chart identity. -- docs/concepts/scales-and-d3.md — Scales: Start with compact scales, upgrade individual mappings to D3 when their semantics require it, and keep responsive range ownership clear. -- docs/concepts/marks-and-layering.md — Marks and Layering: Choose built-in geometric marks, compose them in render order, and preserve stable layer identity. -- docs/concepts/layout-axes-and-coordinates.md — Layout, Axes, and Coordinates: Learn how responsive ranges, automatic margins, guides, band alignment, and scene coordinates work. -- docs/guides/choosing-a-chart.md — Choosing a Chart: Choose a visual encoding from the question, data shape, and comparison task before composing TanStack Charts marks. -- docs/guides/responsive-charts.md — Responsive Charts: Size charts from their containers while preserving readable guides, deterministic SSR, and application-controlled height. -- docs/guides/themes-and-styling.md — Themes and Styling: Apply automatic light and dark color behavior, CSS palette tokens, mark styles, and renderer-aware resources. -- docs/guides/accessibility.md — Accessibility: Give charts useful names, keyboard-equivalent interaction, reduced motion, redundant encodings, and exact-value alternatives. -- docs/guides/dynamic-data-and-animation.md — Dynamic Data and Animation: Recreate memoized definitions when application values change and animate keyed geometry safely. -- docs/guides/transforms-and-reactivity.md — Transforms and Reactivity: Derive typed rows once and memoize them where the data is owned. -- docs/guides/tooltips-and-focus.md — Tooltips and Focus: Configure grouped focus, crosshairs, automatic content, ordering, placement, portaling, pinning, and framework-composed tooltip bodies. -- docs/guides/interactions-and-selections.md — Interactions and Selections: Build controlled cursors, linked selections, brushes, zooming, scrolling, timelines, and editors around semantic chart state. -- docs/guides/faceting-and-composition.md — Faceting and Composition: Layer marks, repeat a shared chart by group, and coordinate multiple views without introducing a fixed chart-type model. -- docs/guides/legends-and-color.md — Legends and Color: Map categorical or quantitative values to color, render responsive legends, and preserve meaning across themes. -- docs/guides/custom-marks-and-renderers.md — Custom Marks and Renderers: Extend the grammar with typed mark channels and keyed scene nodes, or replace the mounted renderer without bypassing chart semantics. -- docs/guides/large-data.md — Large Data: Choose bounded visual representations, measure preparation separately, preserve analytical invariants, and avoid rendering more detail than pixels can communicate. -- docs/guides/ssr-and-hydration.md — SSR and Hydration: Render deterministic chart markup on the server, preserve runtime work through hydration, and handle responsive dimensions without mismatches. -- docs/guides/exporting.md — Exporting: Export accessible chart SVG or browser-rendered raster images while preserving dimensions, styling, and resource identity. -- docs/guides/bundle-size-and-performance.md — Bundle Size and Performance: Keep each chart proportional to the marks, scales, host capabilities, and data representation it actually uses. -- docs/guides/typescript.md — TypeScript: Preserve end-to-end inference from chart data and channels through scales, focus callbacks, adapters, and custom extensions. -- docs/guides/testing-and-debugging.md — Testing and Debugging: Test chart semantics, geometry, interaction, updates, accessibility, and performance without relying on brittle screenshots alone. -- docs/guides/ai-authoring.md — AI Authoring: Give coding agents a deterministic route from a visualization goal to typed TanStack Charts code, validation, and advanced extension. -- docs/guides/migrating.md — Migrating: Move an existing visualization to TanStack Charts by preserving analytical semantics, interaction behavior, and measurable output before changing design. -- docs/examples/index.md — Example Gallery: Choose a TanStack Charts example from the question your reader needs to answer, then follow the linked concepts and reference pages. -- docs/examples/lines-and-areas.md — Lines and Areas: Choose and compose line, range-area, rolling-statistic, and annotation patterns for ordered data. -- docs/examples/bars-and-rankings.md — Bars and Rankings: Choose bar, lollipop, dumbbell, and waterfall compositions for categorical comparison and change. -- docs/examples/scatterplots-and-relationships.md — Scatterplots and Relationships: Choose scatterplot, regression, connected-path, lag, and nearest-point patterns for quantitative relationships. -- docs/examples/distributions.md — Distributions: Choose histograms, boxplots, empirical cumulative distributions, and violins for shape, spread, and rank. -- docs/examples/heatmaps-and-densities.md — Heatmaps and Densities: Choose quantitative matrix cells, contours, or hexagonal bins to show concentration across two dimensions. -- docs/examples/intervals-and-financial.md — Intervals and Financial Charts: Compose timelines, uncertainty intervals, candlesticks, and percentile ribbons from explicit endpoints. -- docs/examples/stacked-and-composition.md — Stacked and Composed Charts: Choose stacked, normalized, streamgraph, funnel, and mosaic compositions for part-to-whole and stage-retention comparisons. -- docs/examples/facets-and-multiple-views.md — Facets and Multiple Views: Choose shared-scale facets, marginal views, and focus-plus-context layouts for coordinated comparisons. -- docs/examples/networks-and-hierarchies.md — Networks and Hierarchies: Choose tidy trees, Sankey flows, spatial adjacency graphs, and force-directed networks for connected or nested data. -- docs/examples/maps-and-spatial.md — Maps and Spatial Charts: Build choropleths, bubble maps, globes, routes, and vector fields for geographic or projected spatial questions. -- docs/examples/polar-and-radar.md — Polar and Radar Charts: Build native pie, donut, gauge, radar, line, scatter, radial bar, rose, and sunburst charts through the opt-in polar coordinate entry. -- docs/examples/annotations-and-overlays.md — Annotations and Overlays: Add direct labels, thresholds, change indicators, and explanatory layers without hiding chart semantics. -- docs/examples/interactive-charts.md — Interactive Charts: Choose chart-owned focus or controlled application interactions for pinned detail, scrolling, zooming, and editing. -- docs/examples/themes-and-motion.md — Themes and Motion: Apply inherited palettes and optional motion while keeping chart definitions separate from card layout and controls. -- docs/reference/index.md — API Reference: Reference for TanStack Charts definitions, marks, scales, runtime, rendering, interaction, and framework adapters. -- docs/reference/chart-definitions.md — Chart Definition API: Reference static and responsive chart definitions, build context, and identity-based updates. -- docs/reference/chart-spec.md — Chart Spec: Reference for marks, axes, color, gradients, clipping, margins, guides, and themes in a TanStack Charts spec. -- docs/reference/view-composition.md — View Composition: Compose complete chart definitions with deterministic grid, layer, fill, and inset layouts. -- docs/reference/transforms.md — Data Transforms: Typed, eager transforms for reusable application data. -- docs/reference/scales-guides-and-color.md — Scales, Guides, and Color: Reference for injected positional scales, automatic axes and margins, color scales, legends, themes, and gradients. -- docs/reference/dom-host.md — DOM Host: Mount, update, size, interact with, and destroy a TanStack Chart in a browser DOM container. -- docs/reference/adapter-controller.md — Adapter Controller: Build framework adapters over shared prerender, mount, update, layout, scene access, and cleanup behavior. -- docs/reference/runtime-and-scene.md — Runtime and Scene: Compile object and responsive chart definitions into renderer-neutral scenes. -- docs/reference/focus-and-interaction.md — Focus and Interaction: Configure pointer focus, grouped focus, crosshairs, controlled cursors, keyboard navigation, built-in tooltips, selection, and spatial indexes. -- docs/reference/rendering-and-export.md — Rendering and Export: Render chart scenes through the default SVG renderer, the optional Canvas renderer, or a custom surface, then export supported output. -- docs/reference/motion.md — Motion: Optional SVG tween and spring motion, definition-local timing, interruption behavior, reduced motion, and the standalone spring sampler. -- docs/reference/custom-extensions.md — Custom Extensions: Extend TanStack Charts with custom marks, distinct scale values, curves, scales, color, legends, text measurement, spatial indexes, and renderers. -- docs/reference/types.md — Types: Public TypeScript types, inference rules, channels, definitions, scenes, hosts, focus, rendering, and custom extension contracts. -- docs/framework/react/reference/chart.md — React Chart: Complete prop and type reference for the @tanstack/charts/react Chart component. -- docs/framework/preact/reference/chart.md — Preact Chart: Complete prop and type reference for the @tanstack/charts/preact Chart component. -- docs/framework/vue/reference/chart.md — Vue Chart: Complete prop and type reference for the @tanstack/charts/vue Chart component. -- docs/framework/solid/reference/chart.md — Solid Chart: Complete prop and type reference for the @tanstack/charts/solid Chart component. -- docs/framework/svelte/reference/chart.md — Svelte Chart: Complete prop and type reference for the @tanstack/charts/svelte Chart component. -- docs/framework/angular/reference/chart.md — Angular Chart: Complete option and type reference for the @tanstack/charts/angular standalone component. -- docs/framework/lit/reference/chart.md — Lit Chart: Complete option and type reference for the @tanstack/charts/lit custom element. -- docs/framework/alpine/reference/chart.md — Alpine Chart: Complete option and type reference for the @tanstack/charts/alpine x-chart directive. -- docs/framework/octane/reference/chart.md — Octane Chart: Complete prop and type reference for the @tanstack/charts/octane Chart component. -- docs/reference/marks/line-and-area.md — Line and Area Marks: Reference for lineY, lineX, areaY, areaX, grouping, null gaps, interval baselines, points, and optional curve bridges. -- docs/reference/marks/difference.md — Difference Marks: Reference for differenceY and differenceX comparison areas, exact crossings, grouping, gaps, lineage, styling, and interaction. -- docs/reference/marks/regression.md — Linear Regression Marks: Reference for eager linearRegressionRowsY/X data and linearRegressionY/X least-squares marks, confidence bands, grouping, sampling, lineage, and interaction. -- docs/reference/marks/bar-and-rect.md — Bar and Rect Marks: Reference for barY, barX, rect, cell, interval endpoints, band sizing, grouped bars, color, insets, and focus anchors. -- docs/reference/marks/box.md — Box Marks: Reference for boxRows, boxY, and boxX Tukey summaries, quartiles, whiskers, outliers, lineage, styling, and interaction. -- docs/reference/marks/dot-and-hexagon.md — Dot and Hexagon Marks: Reference for dot and hexagon channels, pixel radius, injected radius scales, color, grouping, keys, and interaction points. -- docs/reference/marks/dodge.md — Dodge Layouts: Reference for responsive dodgeX and dodgeY dot collision layouts, anchors, padding, radii, identity, and scale ownership. -- docs/reference/marks/waffle.md — Waffle Marks: Reference for responsive waffleY and waffleX unit layouts, cumulative rounding, color, source identity, and interaction. -- docs/reference/marks/ridgeline.md — Ridgeline Marks: Reference for semantic ridgelineY and ridgelineX profile geometry, categorical baselines, overlap, curves, identity, and interaction. -- docs/reference/marks/violin.md — Violin Marks: Reference for semantic violinY and violinX mirrored profile geometry, category-relative spans, curves, identity, and interaction. -- docs/reference/marks/focus-guide.md — Focus Guide Marks: Render datum-bound focus rules, markers, and axis labels with stable renderer-neutral motion. -- docs/reference/marks/treemap.md — Treemap Mark: Reference for responsive hierarchy tiling, flat-row construction, node lineage, color, labels, and the optional treemap import. -- docs/reference/marks/sunburst.md — Sunburst Mark: Reference for flat hierarchy input, responsive radial partitioning, node lineage, branch color, and the optional sunburst import. -- docs/reference/marks/sankey.md — Sankey Diagram Mark: Reference for responsive flow layout, semantic graph input, immutable node and link rows, ordinary child marks, and the optional Sankey import. -- docs/reference/marks/hexbin.md — Hexbin Mark: Reference for responsive screen-space hexagonal aggregation, reducer outputs, source lineage, color, and the optional spatial import. -- docs/reference/marks/contour.md — Contour Mark: Reference for regular scalar-grid contours, thresholds, orientation, source lineage, and structured multipolygon output. -- docs/reference/marks/density.md — Density Contour Mark: Reference for responsive point-density contours, thresholds, grouping, weights, source lineage, and structured multipolygon output. -- docs/reference/marks/delaunay.md — Delaunay Link Mark: Reference for responsive Delaunay adjacency links, source grouping, endpoint lineage, stable keys, and the optional spatial import. -- docs/reference/marks/voronoi.md — Voronoi Mark: Reference for responsive final-screen Voronoi cells, grouping, stable coincident ownership, presentation, and interaction separation. -- docs/reference/marks/rules-links-arrows-vectors-and-ticks.md — Rules, Links, Arrows, Vectors, and Ticks: Reference for ruleX, ruleY, link, arrow, vector, tickX, and tickY geometry, channels, defaults, curves, and focus anchors. -- docs/reference/marks/text-frame-and-facet.md — Text, Frame, and Facet Marks: Reference for data labels, chart frames, responsive small multiples, shared or per-cell axes, labels, and facet constraints. -- docs/reference/marks/geo.md — Geo Shape Mark: Reference for projecting GeoJSON with an opt-in D3-backed geoShape mark. -- docs/reference/marks/polar.md — Polar Marks: Reference for the opt-in polar container, radial marks, guides, injected scale factories, and D3-backed geometry. +- docs/overview.md: Overview: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. +- docs/comparison.md: Compare Libraries: Compare TanStack Charts with measured and documentation-reviewed charting libraries. +- docs/installation.md: Installation: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. +- docs/stability.md: Alpha Stability: Understand what TanStack Charts Alpha versions promise, what may change, and how releases communicate breaking changes. +- docs/quick-start.md: Quick Start: Build, mount, update, and clean up a responsive TanStack Charts line chart with fully inferred types. +- docs/framework/react/quick-start.md: React Quick Start: Install the React adapter, define a typed chart, render responsive SVG, and add native interaction. +- docs/framework/react/adapter.md: React Adapter: Understand the thin React lifecycle, SSR, hydration, update, sizing, class, and style behavior around the shared chart host. +- docs/framework/preact/adapter.md: Preact Adapter: Render and hydrate TanStack Charts with Preact. +- docs/framework/vue/adapter.md: Vue Adapter: Render and hydrate TanStack Charts with Vue 3. +- docs/framework/solid/adapter.md: Solid Adapter: Render and hydrate TanStack Charts with Solid. +- docs/framework/svelte/adapter.md: Svelte Adapter: Render and hydrate TanStack Charts with Svelte 5. +- docs/framework/angular/adapter.md: Angular Adapter: Render TanStack Charts with an Angular standalone component. +- docs/framework/lit/adapter.md: Lit Adapter: Render TanStack Charts with a light-DOM Lit custom element. +- docs/framework/alpine/adapter.md: Alpine Adapter: Mount TanStack Charts with an Alpine directive. +- docs/framework/octane/quick-start.md: Octane Quick Start: Install the Octane adapter, define a typed chart, render responsive SSR-safe SVG, and add native interaction. +- docs/framework/octane/adapter.md: Octane Adapter: Understand the native TSRX lifecycle, conditional SSR target, hydration, sizing, class, and style behavior around the shared chart host. +- docs/concepts/grammar-of-graphics.md: Grammar of Graphics: Understand TanStack Charts as a composition of data, marks, channels, scales, guides, and layers. +- docs/concepts/chart-definitions.md: Chart Definitions: Capture application values in memoized definitions and use responsive builders for surface-dependent choices. +- docs/concepts/data-and-channels.md: Data and Channels: Map typed application data to positions, grouping, color, radius, and stable chart identity. +- docs/concepts/scales-and-d3.md: Scales: Start with compact scales, upgrade individual mappings to D3 when their semantics require it, and keep responsive range ownership clear. +- docs/concepts/marks-and-layering.md: Marks and Layering: Choose built-in geometric marks, compose them in render order, and preserve stable layer identity. +- docs/concepts/layout-axes-and-coordinates.md: Layout, Axes, and Coordinates: Learn how responsive ranges, automatic margins, guides, band alignment, and scene coordinates work. +- docs/guides/choosing-a-chart.md: Choosing a Chart: Choose a visual encoding from the question, data shape, and comparison task before composing TanStack Charts marks. +- docs/guides/responsive-charts.md: Responsive Charts: Size charts from their containers while preserving readable guides, deterministic SSR, and application-controlled height. +- docs/guides/themes-and-styling.md: Themes and Styling: Apply automatic light and dark color behavior, CSS palette tokens, mark styles, and renderer-aware resources. +- docs/guides/accessibility.md: Accessibility: Give charts useful names, keyboard-equivalent interaction, reduced motion, redundant encodings, and exact-value alternatives. +- docs/guides/dynamic-data-and-animation.md: Dynamic Data and Animation: Recreate memoized definitions when application values change and animate keyed geometry safely. +- docs/guides/transforms-and-reactivity.md: Transforms and Reactivity: Derive typed rows once and memoize them where the data is owned. +- docs/guides/tooltips-and-focus.md: Tooltips and Focus: Configure grouped focus, crosshairs, automatic content, ordering, placement, portaling, pinning, and framework-composed tooltip bodies. +- docs/guides/interactions-and-selections.md: Interactions and Selections: Build controlled cursors, linked selections, brushes, zooming, scrolling, timelines, and editors around semantic chart state. +- docs/guides/faceting-and-composition.md: Faceting and Composition: Layer marks, repeat a shared chart by group, and coordinate multiple views without introducing a fixed chart-type model. +- docs/guides/legends-and-color.md: Legends and Color: Map categorical or quantitative values to color, render responsive legends, and preserve meaning across themes. +- docs/guides/custom-marks-and-renderers.md: Custom Marks and Renderers: Extend the grammar with typed mark channels and keyed scene nodes, or replace the mounted renderer without bypassing chart semantics. +- docs/guides/large-data.md: Large Data: Choose bounded visual representations, measure preparation separately, preserve analytical invariants, and avoid rendering more detail than pixels can communicate. +- docs/guides/ssr-and-hydration.md: SSR and Hydration: Render deterministic chart markup on the server, preserve runtime work through hydration, and handle responsive dimensions without mismatches. +- docs/guides/exporting.md: Exporting: Export accessible chart SVG or browser-rendered raster images while preserving dimensions, styling, and resource identity. +- docs/guides/bundle-size-and-performance.md: Bundle Size and Performance: Keep each chart proportional to the marks, scales, host capabilities, and data representation it actually uses. +- docs/guides/typescript.md: TypeScript: Preserve end-to-end inference from chart data and channels through scales, focus callbacks, adapters, and custom extensions. +- docs/guides/testing-and-debugging.md: Testing and Debugging: Test chart semantics, geometry, interaction, updates, accessibility, and performance without relying on brittle screenshots alone. +- docs/guides/ai-authoring.md: AI Authoring: Give coding agents a deterministic route from a visualization goal to typed TanStack Charts code, validation, and advanced extension. +- docs/guides/migrating.md: Migrating: Move an existing visualization to TanStack Charts by preserving analytical semantics, interaction behavior, and measurable output before changing design. +- docs/examples/index.md: Example Gallery: Choose a TanStack Charts example from the question your reader needs to answer, then follow the linked concepts and reference pages. +- docs/examples/lines-and-areas.md: Lines and Areas: Choose and compose line, range-area, rolling-statistic, and annotation patterns for ordered data. +- docs/examples/bars-and-rankings.md: Bars and Rankings: Choose bar, lollipop, dumbbell, and waterfall compositions for categorical comparison and change. +- docs/examples/scatterplots-and-relationships.md: Scatterplots and Relationships: Choose scatterplot, regression, connected-path, lag, and nearest-point patterns for quantitative relationships. +- docs/examples/distributions.md: Distributions: Choose histograms, boxplots, empirical cumulative distributions, and violins for shape, spread, and rank. +- docs/examples/heatmaps-and-densities.md: Heatmaps and Densities: Choose quantitative matrix cells, contours, or hexagonal bins to show concentration across two dimensions. +- docs/examples/intervals-and-financial.md: Intervals and Financial Charts: Compose timelines, uncertainty intervals, candlesticks, and percentile ribbons from explicit endpoints. +- docs/examples/stacked-and-composition.md: Stacked and Composed Charts: Choose stacked, normalized, streamgraph, funnel, and mosaic compositions for part-to-whole and stage-retention comparisons. +- docs/examples/facets-and-multiple-views.md: Facets and Multiple Views: Choose shared-scale facets, marginal views, and focus-plus-context layouts for coordinated comparisons. +- docs/examples/networks-and-hierarchies.md: Networks and Hierarchies: Choose tidy trees, Sankey flows, spatial adjacency graphs, and force-directed networks for connected or nested data. +- docs/examples/maps-and-spatial.md: Maps and Spatial Charts: Build choropleths, bubble maps, globes, routes, and vector fields for geographic or projected spatial questions. +- docs/examples/polar-and-radar.md: Polar and Radar Charts: Build native pie, donut, gauge, radar, line, scatter, radial bar, rose, and sunburst charts through the opt-in polar coordinate entry. +- docs/examples/annotations-and-overlays.md: Annotations and Overlays: Add direct labels, thresholds, change indicators, and explanatory layers without hiding chart semantics. +- docs/examples/interactive-charts.md: Interactive Charts: Choose chart-owned focus or controlled application interactions for pinned detail, scrolling, zooming, and editing. +- docs/examples/themes-and-motion.md: Themes and Motion: Apply inherited palettes and optional motion while keeping chart definitions separate from card layout and controls. +- docs/reference/index.md: API Reference: Reference for TanStack Charts definitions, marks, scales, runtime, rendering, interaction, and framework adapters. +- docs/reference/chart-definitions.md: Chart Definition API: Reference static and responsive chart definitions, build context, and identity-based updates. +- docs/reference/chart-spec.md: Chart Spec: Reference for marks, axes, color, gradients, clipping, margins, guides, and themes in a TanStack Charts spec. +- docs/reference/view-composition.md: View Composition: Compose complete chart definitions with deterministic grid, layer, fill, and inset layouts. +- docs/reference/transforms.md: Data Transforms: Typed, eager transforms for reusable application data. +- docs/reference/scales-guides-and-color.md: Scales, Guides, and Color: Reference for injected positional scales, automatic axes and margins, color scales, legends, themes, and gradients. +- docs/reference/dom-host.md: DOM Host: Mount, update, size, interact with, and destroy a TanStack Chart in a browser DOM container. +- docs/reference/adapter-controller.md: Adapter Controller: Build framework adapters over shared prerender, mount, update, layout, scene access, and cleanup behavior. +- docs/reference/runtime-and-scene.md: Runtime and Scene: Compile object and responsive chart definitions into renderer-neutral scenes. +- docs/reference/focus-and-interaction.md: Focus and Interaction: Configure pointer focus, grouped focus, crosshairs, controlled cursors, keyboard navigation, built-in tooltips, selection, and spatial indexes. +- docs/reference/rendering-and-export.md: Rendering and Export: Render chart scenes through the default SVG renderer, the optional Canvas renderer, or a custom surface, then export supported output. +- docs/reference/motion.md: Motion: Optional SVG tween and spring motion, definition-local timing, interruption behavior, reduced motion, and the standalone spring sampler. +- docs/reference/custom-extensions.md: Custom Extensions: Extend TanStack Charts with custom marks, distinct scale values, curves, scales, color, legends, text measurement, spatial indexes, and renderers. +- docs/reference/types.md: Types: Public TypeScript types, inference rules, channels, definitions, scenes, hosts, focus, rendering, and custom extension contracts. +- docs/framework/react/reference/chart.md: React Chart: Complete prop and type reference for the @tanstack/charts/react Chart component. +- docs/framework/preact/reference/chart.md: Preact Chart: Complete prop and type reference for the @tanstack/charts/preact Chart component. +- docs/framework/vue/reference/chart.md: Vue Chart: Complete prop and type reference for the @tanstack/charts/vue Chart component. +- docs/framework/solid/reference/chart.md: Solid Chart: Complete prop and type reference for the @tanstack/charts/solid Chart component. +- docs/framework/svelte/reference/chart.md: Svelte Chart: Complete prop and type reference for the @tanstack/charts/svelte Chart component. +- docs/framework/angular/reference/chart.md: Angular Chart: Complete option and type reference for the @tanstack/charts/angular standalone component. +- docs/framework/lit/reference/chart.md: Lit Chart: Complete option and type reference for the @tanstack/charts/lit custom element. +- docs/framework/alpine/reference/chart.md: Alpine Chart: Complete option and type reference for the @tanstack/charts/alpine x-chart directive. +- docs/framework/octane/reference/chart.md: Octane Chart: Complete prop and type reference for the @tanstack/charts/octane Chart component. +- docs/reference/marks/line-and-area.md: Line and Area Marks: Reference for lineY, lineX, areaY, areaX, grouping, null gaps, interval baselines, points, and optional curve bridges. +- docs/reference/marks/difference.md: Difference Marks: Reference for differenceY and differenceX comparison areas, exact crossings, grouping, gaps, lineage, styling, and interaction. +- docs/reference/marks/regression.md: Linear Regression Marks: Reference for eager linearRegressionRowsY/X data and linearRegressionY/X least-squares marks, confidence bands, grouping, sampling, lineage, and interaction. +- docs/reference/marks/bar-and-rect.md: Bar and Rect Marks: Reference for barY, barX, rect, cell, interval endpoints, band sizing, grouped bars, color, insets, and focus anchors. +- docs/reference/marks/box.md: Box Marks: Reference for boxRows, boxY, and boxX Tukey summaries, quartiles, whiskers, outliers, lineage, styling, and interaction. +- docs/reference/marks/dot-and-hexagon.md: Dot and Hexagon Marks: Reference for dot and hexagon channels, pixel radius, injected radius scales, color, grouping, keys, and interaction points. +- docs/reference/marks/dodge.md: Dodge Layouts: Reference for responsive dodgeX and dodgeY dot collision layouts, anchors, padding, radii, identity, and scale ownership. +- docs/reference/marks/waffle.md: Waffle Marks: Reference for responsive waffleY and waffleX unit layouts, cumulative rounding, color, source identity, and interaction. +- docs/reference/marks/ridgeline.md: Ridgeline Marks: Reference for semantic ridgelineY and ridgelineX profile geometry, categorical baselines, overlap, curves, identity, and interaction. +- docs/reference/marks/violin.md: Violin Marks: Reference for semantic violinY and violinX mirrored profile geometry, category-relative spans, curves, identity, and interaction. +- docs/reference/marks/focus-guide.md: Focus Guide Marks: Render datum-bound focus rules, markers, and axis labels with stable renderer-neutral motion. +- docs/reference/marks/treemap.md: Treemap Mark: Reference for responsive hierarchy tiling, flat-row construction, node lineage, color, labels, and the optional treemap import. +- docs/reference/marks/sunburst.md: Sunburst Mark: Reference for flat hierarchy input, responsive radial partitioning, node lineage, branch color, and the optional sunburst import. +- docs/reference/marks/sankey.md: Sankey Diagram Mark: Reference for responsive flow layout, semantic graph input, immutable node and link rows, ordinary child marks, and the optional Sankey import. +- docs/reference/marks/hexbin.md: Hexbin Mark: Reference for responsive screen-space hexagonal aggregation, reducer outputs, source lineage, color, and the optional spatial import. +- docs/reference/marks/contour.md: Contour Mark: Reference for regular scalar-grid contours, thresholds, orientation, source lineage, and structured multipolygon output. +- docs/reference/marks/density.md: Density Contour Mark: Reference for responsive point-density contours, thresholds, grouping, weights, source lineage, and structured multipolygon output. +- docs/reference/marks/delaunay.md: Delaunay Link Mark: Reference for responsive Delaunay adjacency links, source grouping, endpoint lineage, stable keys, and the optional spatial import. +- docs/reference/marks/voronoi.md: Voronoi Mark: Reference for responsive final-screen Voronoi cells, grouping, stable coincident ownership, presentation, and interaction separation. +- docs/reference/marks/rules-links-arrows-vectors-and-ticks.md: Rules, Links, Arrows, Vectors, and Ticks: Reference for ruleX, ruleY, link, arrow, vector, tickX, and tickY geometry, channels, defaults, curves, and focus anchors. +- docs/reference/marks/text-frame-and-facet.md: Text, Frame, and Facet Marks: Reference for data labels, chart frames, responsive small multiples, shared or per-cell axes, labels, and facet constraints. +- docs/reference/marks/geo.md: Geo Shape Mark: Reference for projecting GeoJSON with an opt-in D3-backed geoShape mark. +- docs/reference/marks/polar.md: Polar Marks: Reference for the opt-in polar container, radial marks, guides, injected scale factories, and D3-backed geometry. Authoring rules: @@ -112,8 +113,8 @@ Authoring rules: - Start with exact @tanstack/charts/scales/linear, @tanstack/charts/scales/band, @tanstack/charts/scales/point, or @tanstack/charts/scales/ordinal entries. Upgrade only the mapping that needs temporal, nonlinear, radial, interpolated, statistical, or other complete D3 semantics; never import the d3 umbrella. - Treat Date values on compact band or point scales as equally spaced categories. Use d3-scale scaleTime or scaleUtc when elapsed-time spacing or calendar-aware ticks matter. - Let TanStack Charts own responsive pixel ranges. Scale factories infer domains from mark channels; configured instances preserve application-owned domains. -- Put Cartesian scale and guide options under scales.x and scales.y; never generate deprecated root x or y properties. -- Put polar position scales under scales.angle and scales.radius inside polar(); never generate deprecated direct angle or radius properties. +- Put Cartesian scale and guide options under scales.x and scales.y; root x and y properties are invalid. +- Put polar position scales under scales.angle and scales.radius inside polar(); direct angle and radius properties are invalid. - Keep data in its application shape. Map fields or accessors into marks instead of creating a library-owned series model. - Memoize the complete definition against captured application values; definition identity is the application update boundary. - Preserve inferable datum identity across updates; add explicit keys only when IDs or unique positions are unavailable. diff --git a/packages/charts-core/README.md b/packages/charts-core/README.md index a4d5cb14..f474fd3b 100644 --- a/packages/charts-core/README.md +++ b/packages/charts-core/README.md @@ -70,12 +70,14 @@ const downloads = defineChart({ z: 'package', }), ], - x: { scale: () => scalePoint().padding(0.4) }, - y: { - scale: scaleLinear, - nice: true, - grid: true, - axis: { label: 'Monthly downloads' }, + scales: { + x: { scale: () => scalePoint().padding(0.4) }, + y: { + scale: scaleLinear, + nice: true, + grid: true, + axis: { label: 'Monthly downloads' }, + }, }, color: { scale: () => @@ -113,8 +115,10 @@ const history = [ const historicalDownloads = defineChart({ marks: [lineY(history, { x: 'date', y: 'downloads' })], - x: { scale: scaleUtc, nice: true }, - y: { scale: scaleLinear, nice: true }, + scales: { + x: { scale: scaleUtc, nice: true }, + y: { scale: scaleLinear, nice: true }, + }, }) ``` @@ -243,13 +247,15 @@ const rankingRows = [ const chart = defineChart({ marks: [barX(rankingRows, { x: 'downloads', y: 'package' })], - x: { - scale: scaleLinear, - nice: true, - axis: { label: 'Weekly downloads' }, - }, - y: { - scale: () => scaleBand().padding(0.1), + scales: { + x: { + scale: scaleLinear, + nice: true, + axis: { label: 'Weekly downloads' }, + }, + y: { + scale: () => scaleBand().padding(0.1), + }, }, }) ``` @@ -284,8 +290,10 @@ import { scaleLinear } from '@tanstack/charts/scales/linear' const values = [32, 48, 41, 57] const definition = defineChart({ marks: [lineY(values)], - x: { scale: scaleLinear().domain([0, values.length - 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain([0, values.length - 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, }) const scene = createChartScene(definition, { width: 640, height: 320 }) diff --git a/packages/charts-core/docs/config.json b/packages/charts-core/docs/config.json index 48e92d3f..34c4418a 100644 --- a/packages/charts-core/docs/config.json +++ b/packages/charts-core/docs/config.json @@ -21,6 +21,10 @@ "label": "Installation", "to": "installation" }, + { + "label": "Alpha Stability", + "to": "stability" + }, { "label": "Quick Start", "to": "quick-start" diff --git a/packages/charts-core/docs/guides/ai-authoring.md b/packages/charts-core/docs/guides/ai-authoring.md index b25f3110..23a67dc6 100644 --- a/packages/charts-core/docs/guides/ai-authoring.md +++ b/packages/charts-core/docs/guides/ai-authoring.md @@ -22,11 +22,11 @@ path is explicit and repeatable. Start at [Choosing a Chart](./choosing-a-chart.md), then use the relevant [example family](../examples/index.md). -Put Cartesian scale and axis options under `scales.x` and `scales.y`. Never -generate the deprecated root `x` or `y` form. Inside `polar()`, put position -scales under `scales.angle` and `scales.radius`. Use named registry entries and -mark-level `xScale`, `yScale`, `angleScale`, or `radiusScale` selectors when a -chart needs more than one mapping on the same channel. +Put Cartesian scale and axis options under `scales.x` and `scales.y`. The +pre-Alpha root `x` and `y` form is not part of the Alpha API. Inside `polar()`, +put position scales under `scales.angle` and `scales.radius`. Use named registry +entries and mark-level `xScale`, `yScale`, `angleScale`, or `radiusScale` +selectors when a chart needs more than one mapping on the same channel. Keep the default SVG renderer unless a measured paint-heavy mark benefits from Canvas. To mix surfaces, import `canvasChartRenderer` from the exact diff --git a/packages/charts-core/docs/guides/migrating.md b/packages/charts-core/docs/guides/migrating.md index 4b2f535b..c53020f4 100644 --- a/packages/charts-core/docs/guides/migrating.md +++ b/packages/charts-core/docs/guides/migrating.md @@ -7,9 +7,9 @@ Migration is a semantic exercise, not a component-name translation. First describe what the existing chart means and how users operate it. Then express that behavior with data preparation, scales, marks, and host options. -## Move root scales into the registry +## Move pre-Alpha root scales into the registry -Current definitions put Cartesian scale and axis options under `scales`: +Alpha definitions require Cartesian scale and axis options under `scales`: ```ts const definition = defineChart({ @@ -21,37 +21,20 @@ const definition = defineChart({ }) ``` -The earlier root properties still work temporarily: +When updating from a pre-Alpha release, move root `x` to `scales.x` and root +`y` to `scales.y`. Every definition must provide both reserved entries. Use +`null` when the chart does not use one of the Cartesian dimensions. -```ts -const legacyDefinition = defineChart({ - marks, - x: { scale: xScale }, - y: { scale: yScale, grid: true }, -}) -``` - -Development builds warn with the exact replacement. Move `x` to `scales.x` -and `y` to `scales.y`. Root `x` and `y` will be removed when TanStack Charts -enters Alpha. New code and generated code should only use the `scales` form. +Polar definitions follow the same migration. Move pre-Alpha root `angle` and +`radius` options into `polar({ scales: { angle, radius } })`. Every polar +definition must provide both reserved entries. If neither positional scale is +used, write `scales: { angle: null, radius: null }`. -Polar definitions follow the same migration. Move `polar({ angle, radius })` -to `polar({ scales: { angle, radius } })`. If neither positional scale is -used, write `scales: { angle: null, radius: null }`. The old polar properties -also work temporarily, warn in development, and will be removed when TanStack -Charts enters Alpha. - -During this compatibility window, `ChartSpec` is a union of the canonical and -legacy shapes. If application code extended the old interface, replace that -extension with an intersection: - -```ts -import type { ChartSpec } from '@tanstack/charts' - -type ApplicationChartSpec = ChartSpec & { - applicationTag: string -} -``` +Custom mark type code should replace `ChartMarkX` and `ChartMarkY` with +`ChartMarkPointX` and `ChartMarkPointY` from +`@tanstack/charts/mark/scale-values`. A polar length callback should replace +`layout.angle` with `layout.scales.angle` and `layout.radiusScale` with +`layout.scales.radius`. ## Inventory the current contract diff --git a/packages/charts-core/docs/installation.md b/packages/charts-core/docs/installation.md index abd54437..d2a0d1b3 100644 --- a/packages/charts-core/docs/installation.md +++ b/packages/charts-core/docs/installation.md @@ -3,11 +3,12 @@ title: Installation description: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. --- -These docs follow unreleased `main`. The latest published pre-alpha is TanStack -Charts `0.15.0`; use its +These docs follow unreleased `main`, the official Alpha line. The latest +published release is TanStack Charts `0.15.0`; use its [release-source docs](https://github.com/TanStack/charts/tree/v0.15.0/docs) -for the exact surface. Install TanStack Charts in each application that authors -chart definitions: +for the exact surface. Alpha releases use regular `0.x` versions and may break +APIs between minor releases. Install TanStack Charts in each application that +authors chart definitions: ```sh pnpm add @tanstack/charts diff --git a/packages/charts-core/docs/overview.md b/packages/charts-core/docs/overview.md index 582303ba..faba9a1a 100644 --- a/packages/charts-core/docs/overview.md +++ b/packages/charts-core/docs/overview.md @@ -3,8 +3,10 @@ title: Overview description: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. --- -These docs follow unreleased `main`. The latest published TanStack Charts -release is `0.15.0`; it is pre-alpha and its API may change between releases. +These docs follow unreleased `main`, the official Alpha line. The latest +published TanStack Charts release is `0.15.0`. Alpha uses regular `0.x` +versions, and APIs may change between minor releases. See +[Alpha stability](./stability.md) for the release contract. TanStack Charts is a small, framework-agnostic chart grammar for TypeScript and JavaScript. Give each mark its natural data, map fields or accessors to visual diff --git a/packages/charts-core/docs/reference/focus-and-interaction.md b/packages/charts-core/docs/reference/focus-and-interaction.md index 301d6cc7..a1f189aa 100644 --- a/packages/charts-core/docs/reference/focus-and-interaction.md +++ b/packages/charts-core/docs/reference/focus-and-interaction.md @@ -22,6 +22,7 @@ With no custom focus strategy: - `Home` and `End` move to the first and last point - `Enter` and Space toggle an enabled sticky tooltip and call `onSelect` for the focused point +- pressing the pointer outside the chart or tooltip dismisses a pinned tooltip - a configured selection controller receives the same focused point before `onSelect` - a click focuses and selects the nearest point, or selects `null` on the @@ -898,8 +899,9 @@ resize, and content resize, and collide against the viewport instead of the chart box. Clicking, Enter, or Space pins the tooltip. The next activation unpins it. -`Escape` unpins and clears focus. Set `sticky: false` to disable pinning. A -display-only tooltip has `role="status"` and `aria-live="polite"`. +Pressing the pointer outside the chart and tooltip, or pressing `Escape`, +unpins and clears focus. Set `sticky: false` to disable pinning. A display-only +tooltip has `role="status"` and `aria-live="polite"`. Set `visibility: 'pinned'` for click-or-keyboard detail that should not paint a transient shell. Focus and inline mark states still update before activation; diff --git a/packages/charts-core/docs/reference/types.md b/packages/charts-core/docs/reference/types.md index 4fd27c07..f219b23b 100644 --- a/packages/charts-core/docs/reference/types.md +++ b/packages/charts-core/docs/reference/types.md @@ -114,9 +114,6 @@ are available from the root entry point. The four `ChartMarkPoint*` and | `ChartSpecXValue` | Interaction x union across marks | | `ChartSpecYValue` | Interaction y union across marks | -`ChartMarkX` and `ChartMarkY` remain exported as deprecated aliases of the -point extractors. New code should use the explicit names. - Stateful mark presentation uses `ChartMarkStateContext` as one object bag for the datum, index, data, point, focus, pointer, and matching helper. A `ChartMarkStateSelector` handles the common declarative cases, while callbacks diff --git a/packages/charts-core/docs/stability.md b/packages/charts-core/docs/stability.md new file mode 100644 index 00000000..bb41c758 --- /dev/null +++ b/packages/charts-core/docs/stability.md @@ -0,0 +1,49 @@ +--- +title: Alpha Stability +description: Understand what TanStack Charts Alpha versions promise, what may change, and how releases communicate breaking changes. +--- + +TanStack Charts is in Alpha. Packages use regular `0.x` versions on the normal +`latest` npm tag, without an `-alpha` suffix or separate release channel. + +Alpha is ready for evaluation and early application integration. It is not a +stable API promise. Pin an exact version in production applications and test an +upgrade before changing that pin. + +## Version contract + +All public TanStack Charts packages move together as one fixed release group. + +- Patch releases fix defects and do not intentionally remove or rename public + APIs. A fix may correct rendering or interaction that was observably wrong. +- Minor releases may add features and may contain breaking API changes while + the package major remains `0`. +- The project will publish a stable-release compatibility policy before `1.0`. + +## Public surface + +The public surface is the package export map and the APIs documented on this +site. Source files, internal modules, unexported types, generated scene details, +and undocumented behavior may change without a migration path. + +Framework adapters share the same chart definition and release version. Their +runtime support and peer ranges are listed in +[Installation](./installation.md#framework-compatibility). + +## Breaking changes + +A breaking Alpha release must include a changeset, changelog entry, and concrete +migration instructions. We will use a development warning ahead of removal when +that warning is practical and useful, but Alpha does not promise a minimum +deprecation window. + +Production bundles do not retain development migration warnings. Removed APIs +fail through TypeScript or an actionable runtime error instead of silently +falling back to an older interpretation. + +## Report a regression + +[Open a GitHub issue](https://github.com/TanStack/charts/issues/new/choose) with +the exact package version, framework, browser or native runtime, a minimal +reproduction, and the expected and actual result. A regression in a patch +release is treated as a defect against this policy. diff --git a/packages/charts-core/llms.txt b/packages/charts-core/llms.txt index 10b9a3bf..07d1e4fc 100644 --- a/packages/charts-core/llms.txt +++ b/packages/charts-core/llms.txt @@ -4,107 +4,108 @@ TanStack Charts is a framework-agnostic, type-safe visualization grammar with th Read the canonical pages below. Each concept is documented once; guides and examples link back to its owner page. -- docs/overview.md — Overview: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. -- docs/comparison.md — Compare Libraries: Compare TanStack Charts with measured and documentation-reviewed charting libraries. -- docs/installation.md — Installation: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. -- docs/quick-start.md — Quick Start: Build, mount, update, and clean up a responsive TanStack Charts line chart with fully inferred types. -- docs/framework/react/quick-start.md — React Quick Start: Install the React adapter, define a typed chart, render responsive SVG, and add native interaction. -- docs/framework/react/adapter.md — React Adapter: Understand the thin React lifecycle, SSR, hydration, update, sizing, class, and style behavior around the shared chart host. -- docs/framework/preact/adapter.md — Preact Adapter: Render and hydrate TanStack Charts with Preact. -- docs/framework/vue/adapter.md — Vue Adapter: Render and hydrate TanStack Charts with Vue 3. -- docs/framework/solid/adapter.md — Solid Adapter: Render and hydrate TanStack Charts with Solid. -- docs/framework/svelte/adapter.md — Svelte Adapter: Render and hydrate TanStack Charts with Svelte 5. -- docs/framework/angular/adapter.md — Angular Adapter: Render TanStack Charts with an Angular standalone component. -- docs/framework/lit/adapter.md — Lit Adapter: Render TanStack Charts with a light-DOM Lit custom element. -- docs/framework/alpine/adapter.md — Alpine Adapter: Mount TanStack Charts with an Alpine directive. -- docs/framework/octane/quick-start.md — Octane Quick Start: Install the Octane adapter, define a typed chart, render responsive SSR-safe SVG, and add native interaction. -- docs/framework/octane/adapter.md — Octane Adapter: Understand the native TSRX lifecycle, conditional SSR target, hydration, sizing, class, and style behavior around the shared chart host. -- docs/concepts/grammar-of-graphics.md — Grammar of Graphics: Understand TanStack Charts as a composition of data, marks, channels, scales, guides, and layers. -- docs/concepts/chart-definitions.md — Chart Definitions: Capture application values in memoized definitions and use responsive builders for surface-dependent choices. -- docs/concepts/data-and-channels.md — Data and Channels: Map typed application data to positions, grouping, color, radius, and stable chart identity. -- docs/concepts/scales-and-d3.md — Scales: Start with compact scales, upgrade individual mappings to D3 when their semantics require it, and keep responsive range ownership clear. -- docs/concepts/marks-and-layering.md — Marks and Layering: Choose built-in geometric marks, compose them in render order, and preserve stable layer identity. -- docs/concepts/layout-axes-and-coordinates.md — Layout, Axes, and Coordinates: Learn how responsive ranges, automatic margins, guides, band alignment, and scene coordinates work. -- docs/guides/choosing-a-chart.md — Choosing a Chart: Choose a visual encoding from the question, data shape, and comparison task before composing TanStack Charts marks. -- docs/guides/responsive-charts.md — Responsive Charts: Size charts from their containers while preserving readable guides, deterministic SSR, and application-controlled height. -- docs/guides/themes-and-styling.md — Themes and Styling: Apply automatic light and dark color behavior, CSS palette tokens, mark styles, and renderer-aware resources. -- docs/guides/accessibility.md — Accessibility: Give charts useful names, keyboard-equivalent interaction, reduced motion, redundant encodings, and exact-value alternatives. -- docs/guides/dynamic-data-and-animation.md — Dynamic Data and Animation: Recreate memoized definitions when application values change and animate keyed geometry safely. -- docs/guides/transforms-and-reactivity.md — Transforms and Reactivity: Derive typed rows once and memoize them where the data is owned. -- docs/guides/tooltips-and-focus.md — Tooltips and Focus: Configure grouped focus, crosshairs, automatic content, ordering, placement, portaling, pinning, and framework-composed tooltip bodies. -- docs/guides/interactions-and-selections.md — Interactions and Selections: Build controlled cursors, linked selections, brushes, zooming, scrolling, timelines, and editors around semantic chart state. -- docs/guides/faceting-and-composition.md — Faceting and Composition: Layer marks, repeat a shared chart by group, and coordinate multiple views without introducing a fixed chart-type model. -- docs/guides/legends-and-color.md — Legends and Color: Map categorical or quantitative values to color, render responsive legends, and preserve meaning across themes. -- docs/guides/custom-marks-and-renderers.md — Custom Marks and Renderers: Extend the grammar with typed mark channels and keyed scene nodes, or replace the mounted renderer without bypassing chart semantics. -- docs/guides/large-data.md — Large Data: Choose bounded visual representations, measure preparation separately, preserve analytical invariants, and avoid rendering more detail than pixels can communicate. -- docs/guides/ssr-and-hydration.md — SSR and Hydration: Render deterministic chart markup on the server, preserve runtime work through hydration, and handle responsive dimensions without mismatches. -- docs/guides/exporting.md — Exporting: Export accessible chart SVG or browser-rendered raster images while preserving dimensions, styling, and resource identity. -- docs/guides/bundle-size-and-performance.md — Bundle Size and Performance: Keep each chart proportional to the marks, scales, host capabilities, and data representation it actually uses. -- docs/guides/typescript.md — TypeScript: Preserve end-to-end inference from chart data and channels through scales, focus callbacks, adapters, and custom extensions. -- docs/guides/testing-and-debugging.md — Testing and Debugging: Test chart semantics, geometry, interaction, updates, accessibility, and performance without relying on brittle screenshots alone. -- docs/guides/ai-authoring.md — AI Authoring: Give coding agents a deterministic route from a visualization goal to typed TanStack Charts code, validation, and advanced extension. -- docs/guides/migrating.md — Migrating: Move an existing visualization to TanStack Charts by preserving analytical semantics, interaction behavior, and measurable output before changing design. -- docs/examples/index.md — Example Gallery: Choose a TanStack Charts example from the question your reader needs to answer, then follow the linked concepts and reference pages. -- docs/examples/lines-and-areas.md — Lines and Areas: Choose and compose line, range-area, rolling-statistic, and annotation patterns for ordered data. -- docs/examples/bars-and-rankings.md — Bars and Rankings: Choose bar, lollipop, dumbbell, and waterfall compositions for categorical comparison and change. -- docs/examples/scatterplots-and-relationships.md — Scatterplots and Relationships: Choose scatterplot, regression, connected-path, lag, and nearest-point patterns for quantitative relationships. -- docs/examples/distributions.md — Distributions: Choose histograms, boxplots, empirical cumulative distributions, and violins for shape, spread, and rank. -- docs/examples/heatmaps-and-densities.md — Heatmaps and Densities: Choose quantitative matrix cells, contours, or hexagonal bins to show concentration across two dimensions. -- docs/examples/intervals-and-financial.md — Intervals and Financial Charts: Compose timelines, uncertainty intervals, candlesticks, and percentile ribbons from explicit endpoints. -- docs/examples/stacked-and-composition.md — Stacked and Composed Charts: Choose stacked, normalized, streamgraph, funnel, and mosaic compositions for part-to-whole and stage-retention comparisons. -- docs/examples/facets-and-multiple-views.md — Facets and Multiple Views: Choose shared-scale facets, marginal views, and focus-plus-context layouts for coordinated comparisons. -- docs/examples/networks-and-hierarchies.md — Networks and Hierarchies: Choose tidy trees, Sankey flows, spatial adjacency graphs, and force-directed networks for connected or nested data. -- docs/examples/maps-and-spatial.md — Maps and Spatial Charts: Build choropleths, bubble maps, globes, routes, and vector fields for geographic or projected spatial questions. -- docs/examples/polar-and-radar.md — Polar and Radar Charts: Build native pie, donut, gauge, radar, line, scatter, radial bar, rose, and sunburst charts through the opt-in polar coordinate entry. -- docs/examples/annotations-and-overlays.md — Annotations and Overlays: Add direct labels, thresholds, change indicators, and explanatory layers without hiding chart semantics. -- docs/examples/interactive-charts.md — Interactive Charts: Choose chart-owned focus or controlled application interactions for pinned detail, scrolling, zooming, and editing. -- docs/examples/themes-and-motion.md — Themes and Motion: Apply inherited palettes and optional motion while keeping chart definitions separate from card layout and controls. -- docs/reference/index.md — API Reference: Reference for TanStack Charts definitions, marks, scales, runtime, rendering, interaction, and framework adapters. -- docs/reference/chart-definitions.md — Chart Definition API: Reference static and responsive chart definitions, build context, and identity-based updates. -- docs/reference/chart-spec.md — Chart Spec: Reference for marks, axes, color, gradients, clipping, margins, guides, and themes in a TanStack Charts spec. -- docs/reference/view-composition.md — View Composition: Compose complete chart definitions with deterministic grid, layer, fill, and inset layouts. -- docs/reference/transforms.md — Data Transforms: Typed, eager transforms for reusable application data. -- docs/reference/scales-guides-and-color.md — Scales, Guides, and Color: Reference for injected positional scales, automatic axes and margins, color scales, legends, themes, and gradients. -- docs/reference/dom-host.md — DOM Host: Mount, update, size, interact with, and destroy a TanStack Chart in a browser DOM container. -- docs/reference/adapter-controller.md — Adapter Controller: Build framework adapters over shared prerender, mount, update, layout, scene access, and cleanup behavior. -- docs/reference/runtime-and-scene.md — Runtime and Scene: Compile object and responsive chart definitions into renderer-neutral scenes. -- docs/reference/focus-and-interaction.md — Focus and Interaction: Configure pointer focus, grouped focus, crosshairs, controlled cursors, keyboard navigation, built-in tooltips, selection, and spatial indexes. -- docs/reference/rendering-and-export.md — Rendering and Export: Render chart scenes through the default SVG renderer, the optional Canvas renderer, or a custom surface, then export supported output. -- docs/reference/motion.md — Motion: Optional SVG tween and spring motion, definition-local timing, interruption behavior, reduced motion, and the standalone spring sampler. -- docs/reference/custom-extensions.md — Custom Extensions: Extend TanStack Charts with custom marks, distinct scale values, curves, scales, color, legends, text measurement, spatial indexes, and renderers. -- docs/reference/types.md — Types: Public TypeScript types, inference rules, channels, definitions, scenes, hosts, focus, rendering, and custom extension contracts. -- docs/framework/react/reference/chart.md — React Chart: Complete prop and type reference for the @tanstack/charts/react Chart component. -- docs/framework/preact/reference/chart.md — Preact Chart: Complete prop and type reference for the @tanstack/charts/preact Chart component. -- docs/framework/vue/reference/chart.md — Vue Chart: Complete prop and type reference for the @tanstack/charts/vue Chart component. -- docs/framework/solid/reference/chart.md — Solid Chart: Complete prop and type reference for the @tanstack/charts/solid Chart component. -- docs/framework/svelte/reference/chart.md — Svelte Chart: Complete prop and type reference for the @tanstack/charts/svelte Chart component. -- docs/framework/angular/reference/chart.md — Angular Chart: Complete option and type reference for the @tanstack/charts/angular standalone component. -- docs/framework/lit/reference/chart.md — Lit Chart: Complete option and type reference for the @tanstack/charts/lit custom element. -- docs/framework/alpine/reference/chart.md — Alpine Chart: Complete option and type reference for the @tanstack/charts/alpine x-chart directive. -- docs/framework/octane/reference/chart.md — Octane Chart: Complete prop and type reference for the @tanstack/charts/octane Chart component. -- docs/reference/marks/line-and-area.md — Line and Area Marks: Reference for lineY, lineX, areaY, areaX, grouping, null gaps, interval baselines, points, and optional curve bridges. -- docs/reference/marks/difference.md — Difference Marks: Reference for differenceY and differenceX comparison areas, exact crossings, grouping, gaps, lineage, styling, and interaction. -- docs/reference/marks/regression.md — Linear Regression Marks: Reference for eager linearRegressionRowsY/X data and linearRegressionY/X least-squares marks, confidence bands, grouping, sampling, lineage, and interaction. -- docs/reference/marks/bar-and-rect.md — Bar and Rect Marks: Reference for barY, barX, rect, cell, interval endpoints, band sizing, grouped bars, color, insets, and focus anchors. -- docs/reference/marks/box.md — Box Marks: Reference for boxRows, boxY, and boxX Tukey summaries, quartiles, whiskers, outliers, lineage, styling, and interaction. -- docs/reference/marks/dot-and-hexagon.md — Dot and Hexagon Marks: Reference for dot and hexagon channels, pixel radius, injected radius scales, color, grouping, keys, and interaction points. -- docs/reference/marks/dodge.md — Dodge Layouts: Reference for responsive dodgeX and dodgeY dot collision layouts, anchors, padding, radii, identity, and scale ownership. -- docs/reference/marks/waffle.md — Waffle Marks: Reference for responsive waffleY and waffleX unit layouts, cumulative rounding, color, source identity, and interaction. -- docs/reference/marks/ridgeline.md — Ridgeline Marks: Reference for semantic ridgelineY and ridgelineX profile geometry, categorical baselines, overlap, curves, identity, and interaction. -- docs/reference/marks/violin.md — Violin Marks: Reference for semantic violinY and violinX mirrored profile geometry, category-relative spans, curves, identity, and interaction. -- docs/reference/marks/focus-guide.md — Focus Guide Marks: Render datum-bound focus rules, markers, and axis labels with stable renderer-neutral motion. -- docs/reference/marks/treemap.md — Treemap Mark: Reference for responsive hierarchy tiling, flat-row construction, node lineage, color, labels, and the optional treemap import. -- docs/reference/marks/sunburst.md — Sunburst Mark: Reference for flat hierarchy input, responsive radial partitioning, node lineage, branch color, and the optional sunburst import. -- docs/reference/marks/sankey.md — Sankey Diagram Mark: Reference for responsive flow layout, semantic graph input, immutable node and link rows, ordinary child marks, and the optional Sankey import. -- docs/reference/marks/hexbin.md — Hexbin Mark: Reference for responsive screen-space hexagonal aggregation, reducer outputs, source lineage, color, and the optional spatial import. -- docs/reference/marks/contour.md — Contour Mark: Reference for regular scalar-grid contours, thresholds, orientation, source lineage, and structured multipolygon output. -- docs/reference/marks/density.md — Density Contour Mark: Reference for responsive point-density contours, thresholds, grouping, weights, source lineage, and structured multipolygon output. -- docs/reference/marks/delaunay.md — Delaunay Link Mark: Reference for responsive Delaunay adjacency links, source grouping, endpoint lineage, stable keys, and the optional spatial import. -- docs/reference/marks/voronoi.md — Voronoi Mark: Reference for responsive final-screen Voronoi cells, grouping, stable coincident ownership, presentation, and interaction separation. -- docs/reference/marks/rules-links-arrows-vectors-and-ticks.md — Rules, Links, Arrows, Vectors, and Ticks: Reference for ruleX, ruleY, link, arrow, vector, tickX, and tickY geometry, channels, defaults, curves, and focus anchors. -- docs/reference/marks/text-frame-and-facet.md — Text, Frame, and Facet Marks: Reference for data labels, chart frames, responsive small multiples, shared or per-cell axes, labels, and facet constraints. -- docs/reference/marks/geo.md — Geo Shape Mark: Reference for projecting GeoJSON with an opt-in D3-backed geoShape mark. -- docs/reference/marks/polar.md — Polar Marks: Reference for the opt-in polar container, radial marks, guides, injected scale factories, and D3-backed geometry. +- docs/overview.md: Overview: Learn what TanStack Charts provides, how its grammar works, and where charting responsibilities belong. +- docs/comparison.md: Compare Libraries: Compare TanStack Charts with measured and documentation-reviewed charting libraries. +- docs/installation.md: Installation: Install TanStack Charts with compact scales, framework adapters, and optional capabilities behind exact subpaths. +- docs/stability.md: Alpha Stability: Understand what TanStack Charts Alpha versions promise, what may change, and how releases communicate breaking changes. +- docs/quick-start.md: Quick Start: Build, mount, update, and clean up a responsive TanStack Charts line chart with fully inferred types. +- docs/framework/react/quick-start.md: React Quick Start: Install the React adapter, define a typed chart, render responsive SVG, and add native interaction. +- docs/framework/react/adapter.md: React Adapter: Understand the thin React lifecycle, SSR, hydration, update, sizing, class, and style behavior around the shared chart host. +- docs/framework/preact/adapter.md: Preact Adapter: Render and hydrate TanStack Charts with Preact. +- docs/framework/vue/adapter.md: Vue Adapter: Render and hydrate TanStack Charts with Vue 3. +- docs/framework/solid/adapter.md: Solid Adapter: Render and hydrate TanStack Charts with Solid. +- docs/framework/svelte/adapter.md: Svelte Adapter: Render and hydrate TanStack Charts with Svelte 5. +- docs/framework/angular/adapter.md: Angular Adapter: Render TanStack Charts with an Angular standalone component. +- docs/framework/lit/adapter.md: Lit Adapter: Render TanStack Charts with a light-DOM Lit custom element. +- docs/framework/alpine/adapter.md: Alpine Adapter: Mount TanStack Charts with an Alpine directive. +- docs/framework/octane/quick-start.md: Octane Quick Start: Install the Octane adapter, define a typed chart, render responsive SSR-safe SVG, and add native interaction. +- docs/framework/octane/adapter.md: Octane Adapter: Understand the native TSRX lifecycle, conditional SSR target, hydration, sizing, class, and style behavior around the shared chart host. +- docs/concepts/grammar-of-graphics.md: Grammar of Graphics: Understand TanStack Charts as a composition of data, marks, channels, scales, guides, and layers. +- docs/concepts/chart-definitions.md: Chart Definitions: Capture application values in memoized definitions and use responsive builders for surface-dependent choices. +- docs/concepts/data-and-channels.md: Data and Channels: Map typed application data to positions, grouping, color, radius, and stable chart identity. +- docs/concepts/scales-and-d3.md: Scales: Start with compact scales, upgrade individual mappings to D3 when their semantics require it, and keep responsive range ownership clear. +- docs/concepts/marks-and-layering.md: Marks and Layering: Choose built-in geometric marks, compose them in render order, and preserve stable layer identity. +- docs/concepts/layout-axes-and-coordinates.md: Layout, Axes, and Coordinates: Learn how responsive ranges, automatic margins, guides, band alignment, and scene coordinates work. +- docs/guides/choosing-a-chart.md: Choosing a Chart: Choose a visual encoding from the question, data shape, and comparison task before composing TanStack Charts marks. +- docs/guides/responsive-charts.md: Responsive Charts: Size charts from their containers while preserving readable guides, deterministic SSR, and application-controlled height. +- docs/guides/themes-and-styling.md: Themes and Styling: Apply automatic light and dark color behavior, CSS palette tokens, mark styles, and renderer-aware resources. +- docs/guides/accessibility.md: Accessibility: Give charts useful names, keyboard-equivalent interaction, reduced motion, redundant encodings, and exact-value alternatives. +- docs/guides/dynamic-data-and-animation.md: Dynamic Data and Animation: Recreate memoized definitions when application values change and animate keyed geometry safely. +- docs/guides/transforms-and-reactivity.md: Transforms and Reactivity: Derive typed rows once and memoize them where the data is owned. +- docs/guides/tooltips-and-focus.md: Tooltips and Focus: Configure grouped focus, crosshairs, automatic content, ordering, placement, portaling, pinning, and framework-composed tooltip bodies. +- docs/guides/interactions-and-selections.md: Interactions and Selections: Build controlled cursors, linked selections, brushes, zooming, scrolling, timelines, and editors around semantic chart state. +- docs/guides/faceting-and-composition.md: Faceting and Composition: Layer marks, repeat a shared chart by group, and coordinate multiple views without introducing a fixed chart-type model. +- docs/guides/legends-and-color.md: Legends and Color: Map categorical or quantitative values to color, render responsive legends, and preserve meaning across themes. +- docs/guides/custom-marks-and-renderers.md: Custom Marks and Renderers: Extend the grammar with typed mark channels and keyed scene nodes, or replace the mounted renderer without bypassing chart semantics. +- docs/guides/large-data.md: Large Data: Choose bounded visual representations, measure preparation separately, preserve analytical invariants, and avoid rendering more detail than pixels can communicate. +- docs/guides/ssr-and-hydration.md: SSR and Hydration: Render deterministic chart markup on the server, preserve runtime work through hydration, and handle responsive dimensions without mismatches. +- docs/guides/exporting.md: Exporting: Export accessible chart SVG or browser-rendered raster images while preserving dimensions, styling, and resource identity. +- docs/guides/bundle-size-and-performance.md: Bundle Size and Performance: Keep each chart proportional to the marks, scales, host capabilities, and data representation it actually uses. +- docs/guides/typescript.md: TypeScript: Preserve end-to-end inference from chart data and channels through scales, focus callbacks, adapters, and custom extensions. +- docs/guides/testing-and-debugging.md: Testing and Debugging: Test chart semantics, geometry, interaction, updates, accessibility, and performance without relying on brittle screenshots alone. +- docs/guides/ai-authoring.md: AI Authoring: Give coding agents a deterministic route from a visualization goal to typed TanStack Charts code, validation, and advanced extension. +- docs/guides/migrating.md: Migrating: Move an existing visualization to TanStack Charts by preserving analytical semantics, interaction behavior, and measurable output before changing design. +- docs/examples/index.md: Example Gallery: Choose a TanStack Charts example from the question your reader needs to answer, then follow the linked concepts and reference pages. +- docs/examples/lines-and-areas.md: Lines and Areas: Choose and compose line, range-area, rolling-statistic, and annotation patterns for ordered data. +- docs/examples/bars-and-rankings.md: Bars and Rankings: Choose bar, lollipop, dumbbell, and waterfall compositions for categorical comparison and change. +- docs/examples/scatterplots-and-relationships.md: Scatterplots and Relationships: Choose scatterplot, regression, connected-path, lag, and nearest-point patterns for quantitative relationships. +- docs/examples/distributions.md: Distributions: Choose histograms, boxplots, empirical cumulative distributions, and violins for shape, spread, and rank. +- docs/examples/heatmaps-and-densities.md: Heatmaps and Densities: Choose quantitative matrix cells, contours, or hexagonal bins to show concentration across two dimensions. +- docs/examples/intervals-and-financial.md: Intervals and Financial Charts: Compose timelines, uncertainty intervals, candlesticks, and percentile ribbons from explicit endpoints. +- docs/examples/stacked-and-composition.md: Stacked and Composed Charts: Choose stacked, normalized, streamgraph, funnel, and mosaic compositions for part-to-whole and stage-retention comparisons. +- docs/examples/facets-and-multiple-views.md: Facets and Multiple Views: Choose shared-scale facets, marginal views, and focus-plus-context layouts for coordinated comparisons. +- docs/examples/networks-and-hierarchies.md: Networks and Hierarchies: Choose tidy trees, Sankey flows, spatial adjacency graphs, and force-directed networks for connected or nested data. +- docs/examples/maps-and-spatial.md: Maps and Spatial Charts: Build choropleths, bubble maps, globes, routes, and vector fields for geographic or projected spatial questions. +- docs/examples/polar-and-radar.md: Polar and Radar Charts: Build native pie, donut, gauge, radar, line, scatter, radial bar, rose, and sunburst charts through the opt-in polar coordinate entry. +- docs/examples/annotations-and-overlays.md: Annotations and Overlays: Add direct labels, thresholds, change indicators, and explanatory layers without hiding chart semantics. +- docs/examples/interactive-charts.md: Interactive Charts: Choose chart-owned focus or controlled application interactions for pinned detail, scrolling, zooming, and editing. +- docs/examples/themes-and-motion.md: Themes and Motion: Apply inherited palettes and optional motion while keeping chart definitions separate from card layout and controls. +- docs/reference/index.md: API Reference: Reference for TanStack Charts definitions, marks, scales, runtime, rendering, interaction, and framework adapters. +- docs/reference/chart-definitions.md: Chart Definition API: Reference static and responsive chart definitions, build context, and identity-based updates. +- docs/reference/chart-spec.md: Chart Spec: Reference for marks, axes, color, gradients, clipping, margins, guides, and themes in a TanStack Charts spec. +- docs/reference/view-composition.md: View Composition: Compose complete chart definitions with deterministic grid, layer, fill, and inset layouts. +- docs/reference/transforms.md: Data Transforms: Typed, eager transforms for reusable application data. +- docs/reference/scales-guides-and-color.md: Scales, Guides, and Color: Reference for injected positional scales, automatic axes and margins, color scales, legends, themes, and gradients. +- docs/reference/dom-host.md: DOM Host: Mount, update, size, interact with, and destroy a TanStack Chart in a browser DOM container. +- docs/reference/adapter-controller.md: Adapter Controller: Build framework adapters over shared prerender, mount, update, layout, scene access, and cleanup behavior. +- docs/reference/runtime-and-scene.md: Runtime and Scene: Compile object and responsive chart definitions into renderer-neutral scenes. +- docs/reference/focus-and-interaction.md: Focus and Interaction: Configure pointer focus, grouped focus, crosshairs, controlled cursors, keyboard navigation, built-in tooltips, selection, and spatial indexes. +- docs/reference/rendering-and-export.md: Rendering and Export: Render chart scenes through the default SVG renderer, the optional Canvas renderer, or a custom surface, then export supported output. +- docs/reference/motion.md: Motion: Optional SVG tween and spring motion, definition-local timing, interruption behavior, reduced motion, and the standalone spring sampler. +- docs/reference/custom-extensions.md: Custom Extensions: Extend TanStack Charts with custom marks, distinct scale values, curves, scales, color, legends, text measurement, spatial indexes, and renderers. +- docs/reference/types.md: Types: Public TypeScript types, inference rules, channels, definitions, scenes, hosts, focus, rendering, and custom extension contracts. +- docs/framework/react/reference/chart.md: React Chart: Complete prop and type reference for the @tanstack/charts/react Chart component. +- docs/framework/preact/reference/chart.md: Preact Chart: Complete prop and type reference for the @tanstack/charts/preact Chart component. +- docs/framework/vue/reference/chart.md: Vue Chart: Complete prop and type reference for the @tanstack/charts/vue Chart component. +- docs/framework/solid/reference/chart.md: Solid Chart: Complete prop and type reference for the @tanstack/charts/solid Chart component. +- docs/framework/svelte/reference/chart.md: Svelte Chart: Complete prop and type reference for the @tanstack/charts/svelte Chart component. +- docs/framework/angular/reference/chart.md: Angular Chart: Complete option and type reference for the @tanstack/charts/angular standalone component. +- docs/framework/lit/reference/chart.md: Lit Chart: Complete option and type reference for the @tanstack/charts/lit custom element. +- docs/framework/alpine/reference/chart.md: Alpine Chart: Complete option and type reference for the @tanstack/charts/alpine x-chart directive. +- docs/framework/octane/reference/chart.md: Octane Chart: Complete prop and type reference for the @tanstack/charts/octane Chart component. +- docs/reference/marks/line-and-area.md: Line and Area Marks: Reference for lineY, lineX, areaY, areaX, grouping, null gaps, interval baselines, points, and optional curve bridges. +- docs/reference/marks/difference.md: Difference Marks: Reference for differenceY and differenceX comparison areas, exact crossings, grouping, gaps, lineage, styling, and interaction. +- docs/reference/marks/regression.md: Linear Regression Marks: Reference for eager linearRegressionRowsY/X data and linearRegressionY/X least-squares marks, confidence bands, grouping, sampling, lineage, and interaction. +- docs/reference/marks/bar-and-rect.md: Bar and Rect Marks: Reference for barY, barX, rect, cell, interval endpoints, band sizing, grouped bars, color, insets, and focus anchors. +- docs/reference/marks/box.md: Box Marks: Reference for boxRows, boxY, and boxX Tukey summaries, quartiles, whiskers, outliers, lineage, styling, and interaction. +- docs/reference/marks/dot-and-hexagon.md: Dot and Hexagon Marks: Reference for dot and hexagon channels, pixel radius, injected radius scales, color, grouping, keys, and interaction points. +- docs/reference/marks/dodge.md: Dodge Layouts: Reference for responsive dodgeX and dodgeY dot collision layouts, anchors, padding, radii, identity, and scale ownership. +- docs/reference/marks/waffle.md: Waffle Marks: Reference for responsive waffleY and waffleX unit layouts, cumulative rounding, color, source identity, and interaction. +- docs/reference/marks/ridgeline.md: Ridgeline Marks: Reference for semantic ridgelineY and ridgelineX profile geometry, categorical baselines, overlap, curves, identity, and interaction. +- docs/reference/marks/violin.md: Violin Marks: Reference for semantic violinY and violinX mirrored profile geometry, category-relative spans, curves, identity, and interaction. +- docs/reference/marks/focus-guide.md: Focus Guide Marks: Render datum-bound focus rules, markers, and axis labels with stable renderer-neutral motion. +- docs/reference/marks/treemap.md: Treemap Mark: Reference for responsive hierarchy tiling, flat-row construction, node lineage, color, labels, and the optional treemap import. +- docs/reference/marks/sunburst.md: Sunburst Mark: Reference for flat hierarchy input, responsive radial partitioning, node lineage, branch color, and the optional sunburst import. +- docs/reference/marks/sankey.md: Sankey Diagram Mark: Reference for responsive flow layout, semantic graph input, immutable node and link rows, ordinary child marks, and the optional Sankey import. +- docs/reference/marks/hexbin.md: Hexbin Mark: Reference for responsive screen-space hexagonal aggregation, reducer outputs, source lineage, color, and the optional spatial import. +- docs/reference/marks/contour.md: Contour Mark: Reference for regular scalar-grid contours, thresholds, orientation, source lineage, and structured multipolygon output. +- docs/reference/marks/density.md: Density Contour Mark: Reference for responsive point-density contours, thresholds, grouping, weights, source lineage, and structured multipolygon output. +- docs/reference/marks/delaunay.md: Delaunay Link Mark: Reference for responsive Delaunay adjacency links, source grouping, endpoint lineage, stable keys, and the optional spatial import. +- docs/reference/marks/voronoi.md: Voronoi Mark: Reference for responsive final-screen Voronoi cells, grouping, stable coincident ownership, presentation, and interaction separation. +- docs/reference/marks/rules-links-arrows-vectors-and-ticks.md: Rules, Links, Arrows, Vectors, and Ticks: Reference for ruleX, ruleY, link, arrow, vector, tickX, and tickY geometry, channels, defaults, curves, and focus anchors. +- docs/reference/marks/text-frame-and-facet.md: Text, Frame, and Facet Marks: Reference for data labels, chart frames, responsive small multiples, shared or per-cell axes, labels, and facet constraints. +- docs/reference/marks/geo.md: Geo Shape Mark: Reference for projecting GeoJSON with an opt-in D3-backed geoShape mark. +- docs/reference/marks/polar.md: Polar Marks: Reference for the opt-in polar container, radial marks, guides, injected scale factories, and D3-backed geometry. Authoring rules: @@ -112,8 +113,8 @@ Authoring rules: - Start with exact @tanstack/charts/scales/linear, @tanstack/charts/scales/band, @tanstack/charts/scales/point, or @tanstack/charts/scales/ordinal entries. Upgrade only the mapping that needs temporal, nonlinear, radial, interpolated, statistical, or other complete D3 semantics; never import the d3 umbrella. - Treat Date values on compact band or point scales as equally spaced categories. Use d3-scale scaleTime or scaleUtc when elapsed-time spacing or calendar-aware ticks matter. - Let TanStack Charts own responsive pixel ranges. Scale factories infer domains from mark channels; configured instances preserve application-owned domains. -- Put Cartesian scale and guide options under scales.x and scales.y; never generate deprecated root x or y properties. -- Put polar position scales under scales.angle and scales.radius inside polar(); never generate deprecated direct angle or radius properties. +- Put Cartesian scale and guide options under scales.x and scales.y; root x and y properties are invalid. +- Put polar position scales under scales.angle and scales.radius inside polar(); direct angle and radius properties are invalid. - Keep data in its application shape. Map fields or accessors into marks instead of creating a library-owned series model. - Memoize the complete definition against captured application values; definition identity is the application update boundary. - Preserve inferable datum identity across updates; add explicit keys only when IDs or unique positions are unavailable. diff --git a/packages/charts-core/skills/build-chart-interactions/SKILL.md b/packages/charts-core/skills/build-chart-interactions/SKILL.md index 2bda97d2..27aabb83 100644 --- a/packages/charts-core/skills/build-chart-interactions/SKILL.md +++ b/packages/charts-core/skills/build-chart-interactions/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/tooltips-and-focus.md' - 'TanStack/charts:docs/guides/interactions-and-selections.md' @@ -42,8 +42,10 @@ export const chart = defineChart({ lineY(rows, { x: 'week', y: 'value', points: true }), crosshair({ x: { label: true }, y: false }), ], - x: { scale: scalePoint }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scalePoint }, + y: { scale: scaleLinear }, + }, focus: 'nearest-x', maxFocusDistance: Number.POSITIVE_INFINITY, tooltip, @@ -110,8 +112,10 @@ export const chart = defineChart({ selection, ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, selection, }) ``` diff --git a/packages/charts-core/skills/compose-marks-and-views/SKILL.md b/packages/charts-core/skills/compose-marks-and-views/SKILL.md index 438ceb31..f31f929b 100644 --- a/packages/charts-core/skills/compose-marks-and-views/SKILL.md +++ b/packages/charts-core/skills/compose-marks-and-views/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/concepts/grammar-of-graphics.md' - 'TanStack/charts:docs/concepts/marks-and-layering.md' @@ -55,8 +55,10 @@ export const chart = defineChart({ }, ), ], - x: { scale: scalePoint }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scalePoint }, + y: { scale: scaleLinear }, + }, }) ``` diff --git a/packages/charts-core/skills/configure-scales-guides-color/SKILL.md b/packages/charts-core/skills/configure-scales-guides-color/SKILL.md index 3edf34a7..d590c284 100644 --- a/packages/charts-core/skills/configure-scales-guides-color/SKILL.md +++ b/packages/charts-core/skills/configure-scales-guides-color/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/concepts/scales-and-d3.md' - 'TanStack/charts:docs/reference/scales-guides-and-color.md' @@ -43,12 +43,14 @@ const color = scaleOrdinal() export const chart = defineChart({ marks: [lineY(rows, { x: 'week', y: 'downloads', z: 'package' })], - x: { scale: () => scalePoint().padding(0.2) }, - y: { - scale: scaleLinear, - nice: true, - grid: true, - axis: { label: 'Downloads' }, + scales: { + x: { scale: () => scalePoint().padding(0.2) }, + y: { + scale: scaleLinear, + nice: true, + grid: true, + axis: { label: 'Downloads' }, + }, }, color: { scale: color, legend: colorLegend({ label: 'Package' }) }, }) diff --git a/packages/charts-core/skills/coordinate-charts-with-tanstack/SKILL.md b/packages/charts-core/skills/coordinate-charts-with-tanstack/SKILL.md index 621fa9bb..d1fbe3db 100644 --- a/packages/charts-core/skills/coordinate-charts-with-tanstack/SKILL.md +++ b/packages/charts-core/skills/coordinate-charts-with-tanstack/SKILL.md @@ -10,7 +10,7 @@ description: > metadata: type: composition library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/interactions-and-selections.md' - 'TanStack/charts:docs/guides/transforms-and-reactivity.md' @@ -78,8 +78,10 @@ const chartDefinition = defineChart({ key: ({ datum }) => `${datum.region}:${datum.day.toISOString()}`, }), ], - x: { scale: scaleUtc }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleUtc }, + y: { scale: scaleLinear }, + }, }) ``` diff --git a/packages/charts-core/skills/debug-and-verify-charts/SKILL.md b/packages/charts-core/skills/debug-and-verify-charts/SKILL.md index d13f2f67..33833066 100644 --- a/packages/charts-core/skills/debug-and-verify-charts/SKILL.md +++ b/packages/charts-core/skills/debug-and-verify-charts/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: lifecycle library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/testing-and-debugging.md' - 'TanStack/charts:docs/guides/typescript.md' @@ -44,8 +44,10 @@ const rows = [ const definition = defineChart({ marks: [lineY(rows, { x: 'month', y: 'value', points: true })], - x: { scale: scalePoint }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scalePoint }, + y: { scale: scaleLinear }, + }, }) export const scene = createChartScene(definition, { width: 640, height: 360 }) diff --git a/packages/charts-core/skills/design-a-chart/SKILL.md b/packages/charts-core/skills/design-a-chart/SKILL.md index bb757376..10f1f11b 100644 --- a/packages/charts-core/skills/design-a-chart/SKILL.md +++ b/packages/charts-core/skills/design-a-chart/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/choosing-a-chart.md' - 'TanStack/charts:docs/examples/*.md' @@ -81,8 +81,10 @@ export const chart = defineChart({ lineY(rows, { x: 'month', y: 'forecast', strokeDasharray: '5 4' }), ruleY([100], { strokeDasharray: '2 3' }), ], - x: { scale: scalePoint }, - y: { scale: scaleLinear, axis: { label: 'Indexed revenue' } }, + scales: { + x: { scale: scalePoint }, + y: { scale: scaleLinear, axis: { label: 'Indexed revenue' } }, + }, }) ``` diff --git a/packages/charts-core/skills/design-responsive-charts/SKILL.md b/packages/charts-core/skills/design-responsive-charts/SKILL.md index e200c94a..34a87769 100644 --- a/packages/charts-core/skills/design-responsive-charts/SKILL.md +++ b/packages/charts-core/skills/design-responsive-charts/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/responsive-charts.md' - 'TanStack/charts:docs/reference/chart-definitions.md' @@ -37,12 +37,14 @@ const rows = [ const definition = defineChart(({ width }) => ({ marks: [barX(rows, { x: 'requests', y: 'feature' })], - x: { - scale: scaleLinear, - nice: true, - axis: { ticks: { count: width < 420 ? 3 : 6 } }, + scales: { + x: { + scale: scaleLinear, + nice: true, + axis: { ticks: { count: width < 420 ? 3 : 6 } }, + }, + y: { scale: () => scaleBand().padding(0.1) }, }, - y: { scale: () => scaleBand().padding(0.1) }, })) const element = document.querySelector('#feature-chart') @@ -105,9 +107,12 @@ Correct: ```ts defineChart(({ width }) => ({ marks, - x: { - scale: scaleLinear, - axis: { ticks: { count: width < 420 ? 4 : 8 } }, + scales: { + x: { + scale: scaleLinear, + axis: { ticks: { count: width < 420 ? 4 : 8 } }, + }, + y: { scale: scaleLinear }, }, })) ``` diff --git a/packages/charts-core/skills/extend-tanstack-charts/SKILL.md b/packages/charts-core/skills/extend-tanstack-charts/SKILL.md index a479da2d..d0885cf6 100644 --- a/packages/charts-core/skills/extend-tanstack-charts/SKILL.md +++ b/packages/charts-core/skills/extend-tanstack-charts/SKILL.md @@ -7,7 +7,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' requires: - compose-marks-and-views sources: @@ -61,7 +61,10 @@ const threshold = createMark(({ markIndex }) => { export const chart = defineChart({ marks: [threshold], - y: { scale: scaleLinear }, + scales: { + x: null, + y: { scale: scaleLinear }, + }, }) ``` diff --git a/packages/charts-core/skills/migrate-to-tanstack-charts/SKILL.md b/packages/charts-core/skills/migrate-to-tanstack-charts/SKILL.md index a7df871c..a2c6f0cd 100644 --- a/packages/charts-core/skills/migrate-to-tanstack-charts/SKILL.md +++ b/packages/charts-core/skills/migrate-to-tanstack-charts/SKILL.md @@ -7,7 +7,7 @@ description: > metadata: type: composition library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' requires: - design-a-chart - debug-and-verify-charts diff --git a/packages/charts-core/skills/prepare-chart-data/SKILL.md b/packages/charts-core/skills/prepare-chart-data/SKILL.md index c7f45706..39bb5e96 100644 --- a/packages/charts-core/skills/prepare-chart-data/SKILL.md +++ b/packages/charts-core/skills/prepare-chart-data/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/transforms-and-reactivity.md' - 'TanStack/charts:docs/reference/transforms.md' diff --git a/packages/charts-core/skills/ship-accessible-charts/SKILL.md b/packages/charts-core/skills/ship-accessible-charts/SKILL.md index ba24549f..bef11061 100644 --- a/packages/charts-core/skills/ship-accessible-charts/SKILL.md +++ b/packages/charts-core/skills/ship-accessible-charts/SKILL.md @@ -7,7 +7,7 @@ description: > metadata: type: lifecycle library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/accessibility.md' - 'TanStack/charts:docs/guides/ssr-and-hydration.md' @@ -84,8 +84,10 @@ const rows: readonly TrafficRow[] = [ const definition = defineChart({ marks: [lineY(rows, { x: 'date', y: 'visits' })], - x: { scale: scaleUtc }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleUtc }, + y: { scale: scaleLinear }, + }, }) const runtime = createChartRuntime() diff --git a/packages/charts-core/skills/update-and-animate-charts/SKILL.md b/packages/charts-core/skills/update-and-animate-charts/SKILL.md index 0f165fe5..36aa00aa 100644 --- a/packages/charts-core/skills/update-and-animate-charts/SKILL.md +++ b/packages/charts-core/skills/update-and-animate-charts/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: type: core library: '@tanstack/charts' - library_version: '0.9.0' + library_version: '0.15.0' sources: - 'TanStack/charts:docs/guides/dynamic-data-and-animation.md' - 'TanStack/charts:docs/reference/motion.md' @@ -40,8 +40,10 @@ function createRanking(rows: readonly Row[]) { return defineChart({ svgAnimation: { duration: 280, easing: 'ease-out' }, marks: [barX(ranked, { id: 'ranking', x: 'value', y: 'label', key: 'id' })], - x: { scale: scaleLinear, nice: true }, - y: { scale: () => scaleBand().padding(0.1) }, + scales: { + x: { scale: scaleLinear, nice: true }, + y: { scale: () => scaleBand().padding(0.1) }, + }, }) } diff --git a/packages/charts-core/src/adapter.test.ts b/packages/charts-core/src/adapter.test.ts index 7683ac42..4f6fe519 100644 --- a/packages/charts-core/src/adapter.test.ts +++ b/packages/charts-core/src/adapter.test.ts @@ -15,8 +15,10 @@ const rows = [ const definition = defineChart({ marks: [lineY(rows, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) const focusDisabledDefinition = defineChart(definition, { focus: false }) const freeCursorDefinition = defineChart(definition, { @@ -31,8 +33,10 @@ describe('chart adapter controller', () => { it('mounts a prerendered dynamic definition', () => { const dynamic = defineChart(() => ({ marks: [lineY(rows, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, })) const adapter = createChartAdapter({ definition: dynamic, diff --git a/packages/charts-core/src/area-x.ts b/packages/charts-core/src/area-x.ts index c0f50750..86814b11 100644 --- a/packages/charts-core/src/area-x.ts +++ b/packages/charts-core/src/area-x.ts @@ -21,7 +21,9 @@ import type { ChartAreaStateStyle, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -53,22 +55,40 @@ export interface AreaXOptions states?: readonly ChartMarkState>[] } +type AreaXCallOptions< + TDatum, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + AreaXOptions>, + { + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + export function areaX( source: Iterable, ): ChartMark export function areaX< TDatum, - const TOptions extends AreaXOptions> | undefined, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + AreaXCallOptions | undefined, ): CartesianChartMark< TDatum, number, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, - TOptions + MarkChannelOutput, + MarkScaleBindings > export function areaX( source: Iterable, diff --git a/packages/charts-core/src/area.ts b/packages/charts-core/src/area.ts index 4b9273d2..d0152955 100644 --- a/packages/charts-core/src/area.ts +++ b/packages/charts-core/src/area.ts @@ -22,7 +22,9 @@ import type { ChartPoint, ChartValue, ChartCurve, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -47,22 +49,40 @@ export interface AreaYOptions states?: readonly ChartMarkState>[] } +type AreaYCallOptions< + TDatum, + TXChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + AreaYOptions>, + { + x?: TXChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + export function areaY( source: Iterable, ): ChartMark export function areaY< TDatum, - const TOptions extends AreaYOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + AreaYCallOptions | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, + MarkChannelOutput, number, - TOptions + MarkScaleBindings > export function areaY( source: Iterable, diff --git a/packages/charts-core/src/arrow.ts b/packages/charts-core/src/arrow.ts index 6321e547..7b30d7aa 100644 --- a/packages/charts-core/src/arrow.ts +++ b/packages/charts-core/src/arrow.ts @@ -18,6 +18,8 @@ import type { ChartMarkMotionOptions, ChartPoint, ChartValue, + MarkCallOptions, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -39,28 +41,69 @@ export interface ArrowOptions headAngle?: number } -type ArrowXOutput = - | ChannelOutput - | ChannelOutput - -type ArrowYOutput = - | ChannelOutput - | ChannelOutput +type ArrowCallOptions< + TDatum, + TX1Channel, + TY1Channel, + TX2Channel, + TY2Channel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + ArrowOptions>, + { + x1: TX1Channel + y1: TY1Channel + x2: TX2Channel + y2: TY2Channel + xScale?: TXScaleId + yScale?: TYScaleId + } +> /** Draws one straight directed segment per datum with a scale-independent head. */ export function arrow< TDatum, - const TOptions extends ArrowOptions>, + const TX1Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TY1Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TX2Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TY2Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: ArrowCallOptions< + TDatum, + TX1Channel, + TY1Channel, + TX2Channel, + TY2Channel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - ArrowXOutput, - ArrowYOutput, - ArrowXOutput, - ArrowYOutput, - TOptions + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + MarkScaleBindings > export function arrow( source: Iterable, diff --git a/packages/charts-core/src/band.ts b/packages/charts-core/src/band.ts index 69730b8e..143a2646 100644 --- a/packages/charts-core/src/band.ts +++ b/packages/charts-core/src/band.ts @@ -17,7 +17,9 @@ import type { ChartMarkMotionOptions, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, ResolvedScale, SceneNode, VisualChannel, @@ -51,42 +53,74 @@ export interface BandYOptions extends ChartMarkMotionOptions { yScale?: string } +type BandXCallOptions< + TDatum, + TXChannel, + TXScaleId extends string | undefined, +> = MarkCallOptions< + BandXOptions>, + { + x?: TXChannel + xScale?: TXScaleId + } +> + +type BandYCallOptions< + TDatum, + TYChannel, + TYScaleId extends string | undefined, +> = MarkCallOptions< + BandYOptions>, + { + y?: TYChannel + yScale?: TYScaleId + } +> + +export function bandX( + source: Iterable, +): ChartMark export function bandX< TDatum, - const TOptions extends BandXOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, >( source: Iterable, - options?: TOptions, + options: BandXCallOptions | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, + MarkChannelOutput, never, - TOptions -> { + MarkScaleBindings +> +export function bandX( + source: Iterable, + options: BandXOptions> = {}, +): CartesianChartMark> { const data = Array.isArray(source) ? source : Array.from(source) - const resolved = options ?? ({} as BandXOptions>) - const xScale = resolved.xScale ?? 'x' + const xScale = options.xScale ?? 'x' return createMarkWithScaleValues( ({ markIndex }) => { - const id = resolved.id ?? `band-x-${markIndex}` + const id = options.id ?? `band-x-${markIndex}` const values = channelValues( data, - resolved.x, + options.x, (_datum, { index }) => index, ) - const zValues = channelValues(data, resolved.z, () => null) + const zValues = channelValues(data, options.z, () => null) const colorValues = - resolved.color === undefined + options.color === undefined ? zValues - : channelValues(data, resolved.color, () => null) - const keys = inferredKeyValues(data, resolved.key, { + : channelValues(data, options.color, () => null) + const keys = inferredKeyValues(data, options.key, { groups: zValues, candidates: [values], markId: id, - warningIdentity: resolved, + warningIdentity: options, }) return { @@ -96,18 +130,18 @@ export function bandX< color: { scale: 'color', values: colorValues.filter(isChartKey) }, }, render: ({ chart, scales, color }) => { - const width = Number.isFinite(resolved.width) - ? Math.max(0, resolved.width!) + const width = Number.isFinite(options.width) + ? Math.max(0, options.width!) : scales[xScale]!.bandwidth || inferBandwidth(scales[xScale]!, values, chart.width, data.length) - const inset = Number.isFinite(resolved.inset) ? resolved.inset! : 0 + const inset = Number.isFinite(options.inset) ? options.inset! : 0 const nodes: SceneNode[] = [] data.forEach((datum, index) => { const xValue = values[index] if (!isChartValue(xValue)) return const x = scales[xScale]!.map(xValue) const fill = visualValue( - resolved.fill, + options.fill, datum, index, data, @@ -137,9 +171,9 @@ export function bandX< y: chart.y, width: paintedWidth, height: chart.height, - radius: resolved.radius, + radius: options.radius, interaction: { point, affinity: 'x' }, - style: { fill, fillOpacity: resolved.fillOpacity }, + style: { fill, fillOpacity: options.fillOpacity }, }) }) return { @@ -156,54 +190,55 @@ export function bandX< }, } }, - resolved.motion, - resolved.renderer, - ) as CartesianChartMark< - TDatum, - OptionChannelOutput, - number, - OptionChannelOutput, - never, - TOptions - > + options.motion, + options.renderer, + ) } +export function bandY( + source: Iterable, +): ChartMark export function bandY< TDatum, - const TOptions extends BandYOptions> | undefined, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options?: TOptions, + options: BandYCallOptions | undefined, ): CartesianChartMark< TDatum, number, - OptionChannelOutput, + MarkChannelOutput, never, - OptionChannelOutput, - TOptions -> { + MarkChannelOutput, + MarkScaleBindings<'x', TYScaleId> +> +export function bandY( + source: Iterable, + options: BandYOptions> = {}, +): CartesianChartMark> { const data = Array.isArray(source) ? source : Array.from(source) - const resolved = options ?? ({} as BandYOptions>) - const yScale = resolved.yScale ?? 'y' + const yScale = options.yScale ?? 'y' return createMark( ({ markIndex }) => { - const id = resolved.id ?? `band-y-${markIndex}` + const id = options.id ?? `band-y-${markIndex}` const values = channelValues( data, - resolved.y, + options.y, (_datum, { index }) => index, ) - const zValues = channelValues(data, resolved.z, () => null) + const zValues = channelValues(data, options.z, () => null) const colorValues = - resolved.color === undefined + options.color === undefined ? zValues - : channelValues(data, resolved.color, () => null) - const keys = inferredKeyValues(data, resolved.key, { + : channelValues(data, options.color, () => null) + const keys = inferredKeyValues(data, options.key, { groups: zValues, candidates: [values], markId: id, - warningIdentity: resolved, + warningIdentity: options, }) return { @@ -213,18 +248,18 @@ export function bandY< color: { scale: 'color', values: colorValues.filter(isChartKey) }, }, render: ({ chart, scales, color }) => { - const height = Number.isFinite(resolved.height) - ? Math.max(0, resolved.height!) + const height = Number.isFinite(options.height) + ? Math.max(0, options.height!) : scales[yScale]!.bandwidth || inferBandwidth(scales[yScale]!, values, chart.height, data.length) - const inset = Number.isFinite(resolved.inset) ? resolved.inset! : 0 + const inset = Number.isFinite(options.inset) ? options.inset! : 0 const nodes: SceneNode[] = [] data.forEach((datum, index) => { const yValue = values[index] if (!isChartValue(yValue)) return const y = scales[yScale]!.map(yValue) const fill = visualValue( - resolved.fill, + options.fill, datum, index, data, @@ -254,9 +289,9 @@ export function bandY< y: top, width: chart.width, height: paintedHeight, - radius: resolved.radius, + radius: options.radius, interaction: { point, affinity: 'y' }, - style: { fill, fillOpacity: resolved.fillOpacity }, + style: { fill, fillOpacity: options.fillOpacity }, }) }) return { @@ -273,16 +308,9 @@ export function bandY< }, } }, - resolved.motion, - resolved.renderer, - ) as CartesianChartMark< - TDatum, - number, - OptionChannelOutput, - never, - OptionChannelOutput, - TOptions - > + options.motion, + options.renderer, + ) } function inferBandwidth( diff --git a/packages/charts-core/src/bar.ts b/packages/charts-core/src/bar.ts index 1d93a476..71cc6fd4 100644 --- a/packages/charts-core/src/bar.ts +++ b/packages/charts-core/src/bar.ts @@ -24,7 +24,9 @@ import type { ChartBarStateStyle, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, ResolvedScale, SceneNode, VisualChannel, @@ -84,22 +86,53 @@ export interface BarXOptions states?: readonly ChartMarkState>[] } +type BarYCallOptions< + TDatum, + TXChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + BarYOptions>, + { + x?: TXChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + +type BarXCallOptions< + TDatum, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + BarXOptions>, + { + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + export function barY( source: Iterable, ): ChartMark export function barY< TDatum, - const TOptions extends BarYOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: BarYCallOptions | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, + MarkChannelOutput, number, - TOptions + MarkScaleBindings > export function barY( source: Iterable, @@ -313,17 +346,20 @@ export function barX( ): ChartMark export function barX< TDatum, - const TOptions extends BarXOptions> | undefined, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: BarXCallOptions | undefined, ): CartesianChartMark< TDatum, number, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, - TOptions + MarkChannelOutput, + MarkScaleBindings > export function barX( source: Iterable, diff --git a/packages/charts-core/src/box.test.ts b/packages/charts-core/src/box.test.ts index e957603b..1b4cd6a4 100644 --- a/packages/charts-core/src/box.test.ts +++ b/packages/charts-core/src/box.test.ts @@ -25,8 +25,10 @@ describe('box marks', () => { const scene = createChartScene( defineChart({ marks: [boxY(rows, { x: 'group', y: 'value', key: 'id' })], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -85,8 +87,10 @@ describe('box marks', () => { stroke: '#2563eb', }), ], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 640, height: 400 }) type Datum = ChartSpecDatum @@ -214,8 +218,10 @@ describe('box marks', () => { const original = [...rows] const definition = defineChart({ marks: [boxY(rows, { x: 'group', y: 'value', key: 'id' })], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 600, height: 360 }) const summaries = scene.points.flatMap(({ datum }) => @@ -246,8 +252,10 @@ describe('box marks', () => { ] const definition = defineChart({ marks: [boxX(rows, { id: 'horizontal', x: 'value', y: 'group' })], - x: { scale: scaleLinear }, - y: { scale: scaleBand }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleBand }, + }, }) const scene = createChartScene(definition, { width: 600, height: 360 }) type Datum = ChartSpecDatum @@ -287,8 +295,10 @@ describe('box marks', () => { const scene = createChartScene( defineChart({ marks: [boxY(rows, { x: 'group', y: 'value', key: 'id' })], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 600, height: 360 }, ) @@ -310,8 +320,10 @@ describe('box marks', () => { const mark = boxY(rows, { x: 'group', y: 'value', key: 'id' }) const definition = defineChart({ marks: [mark], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) const median = () => createChartScene(definition, { width: 480, height: 280 }).points.find( @@ -347,8 +359,10 @@ describe('box marks', () => { }) const definition = defineChart({ marks: [mark], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) const motion = mark.initialize({ markIndex: 0 }).motion @@ -386,8 +400,10 @@ function renderMorley() { stroke: '#2563eb', }), ], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) return createChartScene(definition, { width: 640, height: 400 }) } diff --git a/packages/charts-core/src/canvas.test.ts b/packages/charts-core/src/canvas.test.ts index f3c482bc..5ce7d73c 100644 --- a/packages/charts-core/src/canvas.test.ts +++ b/packages/charts-core/src/canvas.test.ts @@ -318,7 +318,7 @@ describe('Canvas renderer', () => { }) const adapter = createChartRendererAdapter({ definition, - renderer: createSvgChartRenderer(), + renderer: createSvgChartRenderer(), initialWidth: 320, height: 180, ariaLabel: 'Mixed server chart', @@ -621,8 +621,10 @@ describe('Canvas renderer', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([1, 2]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([1, 2]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, margin: 0, }), @@ -636,10 +638,13 @@ describe('Canvas renderer', () => { innerRadius: ({ radius }) => radius * 0.55, }), ], + scales: { angle: null, radius: null }, }), ], - x: null, - y: null, + scales: { + x: null, + y: null, + }, guides: false, margin: 0, }), @@ -1830,8 +1835,10 @@ describe('Canvas renderer', () => { ] const definition = defineChart({ marks: [lineY(data, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 8]) }, + }, }) const container = document.createElement('div') const onFocusChange = vi.fn() @@ -1907,8 +1914,10 @@ describe('Canvas renderer', () => { const options = () => ({ definition: defineChart({ marks: [lineY(data, { x: 'x', y: 'y', color: 'series' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 8]) }, + }, color: { domain: ['a', 'b'] as const, range: ['#2563eb', '#f97316'], @@ -1955,8 +1964,10 @@ describe('Canvas renderer', () => { const onChange = vi.fn() const definition = defineChart({ marks: [lineY(data, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, controls: [ continuousCursor({ id: 'canvas-cursor', @@ -2024,8 +2035,10 @@ describe('Canvas renderer', () => { const chart = (withHandle: boolean) => defineChart({ marks: [lineY(data, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, controls: withHandle ? [ handleX({ @@ -2101,10 +2114,15 @@ describe('Canvas renderer', () => { ] const definition = defineChart({ marks: [lineY(data, { x: 'x', y: 'y' })], - x: { - scale: scaleLinear().domain([acceptedWindow.start, acceptedWindow.end]), + scales: { + x: { + scale: scaleLinear().domain([ + acceptedWindow.start, + acceptedWindow.end, + ]), + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, controls: [ zoomX({ id: 'canvas-zoom', @@ -2120,8 +2138,10 @@ describe('Canvas renderer', () => { }) const withoutZoom = defineChart({ marks: [lineY(data, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, keyboard: false, }) const container = document.createElement('div') @@ -2236,8 +2256,10 @@ describe('Canvas renderer', () => { const host = mountCanvasChart(container, { definition: defineChart({ marks: [lineX([2, 6, 4])], - x: { scale: scaleLinear().domain([0, 6]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 6]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, }), width: 300, diff --git a/packages/charts-core/src/cartesian-scale-types.test.ts b/packages/charts-core/src/cartesian-scale-types.test.ts index 9b7b986e..1f9060bf 100644 --- a/packages/charts-core/src/cartesian-scale-types.test.ts +++ b/packages/charts-core/src/cartesian-scale-types.test.ts @@ -8,6 +8,7 @@ import { boxX, boxY } from './box' import { crosshair } from './crosshair' import { differenceX, differenceY } from './difference' import { dot } from './dot' +import type { DotOptions } from './dot' import { focusGuideX } from './focus-guide' import { whenFocused } from './focus-mark' import { hexagon } from './hexagon' @@ -17,6 +18,8 @@ import { compositeMark } from './mark-composite' import { decorative } from './mark-decorative' import { createMark } from './mark' import { createMarkWithScaleValues } from './mark-with-scale-values' +import { radialDot } from './polar' +import type { PolarMark, RadialDotOptions } from './polar' import { rect } from './rect' import { linearRegressionX, linearRegressionY } from './regression' import { ridgelineX, ridgelineY } from './ridgeline' @@ -46,6 +49,11 @@ type ScaleIds = ? readonly [TXScaleId, TYScaleId] : never +type PointValues = + TMark extends ChartMark + ? readonly [TXValue, TYValue] + : never + interface Row { id: string category: string @@ -59,6 +67,28 @@ interface Row { const rows: readonly Row[] = [] +function forwardDot>(options: TOptions) { + return dot(rows, options) +} + +function forwardOptionalDot>( + options: TOptions | undefined, +) { + return dot(rows, options) +} + +function forwardRadialDot>( + options: TOptions, +) { + return radialDot(rows, options) +} + +function forwardOptionalRadialDot>( + options: TOptions | undefined, +) { + return radialDot(rows, options) +} + if (false) { const twoAxisMarks = [ areaY(rows, { x: 'x', y: 'y', xScale: 'namedX', yScale: 'namedY' }), @@ -291,6 +321,55 @@ if (false) { expectTypeOf>().toEqualTypeOf< readonly ['x', 'y'] >() + + const inferredDot = dot(rows, { x: 'category', y: 'y' }) + expectTypeOf>().toEqualTypeOf< + readonly [string, number] + >() + expectTypeOf>().toEqualTypeOf< + readonly ['x', 'y'] + >() + const inferredLine = lineY(rows, { x: 'x', y: 'y' }) + expectTypeOf>().toEqualTypeOf< + readonly ['x', 'y'] + >() + + // @ts-expect-error Dot marks reject unsupported fresh options. + dot(rows, { x: 'x', y: 'y', opacity: 0 }) + // @ts-expect-error Composite factories reject unsupported fresh options. + boxY(rows, { x: 'category', y: 'y', unknownOption: true }) + + const structuralDotOptions = { + x: 'x', + y: 'y', + opacity: 0, + } as const + dot(rows, structuralDotOptions) + forwardOptionalDot(rows.length > 0 ? structuralDotOptions : undefined) + // @ts-expect-error Other literal-preserving mark factories reject unknown keys. + vector(rows, { x: 'x', y: 'y', unknownOption: true }) + + const namedRadialDot = radialDot(rows, { + angle: 'x', + radius: 'y', + angleScale: 'namedAngle', + radiusScale: 'namedRadius', + }) + expectTypeOf(namedRadialDot).toMatchTypeOf< + PolarMark + >() + // @ts-expect-error Polar mark factories reject unknown keys too. + radialDot(rows, { angle: 'x', radius: 'y', unknownOption: true }) + + forwardDot({ + x: 'x', + y: 'y', + xScale: 'namedX', + yScale: 'namedY', + }) + forwardOptionalDot({ x: 'x', y: 'y' }) + forwardRadialDot({ angle: 'x', radius: 'y' }) + forwardOptionalRadialDot({ angle: 'x', radius: 'y' }) } describe('Cartesian scale ID types', () => { diff --git a/packages/charts-core/src/cartesian-scales.test.ts b/packages/charts-core/src/cartesian-scales.test.ts index ed798c5e..94f657cb 100644 --- a/packages/charts-core/src/cartesian-scales.test.ts +++ b/packages/charts-core/src/cartesian-scales.test.ts @@ -1,93 +1,12 @@ import { scaleBand, scaleLinear } from 'd3-scale' -import { execFileSync } from 'node:child_process' -import { resolve } from 'node:path' -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { crosshair } from './crosshair' import { lineY } from './line' +import { ruleY } from './rule' import { createChartScene, defineChart } from './scene' import type { SceneNode, SceneRule } from './types' -const legacyScaleWarning = - '[TanStack Charts] Root `x` and `y` options are deprecated. Move them to `scales.x` and `scales.y`. When neither Cartesian scale is used, set `scales` to `{ x: null, y: null }`. This compatibility will be removed when TanStack Charts enters Alpha.' - describe('Cartesian scale registry', () => { - it('renders legacy root scales and warns once with migration guidance', async () => { - vi.resetModules() - const freshScene = await import('./scene') - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const definition = freshScene.defineChart({ - marks: [lineY([2, 4, 6])], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 6]) }, - }) - - const first = freshScene.createChartScene(definition, { - width: 480, - height: 260, - }) - freshScene.createChartScene(definition, { width: 640, height: 320 }) - - expect(first.points).toHaveLength(3) - expect(first.scales.x?.domain).toEqual([0, 2]) - expect(first.scales.y?.domain).toEqual([0, 6]) - expect(warn).toHaveBeenCalledOnce() - expect(warn).toHaveBeenCalledWith(legacyScaleWarning) - }) - - it('warns when a scale-free legacy definition omits the registry', async () => { - vi.resetModules() - const freshScene = await import('./scene') - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - freshScene.createChartScene(freshScene.defineChart({ marks: [] }), { - width: 320, - height: 180, - }) - - expect(warn).toHaveBeenCalledOnce() - expect(warn).toHaveBeenCalledWith(legacyScaleWarning) - }) - - it('warns in browsers without a process global', async () => { - vi.resetModules() - const freshScene = await import('./scene') - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const nodeProcess = globalThis.process - vi.stubGlobal('process', undefined) - - try { - freshScene.createChartScene( - freshScene.defineChart({ - marks: [lineY([1, 2, 3])], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 3]) }, - }), - { width: 480, height: 260 }, - ) - } finally { - vi.stubGlobal('process', nodeProcess) - } - - expect(warn).toHaveBeenCalledWith(legacyScaleWarning) - }) - - it('removes the full legacy warning from production bundles', async () => { - const output = execFileSync( - resolve('node_modules/.bin/esbuild'), - [ - resolve('packages/charts-core/src/scene.ts'), - '--bundle', - '--platform=browser', - '--format=esm', - '--minify', - '--define:process.env.NODE_ENV="production"', - ], - { encoding: 'utf8' }, - ) - - expect(output).not.toContain(legacyScaleWarning) - }) - it('binds marks to named y scales and stacks axes sharing the right side', () => { const scene = createNamedScaleScene() const percentPoint = scene.points.find( @@ -184,7 +103,7 @@ describe('Cartesian scale registry', () => { marks: [lineY([1, 2, 3], { yScale: 'missing' })], scales: { x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + y: null, }, }), { width: 480, height: 260 }, @@ -199,7 +118,7 @@ describe('Cartesian scale registry', () => { marks: [lineY([1, 2, 3], { yScale: 'color' })], scales: { x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + y: null, }, }), { width: 480, height: 260 }, @@ -212,7 +131,7 @@ describe('Cartesian scale registry', () => { marks: [lineY([1, 2, 3], { yScale: 'alternate' })], scales: { x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + y: null, alternate: { channel: 'x', scale: scaleLinear().domain([0, 3]), @@ -250,7 +169,7 @@ describe('Cartesian scale registry', () => { scales: { x: { channel: 'y', scale: scaleLinear() }, y: null, - }, + } as any, }), { width: 200, height: 120 }, ), @@ -277,22 +196,6 @@ describe('Cartesian scale registry', () => { ) }) - it('does not replace canonical null scales with legacy root values', () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const scene = createChartScene( - defineChart({ - marks: [], - guides: false, - scales: { x: null, y: null }, - x: { scale: scaleLinear().domain([0, 10]) }, - }), - { width: 480, height: 260 }, - ) - - expect(scene.scales.x?.type).toBe('none') - expect(scene.scales.y?.type).toBe('none') - }) - it('allows focus-guide-only marks to bind null scales', () => { const scene = createChartScene( defineChart({ @@ -311,7 +214,7 @@ describe('Cartesian scale registry', () => { it('keeps stacked empty-domain axes inside the scene', () => { const scene = createChartScene( defineChart({ - marks: [], + marks: [ruleY([] as string[])], scales: { x: null, y: { scale: scaleBand().domain([]) }, diff --git a/packages/charts-core/src/configured-scale.test.ts b/packages/charts-core/src/configured-scale.test.ts index d40354cd..9c353fb7 100644 --- a/packages/charts-core/src/configured-scale.test.ts +++ b/packages/charts-core/src/configured-scale.test.ts @@ -8,6 +8,7 @@ import { } from 'd3-scale' import { describe, expect, it } from 'vitest' import { barY } from './bar' +import { createMark } from './mark' import { createChartScene, defineChart } from './scene' import { lineY } from './line' import { ruleY } from './rule' @@ -30,8 +31,10 @@ describe('configured scales', () => { it('accepts D3 scales directly', () => { const definition = defineChart({ marks: [lineY([4, 9, 7])], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }) const scene = createChartScene(definition, { width: 480, @@ -52,8 +55,10 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY(rows, { x: 'category', y: 'value' })], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) @@ -70,8 +75,10 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY(rows, { x: 'date', y: 'value' })], - x: { scale: scaleUtc, nice: true }, - y: { scale: () => scaleLinear().clamp(true), nice: true }, + scales: { + x: { scale: scaleUtc, nice: true }, + y: { scale: () => scaleLinear().clamp(true), nice: true }, + }, }), { width: 480, height: 260 }, ) @@ -85,8 +92,10 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY([10, 20, 30])], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, }), { width: 480, height: 260 }, ) @@ -100,12 +109,14 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY(rows, { x: 'x', y: 'y' })], - x: { - scale: scaleLinear, - viewport: { domain: [10, 20], translate: 24 }, - axis: { ticks: { count: 3 } }, + scales: { + x: { + scale: scaleLinear, + viewport: { domain: [10, 20], translate: 24 }, + axis: { ticks: { count: 3 } }, + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, }), { width: 480, height: 260 }, ) @@ -141,8 +152,10 @@ describe('configured scales', () => { { x: 'at', y: 'value' }, ), ], - x: { scale: source, viewport: { domain: [middle, end] } }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: source, viewport: { domain: [middle, end] } }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }), { width: 480, height: 260 }, ) @@ -169,12 +182,14 @@ describe('configured scales', () => { { x: 'at', y: 'value' }, ), ], - x: { - scale: scaleUtc().domain([start, end]), - viewport: { domain: viewportDomain, translate: -10 }, - reverse: true, + scales: { + x: { + scale: scaleUtc().domain([start, end]), + viewport: { domain: viewportDomain, translate: -10 }, + reverse: true, + }, + y: { scale: scaleLinear().domain([0, 2]) }, }, - y: { scale: scaleLinear().domain([0, 2]) }, }), { width: 480, height: 260 }, ) @@ -195,11 +210,13 @@ describe('configured scales', () => { const numeric = createChartScene( defineChart({ marks: [lineY([0, 10, 20, 30])], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { - scale: scaleLinear().domain([0, 30]), - viewport: { domain: [10, 20], translate: 12 }, - reverse: true, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { + scale: scaleLinear().domain([0, 30]), + viewport: { domain: [10, 20], translate: 12 }, + reverse: true, + }, }, }), { width: 480, height: 260 }, @@ -221,11 +238,13 @@ describe('configured scales', () => { y: 'value', }), ], - x: { - scale: scaleBand, - viewport: { domain: ['A', 'B'] }, - } as unknown as ChartPositionScaleOptions, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { + scale: scaleBand, + viewport: { domain: ['A', 'B'] }, + } as unknown as ChartPositionScaleOptions, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 480, height: 260 }, ), @@ -240,8 +259,10 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 1])], - x: { scale: scaleLinear, viewport }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear, viewport }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 480, height: 260 }, ), @@ -254,11 +275,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 100])], - x: { - scale: scaleQuantize().domain([0, 100]), - viewport: { domain: [20, 80] }, + scales: { + x: { + scale: scaleQuantize().domain([0, 100]), + viewport: { domain: [20, 80] }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ), @@ -268,11 +291,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 100])], - x: { - scale: scaleLinear().domain([0, 100]).clamp(true), - viewport: { domain: [20, 80] }, + scales: { + x: { + scale: scaleLinear().domain([0, 100]).clamp(true), + viewport: { domain: [20, 80] }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ), @@ -282,11 +307,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 100])], - x: { - scale: scaleIdentity().domain([0, 100]), - viewport: { domain: [20, 80] }, + scales: { + x: { + scale: scaleIdentity().domain([0, 100]), + viewport: { domain: [20, 80] }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ), @@ -316,11 +343,13 @@ describe('configured scales', () => { { x: 'x', y: 'y' }, ), ], - x: { - scale: scaleLog().domain(contentDomain), - viewport: { domain: viewportDomain }, + scales: { + x: { + scale: scaleLog().domain(contentDomain), + viewport: { domain: viewportDomain }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ) @@ -361,11 +390,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([1, 10])], - x: { - scale: scaleLog().domain(contentDomain), - viewport: { domain: viewportDomain }, + scales: { + x: { + scale: scaleLog().domain(contentDomain), + viewport: { domain: viewportDomain }, + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, }), { width: 480, height: 260 }, ), @@ -377,11 +408,13 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY([0, 100])], - x: { - scale: mutableDomainScale(), - viewport: { domain: [20, 80] }, + scales: { + x: { + scale: mutableDomainScale(), + viewport: { domain: [20, 80] }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ) @@ -395,11 +428,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 100])], - x: { - scale: mutableDomainScale(true), - viewport: { domain: [0, 100] }, + scales: { + x: { + scale: mutableDomainScale(true), + viewport: { domain: [0, 100] }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }), { width: 480, height: 260 }, ), @@ -418,11 +453,13 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([0, 1])], - x: { - scale: customScale, - viewport: { domain: [0, 1] }, + scales: { + x: { + scale: customScale, + viewport: { domain: [0, 1] }, + }, + y: { scale: scaleLinear().domain([0, 1]) }, }, - y: { scale: scaleLinear().domain([0, 1]) }, }), { width: 480, height: 260 }, ), @@ -455,8 +492,10 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY([0, 10])], - x: { scale: customScale }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: customScale }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width: 480, height: 260 }, @@ -485,8 +524,10 @@ describe('configured scales', () => { y: 'value', }), ], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) @@ -499,8 +540,10 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [barY([12])], - x: { scale: scaleBand }, - y: { scale: scaleLog }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLog }, + }, }), { width: 480, height: 260 }, ), @@ -511,8 +554,10 @@ describe('configured scales', () => { const scene = createChartScene( defineChart({ marks: [lineY([])], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) @@ -526,8 +571,10 @@ describe('configured scales', () => { createChartScene( defineChart({ marks: [lineY([1, 2, 3])], - x: { scale: (() => 42) as never }, - y: { scale: scaleLinear }, + scales: { + x: { scale: (() => 42) as never }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ), @@ -546,8 +593,10 @@ describe('configured scales', () => { y: 'value', }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ), @@ -593,11 +642,13 @@ describe('configured scales', () => { }, ), ], - x: { - scale: xSource, - }, - y: { - scale: scaleLinear().domain([0, 10]), + scales: { + x: { + scale: xSource, + }, + y: { + scale: scaleLinear().domain([0, 10]), + }, }, }) const scene = createChartScene(definition, { @@ -634,8 +685,10 @@ describe('configured scales', () => { }, ), ], - x: { scale: scaleUtc().domain([start, end]) }, - y: { scale: xSource }, + scales: { + x: { scale: scaleUtc().domain([start, end]) }, + y: { scale: xSource }, + }, }), { width: 480, height: 260 }, ) @@ -662,8 +715,10 @@ describe('configured scales', () => { }, ), ], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 320, height: 180 }, ) @@ -681,7 +736,7 @@ describe('configured scales', () => { width: 480, height: 260, }), - ).toThrow(/requires a configured scale/) + ).toThrow(/must define reserved `x` and `y` entries/) }) it('supports explicit null axes only for unused dimensions', () => { @@ -689,8 +744,10 @@ describe('configured scales', () => { defineChart({ marks: [], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 480, height: 260 }, ) @@ -701,7 +758,10 @@ describe('configured scales', () => { const oneDimensional = createChartScene( defineChart({ marks: [ruleY([1, 2])], - y: { scale: scaleLinear }, + scales: { + x: null, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) @@ -712,16 +772,52 @@ describe('configured scales', () => { expect(() => createChartScene( - // @ts-expect-error Materialized x channels cannot omit their scale. + // @ts-expect-error Materialized x channels cannot use a null reserved scale. defineChart({ marks: [lineY([])], - x: null, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: null, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 480, height: 260 }, ), ).toThrow(/cannot be null/) }) + + it('keeps configured scales for deferred custom marks and erased definitions', () => { + const deferred = createMark(() => ({ + id: 'deferred', + channels: {}, + render: () => ({ nodes: [] }), + })) + const deferredScene = createChartScene( + defineChart({ + marks: [deferred], + guides: false, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, + }), + { width: 480, height: 260 }, + ) + expect(deferredScene.scales.x.type).toBe('configured') + expect(deferredScene.scales.y.type).toBe('configured') + + const erasedPositionless = { + marks: [], + guides: false, + scales: { + x: { scale: scaleLinear() }, + y: null, + }, + } as unknown as ChartSpec + expect( + createChartScene(erasedPositionless, { width: 480, height: 260 }).scales.x + .type, + ).toBe('configured') + }) }) function mutableDomainScale( diff --git a/packages/charts-core/src/crosshair.test.ts b/packages/charts-core/src/crosshair.test.ts index 1466c2e0..f9b6d6e2 100644 --- a/packages/charts-core/src/crosshair.test.ts +++ b/packages/charts-core/src/crosshair.test.ts @@ -102,8 +102,10 @@ describe('crosshair', () => { inset: 4, }), ], - x: { scale: scaleBand().domain(['A', 'B']).padding(0.18) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']).padding(0.18) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -165,8 +167,10 @@ describe('crosshair', () => { layout: group(), }), ], - x: { scale: scaleBand().domain(['A']).padding(0.18) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']).padding(0.18) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -188,8 +192,10 @@ describe('crosshair', () => { layout: group(), }), ], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleBand().domain(['A']).padding(0.18) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleBand().domain(['A']).padding(0.18) }, + }, guides: false, focusRing: false, }), @@ -297,8 +303,10 @@ describe('crosshair', () => { y: { label: { format: (value) => `y=${value}` } }, }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -370,8 +378,10 @@ describe('crosshair', () => { y: false, }), ], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleBand().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleBand().domain(['A']) }, + }, guides: false, focusRing: false, }), @@ -411,8 +421,10 @@ describe('crosshair', () => { }), }), ], - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 420, height: 180 }, ) @@ -472,11 +484,13 @@ describe('crosshair', () => { dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' }), crosshair({ y: false }), ], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate: 20 }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate: 20 }, + }, + y: { scale: scaleLinear().domain([0, 2]) }, }, - y: { scale: scaleLinear().domain([0, 2]) }, guides: false, }), { width: 240, height: 160 }, @@ -529,21 +543,25 @@ describe('crosshair', () => { y: false, }), ], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { - ticks: { - count: 2, - format: (value) => `${data[0]!.facet}:${value}`, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { + ticks: { + count: 2, + format: (value) => `${data[0]!.facet}:${value}`, + }, }, }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, }), }), ], - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 420, height: 180 }, ) diff --git a/packages/charts-core/src/cursor.test.ts b/packages/charts-core/src/cursor.test.ts index 676222bb..35dee79f 100644 --- a/packages/charts-core/src/cursor.test.ts +++ b/packages/charts-core/src/cursor.test.ts @@ -41,8 +41,10 @@ function numericScene(width = 320, height = 180) { return createChartScene( defineChart({ marks: [lineY(numericRows, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width, height }, @@ -364,13 +366,15 @@ describe('chart cursor projection', () => { const scene = createChartScene( defineChart({ marks: [lineY(numericRows, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 10]), - viewport: { domain: [2, 8], translate: 24 }, - }, - y: { - scale: scaleLinear().domain([0, 10]), - viewport: { domain: [2, 8], translate: -16 }, + scales: { + x: { + scale: scaleLinear().domain([0, 10]), + viewport: { domain: [2, 8], translate: 24 }, + }, + y: { + scale: scaleLinear().domain([0, 10]), + viewport: { domain: [2, 8], translate: -16 }, + }, }, guides: false, }), @@ -462,8 +466,10 @@ describe('chart cursor projection', () => { { x: 'x', y: 'y' }, ), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width: 320, height: 180 }, @@ -513,8 +519,10 @@ describe('chart cursor projection', () => { axes: 'cell', chart: (data) => ({ marks: [lineY(data, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, margin: 0, }), @@ -577,10 +585,12 @@ describe('focus cursor semantics', () => { key: 'id', }), ], - x: { - scale: scaleUtc().domain([instant, rows[2]!.date]), + scales: { + x: { + scale: scaleUtc().domain([instant, rows[2]!.date]), + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, guides: false, }), { width: 320, height: 180 }, @@ -684,13 +694,15 @@ describe('focus cursor semantics', () => { key: 'id', }), ], - x: { - scale: scaleUtc().domain([ - instant, - new Date(instant.getTime() + 86_400_000), - ]), + scales: { + x: { + scale: scaleUtc().domain([ + instant, + new Date(instant.getTime() + 86_400_000), + ]), + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, guides: false, }), { width: 320, height: 180 }, @@ -820,8 +832,10 @@ if (false) { const controller = createChartCursor() const definition = defineChart({ marks: [mark], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, cursor: { mode: 'free', use: cursorHost, diff --git a/packages/charts-core/src/difference.test.ts b/packages/charts-core/src/difference.test.ts index de0e0266..7f595443 100644 --- a/packages/charts-core/src/difference.test.ts +++ b/packages/charts-core/src/difference.test.ts @@ -84,8 +84,10 @@ describe('difference marks', () => { comparisonStroke: '#475569', }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([-1, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([-1, 2]) }, + }, }) const scene = createChartScene(definition, { width: 600, height: 360 }) const nodes = flatten(scene.nodes) @@ -144,8 +146,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleUtc().domain([rows[0]!.at, rows[1]!.at]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleUtc().domain([rows[0]!.at, rows[1]!.at]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) const crossingDate = new Date('2026-01-02T00:00:00Z') @@ -187,8 +191,10 @@ describe('difference marks', () => { }, }), ], - x: { scale: scaleLog().domain([1, 100]) }, - y: { scale: scaleLog().domain([1, 100]) }, + scales: { + x: { scale: scaleLog().domain([1, 100]) }, + y: { scale: scaleLog().domain([1, 100]) }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) const crossingValue = 100 ** (1 / 3) @@ -226,8 +232,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleLog().domain([1, 100]) }, - y: { scale: scaleLog().domain([1, 100]) }, + scales: { + x: { scale: scaleLog().domain([1, 100]) }, + y: { scale: scaleLog().domain([1, 100]) }, + }, }), { width: 480, height: 280 }, ) @@ -264,8 +272,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleLog().domain([1, 100]) }, - y: { scale: scaleLog().domain([1, 100]) }, + scales: { + x: { scale: scaleLog().domain([1, 100]) }, + y: { scale: scaleLog().domain([1, 100]) }, + }, }), { width: 480, height: 280 }, ) @@ -297,8 +307,10 @@ describe('difference marks', () => { createChartScene( defineChart({ marks: [vertical], - x: { scale: scaleBand().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleBand().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }), { width: 480, height: 280 }, ), @@ -307,8 +319,10 @@ describe('difference marks', () => { createChartScene( defineChart({ marks: [horizontal], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleBand().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleBand().domain([0, 1]) }, + }, }), { width: 480, height: 280 }, ), @@ -331,8 +345,10 @@ describe('difference marks', () => { ...(negativeFill !== undefined ? { negativeFill } : {}), }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }), { width: 480, height: 280 }, ) @@ -370,8 +386,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([-1, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([-1, 3]) }, + }, }), { width: 480, height: 280 }, ) @@ -411,8 +429,10 @@ describe('difference marks', () => { y2: 'primary', }), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([-1, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([-1, 1]) }, + }, }), { width: 480, height: 280 }, ) @@ -448,8 +468,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([-2, 4]) }, - y: { scale: scaleLinear().domain([-1, 3]) }, + scales: { + x: { scale: scaleLinear().domain([-2, 4]) }, + y: { scale: scaleLinear().domain([-1, 3]) }, + }, }), { width: 480, height: 280 }, ) @@ -484,8 +506,10 @@ describe('difference marks', () => { }, }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleUtc().domain([rows[0]!.at, rows[1]!.at]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleUtc().domain([rows[0]!.at, rows[1]!.at]) }, + }, }) const mark = definition.marks[0]! const scene = createChartScene(definition, { width: 480, height: 280 }) @@ -537,8 +561,10 @@ describe('difference marks', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }), { width: 480, height: 280 }, ) diff --git a/packages/charts-core/src/dodge.test.ts b/packages/charts-core/src/dodge.test.ts index ac8fd82a..06af9c9e 100644 --- a/packages/charts-core/src/dodge.test.ts +++ b/packages/charts-core/src/dodge.test.ts @@ -33,7 +33,10 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - x: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: null, + }, }), { width: 100, height: 100 }, ) @@ -70,7 +73,10 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - x: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: null, + }, }), { width: 200, height: 120 }, ) @@ -110,7 +116,10 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - y: { scale: scaleBand().domain(['A', 'B']) }, + scales: { + x: null, + y: { scale: scaleBand().domain(['A', 'B']) }, + }, }), { width: 120, height: 100 }, ) @@ -156,7 +165,10 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - x: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: null, + }, }), { width: 100, height: 80 }, ) @@ -189,7 +201,10 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - x: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: null, + }, }) const first = createChartScene(definition, { width: 160, height: 80 }) const second = createChartScene(definition, { width: 160, height: 80 }) @@ -230,13 +245,18 @@ describe('dot dodge layouts', () => { ], guides: false, margin: 0, - x: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: null, + }, }), }), ], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 200, height: 100 }, ) @@ -268,7 +288,10 @@ describe('dot dodge layouts', () => { defineChart({ marks: [mark], guides: false, - x: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: null, + }, }), { width: 120, height: 80 }, ) @@ -324,7 +347,10 @@ describe('dot dodge layouts', () => { layout: dodgeY(), }), ], - x: { scale: scaleLinear() }, + scales: { + x: { scale: scaleLinear() }, + y: null, + }, }), { width: 100, height: 100 }, ), diff --git a/packages/charts-core/src/dot.ts b/packages/charts-core/src/dot.ts index 1c38c922..62e0ea91 100644 --- a/packages/charts-core/src/dot.ts +++ b/packages/charts-core/src/dot.ts @@ -31,7 +31,9 @@ import type { ChartNumericScale, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, ResolvedScale, SceneNode, } from './types' @@ -55,52 +57,87 @@ export interface DotOptions states?: readonly ChartMarkState>[] } -type DotOptionLayout = TOptions extends { - readonly layout?: infer TLayout -} - ? NonNullable - : never +type DotCallOptions< + TDatum, + TXChannel, + TYChannel, + TLayout, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + DotOptions>, + { + x?: TXChannel + y?: TYChannel + layout?: TLayout + xScale?: TXScaleId + yScale?: TYScaleId + } +> -type DotPointX = [DotOptionLayout] extends [never] - ? OptionChannelOutput - : DotOptionLayout extends DotLayout<'x', infer TAnchor> +type DotPointX = [NonNullable] extends [ + never, +] + ? MarkChannelOutput + : NonNullable extends DotLayout<'x', infer TAnchor> ? TAnchor - : OptionChannelOutput + : MarkChannelOutput -type DotPointY = [DotOptionLayout] extends [never] - ? OptionChannelOutput - : DotOptionLayout extends DotLayout<'y', infer TAnchor> +type DotPointY = [NonNullable] extends [ + never, +] + ? MarkChannelOutput + : NonNullable extends DotLayout<'y', infer TAnchor> ? TAnchor - : OptionChannelOutput + : MarkChannelOutput -type DotScaleX = [DotOptionLayout] extends [never] - ? OptionChannelOutput - : DotOptionLayout extends DotLayout<'x'> +type DotScaleX = [NonNullable] extends [ + never, +] + ? MarkChannelOutput + : NonNullable extends DotLayout<'x'> ? never - : OptionChannelOutput + : MarkChannelOutput -type DotScaleY = [DotOptionLayout] extends [never] - ? OptionChannelOutput - : DotOptionLayout extends DotLayout<'y'> +type DotScaleY = [NonNullable] extends [ + never, +] + ? MarkChannelOutput + : NonNullable extends DotLayout<'y'> ? never - : OptionChannelOutput + : MarkChannelOutput export function dot( source: Iterable, ): ChartMark export function dot< TDatum, - const TOptions extends DotOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TLayout extends DotLayout | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | DotCallOptions< + TDatum, + TXChannel, + TYChannel, + TLayout, + TXScaleId, + TYScaleId + > + | undefined, ): CartesianChartMark< TDatum, - DotPointX, - DotPointY, - DotScaleX, - DotScaleY, - TOptions + DotPointX, + DotPointY, + DotScaleX, + DotScaleY, + MarkScaleBindings > export function dot( source: Iterable, diff --git a/packages/charts-core/src/export.test.ts b/packages/charts-core/src/export.test.ts index a728069e..0e64ed83 100644 --- a/packages/charts-core/src/export.test.ts +++ b/packages/charts-core/src/export.test.ts @@ -96,8 +96,10 @@ describe('optional export', () => { dot([{ x: 1, y: 2 }], { x: 'x', y: 'y' }), crosshair({ marker: true }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, guides: false, }), { width: 300, height: 180 }, diff --git a/packages/charts-core/src/facet.test.ts b/packages/charts-core/src/facet.test.ts index adb724f8..e1d0fd43 100644 --- a/packages/charts-core/src/facet.test.ts +++ b/packages/charts-core/src/facet.test.ts @@ -43,8 +43,10 @@ describe('facets', () => { firstGroup = groupRows[0] return { marks: [lineY(children, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, guides: false, margin: 0, } @@ -169,13 +171,15 @@ describe('facets', () => { columns: 2, chart: (group) => ({ marks: [lineY(group, { x: 'x', y: 'y' })], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { label: 'Horizontal' }, - }, - y: { - scale: scaleLinear().domain([0, 6]), - axis: { label: 'Vertical' }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { label: 'Horizontal' }, + }, + y: { + scale: scaleLinear().domain([0, 6]), + axis: { label: 'Vertical' }, + }, }, }), }), @@ -226,13 +230,15 @@ describe('facets', () => { columns: 2, chart: (group) => ({ marks: [lineY(group, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { label: 'Horizontal' }, - }, - y: { - scale: scaleLinear().domain([0, 6]), - axis: { label: 'Vertical' }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { label: 'Horizontal' }, + }, + y: { + scale: scaleLinear().domain([0, 6]), + axis: { label: 'Vertical' }, + }, }, }), }), @@ -301,13 +307,15 @@ describe('facets', () => { gap: 12, chart: (group) => ({ marks: [lineY(group, { x: 'x', y: 'y' })], - x: { - scale: scaleLinear().domain([3, 20]), - axis: { ticks: { count: 5 } }, - }, - y: { - scale: scaleLinear().domain([2, 14]), - axis: { ticks: { count: 4 } }, + scales: { + x: { + scale: scaleLinear().domain([3, 20]), + axis: { ticks: { count: 5 } }, + }, + y: { + scale: scaleLinear().domain([2, 14]), + axis: { ticks: { count: 4 } }, + }, }, }), }), @@ -405,8 +413,10 @@ describe('facets', () => { ], guides: false, margin: 0, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), }) const wide = createChartScene(definition, { width: 720, height: 520 }) @@ -450,8 +460,10 @@ describe('facets', () => { axes: 'cell', chart: (rows) => ({ marks: [barY(rows, { x: 'x', y: 'value' })], - x: { scale: scaleBand().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, margin: 0, }), @@ -515,8 +527,10 @@ describe('facets', () => { ), barY(rows, { x: 'x', y: 'value' }), ], - x: { scale: scaleBand().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, margin: 0, }), @@ -571,8 +585,10 @@ describe('facets', () => { }), barY(rows, { x: 'x', y: 'value' }), ], - x: { scale: scaleBand().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, margin: 0, }), @@ -633,8 +649,10 @@ describe('facets', () => { ), barY(rows, { x: 'x', y: 'value' }), ], - x: { scale: scaleBand().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, margin: 0, }), @@ -688,9 +706,11 @@ describe('facets', () => { axes, chart: (group, { key }) => ({ marks: [lineY(group, { y: 'value' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { - scale: scaleLinear().domain(key === 'A' ? [0, 2] : [0, 20]), + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { + scale: scaleLinear().domain(key === 'A' ? [0, 2] : [0, 20]), + }, }, }), }) @@ -720,19 +740,21 @@ describe('facets', () => { columns: 2, chart: (group, { key }) => ({ marks: [lineY(group, { y: 'value' })], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { - ticks: { values: [0, 1] }, - tickLabels: { - fontSize: ({ index }: { index: number }) => - index === 0 && (!different || key === 'A') ? 13 : undefined, - anchor: ({ index }: { index: number }) => - index === 0 ? 'start' : undefined, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { + ticks: { values: [0, 1] }, + tickLabels: { + fontSize: ({ index }: { index: number }) => + index === 0 && (!different || key === 'A') ? 13 : undefined, + anchor: ({ index }: { index: number }) => + index === 0 ? 'start' : undefined, + }, }, }, + y: { scale: scaleLinear().domain([0, 2]) }, }, - y: { scale: scaleLinear().domain([0, 2]) }, }), }) @@ -813,8 +835,10 @@ describe('facets', () => { by: 'group', chart: (group) => ({ marks: [lineY(group, { y: 'value' })], - x: null, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: null, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), }), { width: 360, height: 260 }, diff --git a/packages/charts-core/src/facet.ts b/packages/charts-core/src/facet.ts index 74db5558..0d2963f1 100644 --- a/packages/charts-core/src/facet.ts +++ b/packages/charts-core/src/facet.ts @@ -734,7 +734,7 @@ function sameAxis( function positionScaleOptions( definition: StaticChartDefinition, ): Readonly> { - return definition.scales ?? { x: definition.x, y: definition.y } + return definition.scales } function hasUnsupportedOuterAxis(definition: StaticChartDefinition): boolean { @@ -749,12 +749,9 @@ function hasUnsupportedOuterAxis(definition: StaticChartDefinition): boolean { } function sameAxisTicks( - left: Exclude< - NonNullable['axis']>, - false - >['ticks'], + left: Exclude, false>['ticks'], right: Exclude< - NonNullable['axis']>, + NonNullable, false >['ticks'], ): boolean { @@ -770,11 +767,11 @@ function sameAxisTicks( function sameAxisTickLabels( left: Exclude< - NonNullable['axis']>, + NonNullable, false >['tickLabels'], right: Exclude< - NonNullable['axis']>, + NonNullable, false >['tickLabels'], ): boolean { diff --git a/packages/charts-core/src/focus-guide.test.ts b/packages/charts-core/src/focus-guide.test.ts index 233c3f91..e565fcda 100644 --- a/packages/charts-core/src/focus-guide.test.ts +++ b/packages/charts-core/src/focus-guide.test.ts @@ -255,8 +255,10 @@ describe('focus guides', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['Q1', 'Q2']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['Q1', 'Q2']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -306,8 +308,10 @@ describe('focus guides', () => { key: (_value, { index }) => index, }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, focusRing: false, }), @@ -399,8 +403,10 @@ function guideScene( }), guide, ], - x: { scale: scaleBand().domain(['Q1', 'Q2', 'Q3']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['Q1', 'Q2', 'Q3']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, margin: { top: 28, right: 48, bottom: 42, left: 52 }, diff --git a/packages/charts-core/src/focus-layer.ts b/packages/charts-core/src/focus-layer.ts index 242805f2..dfff0e1f 100644 --- a/packages/charts-core/src/focus-layer.ts +++ b/packages/charts-core/src/focus-layer.ts @@ -370,9 +370,6 @@ export function matchesFocusAnchor( return sameFocusedPoint(candidate, focus.primary) } -/** @deprecated Use `matchesFocusAnchor`; points are valid focus anchors. */ -export const matchesFocusPoint = matchesFocusAnchor - function sameFocusedPoint(left: ChartFocusAnchor, right: ChartFocusAnchor) { if (left === right || left.key === right.key) return true if (!Object.is(left.datum, right.datum)) return false diff --git a/packages/charts-core/src/focus-mark.test.ts b/packages/charts-core/src/focus-mark.test.ts index f835098b..ade43e52 100644 --- a/packages/charts-core/src/focus-mark.test.ts +++ b/packages/charts-core/src/focus-mark.test.ts @@ -41,10 +41,12 @@ describe('focus-filtered marks', () => { key: 'id', }), ], - x: { - scale: scaleBand().domain(['A', 'B']).padding(0.1), + scales: { + x: { + scale: scaleBand().domain(['A', 'B']).padding(0.1), + }, + y: { scale: scaleLinear().domain([0, 20]) }, }, - y: { scale: scaleLinear().domain([0, 20]) }, }), { width: 480, height: 260 }, ) @@ -54,14 +56,20 @@ describe('focus-filtered marks', () => { const xScene = createChartScene( defineChart({ marks: [bandX([{ x: 5 }], { x: 'x', width: 1 })], - x: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: null, + }, }), { width: 240, height: 160 }, ) const yScene = createChartScene( defineChart({ marks: [bandY([{ y: 5 }], { y: 'y', height: 2 })], - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: null, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 240, height: 160 }, ) @@ -74,14 +82,20 @@ describe('focus-filtered marks', () => { inset: 2, }), ], - x: { scale: scaleBand().domain(['A']) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: null, + }, }), { width: 240, height: 160 }, ) const zero = createChartScene( defineChart({ marks: [bandY([{ y: 5 }], { y: 'y', height: 0 })], - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: null, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 240, height: 160 }, ) @@ -114,11 +128,13 @@ describe('focus-filtered marks', () => { ], }), ], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate: 25 }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate: 25 }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, clip: true, }), @@ -238,8 +254,10 @@ describe('focus-filtered marks', () => { }), dot(data, { id: 'points', x: 'x', y: 'y', key: 'id' }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 30]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 30]) }, + }, }), { width: 360, height: 220 }, ) @@ -285,8 +303,10 @@ describe('focus-filtered marks', () => { const rulesOnly = createChartScene( defineChart({ marks: [ruleX([1, 2]), ruleY([10, 20])], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 30]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 30]) }, + }, focusRing: false, }), { width: 360, height: 220 }, @@ -312,8 +332,10 @@ describe('focus-filtered marks', () => { }), dot(data, { id: 'points', x: 'x', y: 'y', key: 'id' }), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 30]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 30]) }, + }, }), { width: 360, height: 220 }, ) @@ -373,8 +395,10 @@ describe('focus-filtered marks', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 5]) }, - y: { scale: scaleLinear().domain([0, 30]) }, + scales: { + x: { scale: scaleLinear().domain([0, 5]) }, + y: { scale: scaleLinear().domain([0, 30]) }, + }, }), { width: 360, height: 220 }, ) @@ -419,8 +443,10 @@ describe('focus-filtered marks', () => { }), dot(data, { id: 'points', x: 'x', y: 'y', key: 'id' }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 320, height: 200 }, ) @@ -461,8 +487,10 @@ describe('focus-filtered marks', () => { const resolved = createChartScene( defineChart({ marks: [barY(rows, { x: 'category', y: 'value' })], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 20]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 20]) }, + }, focusRing: false, }), { width: 240, height: 160 }, @@ -503,8 +531,10 @@ describe('focus-filtered marks', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 20]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 20]) }, + }, focusRing: false, }), { width: 320, height: 180 }, @@ -596,8 +626,10 @@ describe('focus-filtered marks', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, focusRing: false, }), { width: 320, height: 180 }, @@ -658,8 +690,10 @@ describe('focus-filtered marks', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 20]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 20]) }, + }, focusRing: false, }), { width: 320, height: 180 }, @@ -702,8 +736,10 @@ describe('focus-filtered marks', () => { }), ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, }), @@ -804,8 +840,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 360, height: 220 }, ) @@ -865,8 +903,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 240, height: 180 }, ) @@ -887,8 +927,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleBand().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleBand().domain(['A']) }, + }, }), { width: 240, height: 180 }, ) @@ -946,8 +988,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 240, height: 180 }, ) @@ -968,8 +1012,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleBand().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleBand().domain(['A']) }, + }, }), { width: 240, height: 180 }, ) @@ -1026,8 +1072,10 @@ describe('inline mark states', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }), { width: 360, height: 220 }, ) diff --git a/packages/charts-core/src/frame.test.ts b/packages/charts-core/src/frame.test.ts index 1e5ca5cb..8e29dc3b 100644 --- a/packages/charts-core/src/frame.test.ts +++ b/packages/charts-core/src/frame.test.ts @@ -16,8 +16,10 @@ describe('frame mark', () => { }), ], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 320, height: 180 }, ) diff --git a/packages/charts-core/src/geo.test.ts b/packages/charts-core/src/geo.test.ts index e3628fdc..b8661845 100644 --- a/packages/charts-core/src/geo.test.ts +++ b/packages/charts-core/src/geo.test.ts @@ -110,6 +110,7 @@ describe('geoShape', () => { }, }), ], + scales: { x: null, y: null }, margin: 10, color: { scale: scaleOrdinal<'west' | 'east', string>() @@ -161,6 +162,7 @@ describe('geoShape', () => { fill: (region) => region.properties.fill, }), ], + scales: { x: null, y: null }, }) const scene = createChartScene(definition, { width: 200, height: 100 }) @@ -182,6 +184,7 @@ describe('geoShape', () => { fill: '#0ea5e9', }), ], + scales: { x: null, y: null }, }) const constantDefinition = defineChart({ marks: [ @@ -191,6 +194,7 @@ describe('geoShape', () => { fill: '#0ea5e9', }), ], + scales: { x: null, y: null }, }) const variableScene = createChartScene(variableDefinition, { width: 100, @@ -234,6 +238,7 @@ describe('geoShape', () => { projection: { type: geoIdentity, fit: 'data' }, }), ], + scales: { x: null, y: null }, color: { domain: ['route'], range: ['#0ea5e9'] }, }) @@ -266,6 +271,7 @@ describe('geoShape', () => { projection: () => null, }), ], + scales: { x: null, y: null }, color: { domain: ['mixed'], range: ['#0ea5e9'] }, }) @@ -296,6 +302,7 @@ describe('geoShape', () => { }, ), ], + scales: { x: null, y: null }, color: { scale: scaleLinear, domain: [0, 100], @@ -363,6 +370,7 @@ describe('geoShape', () => { fill: 'none', }), ], + scales: { x: null, y: null }, }) createChartScene(definition, { width: 240, height: 140 }) diff --git a/packages/charts-core/src/hexagon.ts b/packages/charts-core/src/hexagon.ts index adbdc525..ed68518b 100644 --- a/packages/charts-core/src/hexagon.ts +++ b/packages/charts-core/src/hexagon.ts @@ -19,7 +19,9 @@ import type { ChartNumericScale, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -41,23 +43,46 @@ export interface HexagonOptions strokeWidth?: number } +type HexagonCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + HexagonOptions, + { + x?: TXChannel + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + /** Draws pointy-topped, fixed-pixel hexagons at scaled x/y positions. */ export function hexagon( source: Iterable, ): ChartMark export function hexagon< TDatum, - const TOptions extends HexagonOptions | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | HexagonCallOptions + | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - TOptions + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkScaleBindings > export function hexagon( source: Iterable, diff --git a/packages/charts-core/src/hierarchy-sunburst.test.ts b/packages/charts-core/src/hierarchy-sunburst.test.ts index 2af49bc2..76e590ef 100644 --- a/packages/charts-core/src/hierarchy-sunburst.test.ts +++ b/packages/charts-core/src/hierarchy-sunburst.test.ts @@ -27,7 +27,7 @@ const pathRows = [ ] satisfies PathRow[] function render( - mark: PolarMark, number, number>, + mark: PolarMark, number, number, never, never>, options: { width?: number height?: number @@ -40,10 +40,12 @@ function render( marks: [ polar({ marks: [mark], + scales: { angle: null, radius: null }, startAngle: options.startAngle, endAngle: options.endAngle, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -70,7 +72,7 @@ describe('sunburst', () => { const byId = new Map(scene.points.map((point) => [point.datum.id, point])) expectTypeOf(mark).toEqualTypeOf< - PolarMark, number, number> + PolarMark, number, number, never, never> >() expect(scene.points).toHaveLength(4) expect(byId.has('/root')).toBe(false) diff --git a/packages/charts-core/src/hierarchy-sunburst.ts b/packages/charts-core/src/hierarchy-sunburst.ts index e9cda6f5..a48ac106 100644 --- a/packages/charts-core/src/hierarchy-sunburst.ts +++ b/packages/charts-core/src/hierarchy-sunburst.ts @@ -111,7 +111,7 @@ export function sunburst< readonly nodeId?: never readonly parentId?: never }, -): PolarMark, number, number> +): PolarMark, number, number, never, never> export function sunburst< TDatum, const TNodeId extends TransformValue, @@ -124,11 +124,11 @@ export function sunburst< readonly path?: never readonly delimiter?: never }, -): PolarMark, number, number> +): PolarMark, number, number, never, never> export function sunburst( source: Iterable, options: SunburstOptions, -): PolarMark, number, number> { +): PolarMark, number, number, never, never> { const hierarchyOptions: FlatHierarchyOptions = options.path !== undefined ? { path: options.path, delimiter: options.delimiter } diff --git a/packages/charts-core/src/hierarchy-treemap.test.ts b/packages/charts-core/src/hierarchy-treemap.test.ts index 3eb7efc3..923c995b 100644 --- a/packages/charts-core/src/hierarchy-treemap.test.ts +++ b/packages/charts-core/src/hierarchy-treemap.test.ts @@ -42,6 +42,7 @@ function scene( return createChartScene( defineChart({ marks: [mark], + scales: { x: null, y: null }, guides: false, focusRing: false, margin, @@ -185,6 +186,7 @@ describe('treemap', () => { inset: 0, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -211,6 +213,7 @@ describe('treemap', () => { }, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -249,6 +252,7 @@ describe('treemap', () => { }, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -297,6 +301,7 @@ describe('treemap', () => { inset: 0, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -341,6 +346,7 @@ describe('treemap', () => { inset: 0, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -368,6 +374,7 @@ describe('treemap', () => { method: 'dice', }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, @@ -545,6 +552,7 @@ describe('treemap', () => { createChartScene( defineChart({ marks: [malformed], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, diff --git a/packages/charts-core/src/index.ts b/packages/charts-core/src/index.ts index cc25888d..192eb680 100644 --- a/packages/charts-core/src/index.ts +++ b/packages/charts-core/src/index.ts @@ -371,8 +371,6 @@ export type { ChartLineStateStyle, ChartRectStateStyle, ChartTextStateStyle, - ChartMarkX, - ChartMarkY, ChartMotionContext, ChartMotionDefinition, ChartMotionPhase, diff --git a/packages/charts-core/src/interaction-brush.test.ts b/packages/charts-core/src/interaction-brush.test.ts index 568b4689..759c9aea 100644 --- a/packages/charts-core/src/interaction-brush.test.ts +++ b/packages/charts-core/src/interaction-brush.test.ts @@ -316,8 +316,10 @@ function definition( ) { return defineChart({ marks: [dot(rows, { x: 'date', y: 'value' })], - x: { scale: scaleUtc().domain(dates), reverse }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleUtc().domain(dates), reverse }, + y: { scale: scaleLinear }, + }, controls: [ brushX({ id: 'window', diff --git a/packages/charts-core/src/interaction-cursor.test.ts b/packages/charts-core/src/interaction-cursor.test.ts index 2084a1c0..815cf0c2 100644 --- a/packages/charts-core/src/interaction-cursor.test.ts +++ b/packages/charts-core/src/interaction-cursor.test.ts @@ -66,8 +66,10 @@ describe('continuousCursor', () => { { x: 'x', y: 'y' }, ), ], - x: { scale: scaleUtc().domain(dates) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleUtc().domain(dates) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, controls: [behavior], }) const scene = createChartScene(definition, { width: 480, height: 240 }) @@ -296,8 +298,10 @@ describe('continuousCursor', () => { { x: 'x', y: 'y' }, ), ], - x: { scale: scaleUtc().domain([dates[1], dates[0]]) }, - y: { scale: scaleLinear().domain([10, 0]) }, + scales: { + x: { scale: scaleUtc().domain([dates[1], dates[0]]) }, + y: { scale: scaleLinear().domain([10, 0]) }, + }, controls: [continuousCursor({ id: 'temporal', position })], keyboard: false, }) @@ -388,8 +392,10 @@ function numericDefinition( ) { return defineChart({ marks: [dot(rows, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, controls: [ continuousCursor({ id: 'free', diff --git a/packages/charts-core/src/interaction-handle.test.ts b/packages/charts-core/src/interaction-handle.test.ts index 3a55b7e2..47da15cb 100644 --- a/packages/charts-core/src/interaction-handle.test.ts +++ b/packages/charts-core/src/interaction-handle.test.ts @@ -78,8 +78,10 @@ describe('handleX', () => { { x: 'date', y: 'team' }, ), ], - x: { scale: scaleUtc().domain(dates) }, - y: { scale: scaleBand().domain(teams) }, + scales: { + x: { scale: scaleUtc().domain(dates) }, + y: { scale: scaleBand().domain(teams) }, + }, controls: [ handleX({ value: controlledSignal>( @@ -433,8 +435,10 @@ function edgeDefinition( ) { return defineChart({ marks: [dot(rows, { x: 'date', y: 'value' })], - x: { scale: scaleUtc().domain(dates) }, - y: { scale: scaleLinear().domain([0, dates.length - 1]) }, + scales: { + x: { scale: scaleUtc().domain(dates) }, + y: { scale: scaleLinear().domain([0, dates.length - 1]) }, + }, controls: [ handleX({ id: options.id ?? 'date', diff --git a/packages/charts-core/src/interaction-zoom.test.ts b/packages/charts-core/src/interaction-zoom.test.ts index 6cb1b792..171c4e63 100644 --- a/packages/charts-core/src/interaction-zoom.test.ts +++ b/packages/charts-core/src/interaction-zoom.test.ts @@ -754,8 +754,10 @@ function numericDefinition( ) { return defineChart({ marks: [dot(numericRows, { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([window.start, window.end]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([window.start, window.end]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, controls: [ zoomX({ id: 'window', @@ -788,11 +790,13 @@ function temporalDefinition( { x: 'x', y: 'y' }, ), ], - x: { - scale: scaleUtc().domain([window.start, window.end]), - reverse, + scales: { + x: { + scale: scaleUtc().domain([window.start, window.end]), + reverse, + }, + y: { scale: scaleLinear().domain([0, 1]) }, }, - y: { scale: scaleLinear().domain([0, 1]) }, controls: [ zoomX({ id: 'window', diff --git a/packages/charts-core/src/interactive-legend.test.ts b/packages/charts-core/src/interactive-legend.test.ts index 06f5a30a..ef6f2e7a 100644 --- a/packages/charts-core/src/interactive-legend.test.ts +++ b/packages/charts-core/src/interactive-legend.test.ts @@ -53,8 +53,10 @@ describe('interactiveColorLegend', () => { const withoutLegend = createChartScene( defineChart({ marks: [lineY(rows, { x: 'x', y: 'y', color: 'series' })], - x: { scale: scaleLinear }, - y: { scale: scaleLinear().domain([0, 900]) }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear().domain([0, 900]) }, + }, color: { domain: ['Manufacturing', 'Construction'], range: ['#2563eb', '#f97316'], @@ -152,8 +154,10 @@ describe('interactiveColorLegend', () => { createChartScene( defineChart({ marks: [lineY(rows, { x: 'x', y: 'y', z: 'series', color: 'y' })], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, color: { scale: scaleLinear().range(['#eff6ff', '#1d4ed8']), legend: interactiveColorLegend({ @@ -247,8 +251,10 @@ function createDefinition( color: 'series', }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear().domain([0, 900]) }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear().domain([0, 900]) }, + }, color: { domain: ['Manufacturing', 'Construction'], range: ['#2563eb', '#f97316'], diff --git a/packages/charts-core/src/line.test.ts b/packages/charts-core/src/line.test.ts index be20bae2..7d897ad3 100644 --- a/packages/charts-core/src/line.test.ts +++ b/packages/charts-core/src/line.test.ts @@ -46,16 +46,20 @@ describe('lineX mark', () => { const xScene = createChartScene( defineChart({ marks: [lineX(values)], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) const yScene = createChartScene( defineChart({ marks: [lineY(values)], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 260 }, ) @@ -89,8 +93,10 @@ describe('lineX mark', () => { key: 'id', }), ], - x: { scale: scaleLinear }, - y: { scale: scaleBand }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleBand }, + }, }) const scene = createChartScene(definition, { width: 480, height: 260 }) @@ -125,8 +131,10 @@ describe('lineX mark', () => { }) const definition = defineChart({ marks: [mark], - x: { scale: scaleLinear().domain([0, 8]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 8]) }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }) const first = createChartScene(definition, { width: 480, height: 260 }) const second = createChartScene(definition, { width: 600, height: 300 }) @@ -177,8 +185,10 @@ describe('lineX mark', () => { curve: d3Curve(curveMonotoneY), }), ], - x: { scale: scaleLinear().domain([0, 8]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 8]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }), { width: 480, height: 260 }, ) @@ -202,8 +212,10 @@ describe('lineX mark', () => { const scene = createChartScene( defineChart({ marks: [lineX(rows, { x: 'value', y: 'at', key: 'id' })], - x: { scale: scaleLinear }, - y: { scale: scaleUtc }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleUtc }, + }, }), { width: 480, height: 260 }, ) diff --git a/packages/charts-core/src/line.ts b/packages/charts-core/src/line.ts index af569466..1734f929 100644 --- a/packages/charts-core/src/line.ts +++ b/packages/charts-core/src/line.ts @@ -20,10 +20,12 @@ import type { ChartLineStateStyle, ChartPoint, ChartValue, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, ChartCurve, VisualChannel, - OptionChannelOutput, } from './types' interface LineOptions @@ -51,6 +53,34 @@ export interface LineXOptions extends LineOptions { y?: Channel } +type LineYCallOptions< + TDatum, + TXChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + LineYOptions>, + { + x?: TXChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + +type LineXCallOptions< + TDatum, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + LineXOptions>, + { + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + interface LineRow { datum: TDatum datumIndex: number @@ -74,17 +104,21 @@ export function lineY( ): ChartMark export function lineY< TDatum, - const TOptions extends LineYOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + LineYCallOptions | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, + MarkChannelOutput, number, - TOptions + MarkScaleBindings > export function lineY( source: Iterable, @@ -121,17 +155,21 @@ export function lineX( ): ChartMark export function lineX< TDatum, - const TOptions extends LineXOptions> | undefined, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + LineXCallOptions | undefined, ): CartesianChartMark< TDatum, number, - OptionChannelOutput, + MarkChannelOutput, number, - OptionChannelOutput, - TOptions + MarkChannelOutput, + MarkScaleBindings > export function lineX( source: Iterable, diff --git a/packages/charts-core/src/link.ts b/packages/charts-core/src/link.ts index 3a1d45f1..e50de21b 100644 --- a/packages/charts-core/src/link.ts +++ b/packages/charts-core/src/link.ts @@ -18,6 +18,8 @@ import type { ChartMarkMotionOptions, ChartPoint, ChartValue, + MarkCallOptions, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -40,13 +42,25 @@ export interface LinkOptions curve?: ChartCurve } -type LinkXOutput = - | ChannelOutput - | ChannelOutput - -type LinkYOutput = - | ChannelOutput - | ChannelOutput +type LinkCallOptions< + TDatum, + TX1Channel, + TY1Channel, + TX2Channel, + TY2Channel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + LinkOptions>, + { + x1: TX1Channel + y1: TY1Channel + x2: TX2Channel + y2: TY2Channel + xScale?: TXScaleId + yScale?: TYScaleId + } +> /** * Draws one independent segment per datum between two scaled positions. @@ -56,17 +70,46 @@ type LinkYOutput = */ export function link< TDatum, - const TOptions extends LinkOptions>, + const TX1Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TY1Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TX2Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TY2Channel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: LinkCallOptions< + TDatum, + TX1Channel, + TY1Channel, + TX2Channel, + TY2Channel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - LinkXOutput, - LinkYOutput, - LinkXOutput, - LinkYOutput, - TOptions + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + | ChannelOutput + | ChannelOutput, + MarkScaleBindings > export function link( source: Iterable, diff --git a/packages/charts-core/src/mark-composite.test.ts b/packages/charts-core/src/mark-composite.test.ts index 529577b6..96274b82 100644 --- a/packages/charts-core/src/mark-composite.test.ts +++ b/packages/charts-core/src/mark-composite.test.ts @@ -45,8 +45,10 @@ describe('compositeMark', () => { ) const definition = defineChart({ marks: [mark], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) @@ -108,8 +110,10 @@ describe('compositeMark', () => { const scene = createChartScene( defineChart({ marks: [mark], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) diff --git a/packages/charts-core/src/mark-decorative.test.ts b/packages/charts-core/src/mark-decorative.test.ts index a3e961f0..713955b3 100644 --- a/packages/charts-core/src/mark-decorative.test.ts +++ b/packages/charts-core/src/mark-decorative.test.ts @@ -51,8 +51,10 @@ describe('decorative mark', () => { key: 'id', }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, }), @@ -84,7 +86,10 @@ describe('decorative mark', () => { const scene = createChartScene( defineChart({ marks: [threshold], - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: null, + y: { scale: scaleLinear().domain([0, 8]) }, + }, guides: false, focusRing: false, }), @@ -138,6 +143,7 @@ describe('decorative mark', () => { const scene = createChartScene( defineChart({ marks: [decorative(resolvedPostDomain)], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -165,8 +171,10 @@ describe('decorative mark', () => { const definition = (mark: typeof source) => defineChart({ marks: [mark], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, focusRing: false, }) @@ -215,8 +223,10 @@ describe('decorative mark', () => { createChartScene( defineChart({ marks: [nested], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, }), diff --git a/packages/charts-core/src/motion.test.ts b/packages/charts-core/src/motion.test.ts index 375cbe94..a8f24f91 100644 --- a/packages/charts-core/src/motion.test.ts +++ b/packages/charts-core/src/motion.test.ts @@ -32,8 +32,10 @@ describe('SVG motion', () => { const scene = createChartScene( defineChart({ marks: [barY(rows, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -72,11 +74,13 @@ describe('SVG motion', () => { transition: { type: 'tween', duration: 100, easing: 'linear' }, }, marks: [lineY(history, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, clip: true, }) @@ -110,8 +114,10 @@ describe('SVG motion', () => { it('keeps definition metadata out of the public scene shape', () => { const definition = defineChart({ marks: [barY(rows, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }) const staticScene = createChartScene(definition, { @@ -130,8 +136,10 @@ describe('SVG motion', () => { const scene = createChartScene( defineChart({ marks: [barY(rows, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -168,8 +176,10 @@ describe('SVG motion', () => { const scene = createChartScene( defineChart({ marks: [barY(rows, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -216,8 +226,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }) const firstDefinition = makeDefinition(20) @@ -304,8 +316,10 @@ describe('SVG motion', () => { points: true, }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, clip: true, }) @@ -462,8 +476,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, clip: true, }) @@ -546,8 +562,10 @@ describe('SVG motion', () => { points: true, }), ], - x: { scale: scaleLinear().domain(xDomain) }, - y: { scale: scaleLinear().domain(yDomain) }, + scales: { + x: { scale: scaleLinear().domain(xDomain) }, + y: { scale: scaleLinear().domain(yDomain) }, + }, guides: false, clip: true, }) @@ -627,8 +645,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, clip: true, }) @@ -708,8 +728,10 @@ describe('SVG motion', () => { ], }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, clip: true, }) @@ -798,8 +820,10 @@ describe('SVG motion', () => { points: true, }), ], - x: { scale: scaleLinear().domain(xDomain) }, - y: { scale: scaleLinear().domain(yDomain) }, + scales: { + x: { scale: scaleLinear().domain(xDomain) }, + y: { scale: scaleLinear().domain(yDomain) }, + }, guides: false, clip: true, }) @@ -890,8 +914,10 @@ describe('SVG motion', () => { transition: { type: 'tween', duration: 100, easing: 'linear' }, }, marks: [customLine(middle)], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, guides: false, clip: true, }) @@ -960,8 +986,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, clip: true, }) @@ -1060,8 +1088,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -1123,8 +1153,10 @@ describe('SVG motion', () => { motion: false, }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -1167,22 +1199,24 @@ describe('SVG motion', () => { }, marks: [lineY([0, 1])], margin: 0, - x: { - scale: scaleLinear().domain([0, 1]), - grid: true, - axis: { - motion: false, - ticks: { - values: [0, 1], - motion: { - transition: { type: 'tween', duration: 100, easing: 'linear' }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + grid: true, + axis: { + motion: false, + ticks: { + values: [0, 1], + motion: { + transition: { type: 'tween', duration: 100, easing: 'linear' }, + }, }, + tickLabels: { motion: false }, + label: { text: 'Period' }, }, - tickLabels: { motion: false }, - label: { text: 'Period' }, }, + y: { scale: scaleLinear().domain([0, 1]), axis: false }, }, - y: { scale: scaleLinear().domain([0, 1]), axis: false }, }) const first = createChartScene(definition, { width: 300, height: 200 }) const next = createChartScene(definition, { width: 400, height: 200 }) @@ -1300,35 +1334,37 @@ describe('SVG motion', () => { }, }), ], - x: { - scale: scaleBand().domain(['A']), - axis: { - motion(context) { - guideRoles.push(context.role) - return { transition: { type: 'tween', duration: 120 } } - }, - ticks: { + scales: { + x: { + scale: scaleBand().domain(['A']), + axis: { motion(context) { guideRoles.push(context.role) - return { delay: 5 } + return { transition: { type: 'tween', duration: 120 } } }, - }, - tickLabels: { - motion(context) { - guideRoles.push(context.role) - return { delay: 10 } + ticks: { + motion(context) { + guideRoles.push(context.role) + return { delay: 5 } + }, }, - }, - label: { - text: 'Period', - motion(context) { - guideRoles.push(context.role) - return { transition: { type: 'tween', duration: 80 } } + tickLabels: { + motion(context) { + guideRoles.push(context.role) + return { delay: 10 } + }, + }, + label: { + text: 'Period', + motion(context) { + guideRoles.push(context.role) + return { transition: { type: 'tween', duration: 80 } } + }, }, }, }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }) const first = createChartScene(definition(20), { width: 300, @@ -1399,8 +1435,10 @@ describe('SVG motion', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width: 320, height: 200 }, @@ -1490,8 +1528,10 @@ describe('SVG motion', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, maxFocusDistance: 1_000, }) @@ -1557,8 +1597,10 @@ describe('SVG motion', () => { }, ), ], - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, maxFocusDistance: 0, }) @@ -1651,8 +1693,10 @@ describe('SVG motion', () => { motion: { transition }, }), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width: 320, height: 200 }, @@ -1767,8 +1811,10 @@ describe('SVG motion', () => { motion: { transition }, }), ], - x: { scale: scaleBand().domain(['A', 'B']).padding(0.18) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']).padding(0.18) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -1883,8 +1929,10 @@ describe('SVG motion', () => { motion: { transition }, }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, focusRing: false, }), @@ -1956,8 +2004,10 @@ describe('SVG motion', () => { marks: includeCrosshair ? [dot(data, { x: 'x', y: 'y', key: 'id' }), crosshair()] : [dot(data, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }) const firstScene = createChartScene(definition(true), { @@ -2013,14 +2063,16 @@ describe('SVG motion', () => { }, marks: [lineY([0, 1])], margin: 0, - x: { - scale: scaleLinear().domain([0, 1]), - axis: { - ticks: { values: [0, 1] }, - tickLabels: { fontSize, fontWeight, anchor }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { + ticks: { values: [0, 1] }, + tickLabels: { fontSize, fontWeight, anchor }, + }, }, + y: { scale: scaleLinear().domain([0, 1]), axis: false }, }, - y: { scale: scaleLinear().domain([0, 1]), axis: false }, }) const first = createChartScene(definition(10, 400, 'middle'), { width: 300, @@ -2064,8 +2116,10 @@ describe('SVG motion', () => { }, marks: [ polar({ - angle: { scale: scaleBand().domain(['A']) }, - radius: { scale: scaleLinear().domain([0, 10]) }, + scales: { + angle: { scale: scaleBand().domain(['A']) }, + radius: { scale: scaleLinear().domain([0, 10]) }, + }, marks: [ radialBarRadius([{ id: 'a', category: 'A', value }], { id: 'radial-revenue', @@ -2076,6 +2130,7 @@ describe('SVG motion', () => { ], }), ], + scales: { x: null, y: null }, margin: 0, }) const first = createChartScene(definition(4), { @@ -2138,8 +2193,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, focusRing: false, }), @@ -2295,8 +2352,10 @@ describe('SVG motion', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, focusRing: false, }), @@ -2384,8 +2443,10 @@ describe('SVG motion', () => { barY(rows, { x: 'category', y: 'value', key: 'id' }), crosshair({ y: false }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2430,8 +2491,10 @@ describe('SVG motion', () => { barY(rows, { x: 'category', y: 'value', key: 'id' }), lineY(rows, { x: 'category', y: 'value', key: 'id' }), polar({ - angle: { scale: scaleBand().domain(['A']) }, - radius: { scale: scaleLinear().domain([0, 100]) }, + scales: { + angle: { scale: scaleBand().domain(['A']) }, + radius: { scale: scaleLinear().domain([0, 100]) }, + }, marks: [ radialBarRadius(rows, { angle: 'category', @@ -2441,8 +2504,10 @@ describe('SVG motion', () => { ], }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2483,8 +2548,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 20]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 20]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2561,8 +2628,10 @@ describe('SVG motion', () => { } }, marks: [barY(data, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(data.map((row) => row.category)) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(data.map((row) => row.category)) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2635,8 +2704,10 @@ describe('SVG motion', () => { }, }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2675,8 +2746,10 @@ describe('SVG motion', () => { createChartScene( defineChart({ marks: [barY([datum], { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2727,8 +2800,10 @@ describe('SVG motion', () => { const scene = createChartScene( defineChart({ marks: [barY(rows, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2765,8 +2840,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2830,8 +2907,10 @@ describe('SVG motion', () => { createChartScene( defineChart({ marks: [lineY(data, { x: 'category', y: 'value', key: 'id' })], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2903,8 +2982,10 @@ describe('SVG motion', () => { key: 'id', }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2948,8 +3029,10 @@ describe('SVG motion', () => { points: true, }), ], - x: { scale: scaleBand().domain(['A', 'B']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, }), { width: 300, height: 200 }, @@ -2984,8 +3067,10 @@ describe('SVG motion', () => { defineChart({ marks: [ polar({ - angle: { scale: scaleBand().domain(['A']) }, - radius: { scale: scaleLinear().domain([0, 100]) }, + scales: { + angle: { scale: scaleBand().domain(['A']) }, + radius: { scale: scaleLinear().domain([0, 100]) }, + }, marks: [ radialBarRadius(rows, { angle: 'category', @@ -2995,6 +3080,7 @@ describe('SVG motion', () => { ], }), ], + scales: { x: null, y: null }, guides: false, margin: 0, }), @@ -3039,8 +3125,10 @@ describe('SVG motion', () => { defineChart({ marks: [ polar({ - angle: { scale: scaleBand().domain(['A', 'B']) }, - radius: { scale: scaleLinear().domain([0, 100]) }, + scales: { + angle: { scale: scaleBand().domain(['A', 'B']) }, + radius: { scale: scaleLinear().domain([0, 100]) }, + }, marks: [ radialBarRadius(data, { angle: 'category', @@ -3050,6 +3138,7 @@ describe('SVG motion', () => { ], }), ], + scales: { x: null, y: null }, guides: false, margin: 0, }), @@ -3095,8 +3184,10 @@ describe('SVG motion', () => { defineChart({ marks: [ polar({ - angle: { scale: scaleLinear().domain([0, 3]) }, - radius: { scale: scaleLinear().domain([0, 100]) }, + scales: { + angle: { scale: scaleLinear().domain([0, 3]) }, + radius: { scale: scaleLinear().domain([0, 100]) }, + }, marks: [ radialArea(radialRows, { angle: 'angle', @@ -3105,6 +3196,7 @@ describe('SVG motion', () => { ], }), ], + scales: { x: null, y: null }, guides: false, margin: 0, }), diff --git a/packages/charts-core/src/motion.ts b/packages/charts-core/src/motion.ts index 82ab9980..2c552d0a 100644 --- a/packages/charts-core/src/motion.ts +++ b/packages/charts-core/src/motion.ts @@ -3476,10 +3476,7 @@ function motionDefinitions( const guides: Record> = {} if (source) { const [definition] = source - const configuredScales = definition.scales ?? { - x: definition.x, - y: definition.y, - } + const configuredScales = definition.scales for (const [scaleId, configured] of Object.entries(configuredScales)) { const presentation = !configured || configured.axis === false diff --git a/packages/charts-core/src/nearest.test.ts b/packages/charts-core/src/nearest.test.ts index 8463631f..dbf22816 100644 --- a/packages/charts-core/src/nearest.test.ts +++ b/packages/charts-core/src/nearest.test.ts @@ -432,8 +432,10 @@ describe('scene interaction geometry', () => { y: 'value', }), ], - x: { scale: scaleBand().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleBand().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, guides: false, margin: 0, }), @@ -450,8 +452,10 @@ describe('scene interaction geometry', () => { y: 'category', }), ], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleBand().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleBand().domain(['A']) }, + }, guides: false, margin: 0, }), diff --git a/packages/charts-core/src/network-sankey.test.ts b/packages/charts-core/src/network-sankey.test.ts index bfecb029..44807413 100644 --- a/packages/charts-core/src/network-sankey.test.ts +++ b/packages/charts-core/src/network-sankey.test.ts @@ -129,6 +129,7 @@ describe('sankeyDiagram', () => { }) const definition = defineChart({ marks: [mark], + scales: { x: null, y: null }, color: { domain: ['primary', 'secondary', 'source', 'middle', 'target'], }, @@ -215,6 +216,7 @@ describe('sankeyDiagram', () => { }, }), ], + scales: { x: null, y: null }, guides: false, margin: 0, }) @@ -297,7 +299,12 @@ describe('sankeyDiagram', () => { }, }) createChartRuntime().render( - defineChart({ marks: [mark], guides: false, margin: 0 }), + defineChart({ + marks: [mark], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 300, height: 180 }, ) @@ -353,7 +360,12 @@ describe('sankeyDiagram', () => { }) createChartRuntime().render( - defineChart({ marks: [mark], guides: false, margin: 0 }), + defineChart({ + marks: [mark], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 300, height: 180 }, ) @@ -419,7 +431,12 @@ describe('sankeyDiagram', () => { }) expect(() => createChartRuntime().render( - defineChart({ marks: [invalidAlignment], guides: false, margin: 0 }), + defineChart({ + marks: [invalidAlignment], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 320, height: 200 }, ), ).toThrow('align result must be an integer between 0 and 2') @@ -427,7 +444,12 @@ describe('sankeyDiagram', () => { const invalidSize = sankeyDiagram({ ...base, nodeWidth: 500 }) expect(() => createChartRuntime().render( - defineChart({ marks: [invalidSize], guides: false, margin: 0 }), + defineChart({ + marks: [invalidSize], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 320, height: 200 }, ), ).toThrow('inset leaves less horizontal space than nodeWidth') @@ -435,7 +457,12 @@ describe('sankeyDiagram', () => { const zeroHeight = sankeyDiagram({ ...base, inset: { top: 100 } }) expect(() => createChartRuntime().render( - defineChart({ marks: [zeroHeight], guides: false, margin: 0 }), + defineChart({ + marks: [zeroHeight], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 320, height: 100 }, ), ).toThrow('inset leaves no vertical layout space') @@ -451,7 +478,12 @@ describe('sankeyDiagram', () => { }) expect(() => createChartRuntime().render( - defineChart({ marks: [cycle], guides: false, margin: 0 }), + defineChart({ + marks: [cycle], + scales: { x: null, y: null }, + guides: false, + margin: 0, + }), { width: 320, height: 200 }, ), ).toThrow('circular link') diff --git a/packages/charts-core/src/point-color.test.ts b/packages/charts-core/src/point-color.test.ts index 22f9ebd7..d98d1342 100644 --- a/packages/charts-core/src/point-color.test.ts +++ b/packages/charts-core/src/point-color.test.ts @@ -286,7 +286,10 @@ describe('mark point colors', () => { const scene = createChartScene( defineChart({ marks: [ruleY(data, { y: 'value', color: 'status' })], - y: linearAxes([0, 1], [0, 3]).y, + scales: { + x: null, + y: linearAxes([0, 1], [0, 3]).scales.y, + }, color: { scale: semanticColors() }, }), { width: 480, height: 260 }, diff --git a/packages/charts-core/src/polar-mark-internal.ts b/packages/charts-core/src/polar-mark-internal.ts index 26158d91..54fa2211 100644 --- a/packages/charts-core/src/polar-mark-internal.ts +++ b/packages/charts-core/src/polar-mark-internal.ts @@ -26,8 +26,6 @@ export interface PolarLayoutContext { startAngle: number endAngle: number scales: Readonly> - angle?: PolarResolvedScale - radiusScale?: PolarResolvedScale } export interface InitializedPolarMark< diff --git a/packages/charts-core/src/polar-scales.test.ts b/packages/charts-core/src/polar-scales.test.ts index 0d08ac8a..ee1f1c3b 100644 --- a/packages/charts-core/src/polar-scales.test.ts +++ b/packages/charts-core/src/polar-scales.test.ts @@ -1,5 +1,5 @@ import { scaleBand, scaleLinear } from 'd3-scale' -import { describe, expect, expectTypeOf, it, vi } from 'vitest' +import { describe, expect, expectTypeOf, it } from 'vitest' import { angleGrid, polar, @@ -260,7 +260,6 @@ describe('polar scale registry', () => { }) it('keeps raw arcs scale-free with explicit reserved nulls', () => { - const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) const mark = radialArc([{ startAngle: 0, endAngle: Math.PI }]) const scene = createChartScene( defineChart({ @@ -277,87 +276,9 @@ describe('polar scale registry', () => { ) expect(scene.points).toHaveLength(1) - expect(warning).not.toHaveBeenCalled() expectTypeOf(mark).toMatchTypeOf< PolarMark >() - warning.mockRestore() - }) - - it('adapts legacy root scales with one actionable warning', async () => { - vi.resetModules() - const freshPolar = await import('./polar') - const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const angle = scaleLinear().domain([0, 1]) - const radius = scaleLinear().domain([0, 1]) - const mark = freshPolar.radialDot([{ angle: 0.5, radius: 0.5 }], { - angle: 'angle', - radius: 'radius', - }) - const legacy = defineChart({ - marks: [ - freshPolar.polar({ - angle: { scale: angle }, - radius: { scale: radius }, - marks: [mark], - }), - ], - scales: { x: null, y: null }, - guides: false, - }) - - createChartScene(legacy, { width: 200, height: 200 }) - createChartScene(legacy, { width: 220, height: 220 }) - - const messages = warning.mock.calls - .map(([message]) => String(message)) - .filter((message) => - message.includes('`polar()` scale options have moved'), - ) - expect(messages).toHaveLength(1) - expect(messages[0]).toContain( - 'Move `angle` and `radius` to `scales.angle` and `scales.radius`', - ) - expect(messages[0]).toContain('set both entries to `null`') - expect(messages[0]).toContain('enters Alpha') - warning.mockRestore() - }) - - it('warns for legacy polar scales without a process global', async () => { - vi.resetModules() - const freshPolar = await import('./polar') - const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const nodeProcess = globalThis.process - vi.stubGlobal('process', undefined) - - try { - createChartScene( - defineChart({ - marks: [ - freshPolar.polar({ - angle: { scale: scaleLinear().domain([0, 1]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - freshPolar.radialDot([{ angle: 0.5, radius: 0.5 }], { - angle: 'angle', - radius: 'radius', - }), - ], - }), - ], - scales: { x: null, y: null }, - guides: false, - }), - { width: 200, height: 200 }, - ) - } finally { - vi.stubGlobal('process', nodeProcess) - } - - expect(warning).toHaveBeenCalledWith( - expect.stringContaining('`polar()` scale options have moved'), - ) - warning.mockRestore() }) }) @@ -382,10 +303,32 @@ if (false) { expectTypeOf(namedDot).toMatchTypeOf< PolarMark >() + expectTypeOf(defaultDot).toMatchTypeOf< + PolarMark + >() expectTypeOf(explicitUndefined).toMatchTypeOf< PolarMark >() + polar({ + scales: { + // @ts-expect-error Scale-free marks require a null reserved angle entry. + angle: { scale: scaleLinear() }, + radius: null, + }, + marks: [radialArc([{ startAngle: 0, endAngle: Math.PI }])], + }) + + polar({ + scales: { + // @ts-expect-error Scale-backed marks require a configured angle entry. + angle: null, + // @ts-expect-error Scale-backed marks require a configured radius entry. + radius: null, + }, + marks: [defaultDot], + }) + const extended: ExtendedPolarOptions = { applicationTag: 'extended', scales: { diff --git a/packages/charts-core/src/polar.test.ts b/packages/charts-core/src/polar.test.ts index c21863b9..3f1d68a8 100644 --- a/packages/charts-core/src/polar.test.ts +++ b/packages/charts-core/src/polar.test.ts @@ -916,7 +916,7 @@ describe('polar marks', () => { const missingScale = defineChart({ marks: [ polar({ - scales: { angle: null, radius: null }, + scales: { angle: null, radius: null } as any, marks: [radialLine([1, 2, 3])], }), ], @@ -932,9 +932,19 @@ describe('polar marks', () => { polar({ scales: { angle: null, - radius: { scale: scaleLinear().domain([0, 1]) }, + radius: null, + guideRadius: { + channel: 'radius', + scale: scaleLinear().domain([0, 1]), + }, }, - guides: [radialGrid({ values: [1], shape: 'polygon' })], + guides: [ + radialGrid({ + scale: 'guideRadius', + values: [1], + shape: 'polygon', + }), + ], marks: [], }), ], @@ -947,6 +957,23 @@ describe('polar marks', () => { height: 200, }), ).toThrow(/requires a configured angle scale/) + + const configuredPhantom = defineChart({ + marks: [ + polar({ + scales: { + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: null, + } as any, + marks: [radialArc([{ startAngle: 0, endAngle: Math.PI }])], + }), + ], + scales: { x: null, y: null }, + guides: false, + }) + expect(() => + createChartScene(configuredPhantom, { width: 200, height: 200 }), + ).toThrow(/cannot be configured when no mark materializes its channel/) }) it('supports authored D3 arc generators with per-datum radii', () => { diff --git a/packages/charts-core/src/polar.ts b/packages/charts-core/src/polar.ts index ead17066..9f21807c 100644 --- a/packages/charts-core/src/polar.ts +++ b/packages/charts-core/src/polar.ts @@ -28,6 +28,7 @@ import type { Arc, CurveFactory, CurveFactoryLineOnly } from 'd3-shape' import type { Channel, ChannelAccessor, + ChannelOutput, ChartAxisValue, ChartBounds, ChartKey, @@ -42,8 +43,6 @@ import type { ChartTheme, ChartValue, ChartScaleInput, - OptionChannelOutput, - OptionScaleId, SceneNode, VisualChannel, } from './types' @@ -61,8 +60,6 @@ export type { PieDatum, PieOptions } from './polar-pie' const tau = Math.PI * 2 -declare const process: { readonly env: { readonly NODE_ENV?: string } } - export type PolarPositionChannel = 'angle' | 'radius' export interface PolarResolvedScale< @@ -74,10 +71,6 @@ export interface PolarResolvedScale< export interface PolarLayoutContext extends InternalPolarLayoutContext { scales: Readonly> - /** @deprecated Read the reserved `angle` entry from `scales` instead. */ - angle?: PolarResolvedScale - /** @deprecated Read the reserved `radius` entry from `scales` instead. */ - radiusScale?: PolarResolvedScale } interface InitializedPolarMark< @@ -182,16 +175,43 @@ type PolarMarkScaleRadius = : never : never +type PolarIsAny = 0 extends 1 & TValue ? true : false + +type PolarAngleScaleSpec = + PolarIsAny> extends true + ? { + angle: PolarAngleOptions< + ChartAxisValue> + > | null + } + : [PolarMarkScaleAngle] extends [never] + ? { angle: null } + : { + angle: PolarAngleOptions< + ChartAxisValue> + > + } + +type PolarRadiusScaleSpec = + PolarIsAny> extends true + ? { + radius: PolarRadiusOptions< + ChartAxisValue> + > | null + } + : [PolarMarkScaleRadius] extends [never] + ? { radius: null } + : { + radius: PolarRadiusOptions< + ChartAxisValue> + > + } + export type PolarScales< TMarks extends readonly AnyPolarMark[] = readonly AnyPolarMark[], -> = Readonly> & { - angle: PolarAngleOptions< - ChartAxisValue> - > | null - radius: PolarRadiusOptions< - ChartAxisValue> - > | null -} +> = Readonly> & + PolarAngleScaleSpec & + PolarRadiusScaleSpec export interface PolarOptions< TMarks extends readonly AnyPolarMark[] = readonly AnyPolarMark[], @@ -200,13 +220,7 @@ export interface PolarOptions< className?: string marks: TMarks guides?: readonly PolarGuide[] - scales?: PolarScales - /** @deprecated Move this value to `scales.angle`. */ - angle?: PolarAngleOptions>> - /** @deprecated Move this value to `scales.radius`. */ - radius?: PolarRadiusOptions< - ChartAxisValue> - > + scales: PolarScales startAngle?: number endAngle?: number /** Pixel inset applied before radiusRatio. */ @@ -544,6 +558,16 @@ interface RadialBarBaseOptions extends ChartMarkMotionOptions { opacity?: number } +type InferredPolarChannelOutput = [TChannel] extends [never] + ? number + : ChannelOutput + +type InferredPolarScaleId = [ + NonNullable, +] extends [never] + ? TFallback + : Extract, string> + export interface RadialBarRadiusOptions< TDatum, > extends RadialBarBaseOptions { @@ -553,21 +577,44 @@ export interface RadialBarRadiusOptions< radius2?: number | Channel } +type RadialBarRadiusCallOptions< + TDatum, + TAngle extends RadialBarRadiusOptions['angle'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialBarRadiusOptions, + 'angle' | 'angleScale' | 'radiusScale' +> & { + angle?: TAngle | NoInfer['angle']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialBarRadius( source: Iterable, ): PolarMark export function radialBarRadius< TDatum, - const TOptions extends RadialBarRadiusOptions> | undefined, + const TAngle extends RadialBarRadiusOptions>['angle'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialBarRadiusCallOptions< + NoInfer, + TAngle, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, - OptionChannelOutput, + InferredPolarChannelOutput, number, - OptionScaleId, - OptionScaleId + InferredPolarScaleId, + InferredPolarScaleId > export function radialBarRadius( source: Iterable, @@ -779,21 +826,45 @@ export interface RadialBarAngleOptions< radius?: Channel } +type RadialBarAngleCallOptions< + TDatum, + TRadius extends RadialBarAngleOptions['radius'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialBarAngleOptions, + 'radius' | 'angleScale' | 'radiusScale' +> & { + radius?: TRadius | NoInfer['radius']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialBarAngle( source: Iterable, ): PolarMark export function radialBarAngle< TDatum, - const TOptions extends RadialBarAngleOptions> | undefined, + const TRadius extends RadialBarAngleOptions>['radius'] = + never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialBarAngleCallOptions< + NoInfer, + TRadius, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, number, - OptionChannelOutput, - OptionScaleId, - OptionScaleId + InferredPolarChannelOutput, + InferredPolarScaleId, + InferredPolarScaleId > export function radialBarAngle( source: Iterable, @@ -1017,21 +1088,48 @@ export interface RadialLineOptions extends RadialPathOptions { points?: boolean } +type RadialLineCallOptions< + TDatum, + TAngle extends RadialLineOptions['angle'], + TRadius extends RadialLineOptions['radius'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialLineOptions, + 'angle' | 'radius' | 'angleScale' | 'radiusScale' +> & { + angle?: TAngle | NoInfer['angle']> + radius?: TRadius | NoInfer['radius']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialLine( source: Iterable, ): PolarMark export function radialLine< TDatum, - const TOptions extends RadialLineOptions> | undefined, + const TAngle extends RadialLineOptions>['angle'] = never, + const TRadius extends RadialLineOptions>['radius'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialLineCallOptions< + NoInfer, + TAngle, + TRadius, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionScaleId, - OptionScaleId + InferredPolarChannelOutput, + InferredPolarChannelOutput, + InferredPolarScaleId, + InferredPolarScaleId > export function radialLine( source: Iterable, @@ -1219,21 +1317,48 @@ export interface RadialAreaOptions extends RadialPathOptions { opacity?: number } +type RadialAreaCallOptions< + TDatum, + TAngle extends RadialAreaOptions['angle'], + TRadius extends RadialAreaOptions['radius'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialAreaOptions, + 'angle' | 'radius' | 'angleScale' | 'radiusScale' +> & { + angle?: TAngle | NoInfer['angle']> + radius?: TRadius | NoInfer['radius']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialArea( source: Iterable, ): PolarMark export function radialArea< TDatum, - const TOptions extends RadialAreaOptions> | undefined, + const TAngle extends RadialAreaOptions>['angle'] = never, + const TRadius extends RadialAreaOptions>['radius'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialAreaCallOptions< + NoInfer, + TAngle, + TRadius, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionScaleId, - OptionScaleId + InferredPolarChannelOutput, + InferredPolarChannelOutput, + InferredPolarScaleId, + InferredPolarScaleId > export function radialArea( source: Iterable, @@ -1438,21 +1563,48 @@ export interface RadialTextOptions extends RadialPathOptions { dy?: VisualChannel } +type RadialTextCallOptions< + TDatum, + TAngle extends RadialTextOptions['angle'], + TRadius extends RadialTextOptions['radius'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialTextOptions, + 'angle' | 'radius' | 'angleScale' | 'radiusScale' +> & { + angle?: TAngle | NoInfer['angle']> + radius?: TRadius | NoInfer['radius']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialText( source: Iterable, ): PolarMark export function radialText< TDatum, - const TOptions extends RadialTextOptions> | undefined, + const TAngle extends RadialTextOptions>['angle'] = never, + const TRadius extends RadialTextOptions>['radius'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialTextCallOptions< + NoInfer, + TAngle, + TRadius, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionScaleId, - OptionScaleId + InferredPolarChannelOutput, + InferredPolarChannelOutput, + InferredPolarScaleId, + InferredPolarScaleId > export function radialText( source: Iterable, @@ -1657,21 +1809,41 @@ export interface RadialRuleOptions< opacity?: number } +type RadialRuleCallOptions< + TDatum, + TAngle extends RadialRuleOptions['angle'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit, 'angle' | 'angleScale' | 'radiusScale'> & { + angle?: TAngle | NoInfer['angle']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialRule( source: Iterable, ): PolarMark export function radialRule< TDatum, - const TOptions extends RadialRuleOptions> | undefined, + const TAngle extends RadialRuleOptions>['angle'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialRuleCallOptions< + NoInfer, + TAngle, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< never, - OptionChannelOutput, + InferredPolarChannelOutput, number, - OptionScaleId, - OptionScaleId + InferredPolarScaleId, + InferredPolarScaleId > export function radialRule( source: Iterable, @@ -1829,21 +2001,48 @@ export function radialRule( ) } +type RadialDotCallOptions< + TDatum, + TAngle extends RadialDotOptions['angle'], + TRadius extends RadialDotOptions['radius'], + TAngleScaleId extends string | undefined, + TRadiusScaleId extends string | undefined, +> = Omit< + RadialDotOptions, + 'angle' | 'radius' | 'angleScale' | 'radiusScale' +> & { + angle?: TAngle | NoInfer['angle']> + radius?: TRadius | NoInfer['radius']> + angleScale?: TAngleScaleId | NoInfer + radiusScale?: TRadiusScaleId | NoInfer +} + export function radialDot( source: Iterable, ): PolarMark export function radialDot< TDatum, - const TOptions extends RadialDotOptions> | undefined, + const TAngle extends RadialDotOptions>['angle'] = never, + const TRadius extends RadialDotOptions>['radius'] = never, + const TAngleScaleId extends string | undefined = undefined, + const TRadiusScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | RadialDotCallOptions< + NoInfer, + TAngle, + TRadius, + TAngleScaleId, + TRadiusScaleId + > + | undefined, ): PolarMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionScaleId, - OptionScaleId + InferredPolarChannelOutput, + InferredPolarChannelOutput, + InferredPolarScaleId, + InferredPolarScaleId > export function radialDot( source: Iterable, @@ -2249,8 +2448,6 @@ export function angleGrid(options: AngleGridOptions = {}): PolarGuide { } } -let warnedLegacyPolarScales = false - function resolvePolarLayout( options: PolarOptions, chart: ChartBounds, @@ -2288,6 +2485,18 @@ function resolvePolarLayout( `Polar scale "${id}" is reserved for ${channel} but declares channel: "${scaleOptions.channel}"`, ) } + if ( + reservedChannel && + !marks.some((mark) => + reservedChannel === 'angle' + ? mark.angleScale === id + : mark.radiusScale === id, + ) + ) { + throw new TypeError( + `Polar scale "${id}" cannot be configured when no mark materializes its channel`, + ) + } const valuesKey = channel === 'angle' ? 'angleValues' : 'radiusValues' const values = collectPolarValues(marks, valuesKey, id) @@ -2325,48 +2534,23 @@ function resolvePolarLayout( ) } - layout.angle = scales.angle - layout.radiusScale = scales.radius return layout } function resolvePolarScaleOptions( options: PolarOptions, ): Readonly> { + const scales = options.scales if ( - (!options.scales || - options.angle !== undefined || - options.radius !== undefined) && - !warnedLegacyPolarScales - ) { - try { - if (process.env.NODE_ENV !== 'production') { - warnedLegacyPolarScales = true - console.warn( - '[TanStack Charts] `polar()` scale options have moved to `scales`. Move `angle` and `radius` to `scales.angle` and `scales.radius`. When neither scale is used, set both entries to `null`. This compatibility will be removed when TanStack Charts enters Alpha.', - ) - } - } catch { - warnedLegacyPolarScales = true - // Keep raw-browser migration help removable by production minifiers. - /* @__PURE__ */ console.warn( - '[TanStack Charts] `polar()` scale options have moved to `scales`. Move `angle` and `radius` to `scales.angle` and `scales.radius`. When neither scale is used, set both entries to `null`. This compatibility will be removed when TanStack Charts enters Alpha.', - ) - } - } - - if (!options.scales) { - return { angle: options.angle, radius: options.radius } - } - if ( - !Object.hasOwn(options.scales, 'angle') || - !Object.hasOwn(options.scales, 'radius') + !scales || + !Object.hasOwn(scales, 'angle') || + !Object.hasOwn(scales, 'radius') ) { throw new TypeError( 'Polar scales must define reserved `angle` and `radius` entries', ) } - return options.scales + return scales } function resolvePolarScale( diff --git a/packages/charts-core/src/rect.ts b/packages/charts-core/src/rect.ts index 5a4956ed..38c85285 100644 --- a/packages/charts-core/src/rect.ts +++ b/packages/charts-core/src/rect.ts @@ -10,7 +10,6 @@ import { import { valueKey } from './scales' import type { Channel, - ChannelOutput, CartesianChartMark, CartesianScaleBindings, ChartKey, @@ -20,6 +19,9 @@ import type { ChartRectStateStyle, ChartPoint, ChartValue, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, } from './types' @@ -49,54 +51,96 @@ export type CellOptions = Omit< 'x1' | 'x2' | 'y1' | 'y2' > -type RectSideOutput< +type RectCallOptions< TDatum, - TOptions, - TEndpoint extends PropertyKey, - TCenter extends PropertyKey, -> = TOptions extends unknown - ? TEndpoint extends keyof TOptions - ? ChannelOutput - : TCenter extends keyof TOptions - ? ChannelOutput - : number - : never - -type RectXOutput = - | RectSideOutput - | RectSideOutput + TXChannel, + TX1Channel, + TX2Channel, + TYChannel, + TY1Channel, + TY2Channel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + RectOptions>, + { + x?: TXChannel + x1?: TX1Channel + x2?: TX2Channel + y?: TYChannel + y1?: TY1Channel + y2?: TY2Channel + xScale?: TXScaleId + yScale?: TYScaleId + } +> -type RectYOutput = - | RectSideOutput - | RectSideOutput +type CellCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + CellOptions>, + { + x?: TXChannel + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> -type RectPointXOutput = TOptions extends unknown - ? 'x' extends keyof TOptions - ? ChannelOutput - : number - : never +type RectEndpointOutput = [ + NonNullable, +] extends [never] + ? MarkChannelOutput + : MarkChannelOutput -type RectPointYOutput = TOptions extends unknown - ? 'y' extends keyof TOptions - ? ChannelOutput - : 'y2' extends keyof TOptions - ? ChannelOutput - : number - : never +type RectPointYChannelOutput = [ + NonNullable, +] extends [never] + ? MarkChannelOutput + : MarkChannelOutput export function rect< TDatum, - const TOptions extends RectOptions>, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TX1Channel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TX2Channel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TY1Channel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TY2Channel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: RectCallOptions< + TDatum, + TXChannel, + TX1Channel, + TX2Channel, + TYChannel, + TY1Channel, + TY2Channel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - RectPointXOutput, - RectPointYOutput, - RectXOutput, - RectYOutput, - TOptions + MarkChannelOutput, + RectPointYChannelOutput, + | RectEndpointOutput + | RectEndpointOutput, + | RectEndpointOutput + | RectEndpointOutput, + MarkScaleBindings > export function rect( source: Iterable, @@ -276,17 +320,22 @@ export function rect( export function cell< TDatum, - const TOptions extends CellOptions>, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: CellCallOptions, ): CartesianChartMark< TDatum, - RectPointXOutput, - RectPointYOutput, - RectXOutput, - RectYOutput, - TOptions + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkScaleBindings > export function cell( source: Iterable, diff --git a/packages/charts-core/src/regression.test.ts b/packages/charts-core/src/regression.test.ts index 3605e763..70215913 100644 --- a/packages/charts-core/src/regression.test.ts +++ b/packages/charts-core/src/regression.test.ts @@ -46,8 +46,10 @@ describe('linear regression marks', () => { samples: 3, }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -102,8 +104,10 @@ describe('linear regression marks', () => { samples: 3, }), ], - x: { scale: scaleLinear }, - y: { scale: scaleTime }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleTime }, + }, }), { width: 480, height: 280 }, ) @@ -190,8 +194,10 @@ describe('linear regression marks', () => { stroke: '#dc2626', }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 640, height: 360 }) const nodes = flatten(scene.nodes) @@ -237,8 +243,10 @@ describe('linear regression marks', () => { const scene = createChartScene( defineChart({ marks: [linearRegressionY(rows, { x: 'x', y: 'y', ci: 0 })], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -273,8 +281,10 @@ describe('linear regression marks', () => { samples: 3, }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -323,8 +333,10 @@ describe('linear regression marks', () => { const scene = createChartScene( defineChart({ marks: [linearRegressionY(rows, { x: 'x', y: 'y', samples: 2 })], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -358,8 +370,10 @@ describe('linear regression marks', () => { samples: 2, }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) @@ -387,8 +401,10 @@ describe('linear regression marks', () => { samples: 3, }), ], - x: { scale: scaleTime }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleTime }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) type Datum = ChartSpecDatum @@ -419,8 +435,10 @@ describe('linear regression marks', () => { samples: 5, }), ], - x: { scale: scaleLog }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLog }, + y: { scale: scaleLinear }, + }, }), { width: 600, height: 320 }, ) @@ -453,8 +471,10 @@ describe('linear regression marks', () => { samples: 3, }), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 480, height: 280 }) type Datum = ChartSpecDatum @@ -503,8 +523,10 @@ describe('linear regression marks', () => { const scene = createChartScene( defineChart({ marks: [mark], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 480, height: 280 }, ) diff --git a/packages/charts-core/src/renderer.test.ts b/packages/charts-core/src/renderer.test.ts index 551a6992..623481b0 100644 --- a/packages/charts-core/src/renderer.test.ts +++ b/packages/charts-core/src/renderer.test.ts @@ -45,8 +45,10 @@ const definition = defineChart({ stroke: '#2563eb', }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 8]) }, + }, guides: false, }) @@ -632,8 +634,10 @@ describe('renderer-neutral chart host', () => { layout: stack({ order: rows.map((row) => row.id) }), }), ], - x: { scale: scaleBand().domain([0]) }, - y: { scale: scaleLinear().domain([0, 160]) }, + scales: { + x: { scale: scaleBand().domain([0]) }, + y: { scale: scaleLinear().domain([0, 160]) }, + }, guides: false, margin: 0, focus: 'group-x', @@ -838,8 +842,10 @@ describe('renderer-neutral chart host', () => { key: 'id', }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 8]) }, + }, guides: false, maxFocusDistance: 1, }, @@ -1170,11 +1176,13 @@ describe('renderer-neutral chart host', () => { const host = mountChartRenderer(container, { definition: defineChart({ marks: [lineY(history, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate: 30 }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate: 30 }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, tooltip: { use: tooltipExtension, @@ -1206,11 +1214,13 @@ describe('renderer-neutral chart host', () => { const makeDefinition = (translate: number) => defineChart({ marks: [lineY(history, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, focus: 'nearest-x', maxFocusDistance: 1, @@ -1259,11 +1269,13 @@ describe('renderer-neutral chart host', () => { const host = mountChartRenderer(container, { definition: defineChart({ marks: [lineY(history, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2] }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2] }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, focus: 'nearest-x', }), @@ -1298,11 +1310,13 @@ describe('renderer-neutral chart host', () => { { x: 'x', y: 'y', key: 'id' }, ), ], - x: { - scale: scaleLinear().domain([0, 1]), - viewport: { domain: [0, 1] }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + viewport: { domain: [0, 1] }, + }, + y: { scale: scaleLinear().domain([0, 1]) }, }, - y: { scale: scaleLinear().domain([0, 1]) }, guides: false, maxFocusDistance: 0, }), @@ -1332,11 +1346,13 @@ describe('renderer-neutral chart host', () => { const makeDefinition = (translate: number) => defineChart({ marks: [lineY(history, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 3]), - viewport: { domain: [1, 2], translate }, + scales: { + x: { + scale: scaleLinear().domain([0, 3]), + viewport: { domain: [1, 2], translate }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, guides: false, maxFocusDistance: 0, }) @@ -1394,11 +1410,13 @@ describe('renderer-neutral chart host', () => { }) const viewportDefinition = defineChart({ marks: [lineY(data, { x: 'x', y: 'y', key: 'id' })], - x: { - scale: scaleLinear().domain([0, 1]), - viewport: { domain: [0, 1] }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + viewport: { domain: [0, 1] }, + }, + y: { scale: scaleLinear().domain([0, 8]) }, }, - y: { scale: scaleLinear().domain([0, 8]) }, guides: false, }) const host = mountChartRenderer(container, { @@ -1534,6 +1552,167 @@ describe('renderer-neutral chart host', () => { host.destroy() }) + it('dismisses a pinned tooltip when the pointer is pressed outside the chart', () => { + const fake = createFakeRenderer() + const container = document.createElement('div') + const outside = document.createElement('button') + document.body.append(container, outside) + const onFocusChange = vi.fn() + const host = mountChartRenderer(container, { + definition: defineChart(definition, { + maxFocusDistance: 1_000, + tooltip: tooltipExtension, + }), + renderer: fake.renderer, + width: 480, + height: 260, + ariaLabel: 'Outside dismissal tooltip', + onFocusChange, + }) + + fake.element.dispatchEvent( + new MouseEvent('pointermove', { + bubbles: true, + clientX: 20, + clientY: 20, + }), + ) + fake.element.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + clientX: 20, + clientY: 20, + }), + ) + expect(fake.paintFocus.mock.calls.at(-1)?.[0]).toMatchObject({ + pinned: true, + }) + + outside.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })) + expect(onFocusChange).toHaveBeenLastCalledWith(null) + expect(fake.paintFocus.mock.calls.at(-1)?.[0]).toBeNull() + + fake.element.dispatchEvent( + new MouseEvent('pointermove', { + bubbles: true, + clientX: 20, + clientY: 20, + }), + ) + expect(onFocusChange.mock.calls.at(-1)?.[0]?.datum).toBe(data[0]) + expect(fake.paintFocus.mock.calls.at(-1)?.[0]).toMatchObject({ + source: 'pointer', + pinned: false, + }) + + host.destroy() + outside.remove() + container.remove() + }) + + it('keeps application-controlled focus pinned when automatic pointer handling is disabled', () => { + const fake = createFakeRenderer() + const container = document.createElement('div') + const outside = document.createElement('button') + document.body.append(container, outside) + const host = mountChartRenderer(container, { + definition: defineChart(definition, { + pointer: false, + tooltip: tooltipExtension, + }), + renderer: fake.renderer, + width: 480, + height: 260, + ariaLabel: 'Controlled focus tooltip', + }) + const point = host.getScene().points[0] + if (!point) throw new Error('Expected a chart point') + host.interaction.setControlledFocus(point, { pinned: true }) + const paintCount = fake.paintFocus.mock.calls.length + + outside.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })) + + expect(fake.paintFocus).toHaveBeenCalledTimes(paintCount) + expect(fake.paintFocus.mock.calls.at(-1)?.[0]).toMatchObject({ + pinned: true, + }) + host.destroy() + outside.remove() + container.remove() + }) + + it('recognizes chart pointer presses through a shadow boundary', () => { + const fake = createFakeRenderer() + const shadowHost = document.createElement('div') + const container = document.createElement('div') + shadowHost.attachShadow({ mode: 'open' }).append(container) + document.body.append(shadowHost) + const host = mountChartRenderer(container, { + definition: defineChart(definition, { + maxFocusDistance: 1_000, + tooltip: tooltipExtension, + }), + renderer: fake.renderer, + width: 480, + height: 260, + ariaLabel: 'Shadow tooltip', + }) + fake.element.dispatchEvent( + new MouseEvent('pointermove', { + bubbles: true, + composed: true, + clientX: 20, + clientY: 20, + }), + ) + fake.element.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + composed: true, + clientX: 20, + clientY: 20, + }), + ) + const paintCount = fake.paintFocus.mock.calls.length + + fake.element.dispatchEvent( + new MouseEvent('pointerdown', { bubbles: true, composed: true }), + ) + + expect(fake.paintFocus).toHaveBeenCalledTimes(paintCount) + expect(fake.paintFocus.mock.calls.at(-1)?.[0]).toMatchObject({ + pinned: true, + }) + host.destroy() + shadowHost.remove() + }) + + it('does not install outside dismissal when initial rendering fails', () => { + const fake = createFakeRenderer() + fake.render.mockImplementationOnce(() => { + throw new Error('initial render failed') + }) + const container = document.createElement('div') + const addEventListener = vi.spyOn( + container.ownerDocument, + 'addEventListener', + ) + + expect(() => + mountChartRenderer(container, { + definition, + renderer: fake.renderer, + width: 480, + height: 260, + ariaLabel: 'Failed chart', + }), + ).toThrow('initial render failed') + expect( + addEventListener.mock.calls.filter(([type]) => type === 'pointerdown'), + ).toHaveLength(0) + addEventListener.mockRestore() + }) + it('uses the tooltip as a top-layer popover, retains ancestry, and reopens it while active', () => { const popover = installPopoverMock(window) const viewport = installVisualViewport(window, { @@ -1653,6 +1832,7 @@ describe('renderer-neutral chart host', () => { const second = createFakeRenderer() const firstContainer = document.createElement('div') const secondContainer = document.createElement('div') + document.body.append(firstContainer, secondContainer) const bounds = { x: 0, y: 0, @@ -2342,6 +2522,11 @@ describe('renderer-neutral chart host', () => { source: 'pointer', pinned: true, }) + const pinned = controller.getState() + first.element.dispatchEvent( + new MouseEvent('pointerdown', { bubbles: true, composed: true }), + ) + expect(controller.getState()).toBe(pinned) const firstPaintCount = first.paintFocus.mock.calls.length const secondPaintCount = second.paintFocus.mock.calls.length first.element.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })) @@ -2363,6 +2548,8 @@ describe('renderer-neutral chart host', () => { firstHost.destroy() secondHost.destroy() + firstContainer.remove() + secondContainer.remove() }) it('projects, pins, and clears a free cursor without datum focus or keyboard stepping', () => { @@ -3133,8 +3320,10 @@ const categoricalDefinition = defineChart({ key: 'id', }), ], - x: { scale: scaleBand().domain(['Alpha']) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleBand().domain(['Alpha']) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) if (false) { diff --git a/packages/charts-core/src/renderer.ts b/packages/charts-core/src/renderer.ts index 55ea745c..b5abc9b3 100644 --- a/packages/charts-core/src/renderer.ts +++ b/packages/charts-core/src/renderer.ts @@ -679,6 +679,29 @@ export function mountChartRenderer< } } + const dismissPinnedInteractionOutside = (event: PointerEvent) => { + if ( + options.definition.pointer === false || + pinnedKey === null || + cursorIsPinned() + ) { + return + } + const path = event.composedPath() + if ( + path.includes(container) || + path.some( + (target) => + view && + target instanceof view.Node && + tooltipInstance?.contains(target), + ) + ) { + return + } + dismissTooltip() + } + const handleClick = (event: MouseEvent) => { if (controlContains(event.target)) return if (options.definition.pointer === false) return @@ -830,6 +853,11 @@ export function mountChartRenderer< configureCursorController() render() configureObserver() + container.ownerDocument.addEventListener( + 'pointerdown', + dismissPinnedInteractionOutside, + true, + ) return { interaction, @@ -919,6 +947,11 @@ export function mountChartRenderer< container.removeEventListener('keydown', handleKeyDown) container.removeEventListener('focusin', handleFocus) container.removeEventListener('focusout', clearKeyboardFocus) + container.ownerDocument.removeEventListener( + 'pointerdown', + dismissPinnedInteractionOutside, + true, + ) container.replaceChildren() if (ownsPosition && container.style.position === 'relative') { container.style.position = previousPosition diff --git a/packages/charts-core/src/resolved-layout.test.ts b/packages/charts-core/src/resolved-layout.test.ts index 2be54c18..d4433906 100644 --- a/packages/charts-core/src/resolved-layout.test.ts +++ b/packages/charts-core/src/resolved-layout.test.ts @@ -82,8 +82,10 @@ describe('mark resolved layout', () => { marks: [createMark(initialize)], guides: false, focusRing: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, color: { scale: scaleLinear, range: ['#eff6ff', '#1d4ed8'], @@ -133,8 +135,10 @@ describe('mark resolved layout', () => { defineChart({ marks: [mark], guides: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, color: { scale: scaleLinear, range: ['#eff6ff', '#1d4ed8'], diff --git a/packages/charts-core/src/ridgeline.test.ts b/packages/charts-core/src/ridgeline.test.ts index 0968ec23..165fb362 100644 --- a/packages/charts-core/src/ridgeline.test.ts +++ b/packages/charts-core/src/ridgeline.test.ts @@ -47,8 +47,10 @@ describe('ridgeline marks', () => { marks: [mark], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10, 20]).padding(0.75) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10, 20]).padding(0.75) }, + }, }) const scene = createChartScene(definition, { width: 480, height: 300 }) const resized = createChartScene(definition, { width: 720, height: 480 }) @@ -120,8 +122,10 @@ describe('ridgeline marks', () => { marks: [mark], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['A', 'B']).padding(0.5) }, - y: { scale: scaleUtc().domain(at) }, + scales: { + x: { scale: scalePoint().domain(['A', 'B']).padding(0.5) }, + y: { scale: scaleUtc().domain(at) }, + }, }), { width: 480, height: 300 }, ) @@ -165,8 +169,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scaleBand().domain([10, 20]).padding(0.2) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scaleBand().domain([10, 20]).padding(0.2) }, + }, }), { width: 400, height: 240 }, ) @@ -183,8 +189,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10]).padding(0.5) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10]).padding(0.5) }, + }, }), { width: 400, height: 240 }, ) @@ -211,8 +219,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain(domain) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain(domain) }, + }, }), { width: 400, height: 240 }, ) @@ -256,8 +266,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: pointScale }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: pointScale }, + }, }), { width: 400, height: 240 }, ) @@ -287,8 +299,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scalePoint().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scalePoint().domain(['A']) }, + }, }), { width: 400, height: 240 }, ) @@ -317,8 +331,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10]) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10]) }, + }, }), { width: 400, height: 240 }, ) @@ -336,8 +352,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10]) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10]) }, + }, }), { width: 400, height: 240 }, ) @@ -373,8 +391,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10]) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10]) }, + }, }), { width: 400, height: 240 }, ) @@ -427,8 +447,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scalePoint().domain([10]) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scalePoint().domain([10]) }, + }, }), { width: 400, height: 240 }, ) @@ -489,8 +511,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scalePoint().domain(['A']) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scalePoint().domain(['A']) }, + }, }), { width: 240, height: 160 }, ) @@ -509,8 +533,10 @@ describe('ridgeline marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([4, 6]) }, - y: { scale: scaleLinear().domain([10, 20]) }, + scales: { + x: { scale: scaleLinear().domain([4, 6]) }, + y: { scale: scaleLinear().domain([10, 20]) }, + }, }), { width: 240, height: 160 }, ), diff --git a/packages/charts-core/src/rule.ts b/packages/charts-core/src/rule.ts index 829c73d4..6dfefbb9 100644 --- a/packages/charts-core/src/rule.ts +++ b/packages/charts-core/src/rule.ts @@ -14,7 +14,9 @@ import type { ChartMarkMotionOptions, ChartFocusAnchor, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneNode, VisualChannel, WidenChartValue, @@ -42,6 +44,24 @@ export interface RuleXOptions extends ChartMarkMotionOptions { xScale?: string } +type RuleYCallOptions< + TDatum, + TYChannel, + TYScaleId extends string | undefined, +> = MarkCallOptions< + RuleYOptions>, + { y?: TYChannel; yScale?: TYScaleId } +> + +type RuleXCallOptions< + TDatum, + TXChannel, + TXScaleId extends string | undefined, +> = MarkCallOptions< + RuleXOptions>, + { x?: TXChannel; xScale?: TXScaleId } +> + type RuleFallback = [ WidenChartValue>, ] extends [never] @@ -53,17 +73,19 @@ export function ruleY( ): ChartMark> export function ruleY< TDatum, - const TOptions extends RuleYOptions> | undefined, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: RuleYCallOptions | undefined, ): CartesianChartMark< never, never, - OptionChannelOutput>, + MarkChannelOutput>, never, - OptionChannelOutput>, - TOptions + MarkChannelOutput>, + MarkScaleBindings<'x', TYScaleId> > export function ruleY( source: Iterable, @@ -151,17 +173,19 @@ export function ruleX( ): ChartMark, never> export function ruleX< TDatum, - const TOptions extends RuleXOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: RuleXCallOptions | undefined, ): CartesianChartMark< never, - OptionChannelOutput>, + MarkChannelOutput>, never, - OptionChannelOutput>, + MarkChannelOutput>, never, - TOptions + MarkScaleBindings > export function ruleX( source: Iterable, diff --git a/packages/charts-core/src/runtime.test.ts b/packages/charts-core/src/runtime.test.ts index 4b10c8b0..f1335bb6 100644 --- a/packages/charts-core/src/runtime.test.ts +++ b/packages/charts-core/src/runtime.test.ts @@ -64,13 +64,16 @@ describe('dynamic chart runtime', () => { const definition = { chart: () => ({ marks: [lineY([{ id: 'a', x: 0, y: 4 }], { x: 'x', y: 'y' })], + scales: { x: null, y: null }, }), } as unknown as ChartDefinition const runtime = createChartRuntime() expect(() => runtime.render(definition, { width: 480, height: 260 }), - ).toThrow(/requires a configured scale/) + ).toThrow( + 'Chart scale "x" cannot be null when a mark materializes its channel', + ) runtime.destroy() }) @@ -696,12 +699,17 @@ describe('dynamic chart runtime', () => { key: 'id', }), ], - x: { ...bandXAxes(['A'], [0, 12]).x, axis: { label: 'Period' } }, - y: { - ...linearAxes([0, 1], [0, 12]).y, - axis: { - ticks: { format: (value) => `${value}k` }, - label: 'Downloads', + scales: { + x: { + ...bandXAxes(['A'], [0, 12]).scales.x, + axis: { label: 'Period' }, + }, + y: { + ...linearAxes([0, 1], [0, 12]).scales.y, + axis: { + ticks: { format: (value) => `${value}k` }, + label: 'Downloads', + }, }, }, focus: 'group-x', @@ -824,12 +832,14 @@ describe('dynamic chart runtime', () => { y: 'end', }), ], - x: bandXAxes(['A'], [0, 20]).x, - y: { - ...linearAxes([0, 1], [0, 20]).y, - axis: { - ticks: { format: (value) => `${value} units` }, - label: 'Change', + scales: { + x: bandXAxes(['A'], [0, 20]).scales.x, + y: { + ...linearAxes([0, 1], [0, 20]).scales.y, + axis: { + ticks: { format: (value) => `${value} units` }, + label: 'Change', + }, }, }, tooltip: tooltipExtension, @@ -854,10 +864,15 @@ describe('dynamic chart runtime', () => { const container = document.createElement('div') const definition = defineChart({ marks: [lineY([{ x: 0, y: 4, note: 'Released' }], { x: 'x', y: 'y' })], - x: { ...linearAxes([0, 1], [0, 4]).x, axis: { label: 'Week' } }, - y: { - ...linearAxes([0, 1], [0, 4]).y, - axis: { ticks: { format: (value) => `${value}k` } }, + scales: { + x: { + ...linearAxes([0, 1], [0, 4]).scales.x, + axis: { label: 'Week' }, + }, + y: { + ...linearAxes([0, 1], [0, 4]).scales.y, + axis: { ticks: { format: (value) => `${value}k` } }, + }, }, }) const host = mountChart(container, { @@ -919,8 +934,16 @@ describe('dynamic chart runtime', () => { const data = [{ id: 'released', x: 1, y: 4 }] const definition = defineChart({ marks: [lineY(data, { x: 'x', y: 'y', key: 'id' })], - x: { ...linearAxes([0, 2], [0, 4]).x, axis: { label: 'Week' } }, - y: { ...linearAxes([0, 2], [0, 4]).y, axis: { label: 'Downloads' } }, + scales: { + x: { + ...linearAxes([0, 2], [0, 4]).scales.x, + axis: { label: 'Week' }, + }, + y: { + ...linearAxes([0, 2], [0, 4]).scales.y, + axis: { label: 'Downloads' }, + }, + }, }) const resolveText = (context: ChartTooltipContentContext) => { contexts.push(context) @@ -1726,12 +1749,17 @@ describe('dynamic chart runtime', () => { const spacious = textMeasurer(1.2) const definition = defineChart({ marks: [lineY([1, 2, 3])], - x: { ...linearAxes([0, 2], [0, 3]).x, axis: { label: 'Release' } }, - y: { - ...linearAxes([0, 2], [0, 3]).y, - axis: { - ticks: { format: () => 'Long formatted tick' }, - label: 'Downloads', + scales: { + x: { + ...linearAxes([0, 2], [0, 3]).scales.x, + axis: { label: 'Release' }, + }, + y: { + ...linearAxes([0, 2], [0, 3]).scales.y, + axis: { + ticks: { format: () => 'Long formatted tick' }, + label: 'Downloads', + }, }, }, }) diff --git a/packages/charts-core/src/scene-embed-internal.test.ts b/packages/charts-core/src/scene-embed-internal.test.ts index 8f63cc38..5b3ac031 100644 --- a/packages/charts-core/src/scene-embed-internal.test.ts +++ b/packages/charts-core/src/scene-embed-internal.test.ts @@ -37,8 +37,10 @@ describe('embedded chart scenes', () => { { match: 'x', retarget: true }, ), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, }), { width: 240, height: 180 }, ) diff --git a/packages/charts-core/src/scene-layout.test.ts b/packages/charts-core/src/scene-layout.test.ts index c94758ef..8d2d8492 100644 --- a/packages/charts-core/src/scene-layout.test.ts +++ b/packages/charts-core/src/scene-layout.test.ts @@ -40,15 +40,17 @@ describe('automatic scene guide layout', () => { const scene = createChartScene( defineChart({ marks: [lineY([1, 2, 3])], - x: { - scale: scaleLinear().domain([0, 2]), - grid: true, - axis: { ticks: { count: 3, size: 0 } }, - }, - y: { - scale: scaleLinear().domain([0, 3]), - grid: true, - axis: { ticks: { count: 4, size: 0 } }, + scales: { + x: { + scale: scaleLinear().domain([0, 2]), + grid: true, + axis: { ticks: { count: 3, size: 0 } }, + }, + y: { + scale: scaleLinear().domain([0, 3]), + grid: true, + axis: { ticks: { count: 4, size: 0 } }, + }, }, }), { width: 480, height: 260 }, @@ -76,11 +78,13 @@ describe('automatic scene guide layout', () => { { x: 'category', y: 'value' }, ), ], - x: { - scale: scaleBand().domain(categories), - axis: { tickLabels: { rotate: -30 } }, + scales: { + x: { + scale: scaleBand().domain(categories), + axis: { tickLabels: { rotate: -30 } }, + }, + y: { scale: scaleLinear().domain([0, categories.length]) }, }, - y: { scale: scaleLinear().domain([0, categories.length]) }, }) const narrow = createChartScene( definition, @@ -95,9 +99,12 @@ describe('automatic scene guide layout', () => { const all = createChartScene( { ...definition, - x: { - ...definition.x, - axis: { tickLabels: { rotate: -30, thin: false } }, + scales: { + ...definition.scales, + x: { + ...definition.scales.x, + axis: { tickLabels: { rotate: -30, thin: false } }, + }, }, }, { width: 320, height: 260 }, @@ -138,11 +145,16 @@ describe('automatic scene guide layout', () => { ), ], margin: 0, - x: { - scale: scaleBand().domain(categories), - axis: { tickLabels }, + scales: { + x: { + scale: scaleBand().domain(categories), + axis: { tickLabels }, + }, + y: { + scale: scaleLinear().domain([0, categories.length]), + axis: false, + }, }, - y: { scale: scaleLinear().domain([0, categories.length]), axis: false }, }), { width: 260, height: 180 }, { measureText }, @@ -172,24 +184,26 @@ describe('automatic scene guide layout', () => { defineChart({ marks: [lineY([0, 1])], margin: 0, - x: { - scale: scaleLinear().domain([0, 1]), - axis: { - ticks: { values: [0, 1] }, - tickLabels: { - rotate: -30, - fontSize: ({ index }) => (index === 0 ? 18 : undefined), - fontWeight: ({ index }) => (index === 0 ? 700 : undefined), - opacity: ({ index }) => (index === 0 ? 0.4 : undefined), - anchor: ({ index }) => (index === 0 ? 'start' : undefined), - dx: ({ index }) => (index === 0 ? 3 : undefined), - dy: ({ index }) => (index === 0 ? 5 : undefined), + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { + ticks: { values: [0, 1] }, + tickLabels: { + rotate: -30, + fontSize: ({ index }) => (index === 0 ? 18 : undefined), + fontWeight: ({ index }) => (index === 0 ? 700 : undefined), + opacity: ({ index }) => (index === 0 ? 0.4 : undefined), + anchor: ({ index }) => (index === 0 ? 'start' : undefined), + dx: ({ index }) => (index === 0 ? 3 : undefined), + dy: ({ index }) => (index === 0 ? 5 : undefined), + }, }, }, - }, - y: { - scale: scaleLinear().domain([0, 1]), - axis: { ticks: { values: [0, 1] } }, + y: { + scale: scaleLinear().domain([0, 1]), + axis: { ticks: { values: [0, 1] } }, + }, }, }), { width: 480, height: 240 }, @@ -230,18 +244,20 @@ describe('automatic scene guide layout', () => { const definition = (fontSize: number, dx: number) => defineChart({ marks: [lineY([0, 1])], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { - ticks: { values: [0, 1] }, - tickLabels: { - fontSize: () => fontSize, - anchor: () => 'start' as const, - dx: () => dx, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { + ticks: { values: [0, 1] }, + tickLabels: { + fontSize: () => fontSize, + anchor: () => 'start' as const, + dx: () => dx, + }, }, }, + y: { scale: scaleLinear().domain([0, 1]), axis: false }, }, - y: { scale: scaleLinear().domain([0, 1]), axis: false }, }) const regular = createChartScene( definition(11, 0), @@ -274,15 +290,17 @@ describe('automatic scene guide layout', () => { const scene = createChartScene( defineChart({ marks: [lineY([0, 10])], - x: { - scale: scaleLinear().domain([0, 10]), - grid: true, - axis: { - ticks: { values: [0, 10], size: 0 }, - tickLabels: { thin: { keep: [5] } }, + scales: { + x: { + scale: scaleLinear().domain([0, 10]), + grid: true, + axis: { + ticks: { values: [0, 10], size: 0 }, + tickLabels: { thin: { keep: [5] } }, + }, }, + y: { scale: scaleLinear().domain([0, 10]), axis: false }, }, - y: { scale: scaleLinear().domain([0, 10]), axis: false }, }), { width: 320, height: 180 }, { measureText }, @@ -298,11 +316,13 @@ describe('automatic scene guide layout', () => { it('uses axis length for spacing-based semantic candidates', () => { const definition = defineChart({ marks: [lineY([0, 100])], - x: { - scale: scaleLinear().domain([0, 100]), - axis: { ticks: { spacing: 80 } }, + scales: { + x: { + scale: scaleLinear().domain([0, 100]), + axis: { ticks: { spacing: 80 } }, + }, + y: { scale: scaleLinear().domain([0, 100]) }, }, - y: { scale: scaleLinear().domain([0, 100]) }, }) const narrow = createChartScene(definition, { width: 240, height: 180 }) const wide = createChartScene(definition, { width: 960, height: 180 }) @@ -317,11 +337,13 @@ describe('automatic scene guide layout', () => { createChartScene( defineChart({ marks: [lineY([0, 10])], - x: { - scale: scaleLinear().domain([0, 10]), - axis: { ticks: { count: 3, spacing: 80 } }, + scales: { + x: { + scale: scaleLinear().domain([0, 10]), + axis: { ticks: { count: 3, spacing: 80 } }, + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, }), { width: 320, height: 180 }, ), @@ -374,11 +396,16 @@ describe('automatic scene guide layout', () => { { x: 'category', y: 'value' }, ), ], - x: { - scale: scaleBand().domain(domain).padding(0.1), - axis: { tickLabels: { rotate: -35 }, label: 'Package' }, + scales: { + x: { + scale: scaleBand().domain(domain).padding(0.1), + axis: { tickLabels: { rotate: -35 }, label: 'Package' }, + }, + y: { + scale: scaleLinear().domain([0, 3]), + axis: { label: 'Downloads' }, + }, }, - y: { scale: scaleLinear().domain([0, 3]), axis: { label: 'Downloads' } }, }) const narrow = createChartScene( definition, @@ -419,13 +446,15 @@ describe('automatic scene guide layout', () => { it('keeps long axis titles inside narrow surfaces', () => { const definition = defineChart({ marks: [lineY([1, 2, 3])], - x: { - scale: scaleLinear().domain([0, 2]), - axis: { - label: '← more disagree · Number of responses · more agree →', + scales: { + x: { + scale: scaleLinear().domain([0, 2]), + axis: { + label: '← more disagree · Number of responses · more agree →', + }, }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, }) const scene = createChartScene( definition, @@ -470,8 +499,10 @@ describe('automatic scene guide layout', () => { dy: (row) => (row.y === 0 ? 13 : -13), }), ], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, }) const automatic = createChartScene( definition, @@ -518,12 +549,14 @@ describe('automatic scene guide layout', () => { it('treats numeric margin sides as locks', () => { const definition = defineChart({ marks: [lineY([1, 2, 3])], - x: { scale: scaleLinear().domain([0, 2]), axis: { label: 'Index' } }, - y: { - scale: scaleLinear().domain([0, 3]), - axis: { - ticks: { format: () => 'A deliberately long tick label' }, - label: 'Value', + scales: { + x: { scale: scaleLinear().domain([0, 2]), axis: { label: 'Index' } }, + y: { + scale: scaleLinear().domain([0, 3]), + axis: { + ticks: { format: () => 'A deliberately long tick label' }, + label: 'Value', + }, }, }, margin: { left: 7 }, @@ -557,8 +590,10 @@ describe('automatic scene guide layout', () => { dy: -8, }), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, margin: { right: 7 }, }), { width: 480, height: 260 }, @@ -579,8 +614,10 @@ describe('automatic scene guide layout', () => { }), ] const axes = { - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, } const locked = createChartScene( defineChart({ @@ -619,8 +656,10 @@ describe('automatic scene guide layout', () => { }), ], guides: false, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 480, height: 260 }, { measureText }, @@ -641,13 +680,15 @@ describe('automatic scene guide layout', () => { })) const definition = defineChart({ marks: [createMark(initialize)], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { label: 'Horizontal axis' }, - }, - y: { - scale: scaleLinear().domain([0, 1]), - axis: { label: 'Vertical axis' }, + scales: { + x: { + scale: scaleLinear().domain([0, 1]), + axis: { label: 'Horizontal axis' }, + }, + y: { + scale: scaleLinear().domain([0, 1]), + axis: { label: 'Vertical axis' }, + }, }, }) @@ -694,8 +735,10 @@ describe('automatic scene guide layout', () => { defineChart({ marks: [mark], guides: false, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 480, height: 260 }, { measureText }, @@ -719,8 +762,10 @@ describe('automatic scene guide layout', () => { }), ], guides: false, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }) const measuredScene = (height: number) => createChartScene( @@ -746,8 +791,10 @@ describe('automatic scene guide layout', () => { defineChart({ marks: [], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 480, height: 260 }, { measureText }, @@ -760,19 +807,33 @@ describe('automatic scene guide layout', () => { it('controls x and y guide visibility independently', () => { const definition = defineChart({ marks: [lineY([1, 2, 3])], - x: { scale: scaleLinear().domain([0, 2]), axis: { label: 'Horizontal' } }, - y: { - scale: scaleLinear().domain([0, 3]), - grid: true, - axis: { label: 'Vertical' }, + scales: { + x: { + scale: scaleLinear().domain([0, 2]), + axis: { label: 'Horizontal' }, + }, + y: { + scale: scaleLinear().domain([0, 3]), + grid: true, + axis: { label: 'Vertical' }, + }, }, }) const sceneFor = (x: boolean, y: boolean) => createChartScene( { ...definition, - x: { ...definition.x, axis: x ? definition.x.axis : false }, - y: { ...definition.y, axis: y ? definition.y.axis : false }, + scales: { + ...definition.scales, + x: { + ...definition.scales.x, + axis: x ? definition.scales.x.axis : false, + }, + y: { + ...definition.scales.y, + axis: y ? definition.scales.y.axis : false, + }, + }, }, { width: 480, height: 260 }, { measureText }, @@ -809,10 +870,12 @@ function sceneWithYFormat(format: (value: unknown) => string) { return createChartScene( defineChart({ marks: [lineY([1, 2, 3])], - x: { scale: scaleLinear().domain([0, 2]), axis: { label: 'Release' } }, - y: { - scale: scaleLinear().domain([0, 3]), - axis: { ticks: { format }, label: 'Downloads' }, + scales: { + x: { scale: scaleLinear().domain([0, 2]), axis: { label: 'Release' } }, + y: { + scale: scaleLinear().domain([0, 3]), + axis: { ticks: { format }, label: 'Downloads' }, + }, }, }), { width: 640, height: 320 }, diff --git a/packages/charts-core/src/scene.test.ts b/packages/charts-core/src/scene.test.ts index 695a7475..77158588 100644 --- a/packages/charts-core/src/scene.test.ts +++ b/packages/charts-core/src/scene.test.ts @@ -373,12 +373,14 @@ describe('native mark and channel scene', () => { key: 'id', }), ], - x: { - scale: linearAxes([0, 30], [0, 10]).x.scale, - viewport: { domain: [10, 20], translate }, - grid: true, + scales: { + x: { + scale: linearAxes([0, 30], [0, 10]).scales.x.scale, + viewport: { domain: [10, 20], translate }, + grid: true, + }, + y: { ...linearAxes([0, 30], [0, 10]).scales.y, grid: true }, }, - y: { ...linearAxes([0, 30], [0, 10]).y, grid: true }, }), { width: 480, height: 260 }, ) @@ -497,11 +499,13 @@ describe('native mark and channel scene', () => { }), lineY(history, { id: 'history-x', x: 'x', y: 'y', key: 'id' }), ], - x: { - scale: axes.x.scale, - viewport: { domain: [2, 8], translate: 36 }, + scales: { + x: { + scale: axes.scales.x.scale, + viewport: { domain: [2, 8], translate: 36 }, + }, + y: axes.scales.y, }, - y: axes.y, guides: false, }), { width: 480, height: 260 }, @@ -518,10 +522,12 @@ describe('native mark and channel scene', () => { }), lineY(history, { id: 'history-y', x: 'x', y: 'y', key: 'id' }), ], - x: axes.x, - y: { - scale: axes.y.scale, - viewport: { domain: [2, 8], translate: -24 }, + scales: { + x: axes.scales.x, + y: { + scale: axes.scales.y.scale, + viewport: { domain: [2, 8], translate: -24 }, + }, }, guides: false, }), @@ -552,20 +558,22 @@ describe('native mark and channel scene', () => { key: 'id', }), ], - x: - axis === 'x' - ? { - scale: axes.x.scale, - viewport: { domain: [2, 8], translate: 0 }, - } - : axes.x, - y: - axis === 'y' - ? { - scale: axes.y.scale, - viewport: { domain: [2, 8], translate: 0 }, - } - : axes.y, + scales: { + x: + axis === 'x' + ? { + scale: axes.scales.x.scale, + viewport: { domain: [2, 8], translate: 0 }, + } + : axes.scales.x, + y: + axis === 'y' + ? { + scale: axes.scales.y.scale, + viewport: { domain: [2, 8], translate: 0 }, + } + : axes.scales.y, + }, guides: false, }), { width: 480, height: 260 }, @@ -653,11 +661,13 @@ describe('native mark and channel scene', () => { : []), lineY(rows, { id: 'history-b', x: 'x', y: 'y', key: 'id' }), ], - x: { - scale: linearAxes([0, 10], [0, 10]).x.scale, - viewport: { domain: [2, 8], translate: 12 }, + scales: { + x: { + scale: linearAxes([0, 10], [0, 10]).scales.x.scale, + viewport: { domain: [2, 8], translate: 12 }, + }, + y: linearAxes([0, 10], [0, 10]).scales.y, }, - y: linearAxes([0, 10], [0, 10]).y, guides: false, }), { width: 480, height: 260 }, @@ -716,11 +726,13 @@ describe('native mark and channel scene', () => { annotation('fixed-annotation', 'fixed', true), annotation('content-annotation', 'content', false), ], - x: { - scale: linearAxes([0, 10], [0, 1]).x.scale, - viewport: { domain: [2, 8], translate: 20 }, + scales: { + x: { + scale: linearAxes([0, 10], [0, 1]).scales.x.scale, + viewport: { domain: [2, 8], translate: 20 }, + }, + y: linearAxes([0, 10], [0, 1]).scales.y, }, - y: linearAxes([0, 10], [0, 1]).y, guides: false, }), { width: 480, height: 260 }, @@ -789,11 +801,13 @@ describe('native mark and channel scene', () => { lineY(rows, { id: 'history', x: 'x', y: 'y', key: 'id' }), fixed, ], - x: { - scale: linearAxes([0, 10], [0, 10]).x.scale, - viewport: { domain: [4, 6] }, + scales: { + x: { + scale: linearAxes([0, 10], [0, 10]).scales.x.scale, + viewport: { domain: [4, 6] }, + }, + y: linearAxes([0, 10], [0, 10]).scales.y, }, - y: linearAxes([0, 10], [0, 10]).y, guides: false, }), { width: 400, height: 200 }, @@ -839,11 +853,13 @@ describe('native mark and channel scene', () => { { x: 'x', y: 'y', text: 'label', key: 'id', anchor: 'end' }, ), ], - x: { - scale: linearAxes([-100, 100], [0, 1]).x.scale, - viewport: { domain: [0, 1] }, + scales: { + x: { + scale: linearAxes([-100, 100], [0, 1]).scales.x.scale, + viewport: { domain: [0, 1] }, + }, + y: linearAxes([-100, 100], [0, 1]).scales.y, }, - y: linearAxes([-100, 100], [0, 1]).y, guides: false, }), { width: 400, height: 200 }, @@ -896,8 +912,10 @@ describe('native mark and channel scene', () => { const scene = createChartScene( defineChart({ marks: [lineY([1, 3, 2])], - x: { ...linearAxes([0, 2], [0, 3]).x, grid: true }, - y: { ...linearAxes([0, 2], [0, 3]).y, grid: true }, + scales: { + x: { ...linearAxes([0, 2], [0, 3]).scales.x, grid: true }, + y: { ...linearAxes([0, 2], [0, 3]).scales.y, grid: true }, + }, }), { width: 480, height: 260 }, ) diff --git a/packages/charts-core/src/scene.ts b/packages/charts-core/src/scene.ts index c30663bc..5f465fd4 100644 --- a/packages/charts-core/src/scene.ts +++ b/packages/charts-core/src/scene.ts @@ -59,8 +59,6 @@ import type { SceneNode, } from './types' -declare const process: { readonly env: { readonly NODE_ENV?: string } } - export const defaultChartTheme: ChartTheme = { foreground: 'currentColor', muted: 'currentColor', @@ -98,6 +96,13 @@ type DefinedStaticChart< > & Pick> +type ErasedChartMarks = readonly ChartMark[] + +type ErasedChartSpec = { + marks: ErasedChartMarks + scales: Readonly> +} + type ChartDefinitionDatum = TDefinition extends ChartDefinition ? TDatum : never @@ -113,14 +118,7 @@ type ChartDefinitionWithOptions = Omit< > & TOptions -type DefinedResponsiveChart< - TSpec extends { - marks: readonly ChartMark[] - scales?: Readonly> - x?: ChartPositionScaleOptions | null - y?: ChartPositionScaleOptions | null - }, -> = Omit< +type DefinedResponsiveChart = Omit< ResponsiveChartDefinition< ChartSpecDatum, ChartSpecXValue, @@ -142,12 +140,7 @@ export function defineChart< >, ): DefinedStaticChart export function defineChart< - const TSpec extends { - marks: readonly ChartMark[] - scales?: Readonly> - x?: ChartPositionScaleOptions | null - y?: ChartPositionScaleOptions | null - }, + const TSpec extends ErasedChartSpec, TTooltipHost extends string = never, >( config: ResponsiveChartConfig, @@ -160,14 +153,7 @@ export function defineChart< keyof ChartDefinitionOptions > & ResponsiveChartConfig -export function defineChart< - const TSpec extends { - marks: readonly ChartMark[] - scales?: Readonly> - x?: ChartPositionScaleOptions | null - y?: ChartPositionScaleOptions | null - }, ->( +export function defineChart( chart: (context: ChartBuildContext) => CheckedChartSpec, ): DefinedResponsiveChart export function defineChart< @@ -187,19 +173,14 @@ export function defineChart< options: TOptions, ): ChartDefinitionWithOptions, TOptions> export function defineChart< - const TSpec extends { - marks: readonly ChartMark[] - scales?: Readonly> - x?: ChartPositionScaleOptions | null - y?: ChartPositionScaleOptions | null - }, + const TSpec extends ErasedChartSpec, const TOptions extends ChartDefinitionOptions< ChartSpecDatum, ChartSpecXValue, ChartSpecYValue >, >( - chart: (context: ChartBuildContext) => TSpec, + chart: (context: ChartBuildContext) => CheckedChartSpec, options: TOptions, ): ChartDefinitionWithOptions, TOptions> export function defineChart< @@ -219,6 +200,17 @@ export function defineChart< DefinedResponsiveChart, TOptions > +export function defineChart< + const TSpec extends ErasedChartSpec, + const TOptions extends ChartDefinitionOptions< + ChartSpecDatum, + ChartSpecXValue, + ChartSpecYValue + >, +>( + definition: DefinedResponsiveChart, + options: TOptions, +): ChartDefinitionWithOptions, TOptions> export function defineChart< const TDefinition extends StaticChartDefinition, const TOptions extends ChartDefinitionOptions< @@ -238,7 +230,10 @@ export function defineChart< ChartDefinitionYValue >, >( - definition: TDefinition, + definition: TDefinition & + (ErasedChartSpec extends ReturnType + ? unknown + : never), options: TOptions, ): ChartDefinitionWithOptions, NoInfer> export function defineChart(definition?: any, options?: any): any { @@ -857,40 +852,13 @@ function collectPositionScaleChannels( return collected } -let warnedLegacyScaleOptions = false - function resolveScaleDefinitions( definition: StaticChartDefinition, collected: ReadonlyMap, ): readonly PositionScaleDefinition[] { - if ( - (definition.scales === undefined || - definition.x !== undefined || - definition.y !== undefined) && - !warnedLegacyScaleOptions - ) { - try { - if (process.env.NODE_ENV !== 'production') { - warnedLegacyScaleOptions = true - console.warn( - '[TanStack Charts] Root `x` and `y` options are deprecated. Move them to `scales.x` and `scales.y`. When neither Cartesian scale is used, set `scales` to `{ x: null, y: null }`. This compatibility will be removed when TanStack Charts enters Alpha.', - ) - } - } catch { - warnedLegacyScaleOptions = true - // Keep raw-browser migration help removable by production minifiers. - /* @__PURE__ */ console.warn( - '[TanStack Charts] Root `x` and `y` options are deprecated. Move them to `scales.x` and `scales.y`. When neither Cartesian scale is used, set `scales` to `{ x: null, y: null }`. This compatibility will be removed when TanStack Charts enters Alpha.', - ) - } - } - - const scales = definition.scales ?? { - x: definition.x, - y: definition.y, - } + const scales = definition.scales - if (!Object.hasOwn(scales, 'x') || !Object.hasOwn(scales, 'y')) { + if (!scales || !Object.hasOwn(scales, 'x') || !Object.hasOwn(scales, 'y')) { throw new TypeError('Chart scales must define reserved `x` and `y` entries') } diff --git a/packages/charts-core/src/selection.test.ts b/packages/charts-core/src/selection.test.ts index 70329b3e..ff73683f 100644 --- a/packages/charts-core/src/selection.test.ts +++ b/packages/charts-core/src/selection.test.ts @@ -152,8 +152,10 @@ describe('controlled keyed selection', () => { }), selectedMark, ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, selection, }) const scene = createChartScene(definition, { width: 400, height: 240 }) @@ -208,8 +210,10 @@ describe('controlled keyed selection', () => { const scene = createChartScene( defineChart({ marks: [selectedMark], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, selection, }), { width: 320, height: 180 }, @@ -259,8 +263,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, selection, @@ -300,8 +306,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, selection, @@ -345,8 +353,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, selection, @@ -397,8 +407,10 @@ describe('controlled keyed selection', () => { const scene = createChartScene( defineChart({ marks: [selectedMark], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, selection, @@ -454,8 +466,10 @@ describe('controlled keyed selection', () => { const scene = createChartScene( defineChart({ marks: [selectedMark], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, guides: false, focusRing: false, selection, @@ -507,8 +521,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, selection, }), { width: 360, height: 200 }, @@ -556,6 +572,7 @@ describe('controlled keyed selection', () => { selection, ), ], + scales: { x: null, y: null }, guides: false, focusRing: false, selection, @@ -601,8 +618,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, color: { domain: ['shown', 'hidden'], range: ['#2563eb', '#f97316'], @@ -656,8 +675,10 @@ describe('controlled keyed selection', () => { return { definition: defineChart({ marks: [dot(rows, { x: 'x', y: 'y', key: 'id' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, maxFocusDistance: 8, selection, }), @@ -754,8 +775,10 @@ describe('controlled keyed selection', () => { selection, ), ], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, maxFocusDistance: 8, selection, }), diff --git a/packages/charts-core/src/spatial-contour.test.ts b/packages/charts-core/src/spatial-contour.test.ts index 95296ef3..7ed8532b 100644 --- a/packages/charts-core/src/spatial-contour.test.ts +++ b/packages/charts-core/src/spatial-contour.test.ts @@ -54,6 +54,7 @@ describe('scalar-grid contours', () => { const scene = createChartScene( defineChart({ marks: [mark], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: { top: 11, right: 13, bottom: 17, left: 19 }, @@ -204,6 +205,7 @@ function renderNumericContour( smooth: options.smooth, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, margin: 0, diff --git a/packages/charts-core/src/spatial-delaunay.test.ts b/packages/charts-core/src/spatial-delaunay.test.ts index 192b8b30..8499027b 100644 --- a/packages/charts-core/src/spatial-delaunay.test.ts +++ b/packages/charts-core/src/spatial-delaunay.test.ts @@ -35,8 +35,10 @@ describe('spatial Delaunay link', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 240, height: 180 }) const repeated = createChartScene(definition, { @@ -56,8 +58,10 @@ describe('spatial Delaunay link', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 240, height: 180 }, ) @@ -114,8 +118,10 @@ describe('spatial Delaunay link', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }) const narrow = createChartScene(definition, { width: 100, height: 200 }) const wide = createChartScene(definition, { width: 400, height: 200 }) @@ -143,8 +149,10 @@ describe('spatial Delaunay link', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 200, height: 200 }, ) @@ -183,8 +191,10 @@ describe('spatial Delaunay link', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 240, height: 180 }, ) @@ -214,8 +224,10 @@ describe('spatial Delaunay link', () => { ], guides: false, focusRing: false, - x: { scale: scaleBand }, - y: { scale: scaleTime() }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleTime() }, + }, }), { width: 240, height: 180 }, ) @@ -248,14 +260,18 @@ describe('spatial Delaunay link', () => { chart: (cellRows) => ({ marks: [delaunayLink(cellRows, { x: 'x', y: 'y', key: 'id' })], guides: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), }), ], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 320, height: 180 }, ) diff --git a/packages/charts-core/src/spatial-delaunay.ts b/packages/charts-core/src/spatial-delaunay.ts index a6d527ea..10f83452 100644 --- a/packages/charts-core/src/spatial-delaunay.ts +++ b/packages/charts-core/src/spatial-delaunay.ts @@ -27,6 +27,8 @@ import type { ChartKey, ChartMark, ChartValue, + MarkCallOptions, + MarkScaleBindings, } from './types' export interface DelaunayLinkDatum< @@ -60,16 +62,20 @@ export type DelaunayLinkOptions = { 'x1' | 'y1' | 'x2' | 'y2' | 'z' | 'key' > -type DelaunayXOutput = ChannelOutput< +type DelaunayLinkCallOptions< TDatum, - TOptions extends { x: infer TChannel } ? TChannel : never, - number -> - -type DelaunayYOutput = ChannelOutput< - TDatum, - TOptions extends { y: infer TChannel } ? TChannel : never, - number + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + DelaunayLinkOptions>, + { + x: TXChannel + y: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } > type PreparedDelaunayRow = LayoutXYSourceRow< @@ -88,21 +94,36 @@ type ProjectedDelaunayRow = PreparedDelaunayRow & /** Connects final-screen Delaunay neighbors while retaining semantic endpoints. */ export function delaunayLink< TDatum, - const TOptions extends DelaunayLinkOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: DelaunayLinkCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< DelaunayLinkDatum< TDatum, - DelaunayXOutput, - DelaunayYOutput + ChannelOutput, + ChannelOutput >, - DelaunayXOutput, - DelaunayYOutput, - DelaunayXOutput, - DelaunayYOutput, - TOptions + ChannelOutput, + ChannelOutput, + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function delaunayLink( source: Iterable, diff --git a/packages/charts-core/src/spatial-density.test.ts b/packages/charts-core/src/spatial-density.test.ts index c5ba7fe5..7bf58ec4 100644 --- a/packages/charts-core/src/spatial-density.test.ts +++ b/packages/charts-core/src/spatial-density.test.ts @@ -37,8 +37,10 @@ describe('spatial density contours', () => { guides: false, focusRing: false, margin: { top: 17, right: 13, bottom: 11, left: 19 }, - x: { scale: scaleSymlog }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleSymlog }, + y: { scale: scaleLinear }, + }, color: { scale: scaleLinear, range: ['#eff6ff', '#1d4ed8'], @@ -101,8 +103,10 @@ describe('spatial density contours', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([-2, 2]) }, - y: { scale: scaleLinear().domain([-1, 1]) }, + scales: { + x: { scale: scaleLinear().domain([-2, 2]) }, + y: { scale: scaleLinear().domain([-1, 1]) }, + }, }), { width: 320, height: 180 }, ) @@ -137,8 +141,10 @@ describe('spatial density contours', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }) const narrow = createChartScene(definition, { width: 220, height: 160 }) const wide = createChartScene(definition, { width: 660, height: 160 }) @@ -185,8 +191,10 @@ describe('spatial density contours', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width, height: 220 }, ) @@ -228,8 +236,10 @@ describe('spatial density contours', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 240, height: 180 }, ) diff --git a/packages/charts-core/src/spatial-density.ts b/packages/charts-core/src/spatial-density.ts index cb21dc0b..2d1d0678 100644 --- a/packages/charts-core/src/spatial-density.ts +++ b/packages/charts-core/src/spatial-density.ts @@ -38,6 +38,8 @@ import type { ChartMark, ChartMarkMotionOptions, ChartValue, + MarkCallOptions, + MarkScaleBindings, SceneNode, ScenePolygon, VisualChannel, @@ -74,16 +76,20 @@ export interface DensityContourOptions opacity?: number } -type DensityXOutput = ChannelOutput< +type DensityContourCallOptions< TDatum, - TOptions extends { x: infer TChannel } ? TChannel : never, - number -> - -type DensityYOutput = ChannelOutput< - TDatum, - TOptions extends { y: infer TChannel } ? TChannel : never, - number + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + DensityContourOptions>, + { + x: TXChannel + y: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } > interface DensitySourceRow extends LayoutXYSourceRow< @@ -116,17 +122,32 @@ type DensityWithContourAt = ContourDensity & { /** Estimates filled density contours after x/y scales reach final screen space. */ export function densityContour< TDatum, - const TOptions extends DensityContourOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: DensityContourCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< never, never, never, - DensityXOutput, - DensityYOutput, - TOptions + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function densityContour( source: Iterable, diff --git a/packages/charts-core/src/spatial-hexbin.test.ts b/packages/charts-core/src/spatial-hexbin.test.ts index a86ec9c8..a80b2723 100644 --- a/packages/charts-core/src/spatial-hexbin.test.ts +++ b/packages/charts-core/src/spatial-hexbin.test.ts @@ -29,8 +29,10 @@ describe('spatial hexbin', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, color: { scale: scaleLinear, range: ['#dbeafe', '#1d4ed8'], @@ -77,8 +79,10 @@ describe('spatial hexbin', () => { marks: [hexbin(rows, { x: 'x', y: 'y', binWidth: 44 })], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleLinear().domain([0, 100]) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, }) const narrow = createChartScene(definition, { width: 220, height: 160 }) const wide = createChartScene(definition, { width: 880, height: 160 }) @@ -112,8 +116,10 @@ describe('spatial hexbin', () => { defineChart({ marks: [hexbin(rows, { x: 'x', y: 'y' })], guides: false, - x: { scale: scaleBand }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleLinear }, + }, }), { width: 320, height: 180 }, ), diff --git a/packages/charts-core/src/spatial-voronoi.test.ts b/packages/charts-core/src/spatial-voronoi.test.ts index b43e0c38..2dd2d07e 100644 --- a/packages/charts-core/src/spatial-voronoi.test.ts +++ b/packages/charts-core/src/spatial-voronoi.test.ts @@ -42,8 +42,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }) const scene = createChartScene(definition, { width: 240, height: 180 }) const repeated = createChartScene(definition, { width: 240, height: 180 }) @@ -91,8 +93,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 3]) }, - y: { scale: scaleLinear().domain([0, 3]) }, + scales: { + x: { scale: scaleLinear().domain([0, 3]) }, + y: { scale: scaleLinear().domain([0, 3]) }, + }, }) const narrow = createChartScene(definition, { width: 100, height: 200 }) const wide = createChartScene(definition, { width: 400, height: 200 }) @@ -130,8 +134,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 200, height: 120 }, ) @@ -166,8 +172,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 200, height: 120 }, ) @@ -192,8 +200,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }), { width: 200, height: 200 }, ) @@ -223,8 +233,10 @@ describe('spatial Voronoi mark', () => { marks: [mark], guides: false, focusRing: false, - x: { scale: scaleBand }, - y: { scale: scaleTime }, + scales: { + x: { scale: scaleBand }, + y: { scale: scaleTime }, + }, }), { width: 280, height: 180 }, ) @@ -246,8 +258,10 @@ describe('spatial Voronoi mark', () => { guides: false, focusRing: false, margin: 0, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), { width: 200, height: 120 }, ) @@ -280,14 +294,18 @@ describe('spatial Voronoi mark', () => { chart: (cellRows) => ({ marks: [voronoi(cellRows, { x: 'x', y: 'y', key: 'id' })], guides: false, - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, }), }), ], guides: false, - x: null, - y: null, + scales: { + x: null, + y: null, + }, }), { width: 320, height: 180 }, ) diff --git a/packages/charts-core/src/spatial-voronoi.ts b/packages/charts-core/src/spatial-voronoi.ts index 7e71561c..bc458a52 100644 --- a/packages/charts-core/src/spatial-voronoi.ts +++ b/packages/charts-core/src/spatial-voronoi.ts @@ -28,6 +28,8 @@ import type { ChartMark, ChartMarkMotionOptions, ChartValue, + MarkCallOptions, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -50,16 +52,20 @@ export interface VoronoiOptions opacity?: number } -type VoronoiXOutput = ChannelOutput< +type VoronoiCallOptions< TDatum, - TOptions extends { x: infer TChannel } ? TChannel : never, - number -> - -type VoronoiYOutput = ChannelOutput< - TDatum, - TOptions extends { y: infer TChannel } ? TChannel : never, - number + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + VoronoiOptions>, + { + x: TXChannel + y: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } > type PreparedVoronoiRow = LayoutXYSourceRow< @@ -78,17 +84,32 @@ type ProjectedVoronoiRow = PreparedVoronoiRow & /** Draws final-screen Voronoi cells without adding focus candidates. */ export function voronoi< TDatum, - const TOptions extends VoronoiOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: VoronoiCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< never, never, never, - VoronoiXOutput, - VoronoiYOutput, - TOptions + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function voronoi( source: Iterable, diff --git a/packages/charts-core/src/stack-internal.test.ts b/packages/charts-core/src/stack-internal.test.ts index 7a5d6586..e4f9496b 100644 --- a/packages/charts-core/src/stack-internal.test.ts +++ b/packages/charts-core/src/stack-internal.test.ts @@ -359,6 +359,106 @@ describe('anchored stacks', () => { }) }) +describe('diverging offset zero handling', () => { + it('keeps a zero-valued cell on the running top of a positive stack', () => { + const positiveRows = [ + { position: 0, series: 'A', value: 10 }, + { position: 0, series: 'B', value: 5 }, + { position: 1, series: 'A', value: 10 }, + { position: 1, series: 'B', value: 0 }, + ] as const + const extents = stackExtents( + positiveRows.map((row, index) => ({ index, ...row })), + { order: ['A', 'B'] }, + ) + + expectExtent(extents, 0, 0, 10) + expectExtent(extents, 1, 10, 15) + expectExtent(extents, 2, 0, 10) + expectExtent(extents, 3, 10, 10) + }) + + it('keeps a zero-valued cell on the running bottom of an exclusively negative stack', () => { + const negativeRows = [ + { position: 0, series: 'A', value: -10 }, + { position: 0, series: 'B', value: -5 }, + { position: 1, series: 'A', value: -10 }, + { position: 1, series: 'B', value: 0 }, + ] as const + const extents = stackExtents( + negativeRows.map((row, index) => ({ index, ...row })), + { order: ['A', 'B'] }, + ) + + expectExtent(extents, 0, -10, 0) + expectExtent(extents, 1, -15, -10) + expectExtent(extents, 2, -10, 0) + expectExtent(extents, 3, -10, -10) + }) + + it('keeps an exclusively negative series on the axis when its negative baseline is zero', () => { + const mixedRows = [ + { position: 0, series: 'A', value: 10 }, + { position: 0, series: 'B', value: -3 }, + { position: 1, series: 'A', value: 10 }, + { position: 1, series: 'B', value: 0 }, + ] as const + const extents = stackExtents( + mixedRows.map((row, index) => ({ index, ...row })), + { order: ['A', 'B'] }, + ) + + expectExtent(extents, 3, 0, 0) + }) + + it('leaves an all-zero position on the axis', () => { + const allZeroRows = [ + { position: 0, series: 'A', value: 10 }, + { position: 0, series: 'B', value: 5 }, + { position: 1, series: 'A', value: 0 }, + { position: 1, series: 'B', value: 0 }, + ] as const + const extents = stackExtents( + allZeroRows.map((row, index) => ({ index, ...row })), + { order: ['A', 'B'] }, + ) + + expectExtent(extents, 2, 0, 0) + expectExtent(extents, 3, 0, 0) + }) + + it('shares the zero-aware policy with vertical and horizontal row transforms', () => { + const positiveRows = [ + { position: 0, series: 'A', value: 10 }, + { position: 0, series: 'B', value: 5 }, + { position: 1, series: 'A', value: 10 }, + { position: 1, series: 'B', value: 0 }, + ] as const + const options = { order: ['A', 'B'] as const } + const vertical = stackRowsY(positiveRows, { + x: 'position', + y: 'value', + z: 'series', + ...options, + }) + const horizontal = stackRowsX(positiveRows, { + x: 'value', + y: 'position', + z: 'series', + ...options, + }) + + expect(horizontal).toHaveLength(vertical.length) + vertical.forEach((datum, index) => { + const transposed = horizontal[index]! + expect(transposed.x1).toBeCloseTo(datum.y1) + expect(transposed.x2).toBeCloseTo(datum.y2) + }) + expect(vertical[3]!.y1).toBeCloseTo(10) + expect(vertical[3]!.y2).toBeCloseTo(10) + }) +}) + function expectExtent( extents: ReturnType, index: number, diff --git a/packages/charts-core/src/stack-internal.ts b/packages/charts-core/src/stack-internal.ts index c8ee5a6c..f15abbaf 100644 --- a/packages/charts-core/src/stack-internal.ts +++ b/packages/charts-core/src/stack-internal.ts @@ -1,6 +1,5 @@ import { stack as d3Stack, - stackOffsetDiverging, stackOffsetExpand, stackOffsetNone, stackOffsetSilhouette, @@ -81,7 +80,7 @@ export function stackExtents( ? stackOffsetSilhouette : options.offset === 'wiggle' ? stackOffsetWiggle - : stackOffsetDiverging + : stackOffsetDivergingZeroAware const generator = d3Stack, string>() .keys(identities) .value((row, key) => row[key] ?? 0) @@ -111,6 +110,56 @@ export function stackExtents( return output } +/** + * D3 parks exact zeros at the axis. That is invisible for bars, but an area or + * line interpolates through the zero and cuts across the layers below it. + * Keep a zero on the running baseline for the side its series occupies. + */ +function stackOffsetDivergingZeroAware( + series: Series, string>[], + order: number[], +): void { + if (series.length === 0) return + + // A zero has no sign. Treat only exclusively negative series as negative; + // positive, mixed, and all-zero series use the positive baseline. + const negativeSide = series.map((values) => { + let negative = false + let positive = false + for (const [start, end] of values) { + const value = end - start + if (value < 0) negative = true + else if (value > 0) positive = true + } + return negative && !positive + }) + + const positionCount = series[order[0]!]!.length + for (let position = 0; position < positionCount; position += 1) { + let positiveBaseline = 0 + let negativeBaseline = 0 + for (const seriesIndex of order) { + const extent = series[seriesIndex]![position]! + const value = extent[1] - extent[0] + if (value > 0) { + extent[0] = positiveBaseline + extent[1] = positiveBaseline += value + } else if (value < 0) { + extent[1] = negativeBaseline + extent[0] = negativeBaseline += value + } else if (value === 0) { + extent[0] = extent[1] = negativeSide[seriesIndex] + ? negativeBaseline + : positiveBaseline + } else { + // Preserve D3's non-finite behavior so downstream marks keep the gap. + extent[0] = 0 + extent[1] = value + } + } + } +} + function resolveAnchorFraction(options: Readonly) { const anchor = options.anchor if (!anchor) return undefined diff --git a/packages/charts-core/src/svg-surface.test.ts b/packages/charts-core/src/svg-surface.test.ts index 2ac7c2e0..0f23946f 100644 --- a/packages/charts-core/src/svg-surface.test.ts +++ b/packages/charts-core/src/svg-surface.test.ts @@ -23,11 +23,13 @@ describe('SVG surface coordinates', () => { { x: 'x', y: 'y' }, ), ], - x: { - scale: scaleLinear().domain([0, 30]), - viewport: { domain: [10, 20], translate: 32 }, + scales: { + x: { + scale: scaleLinear().domain([0, 30]), + viewport: { domain: [10, 20], translate: 32 }, + }, + y: { scale: scaleLinear().domain([0, 3]) }, }, - y: { scale: scaleLinear().domain([0, 3]) }, }), { width: 640, height: 320 }, ) @@ -70,11 +72,13 @@ describe('SVG surface coordinates', () => { { x: 'x', y: 'y' }, ), ], - x: { - scale: scaleLinear().domain([0, 2]), - viewport: { domain: [0.5, 1.5], translate }, + scales: { + x: { + scale: scaleLinear().domain([0, 2]), + viewport: { domain: [0.5, 1.5], translate }, + }, + y: { scale: scaleLinear().domain([0, 2]) }, }, - y: { scale: scaleLinear().domain([0, 2]) }, guides: false, }) const scene = (translate: number) => @@ -103,8 +107,10 @@ describe('SVG surface coordinates', () => { const scene = createChartScene( defineChart({ marks: [dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, margin: 0, }), { width: 640, height: 480 }, @@ -162,8 +168,10 @@ describe('SVG surface coordinates', () => { const scene = createChartScene( defineChart({ marks: [dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, margin: 0, }), { width: 640, height: 480 }, @@ -209,8 +217,10 @@ describe('SVG surface coordinates', () => { strokeDasharray: '3 2', }), ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, guides: false, margin: 20, }), @@ -279,8 +289,10 @@ describe('SVG surface coordinates', () => { crosshair({ marker: true }), dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]), grid: true }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]), grid: true }, + }, margin: 0, }), { width: 200, height: 100 }, @@ -339,8 +351,10 @@ describe('SVG surface coordinates', () => { ], }, ], - x: { scale: scaleLinear().domain([0, 4]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 4]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, guides: false, margin: 20, }), @@ -419,8 +433,10 @@ describe('SVG surface coordinates', () => { const scene = createChartScene( defineChart({ marks: [dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' }), crosshair()], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, }), { width: 200, height: 120 }, @@ -470,8 +486,10 @@ describe('SVG surface coordinates', () => { ], }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, }), { width: 320, height: 200 }, diff --git a/packages/charts-core/src/test-scales.ts b/packages/charts-core/src/test-scales.ts index 164b478f..0579532c 100644 --- a/packages/charts-core/src/test-scales.ts +++ b/packages/charts-core/src/test-scales.ts @@ -5,8 +5,10 @@ type Axes< TX extends number | string | Date, TY extends number | string | Date, > = { - x: ChartAxisOptions - y: ChartAxisOptions + scales: { + x: ChartAxisOptions + y: ChartAxisOptions + } } export function linearAxes( @@ -14,8 +16,10 @@ export function linearAxes( yDomain: readonly [number, number] = [0, 1], ): Axes { return { - x: { scale: scaleLinear().domain(xDomain) }, - y: { scale: scaleLinear().domain(yDomain) }, + scales: { + x: { scale: scaleLinear().domain(xDomain) }, + y: { scale: scaleLinear().domain(yDomain) }, + }, } } @@ -24,8 +28,10 @@ export function bandXAxes( yDomain: readonly [number, number], ): Axes { return { - x: { scale: scaleBand().domain(xDomain).padding(0.1) }, - y: { scale: scaleLinear().domain(yDomain) }, + scales: { + x: { scale: scaleBand().domain(xDomain).padding(0.1) }, + y: { scale: scaleLinear().domain(yDomain) }, + }, } } @@ -34,8 +40,10 @@ export function bandYAxes( yDomain: readonly string[], ): Axes { return { - x: { scale: scaleLinear().domain(xDomain) }, - y: { scale: scaleBand().domain(yDomain).padding(0.1) }, + scales: { + x: { scale: scaleLinear().domain(xDomain) }, + y: { scale: scaleBand().domain(yDomain).padding(0.1) }, + }, } } @@ -44,8 +52,10 @@ export function bandAxes( yDomain: readonly string[], ): Axes { return { - x: { scale: scaleBand().domain(xDomain).padding(0.1) }, - y: { scale: scaleBand().domain(yDomain).padding(0.1) }, + scales: { + x: { scale: scaleBand().domain(xDomain).padding(0.1) }, + y: { scale: scaleBand().domain(yDomain).padding(0.1) }, + }, } } @@ -54,7 +64,9 @@ export function utcXAxes( yDomain: readonly [number, number], ): Axes { return { - x: { scale: scaleUtc().domain(xDomain) }, - y: { scale: scaleLinear().domain(yDomain) }, + scales: { + x: { scale: scaleUtc().domain(xDomain) }, + y: { scale: scaleLinear().domain(yDomain) }, + }, } } diff --git a/packages/charts-core/src/text.ts b/packages/charts-core/src/text.ts index 8d44e3f0..9f42cc44 100644 --- a/packages/charts-core/src/text.ts +++ b/packages/charts-core/src/text.ts @@ -21,7 +21,9 @@ import type { MarkRenderContext, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkChannelOutput, + MarkScaleBindings, SceneLabel, SceneNode, VisualChannel, @@ -48,22 +50,45 @@ export interface TextOptions states?: readonly ChartMarkState>[] } +type TextCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + TextOptions>, + { + x?: TXChannel + y?: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + export function text( source: Iterable, ): ChartMark export function text< TDatum, - const TOptions extends TextOptions> | undefined, + const TXChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TYChannel extends + Channel, ChartValue | null | undefined> | undefined = never, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: + | TextCallOptions + | undefined, ): CartesianChartMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - TOptions + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkChannelOutput, + MarkScaleBindings > export function text( source: Iterable, diff --git a/packages/charts-core/src/tick.test.ts b/packages/charts-core/src/tick.test.ts index e3393309..a1fd92b9 100644 --- a/packages/charts-core/src/tick.test.ts +++ b/packages/charts-core/src/tick.test.ts @@ -23,8 +23,10 @@ describe('tick marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['A', 'B', 'C']) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scalePoint().domain(['A', 'B', 'C']) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, }), { width: 400, height: 240 }, ) @@ -60,8 +62,10 @@ describe('tick marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 5]) }, - y: { scale: scalePoint().domain(['A', 'B']) }, + scales: { + x: { scale: scaleLinear().domain([0, 5]) }, + y: { scale: scalePoint().domain(['A', 'B']) }, + }, }), { width: 400, height: 240 }, ) @@ -91,8 +95,10 @@ describe('tick marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scalePoint().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, }), { width: 400, height: 240 }, ) @@ -127,8 +133,10 @@ describe('tick marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 5]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 5]) }, + }, }), { width: 400, height: 240 }, ), diff --git a/packages/charts-core/src/tick.ts b/packages/charts-core/src/tick.ts index 14dd74ba..3f0562b4 100644 --- a/packages/charts-core/src/tick.ts +++ b/packages/charts-core/src/tick.ts @@ -14,6 +14,7 @@ import { import { valueKey } from './scales' import type { Channel, + ChannelOutput, CartesianChartMark, CartesianScaleBindings, ChartKey, @@ -21,7 +22,8 @@ import type { ChartMarkMotionOptions, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -60,19 +62,50 @@ export interface TickYOptions inset?: number } +type TickCallOptions< + TOptions, + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + TOptions, + { + x: TXChannel + y: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + export function tickX< TDatum, - const TOptions extends TickXOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: TickCallOptions< + TickXOptions>, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - TOptions + ChannelOutput, + ChannelOutput, + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function tickX( source: Iterable, @@ -83,17 +116,32 @@ export function tickX( export function tickY< TDatum, - const TOptions extends TickYOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: TickCallOptions< + TickYOptions>, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - TOptions + ChannelOutput, + ChannelOutput, + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function tickY( source: Iterable, diff --git a/packages/charts-core/src/type-contract.test.ts b/packages/charts-core/src/type-contract.test.ts index e2f8c0c0..0c1223c6 100644 --- a/packages/charts-core/src/type-contract.test.ts +++ b/packages/charts-core/src/type-contract.test.ts @@ -49,8 +49,6 @@ import type { ChartMarkPointY, ChartMarkScaleX, ChartMarkScaleY, - ChartMarkX, - ChartMarkY, ChartScale, ChartSpec, ChartSpecDatum, @@ -194,24 +192,26 @@ const literalMark = barY(literalRows, { x: 'category', y: 'value' }) const categoricalSpec: ChartSpec = { marks: [categoricalMark], - x: { - scale: scaleBand().domain(['Alpha']), - axis: { - ticks: { - format: (value) => { - expectTypeOf(value).toEqualTypeOf() - return value + scales: { + x: { + scale: scaleBand().domain(['Alpha']), + axis: { + ticks: { + format: (value) => { + expectTypeOf(value).toEqualTypeOf() + return value + }, }, }, }, - }, - y: { - scale: scaleLinear().domain([0, 4]), - axis: { - ticks: { - format: (value) => { - expectTypeOf(value).toEqualTypeOf() - return value.toLocaleString() + y: { + scale: scaleLinear().domain([0, 4]), + axis: { + ticks: { + format: (value) => { + expectTypeOf(value).toEqualTypeOf() + return value.toLocaleString() + }, }, }, }, @@ -219,48 +219,54 @@ const categoricalSpec: ChartSpec = { } const staticDefinition = defineChart({ marks: [categoricalMark], - x: { - scale: scaleBand().domain(['Alpha']), - axis: { - ticks: { - format: (value) => { - expectTypeOf(value).toEqualTypeOf() - return value + scales: { + x: { + scale: scaleBand().domain(['Alpha']), + axis: { + ticks: { + format: (value) => { + expectTypeOf(value).toEqualTypeOf() + return value + }, }, }, }, + y: { scale: scaleLinear().domain([0, 4]) }, }, - y: { scale: scaleLinear().domain([0, 4]) }, }) const numericDefinition = defineChart({ marks: [numericMark], - x: { - scale: scaleLinear().domain([0, 4]), - axis: { - ticks: { - format: (value) => { - expectTypeOf(value).toEqualTypeOf() - return value.toLocaleString() + scales: { + x: { + scale: scaleLinear().domain([0, 4]), + axis: { + ticks: { + format: (value) => { + expectTypeOf(value).toEqualTypeOf() + return value.toLocaleString() + }, }, }, }, + y: { scale: scaleLinear().domain([0, 4]) }, }, - y: { scale: scaleLinear().domain([0, 4]) }, }) const temporalDefinition = defineChart({ marks: [temporalMark], - x: { - scale: scaleUtc().domain(rows.map((row) => row.date)), - axis: { - ticks: { - format: (value) => { - expectTypeOf(value).toEqualTypeOf() - return value.toISOString() + scales: { + x: { + scale: scaleUtc().domain(rows.map((row) => row.date)), + axis: { + ticks: { + format: (value) => { + expectTypeOf(value).toEqualTypeOf() + return value.toISOString() + }, }, }, }, + y: { scale: scaleLinear().domain([0, 4]) }, }, - y: { scale: scaleLinear().domain([0, 4]) }, }) defineChart(temporalDefinition, { controls: [dateBrush] }) @@ -283,18 +289,24 @@ defineChart(numericDefinition, { controls: [dateZoom] }) const unionPositionMark = rows.length > 0 ? temporalMark : categoricalMark const unionPositionDefinition = defineChart({ marks: [unionPositionMark], - x: { scale: scaleTime().domain(rows.map((row) => row.date)) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleTime().domain(rows.map((row) => row.date)) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) const implicitIndexDefinition = defineChart({ marks: [implicitIndexMark], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 8]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 8]) }, + }, }) const literalDefinition = defineChart({ marks: [literalMark], - x: { scale: scaleBand().domain(['Alpha']) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleBand().domain(['Alpha']) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) const responsiveDefinition = defineChart(() => ({ ...categoricalSpec, @@ -335,16 +347,20 @@ const createHeterogeneousDefinition = (input: DynamicInput) => input.kind === 'line' ? { marks: [lineY(input.rows, { x: 'date', y: 'value', key: 'id' })], - x: { scale: scaleUtc().domain(input.rows.map((row) => row.date)) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleUtc().domain(input.rows.map((row) => row.date)) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, } : { marks: [barX(input.rows, { x: 'value', y: 'category', key: 'id' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { - scale: scaleBand().domain( - input.rows.map((row) => row.category), - ), + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { + scale: scaleBand().domain( + input.rows.map((row) => row.category), + ), + }, }, }, ) @@ -389,8 +405,10 @@ const optionalEndpointRect = rect(optionalEndpointRows, { }) const categoricalRectDefinition = defineChart({ marks: [categoricalRect], - x: { scale: scaleBand().domain(['Alpha', 'Beta']) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleBand().domain(['Alpha', 'Beta']) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }) const facetedMark = facet(rows, { by: 'category', @@ -418,14 +436,21 @@ const positionlessMark = createMarkWithScaleValues< })) const positionlessDefinition = defineChart({ marks: [positionlessMark], + scales: { x: null, y: null }, }) const verticalRuleDefinition = defineChart({ marks: [ruleY([1, 2])], - y: { scale: scaleLinear() }, + scales: { + x: null, + y: { scale: scaleLinear() }, + }, }) const horizontalRuleDefinition = defineChart({ marks: [ruleX([1, 2])], - x: { scale: scaleLinear() }, + scales: { + x: { scale: scaleLinear() }, + y: null, + }, }) const endpointCustomMark = createMarkWithScaleValues< Row, @@ -458,8 +483,10 @@ const endpointCustomMark = createMarkWithScaleValues< })) const endpointCustomDefinition = defineChart({ marks: [endpointCustomMark], - x: { scale: scaleBand().domain(rows.map((row) => row.category)) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleBand().domain(rows.map((row) => row.category)) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) const customScale: ChartScale = { id: 'custom', @@ -520,11 +547,58 @@ if (false) { defineChart({ marks: [numericMark] }) // @ts-expect-error A mixed chart still requires axes used by its Cartesian mark. defineChart({ marks: [positionlessMark, numericMark] }) - // @ts-expect-error Positionless marks do not accept a phantom x scale. + + const invalidPositionlessSpec: ChartSpec = + { + marks: [positionlessMark], + scales: { + // @ts-expect-error Positionless marks reject a configured phantom x scale. + x: { scale: scaleLinear() }, + y: null, + }, + } + void invalidPositionlessSpec + + // @ts-expect-error Positionless definitions reject configured phantom axes. defineChart({ marks: [positionlessMark], - x: { scale: scaleLinear() }, + scales: { x: { scale: scaleLinear() }, y: null }, }) + // @ts-expect-error Materialized dimensions reject null reserved scales. + defineChart({ + marks: [numericMark], + scales: { x: null, y: { scale: scaleLinear() } }, + }) + // @ts-expect-error Responsive definitions reject configured phantom axes. + defineChart(() => ({ + marks: [positionlessMark], + scales: { x: { scale: scaleLinear() }, y: null }, + })) + defineChart( + // @ts-expect-error Responsive definitions with options retain the scale contract. + () => ({ + marks: [positionlessMark], + scales: { x: { scale: scaleLinear() }, y: null }, + }), + { keyboard: true }, + ) + defineChart({ + // @ts-expect-error Responsive config objects retain the scale contract. + chart: () => ({ + marks: [positionlessMark], + scales: { x: { scale: scaleLinear() }, y: null }, + }), + }) + defineChart( + // @ts-expect-error Responsive config objects with options retain the scale contract. + { + chart: () => ({ + marks: [positionlessMark], + scales: { x: { scale: scaleLinear() }, y: null }, + }), + }, + { keyboard: true }, + ) const container = document.createElement('div') const temporalBehavior: ChartControl = { id: 'temporal-behavior', @@ -894,25 +968,31 @@ if (false) { const invalidCategoricalSpec: ChartSpec = { marks: [categoricalMark], - // @ts-expect-error The x channel emits strings, so a numeric scale is invalid. - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + // @ts-expect-error The x channel emits strings, so a numeric scale is invalid. + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, } void invalidCategoricalSpec const invalidNumericSpec: ChartSpec = { marks: [numericMark], - // @ts-expect-error Numeric x values cannot use a categorical scale. - x: { scale: scaleBand().domain(['Alpha']) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + // @ts-expect-error Numeric x values cannot use a categorical scale. + x: { scale: scaleBand().domain(['Alpha']) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, } void invalidNumericSpec const invalidTemporalSpec: ChartSpec = { marks: [temporalMark], - // @ts-expect-error Date x values cannot use a numeric scale. - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + // @ts-expect-error Date x values cannot use a numeric scale. + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, } void invalidTemporalSpec @@ -920,75 +1000,98 @@ if (false) { readonly [typeof unionPositionMark] > = { marks: [unionPositionMark], - // @ts-expect-error The string | Date axis has no numeric scale branch. - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + // @ts-expect-error The string | Date axis has no numeric scale branch. + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, } void invalidUnionPositionSpec // @ts-expect-error Static definitions infer and enforce the mark-to-scale contract. defineChart({ marks: [categoricalMark], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }) // @ts-expect-error Responsive definitions retain the mark-to-scale contract. defineChart(() => ({ marks: [barY(rows, { x: 'category', y: 'value' })], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, })) // @ts-expect-error Rect endpoint channels participate in the inferred scale contract. defineChart({ marks: [categoricalRect], - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, }) const validRectSpec: ChartSpec = { marks: [categoricalRect], - x: { scale: scaleBand().domain(['Alpha', 'Beta']) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleBand().domain(['Alpha', 'Beta']) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, } const validCellSpec: ChartSpec = { marks: [categoricalCell], - x: { scale: scaleBand().domain(['Alpha']) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + x: { scale: scaleBand().domain(['Alpha']) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, } const invalidRectSpec: ChartSpec = { marks: [categoricalRect], - // @ts-expect-error Rect endpoint channels emit strings, so a numeric scale is invalid. - x: { scale: scaleLinear().domain([0, 1]) }, - y: { scale: scaleLinear().domain([0, 1]) }, + scales: { + // @ts-expect-error Rect endpoint channels emit strings, so a numeric scale is invalid. + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 1]) }, + }, } const invalidCellSpec: ChartSpec = { marks: [categoricalCell], - x: { scale: scaleBand().domain(['Alpha']) }, - // @ts-expect-error Cell y channels emit numbers, so a categorical scale is invalid. - y: { scale: scaleBand().domain(['row']) }, + scales: { + x: { scale: scaleBand().domain(['Alpha']) }, + // @ts-expect-error Cell y channels emit numbers, so a categorical scale is invalid. + y: { scale: scaleBand().domain(['row']) }, + }, } const uncheckedFacetSpec: ChartSpec = { marks: [facetedMark], + scales: { x: null, y: null }, } const uncheckedCustomMarkSpec: ChartSpec = { marks: [customMark], - x: { scale: scaleBand() }, - y: { scale: scaleUtc() }, + scales: { + x: { scale: scaleBand() }, + y: { scale: scaleUtc() }, + }, } const invalidEndpointCustomSpec: ChartSpec< readonly [typeof endpointCustomMark] > = { marks: [endpointCustomMark], - // @ts-expect-error A custom mark's declared string scale values reject a linear scale. - x: { scale: scaleLinear() }, - y: { scale: scaleLinear() }, + scales: { + // @ts-expect-error A custom mark's declared string scale values reject a linear scale. + x: { scale: scaleLinear() }, + y: { scale: scaleLinear() }, + }, } const uncheckedScaleSpec: ChartSpec = { marks: [categoricalMark], - x: { scale: customScale }, - y: { scale: scaleLinear() }, + scales: { + x: { scale: customScale }, + y: { scale: scaleLinear() }, + }, } void [ validRectSpec, @@ -1052,16 +1155,6 @@ describe('public type contracts', () => { expectTypeOf< ChartMarkPointY >().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() expectTypeOf< ChartMarkScaleX >().toEqualTypeOf() @@ -1074,8 +1167,6 @@ describe('public type contracts', () => { expectTypeOf< ChartMarkPointY >().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() expectTypeOf< NonNullable >().toEqualTypeOf() @@ -1107,7 +1198,6 @@ describe('public type contracts', () => { ChartMarkScaleY >().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() expectTypeOf< ChartMarkScaleX >().toEqualTypeOf() diff --git a/packages/charts-core/src/types.ts b/packages/charts-core/src/types.ts index 3344f344..3ed32bb9 100644 --- a/packages/charts-core/src/types.ts +++ b/packages/charts-core/src/types.ts @@ -108,6 +108,14 @@ export type ChannelOutput = ? WidenChartValue> : WidenChartValue +export type MarkChannelOutput< + TDatum, + TChannel, + TFallback extends ChartValue, +> = [TChannel] extends [never] + ? WidenChartValue + : ChannelOutput + export type OptionChannelOutput< TDatum, TOptions, @@ -119,6 +127,23 @@ export type OptionChannelOutput< : WidenChartValue : never +export type MarkCallOptions = Omit< + TOptions, + keyof TInferred +> & { + [TKey in keyof TInferred]: + | TInferred[TKey] + | NoInfer +} + +export type MarkScaleBindings< + TXScaleId extends string | undefined = 'x', + TYScaleId extends string | undefined = 'y', +> = { + xScale?: TXScaleId + yScale?: TYScaleId +} + export type VisualChannel = TValue | ChannelAccessor @@ -650,12 +675,6 @@ export type ChartMarkPointY = TMark extends DecorativeChartMarkBrand : TYValue : never -/** @deprecated Prefer ChartMarkPointX when distinguishing point and scale values. */ -export type ChartMarkX = ChartMarkPointX - -/** @deprecated Prefer ChartMarkPointY when distinguishing point and scale values. */ -export type ChartMarkY = ChartMarkPointY - type IsAny = 0 extends 1 & TValue ? true : false export type ChartAxisValue = @@ -803,51 +822,33 @@ export interface ChartMarkMotionOptions< motion?: ChartMotionDefinition } -type ChartXSpec = +type ChartXScaleSpec = IsAny> extends true ? { x: ChartXOptionsForMarks | null } : [ChartMarkScaleX] extends [never] - ? { x?: null } + ? { x: null } : { x: ChartXOptionsForMarks } -type ChartYSpec = +type ChartYScaleSpec = IsAny> extends true ? { y: ChartYOptionsForMarks | null } : [ChartMarkScaleY] extends [never] - ? { y?: null } + ? { y: null } : { y: ChartYOptionsForMarks } export type ChartScales = - Readonly> & { - x: ChartXOptionsForMarks | null - y: ChartYOptionsForMarks | null - } - -interface CanonicalChartScaleSpec { - scales: ChartScales - /** @deprecated Move this value to `scales.x`. */ - x?: ChartXOptionsForMarks | null - /** @deprecated Move this value to `scales.y`. */ - y?: ChartYOptionsForMarks | null -} - -type LegacyChartScaleSpec = { - scales?: undefined -} & ChartXSpec & - ChartYSpec + Readonly> & + ChartXScaleSpec & + ChartYScaleSpec type ChartSpecForMarks = { marks: TMarks -} & ChartSpecBase & - (CanonicalChartScaleSpec | LegacyChartScaleSpec) + scales: ChartScales +} & ChartSpecBase interface StoredChartSpec extends ChartSpecBase { marks: AnyChartMarks - scales?: Readonly> - /** @deprecated Move this value to `scales.x`. */ - x?: ChartXOptionsForMarks | null - /** @deprecated Move this value to `scales.y`. */ - y?: ChartYOptionsForMarks | null + scales: Readonly> } export type ChartSpec = [ diff --git a/packages/charts-core/src/universal-types.ts b/packages/charts-core/src/universal-types.ts index 7daadd63..3aef282c 100644 --- a/packages/charts-core/src/universal-types.ts +++ b/packages/charts-core/src/universal-types.ts @@ -263,8 +263,6 @@ export type { ChartLineStateStyle, ChartRectStateStyle, ChartTextStateStyle, - ChartMarkX, - ChartMarkY, ChartMotionContext, ChartMotionDefinition, ChartMotionPhase, diff --git a/packages/charts-core/src/vector.ts b/packages/charts-core/src/vector.ts index 310b11ca..4481dbb2 100644 --- a/packages/charts-core/src/vector.ts +++ b/packages/charts-core/src/vector.ts @@ -11,6 +11,7 @@ import { import { valueKey } from './scales' import type { Channel, + ChannelOutput, CartesianChartMark, CartesianScaleBindings, ChartKey, @@ -18,7 +19,8 @@ import type { ChartMarkMotionOptions, ChartPoint, ChartValue, - OptionChannelOutput, + MarkCallOptions, + MarkScaleBindings, SceneNode, VisualChannel, } from './types' @@ -43,23 +45,54 @@ export interface VectorOptions headAngle?: number } +type VectorCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId extends string | undefined, + TYScaleId extends string | undefined, +> = MarkCallOptions< + VectorOptions>, + { + x: TXChannel + y: TYChannel + xScale?: TXScaleId + yScale?: TYScaleId + } +> + /** * Draws fixed-pixel vectors at scaled anchors. Rotation is clockwise in * degrees, with zero pointing up. */ export function vector< TDatum, - const TOptions extends VectorOptions>, + const TXChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TYChannel extends Channel< + NoInfer, + ChartValue | null | undefined + >, + const TXScaleId extends string | undefined = undefined, + const TYScaleId extends string | undefined = undefined, >( source: Iterable, - options: TOptions, + options: VectorCallOptions< + TDatum, + TXChannel, + TYChannel, + TXScaleId, + TYScaleId + >, ): CartesianChartMark< TDatum, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - OptionChannelOutput, - TOptions + ChannelOutput, + ChannelOutput, + ChannelOutput, + ChannelOutput, + MarkScaleBindings > export function vector( source: Iterable, diff --git a/packages/charts-core/src/view-compose.test.ts b/packages/charts-core/src/view-compose.test.ts index 1e61d3d5..49ce0986 100644 --- a/packages/charts-core/src/view-compose.test.ts +++ b/packages/charts-core/src/view-compose.test.ts @@ -79,8 +79,10 @@ describe('composed views', () => { }) return { marks: [dot([{ x: width, y: height }], { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, width]) }, - y: { scale: scaleLinear().domain([0, height]) }, + scales: { + x: { scale: scaleLinear().domain([0, width]) }, + y: { scale: scaleLinear().domain([0, height]) }, + }, guides: false, margin: 0, } @@ -262,8 +264,10 @@ describe('composed views', () => { views: { main: defineChart({ marks: [dot([centerRow], { id: 'center-dot', x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 10]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 10]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, guides: false, margin: 0, }), @@ -277,10 +281,13 @@ describe('composed views', () => { innerRadius: ({ radius }) => radius * 0.55, }), ], + scales: { angle: null, radius: null }, }), ], - x: null, - y: null, + scales: { + x: null, + y: null, + }, guides: false, margin: 0, }), @@ -646,10 +653,12 @@ function mixedDefinition(rows: readonly MainRow[] = mainRows) { key: 'id', }), ], - x: { - scale: scaleUtc().domain(rows.map((row) => row.at)), + scales: { + x: { + scale: scaleUtc().domain(rows.map((row) => row.at)), + }, + y: { scale: scaleLinear().domain([0, 10]) }, }, - y: { scale: scaleLinear().domain([0, 10]) }, guides: false, margin: 0, }) @@ -666,10 +675,13 @@ function mixedDefinition(rows: readonly MainRow[] = mainRows) { innerRadius: ({ radius }) => radius * 0.55, }), ], + scales: { angle: null, radius: null }, }), ], - x: null, - y: null, + scales: { + x: null, + y: null, + }, guides: false, margin: 0, }) @@ -752,8 +764,10 @@ function linearChild(domain: readonly [number, number]) { key: 'id', }), ], - x: { scale: scaleLinear().domain(domain) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain(domain) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, margin: 0, }) @@ -781,8 +795,10 @@ function controlledLegendChild() { color: 'series', }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, color: { legend }, }) } diff --git a/packages/charts-core/src/view.test.ts b/packages/charts-core/src/view.test.ts index 9984d4ca..36f87c8b 100644 --- a/packages/charts-core/src/view.test.ts +++ b/packages/charts-core/src/view.test.ts @@ -163,8 +163,10 @@ describe('view grids', () => { align: { x: 'detail' }, chart: defineChart({ marks: [dot([{ x: 50, y: 1 }], { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 100]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 100]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, guides: false, }), }, @@ -174,10 +176,12 @@ describe('view grids', () => { column: 'main', chart: defineChart({ marks: [dot([{ x: 5, y: 1 }], { x: 'x', y: 'y' })], - y: { scale: scaleLinear().domain([0, 2]) }, - x: { - scale: scaleLinear().domain([0, 10]), - axis: { label: 'A deliberately long detail-axis label' }, + scales: { + x: { + scale: scaleLinear().domain([0, 10]), + axis: { label: 'A deliberately long detail-axis label' }, + }, + y: { scale: scaleLinear().domain([0, 2]) }, }, }), }, @@ -193,12 +197,14 @@ describe('view grids', () => { const child = (domain: readonly [number, number], power = false) => defineChart({ marks: [dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' })], - x: { - scale: power - ? scalePow().exponent(2).domain(domain) - : scaleLinear().domain(domain), + scales: { + x: { + scale: power + ? scalePow().exponent(2).domain(domain) + : scaleLinear().domain(domain), + }, + y: { scale: scaleLinear().domain([0, 2]) }, }, - y: { scale: scaleLinear().domain([0, 2]) }, guides: false, }) const compose = ( @@ -252,8 +258,10 @@ describe('view grids', () => { it('validates ids, cells, links, resources, and child ownership', () => { const chart = defineChart({ marks: [dot([{ x: 1, y: 1 }], { x: 'x', y: 'y' })], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }) const base = { rows: [{ id: 'main', grow: 1 }], @@ -408,8 +416,10 @@ describe('view grids', () => { motion: timing, }), ], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }), }, ], @@ -450,13 +460,15 @@ function marginalGrid( column: 'main', chart: defineChart({ marks: [dot(rows, { id: 'main-dots', x: 'x', y: 'y', key: 'id' })], - x: { - scale: xScale, - axis: { label: 'A long horizontal measurement label' }, - }, - y: { - scale: yScale, - axis: { label: 'A long vertical measurement label' }, + scales: { + x: { + scale: xScale, + axis: { label: 'A long horizontal measurement label' }, + }, + y: { + scale: yScale, + axis: { label: 'A long vertical measurement label' }, + }, }, }), }, @@ -477,8 +489,10 @@ function marginalGrid( key: 'id', }), ], - x: { scale: xScale }, - y: { scale: scaleLinear }, + scales: { + x: { scale: xScale }, + y: { scale: scaleLinear }, + }, guides: false, }), }, @@ -500,8 +514,10 @@ function marginalGrid( key: 'id', }), ], - x: { scale: scaleLinear }, - y: { scale: yScale }, + scales: { + x: { scale: scaleLinear }, + y: { scale: yScale }, + }, guides: false, }), }, diff --git a/packages/charts-core/src/view.ts b/packages/charts-core/src/view.ts index 9e26e9f3..e02e122f 100644 --- a/packages/charts-core/src/view.ts +++ b/packages/charts-core/src/view.ts @@ -513,8 +513,10 @@ function createViewComposition< marks: [mark], guides: false, margin: 0, - x: null, - y: null, + scales: { + x: null, + y: null, + }, } } @@ -757,7 +759,7 @@ function assertChildDefinition( ) } for (const axis of ['x', 'y'] as const) { - const configured = definition[axis] + const configured = definition.scales[axis] const presentation = !configured || configured.axis === false ? undefined diff --git a/packages/charts-core/src/violin.test.ts b/packages/charts-core/src/violin.test.ts index b9e66269..28be3f30 100644 --- a/packages/charts-core/src/violin.test.ts +++ b/packages/charts-core/src/violin.test.ts @@ -42,10 +42,12 @@ describe('violin marks', () => { marks: [mark], guides: false, focusRing: false, - x: { - scale: scalePoint().domain(['Adelie', 'Chinstrap', 'Gentoo']), + scales: { + x: { + scale: scalePoint().domain(['Adelie', 'Chinstrap', 'Gentoo']), + }, + y: { scale: scaleLinear().domain([3000, 5000]) }, }, - y: { scale: scaleLinear().domain([3000, 5000]) }, }) const scene = createChartScene(definition, { width: 480, height: 300 }) const resized = createChartScene(definition, { width: 720, height: 480 }) @@ -119,8 +121,10 @@ describe('violin marks', () => { marks: [mark], guides: false, focusRing: false, - x: { scale: scaleUtc().domain(at) }, - y: { scale: scalePoint().domain(['A', 'B']) }, + scales: { + x: { scale: scaleUtc().domain(at) }, + y: { scale: scalePoint().domain(['A', 'B']) }, + }, }), { width: 480, height: 300 }, ) @@ -165,10 +169,14 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { - scale: scaleBand().domain(['Adelie', 'Gentoo']).padding(0.2), + scales: { + x: { + scale: scaleBand() + .domain(['Adelie', 'Gentoo']) + .padding(0.2), + }, + y: { scale: scaleLinear().domain([3000, 5000]) }, }, - y: { scale: scaleLinear().domain([3000, 5000]) }, }), { width: 400, height: 240 }, ) @@ -184,8 +192,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['Adelie']) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scalePoint().domain(['Adelie']) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 400, height: 240 }, ) @@ -214,8 +224,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(domain) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scalePoint().domain(domain) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 400, height: 240 }, ) @@ -248,8 +260,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 4]) }, + scales: { + x: { scale: scalePoint().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, }), { width: 400, height: 240 }, ) @@ -279,8 +293,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['Adelie']) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scalePoint().domain(['Adelie']) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 400, height: 240 }, ) @@ -318,8 +334,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['Adelie']) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scalePoint().domain(['Adelie']) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 400, height: 240 }, ) @@ -367,8 +385,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['Adelie']) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scalePoint().domain(['Adelie']) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 400, height: 240 }, ) @@ -428,8 +448,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scalePoint().domain(['A']) }, - y: { scale: scaleLinear().domain([0, 2]) }, + scales: { + x: { scale: scalePoint().domain(['A']) }, + y: { scale: scaleLinear().domain([0, 2]) }, + }, }), { width: 240, height: 160 }, ) @@ -448,8 +470,10 @@ describe('violin marks', () => { ], guides: false, focusRing: false, - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([3000, 5000]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([3000, 5000]) }, + }, }), { width: 240, height: 160 }, ), diff --git a/packages/charts-core/src/waffle.test.ts b/packages/charts-core/src/waffle.test.ts index e5c4aa30..249e7b02 100644 --- a/packages/charts-core/src/waffle.test.ts +++ b/packages/charts-core/src/waffle.test.ts @@ -31,6 +31,7 @@ describe('waffle marks', () => { radius: 2, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, color: { @@ -91,6 +92,7 @@ describe('waffle marks', () => { radius: 8, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -123,6 +125,7 @@ describe('waffle marks', () => { gap: 2, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, color: { @@ -169,6 +172,7 @@ describe('waffle marks', () => { gap: 0, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -209,6 +213,7 @@ describe('waffle marks', () => { ], }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -258,6 +263,7 @@ describe('waffle marks', () => { }, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, }) @@ -298,6 +304,7 @@ describe('waffle marks', () => { gap: 0, }), ], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -331,6 +338,7 @@ describe('waffle marks', () => { const scene = createChartScene( defineChart({ marks: [waffleY(rows, { y: 'value', key: 'id' })], + scales: { x: null, y: null }, guides: false, focusRing: false, }), @@ -361,6 +369,7 @@ describe('waffle marks', () => { createChartScene( defineChart({ marks: [waffleY([{ value: -1 }], { y: 'value' })], + scales: { x: null, y: null }, guides: false, }), { width: 100, height: 100 }, @@ -370,6 +379,7 @@ describe('waffle marks', () => { createChartScene( defineChart({ marks: [waffleY(rows, { y: 'value', unit: Number.MIN_VALUE })], + scales: { x: null, y: null }, guides: false, }), { width: 100, height: 100 }, diff --git a/packages/charts-scales/README.md b/packages/charts-scales/README.md index 569a38f1..c795c721 100644 --- a/packages/charts-scales/README.md +++ b/packages/charts-scales/README.md @@ -34,8 +34,10 @@ const revenue = [ const chart = defineChart({ marks: [barY(revenue, { x: 'month', y: 'value' })], - x: { scale: () => scaleBand().padding(0.2) }, - y: { scale: scaleLinear, nice: true, grid: true }, + scales: { + x: { scale: () => scaleBand().padding(0.2) }, + y: { scale: scaleLinear, nice: true, grid: true }, + }, }) ``` diff --git a/packages/react-native-charts/README.md b/packages/react-native-charts/README.md index 51376282..9d751d8b 100644 --- a/packages/react-native-charts/README.md +++ b/packages/react-native-charts/README.md @@ -36,8 +36,10 @@ import { tooltip } from '@tanstack/charts/react-native/tooltip' const definition = defineChart({ marks: [lineY([4, 9, 7])], - x: { scale: scaleLinear().domain([0, 2]) }, - y: { scale: scaleLinear().domain([0, 10]) }, + scales: { + x: { scale: scaleLinear().domain([0, 2]) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, tooltip: { use: tooltip, sticky: true }, }) diff --git a/packages/react-native-charts/src/interaction.test.ts b/packages/react-native-charts/src/interaction.test.ts index 97a079b6..fc6f2b7e 100644 --- a/packages/react-native-charts/src/interaction.test.ts +++ b/packages/react-native-charts/src/interaction.test.ts @@ -161,7 +161,7 @@ describe('native focus model', () => { function definition( options: Partial>, ): ChartDefinition { - return { marks: [], ...options } + return { marks: [], scales: { x: null, y: null }, ...options } } function point( diff --git a/scripts/check-packed-consumers.mjs b/scripts/check-packed-consumers.mjs index fa94d189..6d84c8d3 100644 --- a/scripts/check-packed-consumers.mjs +++ b/scripts/check-packed-consumers.mjs @@ -5471,11 +5471,6 @@ async function verifyProductionBundles() { contents.byteLength > (entry.minimumBytes ?? 100), `${entry.label} bundle is empty`, ) - assert.equal( - contents.includes('enters Alpha'), - false, - `${entry.label} retained a development-only migration warning`, - ) assert.ok( contents.byteLength < 500_000, `${entry.label} bundle unexpectedly exceeds 500 kB`, diff --git a/scripts/measure-bundles.mjs b/scripts/measure-bundles.mjs index e1310cc3..bb05b66a 100644 --- a/scripts/measure-bundles.mjs +++ b/scripts/measure-bundles.mjs @@ -455,7 +455,7 @@ const entries = [ budgeted( 'Transform: stack', 'benchmarks/entries/charts-transform-stack.ts', - 2.65, + 2.68, { inputBoundary: granularTransformBoundary('transformStack', { allowD3Shape: true, @@ -696,7 +696,7 @@ const entries = [ budgeted( 'D3-scale areaX + static SVG', 'benchmarks/entries/charts-area-x-svg.ts', - 24.23, + 24.25, ), budgeted( 'D3-scale dots + static SVG', @@ -903,7 +903,7 @@ const entries = [ budgeted( 'Composite mark + static SVG', 'benchmarks/entries/charts-composite-mark.ts', - 27.71, + 27.75, { inputBoundary: { require: [ @@ -943,7 +943,7 @@ const entries = [ 'Linear regression + static SVG', 'benchmarks/entries/charts-regression-svg.ts', 'D3-scale line + static SVG', - 6.54, + 6.59, { inputBoundary: { require: [ @@ -977,7 +977,7 @@ const entries = [ 'Difference mark + static SVG', 'benchmarks/entries/charts-difference-svg.ts', 'D3-scale line + static SVG', - 6.98, + 7.03, { inputBoundary: { require: [ @@ -1072,7 +1072,7 @@ const entries = [ budgeted( 'D3-scale hexagons + static SVG', 'benchmarks/entries/charts-hexagon-svg.ts', - 20.12, + 20.11, { inputBoundary: { forbid: ['d3Hexbin'] } }, ), incrementalBudgeted( @@ -1661,7 +1661,7 @@ const entries = [ lockedBudgeted( 'React compact-scale line consumer', 'benchmarks/entries/charts-react-compact-line.ts', - 28.41, + 28.47, { external: ['react', 'react/jsx-runtime', 'react-dom'], rendererBoundary: 'svg', @@ -1829,7 +1829,7 @@ const entries = [ budgeted( 'Stats parity surface', 'benchmarks/entries/charts-stats-parity.ts', - 52.72, + 52.82, ), locked( 'Custom-scale line scene', @@ -1880,12 +1880,12 @@ const entries = [ budgeted( 'Direct D3 quadtree + TanStack DOM host', 'benchmarks/entries/charts-d3-quadtree-dom.ts', - 36.79, + 36.84, ), budgeted( 'Direct D3 Delaunay + TanStack DOM host', 'benchmarks/entries/charts-d3-delaunay-dom.ts', - 42.01, + 42.07, ), measured('D3 array numeric kernel', 'benchmarks/entries/d3-array-kernel.ts'), measured( @@ -2089,7 +2089,7 @@ const entries = [ budgeted( 'React Stats parity surface', 'benchmarks/entries/charts-react-stats-parity.tsx', - 53.61, + 53.72, { external: ['react', 'react/jsx-runtime', 'react-dom'] }, ), measured('Plot renderer integration', 'benchmarks/entries/plot-renderer.ts'), diff --git a/scripts/sync-package-docs.mjs b/scripts/sync-package-docs.mjs index a167eda0..e43e9650 100644 --- a/scripts/sync-package-docs.mjs +++ b/scripts/sync-package-docs.mjs @@ -76,7 +76,7 @@ export async function createLlmsIndex(root) { const source = await readFile(resolve(docsRoot, `${path}.md`), 'utf8') const frontmatter = parseFrontmatter(source) lines.push( - `- docs/${path}.md — ${frontmatter?.title ?? path}: ${frontmatter?.description ?? ''}`, + `- docs/${path}.md: ${frontmatter?.title ?? path}: ${frontmatter?.description ?? ''}`, ) } @@ -88,8 +88,8 @@ export async function createLlmsIndex(root) { '- Start with exact @tanstack/charts/scales/linear, @tanstack/charts/scales/band, @tanstack/charts/scales/point, or @tanstack/charts/scales/ordinal entries. Upgrade only the mapping that needs temporal, nonlinear, radial, interpolated, statistical, or other complete D3 semantics; never import the d3 umbrella.', '- Treat Date values on compact band or point scales as equally spaced categories. Use d3-scale scaleTime or scaleUtc when elapsed-time spacing or calendar-aware ticks matter.', '- Let TanStack Charts own responsive pixel ranges. Scale factories infer domains from mark channels; configured instances preserve application-owned domains.', - '- Put Cartesian scale and guide options under scales.x and scales.y; never generate deprecated root x or y properties.', - '- Put polar position scales under scales.angle and scales.radius inside polar(); never generate deprecated direct angle or radius properties.', + '- Put Cartesian scale and guide options under scales.x and scales.y; root x and y properties are invalid.', + '- Put polar position scales under scales.angle and scales.radius inside polar(); direct angle and radius properties are invalid.', '- Keep data in its application shape. Map fields or accessors into marks instead of creating a library-owned series model.', '- Memoize the complete definition against captured application values; definition identity is the application update boundary.', '- Preserve inferable datum identity across updates; add explicit keys only when IDs or unique positions are unavailable.', diff --git a/scripts/sync-release-version.mjs b/scripts/sync-release-version.mjs index 207878cf..828fabb2 100644 --- a/scripts/sync-release-version.mjs +++ b/scripts/sync-release-version.mjs @@ -6,10 +6,58 @@ import { readReleasePackages } from './release-package-config.mjs' export const releaseVersionSources = [ { path: 'README.md', references: 2, tagReferences: 1 }, - { path: 'MARKETING.md', references: 5 }, + { path: 'MARKETING.md', references: 3 }, { path: 'docs/overview.md', references: 1 }, { path: 'docs/installation.md', references: 2, tagReferences: 1 }, { path: 'docs/comparison.md', references: 5, tagReferences: 4 }, + { + path: 'packages/charts-core/skills/build-chart-interactions/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/compose-marks-and-views/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/configure-scales-guides-color/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/coordinate-charts-with-tanstack/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/debug-and-verify-charts/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/design-a-chart/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/design-responsive-charts/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/extend-tanstack-charts/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/migrate-to-tanstack-charts/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/prepare-chart-data/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/ship-accessible-charts/SKILL.md', + references: 1, + }, + { + path: 'packages/charts-core/skills/update-and-animate-charts/SKILL.md', + references: 1, + }, ] export function changelogVersions(source) { diff --git a/scripts/sync-release-version.test.mjs b/scripts/sync-release-version.test.mjs index 3fc06597..bb7a8ad7 100644 --- a/scripts/sync-release-version.test.mjs +++ b/scripts/sync-release-version.test.mjs @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs/promises' +import { readFile, readdir } from 'node:fs/promises' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { @@ -9,6 +9,45 @@ import { } from './sync-release-version.mjs' describe('release version synchronization', () => { + it('tracks every shipped skill version', async () => { + const repositoryRoot = resolve(import.meta.dirname, '..') + const skillsRoot = resolve(repositoryRoot, 'packages/charts-core/skills') + const releaseManifest = JSON.parse( + await readFile( + resolve(repositoryRoot, 'packages/charts-core/package.json'), + 'utf8', + ), + ) + const shippedSkillPaths = ( + await readdir(skillsRoot, { withFileTypes: true }) + ) + .filter((entry) => entry.isDirectory()) + .map((entry) => `packages/charts-core/skills/${entry.name}/SKILL.md`) + .sort() + const trackedSkillSources = releaseVersionSources.filter(({ path }) => + path.startsWith('packages/charts-core/skills/'), + ) + const trackedSkillPaths = trackedSkillSources.map(({ path }) => path).sort() + + expect(trackedSkillPaths).toEqual(shippedSkillPaths) + for (const { path, references } of trackedSkillSources) { + const source = await readFile(resolve(repositoryRoot, path), 'utf8') + const nextVersion = '999.999.999' + + expect(references).toBe(1) + expect(source).toContain(`library_version: '${releaseManifest.version}'`) + expect( + syncReleaseVersionReference( + source, + releaseManifest.version, + nextVersion, + path, + references, + ), + ).toContain(`library_version: '${nextVersion}'`) + } + }) + it('tracks every current release-facing reference', async () => { const repositoryRoot = resolve(import.meta.dirname, '..') const releaseManifest = JSON.parse( From 064d3c6ab5d3d112e6df7703d50aa6904fc9557e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 26 Aug 2026 15:54:15 -0600 Subject: [PATCH 2/3] Refresh Alpha comparison evidence --- benchmarks/comparison/bundle-baseline.json | 128 ++++++++++----------- docs/comparison.md | 8 +- packages/charts-core/docs/comparison.md | 8 +- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json index 7a65c6f5..a22a54c1 100644 --- a/benchmarks/comparison/bundle-baseline.json +++ b/benchmarks/comparison/bundle-baseline.json @@ -1,8 +1,8 @@ { "schemaVersion": 4, - "generatedAt": "2026-08-15T07:14:39.579Z", + "generatedAt": "2026-08-26T21:51:12.832Z", "packageVersions": { - "tanstack": "0.13.0", + "tanstack": "0.15.0", "chartjs": "4.5.1", "echarts": "6.1.0", "recharts": "3.10.1", @@ -11,8 +11,8 @@ "sources": { "tanstack": { "kind": "workspace", - "revision": "91e2eef1772dc06118ce5aa1e0b388d12bf91918", - "inputDigest": "sha256:08292488d4f7f01b2135f740b2047727a5711134fe05af96612f90bc73a63cee" + "revision": "3df87d71f0305e5a450c10b66940f76f3e14259a", + "inputDigest": "sha256:b1016e599e63d05e1647b9d27adabaaed7a38bb44dac8d6ba0fef06b8b8bd38e" }, "chartjs": { "kind": "package", @@ -45,88 +45,88 @@ }, "bundles": { "tanstack-line-basic": { - "minifiedBytes": 110224, - "gzipBytes": 39836, - "brotliBytes": 35281, - "incrementalGzipBytes": 39836, - "incrementalBrotliBytes": 35281 + "minifiedBytes": 110431, + "gzipBytes": 39891, + "brotliBytes": 35325, + "incrementalGzipBytes": 39891, + "incrementalBrotliBytes": 35325 }, "tanstack-line-interactive": { - "minifiedBytes": 115665, - "gzipBytes": 41593, - "brotliBytes": 36763, - "incrementalGzipBytes": 41593, - "incrementalBrotliBytes": 36763 + "minifiedBytes": 115872, + "gzipBytes": 41648, + "brotliBytes": 36746, + "incrementalGzipBytes": 41648, + "incrementalBrotliBytes": 36746 }, "tanstack-line-advanced": { - "minifiedBytes": 122855, - "gzipBytes": 43923, - "brotliBytes": 38701, - "incrementalGzipBytes": 43923, - "incrementalBrotliBytes": 38701 + "minifiedBytes": 123062, + "gzipBytes": 43974, + "brotliBytes": 38764, + "incrementalGzipBytes": 43974, + "incrementalBrotliBytes": 38764 }, "tanstack-bar-basic": { - "minifiedBytes": 118895, - "gzipBytes": 43111, - "brotliBytes": 38052, - "incrementalGzipBytes": 43111, - "incrementalBrotliBytes": 38052 + "minifiedBytes": 119225, + "gzipBytes": 43210, + "brotliBytes": 38168, + "incrementalGzipBytes": 43210, + "incrementalBrotliBytes": 38168 }, "tanstack-bar-interactive": { - "minifiedBytes": 123191, - "gzipBytes": 44478, - "brotliBytes": 39092, - "incrementalGzipBytes": 44478, - "incrementalBrotliBytes": 39092 + "minifiedBytes": 123521, + "gzipBytes": 44585, + "brotliBytes": 39223, + "incrementalGzipBytes": 44585, + "incrementalBrotliBytes": 39223 }, "tanstack-bar-advanced": { - "minifiedBytes": 123530, - "gzipBytes": 44634, - "brotliBytes": 39245, - "incrementalGzipBytes": 44634, - "incrementalBrotliBytes": 39245 + "minifiedBytes": 123860, + "gzipBytes": 44735, + "brotliBytes": 39349, + "incrementalGzipBytes": 44735, + "incrementalBrotliBytes": 39349 }, "tanstack-area-basic": { - "minifiedBytes": 115310, - "gzipBytes": 41751, - "brotliBytes": 36954, - "incrementalGzipBytes": 41751, - "incrementalBrotliBytes": 36954 + "minifiedBytes": 115640, + "gzipBytes": 41867, + "brotliBytes": 37073, + "incrementalGzipBytes": 41867, + "incrementalBrotliBytes": 37073 }, "tanstack-area-interactive": { - "minifiedBytes": 120755, - "gzipBytes": 43491, - "brotliBytes": 38370, - "incrementalGzipBytes": 43491, - "incrementalBrotliBytes": 38370 + "minifiedBytes": 121085, + "gzipBytes": 43594, + "brotliBytes": 38493, + "incrementalGzipBytes": 43594, + "incrementalBrotliBytes": 38493 }, "tanstack-area-advanced": { - "minifiedBytes": 128127, - "gzipBytes": 45958, - "brotliBytes": 40454, - "incrementalGzipBytes": 45958, - "incrementalBrotliBytes": 40454 + "minifiedBytes": 128457, + "gzipBytes": 46057, + "brotliBytes": 40527, + "incrementalGzipBytes": 46057, + "incrementalBrotliBytes": 40527 }, "tanstack-scatter-basic": { - "minifiedBytes": 111253, - "gzipBytes": 40212, - "brotliBytes": 35564, - "incrementalGzipBytes": 40212, - "incrementalBrotliBytes": 35564 + "minifiedBytes": 111460, + "gzipBytes": 40266, + "brotliBytes": 35596, + "incrementalGzipBytes": 40266, + "incrementalBrotliBytes": 35596 }, "tanstack-scatter-interactive": { - "minifiedBytes": 116694, - "gzipBytes": 41969, - "brotliBytes": 37033, - "incrementalGzipBytes": 41969, - "incrementalBrotliBytes": 37033 + "minifiedBytes": 116901, + "gzipBytes": 42015, + "brotliBytes": 36976, + "incrementalGzipBytes": 42015, + "incrementalBrotliBytes": 36976 }, "tanstack-scatter-advanced": { - "minifiedBytes": 116710, - "gzipBytes": 41975, - "brotliBytes": 37003, - "incrementalGzipBytes": 41975, - "incrementalBrotliBytes": 37003 + "minifiedBytes": 116917, + "gzipBytes": 42020, + "brotliBytes": 37012, + "incrementalGzipBytes": 42020, + "incrementalBrotliBytes": 37012 }, "chartjs-line-basic": { "minifiedBytes": 137909, diff --git a/docs/comparison.md b/docs/comparison.md index 6ba1e700..19777113 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 `91e2eef` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3df87d7` | | [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 `91e2eef`. +at page render time. The measured TanStack workspace revision is `3df87d7`. ## Capability matrix @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-15`. +Baseline date: `2026-08-26`. 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.90–44.88 KiB | Not applicable | Controlled suite | +| TanStack Charts | 38.96–44.98 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/comparison.md b/packages/charts-core/docs/comparison.md index 6ba1e700..19777113 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 `91e2eef` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3df87d7` | | [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 `91e2eef`. +at page render time. The measured TanStack workspace revision is `3df87d7`. ## Capability matrix @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-15`. +Baseline date: `2026-08-26`. 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.90–44.88 KiB | Not applicable | Controlled suite | +| TanStack Charts | 38.96–44.98 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 | From e6bca3f5d93b3d365ce1b5e9fd8b0989fb0a4563 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 26 Aug 2026 16:16:26 -0600 Subject: [PATCH 3/3] Clarify Alpha migration guidance --- .changeset/official-alpha.md | 3 ++- docs/reference/focus-and-interaction.md | 2 +- packages/charts-core/docs/reference/focus-and-interaction.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.changeset/official-alpha.md b/.changeset/official-alpha.md index f99c7f7a..c5beb4fa 100644 --- a/.changeset/official-alpha.md +++ b/.changeset/official-alpha.md @@ -8,7 +8,8 @@ Chart definitions now require Cartesian scales under `scales.x` and `scales.y`. Polar definitions require `scales.angle` and `scales.radius`. The temporary pre-Alpha root properties, runtime adapters, and development warnings have been removed. Custom mark types should replace `ChartMarkX` and `ChartMarkY` with -`ChartMarkPointX` and `ChartMarkPointY`. Polar layout callbacks should read the +`ChartMarkPointX` and `ChartMarkPointY`, imported from +`@tanstack/charts/mark/scale-values`. Polar layout callbacks should read the reserved mappings from `layout.scales`. Reserved scale entries are checked against the marks that use them at the diff --git a/docs/reference/focus-and-interaction.md b/docs/reference/focus-and-interaction.md index a1f189aa..96ddf62d 100644 --- a/docs/reference/focus-and-interaction.md +++ b/docs/reference/focus-and-interaction.md @@ -22,7 +22,7 @@ With no custom focus strategy: - `Home` and `End` move to the first and last point - `Enter` and Space toggle an enabled sticky tooltip and call `onSelect` for the focused point -- pressing the pointer outside the chart or tooltip dismisses a pinned tooltip +- pressing the pointer outside both the chart and the tooltip dismisses a pinned tooltip - a configured selection controller receives the same focused point before `onSelect` - a click focuses and selects the nearest point, or selects `null` on the diff --git a/packages/charts-core/docs/reference/focus-and-interaction.md b/packages/charts-core/docs/reference/focus-and-interaction.md index a1f189aa..96ddf62d 100644 --- a/packages/charts-core/docs/reference/focus-and-interaction.md +++ b/packages/charts-core/docs/reference/focus-and-interaction.md @@ -22,7 +22,7 @@ With no custom focus strategy: - `Home` and `End` move to the first and last point - `Enter` and Space toggle an enabled sticky tooltip and call `onSelect` for the focused point -- pressing the pointer outside the chart or tooltip dismisses a pinned tooltip +- pressing the pointer outside both the chart and the tooltip dismisses a pinned tooltip - a configured selection controller receives the same focused point before `onSelect` - a click focuses and selects the nearest point, or selects `null` on the