diff --git a/.changeset/shared-chart-runtime.md b/.changeset/shared-chart-runtime.md
new file mode 100644
index 00000000..76844a74
--- /dev/null
+++ b/.changeset/shared-chart-runtime.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/charts': patch
+---
+
+Reduce chart bundles by sharing Cartesian guide rendering, mark initialization,
+stack ordering, motion tracks, SVG reconciliation, and hit-testing calculations.
+Keep the motion renderer independent of the unused standalone tween engine.
diff --git a/.gitignore b/.gitignore
index 550ea8f4..c7c380fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ coverage
*.log
tanstack.com-parity/
tanstack.com-charts-site/
+
+output/playwright/
diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json
index f0ebd0b4..0ceca201 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": 53412,
- "gzip": 19894
+ "bytes": 52086,
+ "gzip": 19825
},
"D3-scale line + static SVG": {
- "bytes": 59566,
- "gzip": 22210
+ "bytes": 58248,
+ "gzip": 22132
},
"Representative marks": {
- "bytes": 83306,
- "gzip": 30340
+ "bytes": 80993,
+ "gzip": 29994
},
"TanStack DOM host": {
- "bytes": 81870,
- "gzip": 28574
+ "bytes": 80633,
+ "gzip": 28522
},
"React adapter": {
- "bytes": 84195,
- "gzip": 29518
+ "bytes": 82959,
+ "gzip": 29492
},
"React line consumer": {
- "bytes": 107854,
- "gzip": 38849
+ "bytes": 106656,
+ "gzip": 38799
},
"Compact-scale line scene": {
- "bytes": 35865,
- "gzip": 12732
+ "bytes": 34536,
+ "gzip": 12669
},
"React compact-scale line consumer": {
- "bytes": 90354,
- "gzip": 31746
+ "bytes": 89156,
+ "gzip": 31707
},
"Custom-scale line scene": {
- "bytes": 34047,
- "gzip": 11995
+ "bytes": 32718,
+ "gzip": 11947
},
"D3 linear-scale line scene": {
- "bytes": 53344,
- "gzip": 19856
+ "bytes": 52018,
+ "gzip": 19787
}
}
}
diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json
index e85dd9fd..7c2c2a2b 100644
--- a/benchmarks/comparison/bundle-baseline.json
+++ b/benchmarks/comparison/bundle-baseline.json
@@ -1,8 +1,8 @@
{
"schemaVersion": 4,
- "generatedAt": "2026-09-10T04:50:31.051Z",
+ "generatedAt": "2026-09-13T19:48:59.290Z",
"packageVersions": {
- "tanstack": "0.17.0",
+ "tanstack": "0.18.0",
"chartjs": "4.5.1",
"echarts": "6.1.0",
"recharts": "3.10.1",
@@ -11,8 +11,8 @@
"sources": {
"tanstack": {
"kind": "workspace",
- "revision": "8bab934c07762e335108b12687e3587cddc1288e",
- "inputDigest": "sha256:73e433a9402a39e35a475fac52af5655016beae0c80c3a9950e47425ef4cd965"
+ "revision": "92c6da359ae47a7d8d37652e365b3f4565cfa8bf",
+ "inputDigest": "sha256:ae0e348223fa04b2381b59ec6e8a22f8401b740e71246accd7d2cb0273644f87"
},
"chartjs": {
"kind": "package",
@@ -45,88 +45,88 @@
},
"bundles": {
"tanstack-line-basic": {
- "minifiedBytes": 117941,
- "gzipBytes": 42555,
- "brotliBytes": 37623,
- "incrementalGzipBytes": 42555,
- "incrementalBrotliBytes": 37623
+ "minifiedBytes": 116641,
+ "gzipBytes": 42471,
+ "brotliBytes": 37657,
+ "incrementalGzipBytes": 42471,
+ "incrementalBrotliBytes": 37657
},
"tanstack-line-interactive": {
- "minifiedBytes": 123686,
- "gzipBytes": 44460,
- "brotliBytes": 39218,
- "incrementalGzipBytes": 44460,
- "incrementalBrotliBytes": 39218
+ "minifiedBytes": 122388,
+ "gzipBytes": 44372,
+ "brotliBytes": 39156,
+ "incrementalGzipBytes": 44372,
+ "incrementalBrotliBytes": 39156
},
"tanstack-line-advanced": {
- "minifiedBytes": 130878,
- "gzipBytes": 46798,
- "brotliBytes": 41196,
- "incrementalGzipBytes": 46798,
- "incrementalBrotliBytes": 41196
+ "minifiedBytes": 129583,
+ "gzipBytes": 46712,
+ "brotliBytes": 41172,
+ "incrementalGzipBytes": 46712,
+ "incrementalBrotliBytes": 41172
},
"tanstack-bar-basic": {
- "minifiedBytes": 130379,
- "gzipBytes": 47284,
- "brotliBytes": 41635,
- "incrementalGzipBytes": 47284,
- "incrementalBrotliBytes": 41635
+ "minifiedBytes": 128254,
+ "gzipBytes": 46980,
+ "brotliBytes": 41354,
+ "incrementalGzipBytes": 46980,
+ "incrementalBrotliBytes": 41354
},
"tanstack-bar-interactive": {
- "minifiedBytes": 134983,
- "gzipBytes": 48679,
- "brotliBytes": 42750,
- "incrementalGzipBytes": 48679,
- "incrementalBrotliBytes": 42750
+ "minifiedBytes": 132860,
+ "gzipBytes": 48380,
+ "brotliBytes": 42606,
+ "incrementalGzipBytes": 48380,
+ "incrementalBrotliBytes": 42606
},
"tanstack-bar-advanced": {
- "minifiedBytes": 135322,
- "gzipBytes": 48828,
- "brotliBytes": 42851,
- "incrementalGzipBytes": 48828,
- "incrementalBrotliBytes": 42851
+ "minifiedBytes": 133199,
+ "gzipBytes": 48530,
+ "brotliBytes": 42692,
+ "incrementalGzipBytes": 48530,
+ "incrementalBrotliBytes": 42692
},
"tanstack-area-basic": {
- "minifiedBytes": 123121,
- "gzipBytes": 44596,
- "brotliBytes": 39388,
- "incrementalGzipBytes": 44596,
- "incrementalBrotliBytes": 39388
+ "minifiedBytes": 121643,
+ "gzipBytes": 44498,
+ "brotliBytes": 39323,
+ "incrementalGzipBytes": 44498,
+ "incrementalBrotliBytes": 39323
},
"tanstack-area-interactive": {
- "minifiedBytes": 128876,
- "gzipBytes": 46452,
- "brotliBytes": 40950,
- "incrementalGzipBytes": 46452,
- "incrementalBrotliBytes": 40950
+ "minifiedBytes": 127398,
+ "gzipBytes": 46335,
+ "brotliBytes": 40885,
+ "incrementalGzipBytes": 46335,
+ "incrementalBrotliBytes": 40885
},
"tanstack-area-advanced": {
- "minifiedBytes": 136250,
- "gzipBytes": 48807,
- "brotliBytes": 43047,
- "incrementalGzipBytes": 48807,
- "incrementalBrotliBytes": 43047
+ "minifiedBytes": 134774,
+ "gzipBytes": 48745,
+ "brotliBytes": 43041,
+ "incrementalGzipBytes": 48745,
+ "incrementalBrotliBytes": 43041
},
"tanstack-scatter-basic": {
- "minifiedBytes": 118947,
- "gzipBytes": 42908,
- "brotliBytes": 37974,
- "incrementalGzipBytes": 42908,
- "incrementalBrotliBytes": 37974
+ "minifiedBytes": 117647,
+ "gzipBytes": 42827,
+ "brotliBytes": 37990,
+ "incrementalGzipBytes": 42827,
+ "incrementalBrotliBytes": 37990
},
"tanstack-scatter-interactive": {
- "minifiedBytes": 124696,
- "gzipBytes": 44826,
- "brotliBytes": 39490,
- "incrementalGzipBytes": 44826,
- "incrementalBrotliBytes": 39490
+ "minifiedBytes": 123398,
+ "gzipBytes": 44743,
+ "brotliBytes": 39476,
+ "incrementalGzipBytes": 44743,
+ "incrementalBrotliBytes": 39476
},
"tanstack-scatter-advanced": {
- "minifiedBytes": 124712,
- "gzipBytes": 44832,
- "brotliBytes": 39527,
- "incrementalGzipBytes": 44832,
- "incrementalBrotliBytes": 39527
+ "minifiedBytes": 123414,
+ "gzipBytes": 44750,
+ "brotliBytes": 39470,
+ "incrementalGzipBytes": 44750,
+ "incrementalBrotliBytes": 39470
},
"chartjs-line-basic": {
"minifiedBytes": 137909,
diff --git a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
index e45a795f..a74d4320 100644
--- a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
+++ b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg b/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
index a536d1d6..f3565020 100644
--- a/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
+++ b/benchmarks/conformance/previews/190-shadcn-tooltip-default.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg b/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
index 3f91ac67..231b2330 100644
--- a/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
+++ b/benchmarks/conformance/previews/191-shadcn-tooltip-formatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg b/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
index 92998466..a3f74064 100644
--- a/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
+++ b/benchmarks/conformance/previews/192-shadcn-tooltip-icons.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg b/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
index f3fa5dcf..ea3ce186 100644
--- a/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
+++ b/benchmarks/conformance/previews/193-shadcn-tooltip-indicator-line.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg b/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
index 24bc2585..4f2e897e 100644
--- a/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
+++ b/benchmarks/conformance/previews/194-shadcn-tooltip-indicator-none.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg b/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
index 7d9f4e08..ab1a5767 100644
--- a/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
+++ b/benchmarks/conformance/previews/195-shadcn-tooltip-label-custom.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg b/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
index 003322dd..64f12719 100644
--- a/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
+++ b/benchmarks/conformance/previews/196-shadcn-tooltip-label-formatter.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg b/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
index 941b2616..b415f939 100644
--- a/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
+++ b/benchmarks/conformance/previews/197-shadcn-tooltip-label-none.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json
index f18c50d3..42c69dea 100644
--- a/benchmarks/conformance/previews/manifest.json
+++ b/benchmarks/conformance/previews/manifest.json
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"width": 288,
"height": 192,
- "sourceHash": "c5e393081d8428d13d2efdeca32ad1e7fc56062b610b5c014d29c28af02bbde9",
+ "sourceHash": "effdd579df63e4be803a324a318135326e4dccea63385502de6ec945eef94a60",
"assets": [
{
"id": "01-line-gaps",
@@ -616,7 +616,7 @@
},
{
"id": "132-shadcn-tooltip-advanced",
- "sha256": "97738ec4dd4be6c3e40c7b3bae1fa519e809c5f71f451384d2be9b84ef4be79a",
+ "sha256": "8762da7358bcfa728e206a9c906cf596f298f483d39f8e01eeb086c1ffc991e4",
"bytes": 5019
},
{
@@ -906,42 +906,42 @@
},
{
"id": "190-shadcn-tooltip-default",
- "sha256": "fe48eaec0c292c1338d653953946483982ef1b23ec356478b24bbf11730fd3f9",
+ "sha256": "51bd9df24b4f9e1b9e1978c870d901dd7c7e861aba5530e786bddfba070247fc",
"bytes": 5018
},
{
"id": "191-shadcn-tooltip-formatter",
- "sha256": "43eb6270aa388f3a3004cec62e53f0fbdb73ff5ddc5c4190649ff64d61730240",
+ "sha256": "b7e374ccd584b066a7501419d7f021a6397911f90dfc498ac2dc5f79e8b94beb",
"bytes": 5020
},
{
"id": "192-shadcn-tooltip-icons",
- "sha256": "bda712a532d9455b3409e7dd4280e1e9262a1bc805fe6b7ffcc237de9693e39e",
+ "sha256": "2f61168ff6f8a22b04fdea877adfe9c4e2dfbd2c16fc4df734a0d508fac9b959",
"bytes": 5016
},
{
"id": "193-shadcn-tooltip-indicator-line",
- "sha256": "a46a207d4db94529ad698128a0f348300212593c91517fbeb05a4a6e9de07888",
+ "sha256": "4be629683b7b8688e318e27e769e5da19732f1a7b58cbdfdab9a33b63f31fb3e",
"bytes": 5025
},
{
"id": "194-shadcn-tooltip-indicator-none",
- "sha256": "ac6fe965bea2a6588cc0597a595fa8624c7f6cac3b38a20201ae8807f0ba4514",
+ "sha256": "b5606048801c5fc115081a953430c46228ebb6f43dfe79a18891d2fe3f8283fd",
"bytes": 5023
},
{
"id": "195-shadcn-tooltip-label-custom",
- "sha256": "b09982f65a0481e1c2efdee73d3132058594acdc9786d8c1f9ad877816752536",
+ "sha256": "55ab8ea80d10636234d51d03172a5b32fff6541a51b02ff800f939b346131e6f",
"bytes": 5023
},
{
"id": "196-shadcn-tooltip-label-formatter",
- "sha256": "b18e7ed66963b22aa33bc543ab08d119be452cf7e6f77d485f8756694dd16f2f",
+ "sha256": "26bcd0b7899a2545c8a3e0bc46f027836d3ef59a6f99e7fe4d36635897c9f951",
"bytes": 5026
},
{
"id": "197-shadcn-tooltip-label-none",
- "sha256": "af6631dd0b4e88a936d9cff1ae28a90b549ef6c2d72513b90e9dadb80d4f437a",
+ "sha256": "4968486648c607560e8b47274b3fcd628134c2f9453a0d09d9362861e0d011bf",
"bytes": 5019
}
]
diff --git a/docs/comparison.md b/docs/comparison.md
index ea1cb923..015b3f67 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 `8bab934` |
+| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `92c6da3` |
| [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 `8bab934`.
+at page render time. The measured TanStack workspace revision is `92c6da3`.
## Capability matrix
@@ -90,7 +90,7 @@ output model.
## Bundle snapshot
-Baseline date: `2026-09-10`.
+Baseline date: `2026-09-13`.
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 | 41.56–47.68 KiB | Not applicable | Controlled suite |
+| TanStack Charts | 41.48–47.60 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 ea1cb923..015b3f67 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 `8bab934` |
+| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `92c6da3` |
| [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 `8bab934`.
+at page render time. The measured TanStack workspace revision is `92c6da3`.
## Capability matrix
@@ -90,7 +90,7 @@ output model.
## Bundle snapshot
-Baseline date: `2026-09-10`.
+Baseline date: `2026-09-13`.
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 | 41.56–47.68 KiB | Not applicable | Controlled suite |
+| TanStack Charts | 41.48–47.60 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/src/configured-scale.ts b/packages/charts-core/src/configured-scale.ts
index 8d4b99bb..3f4e7936 100644
--- a/packages/charts-core/src/configured-scale.ts
+++ b/packages/charts-core/src/configured-scale.ts
@@ -1,3 +1,4 @@
+import { sameChartValue } from './value-equality-internal'
import type {
ChartContinuousValue,
ChartContinuousDomain,
@@ -253,12 +254,6 @@ function continuousNumber(value: ChartContinuousValue): number {
return value instanceof Date ? value.getTime() : value
}
-function sameChartValue(left: ChartValue | undefined, right: ChartValue) {
- return left instanceof Date && right instanceof Date
- ? left.getTime() === right.getTime()
- : Object.is(left, right)
-}
-
function invalidViewportDomain(id: string): never {
throw new TypeError(
`Chart viewport "${id}" domain must contain two distinct finite numbers or Dates`,
diff --git a/packages/charts-core/src/crosshair.ts b/packages/charts-core/src/crosshair.ts
index 1dfe9b02..fa5424d7 100644
--- a/packages/charts-core/src/crosshair.ts
+++ b/packages/charts-core/src/crosshair.ts
@@ -1,3 +1,4 @@
+import { finiteNumber, finiteNonNegative } from './number-internal'
import { resolveCrosshairGuide } from './crosshair-resolver'
import { createMark } from './mark'
import { valueKey } from './scales'
@@ -170,12 +171,12 @@ function resolveBand(
if (!input) return undefined
const options = typeof input === 'object' ? input : undefined
return {
- bandwidth: finiteNonnegative(scale?.bandwidth, 0),
- inset: finite(options?.inset, 0),
+ bandwidth: finiteNonNegative(scale?.bandwidth, 0),
+ inset: finiteNumber(options?.inset, 0),
radius:
options?.radius === undefined
? undefined
- : finiteNonnegative(options.radius, 0),
+ : finiteNonNegative(options.radius, 0),
style: {
fill: options?.fill ?? fallbackFill,
fillOpacity: options?.fillOpacity ?? 0.12,
@@ -184,7 +185,7 @@ function resolveBand(
strokeWidth:
options?.strokeWidth === undefined
? undefined
- : finiteNonnegative(options.strokeWidth, 0),
+ : finiteNonNegative(options.strokeWidth, 0),
opacity: options?.opacity,
},
}
@@ -198,7 +199,7 @@ function resolveRuleStyle(
return {
stroke: axis?.stroke ?? shared.stroke ?? fallbackStroke,
strokeOpacity: axis?.strokeOpacity ?? shared.strokeOpacity ?? 0.35,
- strokeWidth: finiteNonnegative(axis?.strokeWidth ?? shared.strokeWidth, 1),
+ strokeWidth: finiteNonNegative(axis?.strokeWidth ?? shared.strokeWidth, 1),
strokeDasharray: axis?.strokeDasharray ?? shared.strokeDasharray,
}
}
@@ -216,15 +217,15 @@ function resolveLabel(
: scale
? (value) => formatScaleValue(scale, value)
: undefined,
- offset: finiteNonnegative(options?.offset, 8),
- fontSize: finiteNonnegative(options?.fontSize, 11),
+ offset: finiteNonNegative(options?.offset, 8),
+ fontSize: finiteNonNegative(options?.fontSize, 11),
fontWeight: options?.fontWeight,
style: {
fill: options?.fill ?? fallbackFill,
fillOpacity: options?.fillOpacity,
stroke: options?.stroke ?? 'var(--ts-chart-crosshair-label-halo, Canvas)',
strokeOpacity: options?.strokeOpacity,
- strokeWidth: finiteNonnegative(options?.strokeWidth, 3),
+ strokeWidth: finiteNonNegative(options?.strokeWidth, 3),
opacity: options?.opacity,
},
}
@@ -254,24 +255,14 @@ function resolveMarker(
if (!input) return undefined
const options = typeof input === 'object' ? input : undefined
return {
- radius: finiteNonnegative(options?.radius, 4),
+ radius: finiteNonNegative(options?.radius, 4),
style: {
fill: options?.fill ?? 'var(--ts-chart-crosshair-marker-fill, Canvas)',
fillOpacity: options?.fillOpacity,
stroke: options?.stroke,
strokeOpacity: options?.strokeOpacity,
- strokeWidth: finiteNonnegative(options?.strokeWidth, 2),
+ strokeWidth: finiteNonNegative(options?.strokeWidth, 2),
opacity: options?.opacity,
},
}
}
-
-function finiteNonnegative(value: number | undefined, fallback: number) {
- return typeof value === 'number' && Number.isFinite(value) && value >= 0
- ? value
- : fallback
-}
-
-function finite(value: number | undefined, fallback: number) {
- return typeof value === 'number' && Number.isFinite(value) ? value : fallback
-}
diff --git a/packages/charts-core/src/dom-text.ts b/packages/charts-core/src/dom-text.ts
index ccfb6660..f9cd2c64 100644
--- a/packages/charts-core/src/dom-text.ts
+++ b/packages/charts-core/src/dom-text.ts
@@ -1,3 +1,4 @@
+import { finitePositive } from './number-internal'
import { estimateSceneText, logicalTextAnchorOffset } from './guide-layout'
import type {
ChartTextMeasurer,
@@ -91,7 +92,7 @@ function configureContext(
defaultWeight: string,
options: ChartTextMeasureOptions,
): void {
- const fontScale = positiveFinite(options.fontScale, 1)
+ const fontScale = finitePositive(options.fontScale, 1)
const fontSize = options.fontSize * fontScale
const weight = options.fontWeight ?? defaultWeight
context.font = [
@@ -116,7 +117,7 @@ function paintedBounds(
measured: TextMetrics,
options: ChartTextMeasureOptions,
): ChartTextMetrics {
- const fontSize = options.fontSize * positiveFinite(options.fontScale, 1)
+ const fontSize = options.fontSize * finitePositive(options.fontScale, 1)
const left = measured.actualBoundingBoxLeft
const right = measured.actualBoundingBoxRight
const ascent = measured.actualBoundingBoxAscent
@@ -189,9 +190,3 @@ function finiteCssPixels(value: string | undefined): number {
const parsed = Number.parseFloat(value ?? '')
return Number.isFinite(parsed) ? parsed : 0
}
-
-function positiveFinite(value: number | undefined, fallback: number): number {
- return value !== undefined && Number.isFinite(value) && value > 0
- ? value
- : fallback
-}
diff --git a/packages/charts-core/src/guide-layout.ts b/packages/charts-core/src/guide-layout.ts
index 205f8994..43d71a10 100644
--- a/packages/charts-core/src/guide-layout.ts
+++ b/packages/charts-core/src/guide-layout.ts
@@ -1,3 +1,8 @@
+import {
+ finiteNumber,
+ finitePositive,
+ finiteNonNegative,
+} from './number-internal'
import type {
ChartBounds,
ChartMargin,
@@ -153,8 +158,8 @@ export function resolveGuideMargins(
let bottom = inset
let left = inset
- visitLabels(axes, 0, 0, (label, translateX, translateY) => {
- if (!label.text) return
+ visitGuideNodes(axes, 0, 0, (label, translateX, translateY) => {
+ if (label.kind !== 'label' || !label.text) return
const bounds = measureSceneLabelBounds(label, measureText)
const boundsLeft = bounds.x + translateX
@@ -181,7 +186,9 @@ export function includeGuideStrokeMargins(
guides: SceneGroup,
plot: ChartBounds,
): void {
- visitRules(guides, 0, 0, (rule, translateX, translateY) => {
+ visitGuideNodes(guides, 0, 0, (rule, translateX, translateY) => {
+ if (rule.kind !== 'rule') return
+
const style = rule.style
const extendsGeometry =
style?.strokeWidth !== undefined ||
@@ -214,37 +221,17 @@ export function includeGuideStrokeMargins(
})
}
-function visitLabels(
- node: SceneNode,
- translateX: number,
- translateY: number,
- visit: (label: SceneLabel, translateX: number, translateY: number) => void,
-): void {
- if (node.kind === 'label') {
- visit(node, translateX, translateY)
- return
- }
-
- if (node.kind !== 'group') return
-
- const childTranslateX = translateX + (node.translateX ?? 0)
- const childTranslateY = translateY + (node.translateY ?? 0)
- for (const child of node.children) {
- visitLabels(child, childTranslateX, childTranslateY, visit)
- }
-}
-
-function visitRules(
+function visitGuideNodes(
node: SceneNode,
translateX: number,
translateY: number,
visit: (
- rule: Extract,
+ node: Extract,
translateX: number,
translateY: number,
) => void,
): void {
- if (node.kind === 'rule') {
+ if (node.kind === 'label' || node.kind === 'rule') {
visit(node, translateX, translateY)
return
}
@@ -254,7 +241,7 @@ function visitRules(
const childTranslateX = translateX + (node.translateX ?? 0)
const childTranslateY = translateY + (node.translateY ?? 0)
for (const child of node.children) {
- visitRules(child, childTranslateX, childTranslateY, visit)
+ visitGuideNodes(child, childTranslateX, childTranslateY, visit)
}
}
@@ -293,22 +280,3 @@ function estimateCharacterWidth(character: string): number {
if (character.codePointAt(0)! > 0x7f) return 1
return 0.54
}
-
-function finiteNonNegative(
- value: number | undefined,
- fallback: number,
-): number {
- return value !== undefined && Number.isFinite(value) && value >= 0
- ? value
- : fallback
-}
-
-function finiteNumber(value: number | undefined, fallback: number): number {
- return value !== undefined && Number.isFinite(value) ? value : fallback
-}
-
-function finitePositive(value: number | undefined, fallback: number): number {
- return value !== undefined && Number.isFinite(value) && value > 0
- ? value
- : fallback
-}
diff --git a/packages/charts-core/src/interaction.ts b/packages/charts-core/src/interaction.ts
index a350d0c4..a08ac20c 100644
--- a/packages/charts-core/src/interaction.ts
+++ b/packages/charts-core/src/interaction.ts
@@ -1,3 +1,4 @@
+import { sameChartValue } from './value-equality-internal'
import { focusNearestX, focusNearestY, focusGroupX, focusGroupY } from './focus'
import { findContainingScenePoint } from './nearest'
import type {
@@ -250,9 +251,3 @@ function compareNavigationPoints<
) {
return left.x - right.x || left.y - right.y || leftIndex - rightIndex
}
-
-function sameChartValue(left: ChartValue, right: ChartValue) {
- return left instanceof Date && right instanceof Date
- ? left.getTime() === right.getTime()
- : Object.is(left, right)
-}
diff --git a/packages/charts-core/src/legend-static.ts b/packages/charts-core/src/legend-static.ts
index 2ade3fe1..563aa659 100644
--- a/packages/charts-core/src/legend-static.ts
+++ b/packages/charts-core/src/legend-static.ts
@@ -1,3 +1,4 @@
+import { clampNonnegativeNumber } from './number-internal'
import {
estimateSceneText,
physicalTextAnchor,
@@ -382,14 +383,14 @@ function resolveCategoricalLegendPresentation(
): CategoricalLegendPresentation {
const labelOptions = options.label
const indicatorOptions = options.indicator
- const fontSize = finiteNonnegative(labelOptions?.fontSize, 11)
+ const fontSize = clampNonnegativeNumber(labelOptions?.fontSize, 11)
const fontWeight = Number.isFinite(labelOptions?.fontWeight)
? labelOptions?.fontWeight
: undefined
- const indicatorWidth = finiteNonnegative(indicatorOptions?.width, 8)
- const indicatorHeight = finiteNonnegative(indicatorOptions?.height, 8)
- const indicatorGap = finiteNonnegative(indicatorOptions?.gap, 5)
- const rowGap = finiteNonnegative(options.rowGap, 8)
+ const indicatorWidth = clampNonnegativeNumber(indicatorOptions?.width, 8)
+ const indicatorHeight = clampNonnegativeNumber(indicatorOptions?.height, 8)
+ const indicatorGap = clampNonnegativeNumber(indicatorOptions?.gap, 5)
+ const rowGap = clampNonnegativeNumber(options.rowGap, 8)
const resolvedItems = resolveCategoricalLegendItems(
context.colors,
labelOptions?.format,
@@ -457,7 +458,7 @@ function resolveCategoricalLegendPresentation(
const layout = layoutCategoricalLegendFlow(
items.map((item) => item.width),
context.bounds.width,
- finiteNonnegative(options.gap, 16),
+ clampNonnegativeNumber(options.gap, 16),
justify,
)
return {
@@ -559,12 +560,6 @@ function resolveItemValue(
: (input ?? fallback)
}
-function finiteNonnegative(value: number | undefined, fallback: number) {
- return typeof value === 'number' && Number.isFinite(value)
- ? Math.max(0, value)
- : fallback
-}
-
function validTextMetrics(metrics: ChartTextMetrics) {
return (
Number.isFinite(metrics.x) &&
diff --git a/packages/charts-core/src/mark-with-scale-values.ts b/packages/charts-core/src/mark-with-scale-values.ts
index 17a7a488..75f5924a 100644
--- a/packages/charts-core/src/mark-with-scale-values.ts
+++ b/packages/charts-core/src/mark-with-scale-values.ts
@@ -6,7 +6,7 @@ import type {
MarkInitialization,
MarkInitializeContext,
} from './types'
-import { applyMarkRenderer, normalizeMarkInitialization } from './mark'
+import { createMarkDefinition } from './mark'
export type {
ChartMarkPointX,
@@ -42,19 +42,5 @@ export function createMarkWithScaleValues<
TXScaleId,
TYScaleId
> {
- const normalizedInitialize = (context: MarkInitializeContext) => {
- const initialized = normalizeMarkInitialization(initialize(context))
- const withMotion =
- motion === undefined || initialized.motion !== undefined
- ? initialized
- : { ...initialized, motion }
- return renderer === undefined
- ? withMotion
- : applyMarkRenderer(withMotion, renderer)
- }
- return {
- initialize: normalizedInitialize,
- ...(motion === undefined ? {} : { motion }),
- ...(renderer === undefined ? {} : { renderer }),
- }
+ return createMarkDefinition(initialize, motion, renderer)
}
diff --git a/packages/charts-core/src/mark.ts b/packages/charts-core/src/mark.ts
index d7b0e220..7a2da983 100644
--- a/packages/charts-core/src/mark.ts
+++ b/packages/charts-core/src/mark.ts
@@ -1,3 +1,4 @@
+import { isFiniteNumber } from './number-internal'
import { isChartKey, valueKey } from './scales'
import type {
Channel,
@@ -19,7 +20,7 @@ declare const process: { env: { NODE_ENV?: string } } | undefined
const warnedKeyFallbacks = new WeakSet