diff --git a/.changeset/calm-scales-agree.md b/.changeset/calm-scales-agree.md new file mode 100644 index 00000000..5d411ab8 --- /dev/null +++ b/.changeset/calm-scales-agree.md @@ -0,0 +1,5 @@ +--- +'@tanstack/charts': patch +--- + +Accept configured D3 scale instances for union-valued axes. diff --git a/API-FRICTION.md b/API-FRICTION.md index 9d83ce5c..32fe1d3c 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -233,6 +233,7 @@ Each entry records: | F-195 | Release versions matched dependency substrings | Tooling | resolved | | F-196 | Focus decorations suppressed the primary indicator | API | resolved | | F-197 | Workspace validation omitted comparison provenance | Tooling | resolved | +| F-198 | Union-valued axes rejected configured D3 scales | API | resolved | ## Findings @@ -4908,3 +4909,19 @@ Each entry records: - Verification: the 60-case comparison baseline records revision `e4b5249` and package version 0.6.2, the canonical comparison page identifies the same revision, and `pnpm benchmark:check` passes locally and in pull-request CI. + +### F-198 — Union-valued axes rejected configured D3 scales + +- Status: resolved +- Severity: medium +- Owner: API +- Observed in: issue #34 configured D3 scale instance repro +- Friction: an axis inferred as `string | Date` required one configured scale + to accept both members. A documented `scaleTime().domain(...)` instance was + therefore rejected even though it correctly served the temporal branch. +- Decision: distribute `ChartScaleInput` over axis-value unions so each + configured scale retains its exact input contract. Keep runtime validation + unchanged and narrow the copied scale only after that validation succeeds. +- Verification: the public type regression accepts a configured D3 time scale + for a `string | Date` axis, rejects an unrelated numeric scale, and the full + workspace typecheck plus configured-scale runtime tests pass. diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json index 2ff87528..35b29cc5 100644 --- a/benchmarks/bundle-size/universal-baseline.json +++ b/benchmarks/bundle-size/universal-baseline.json @@ -16,7 +16,7 @@ }, "TanStack DOM host": { "bytes": 44461, - "gzip": 16247 + "gzip": 16246 }, "React adapter": { "bytes": 46629, diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json index 82966ae9..9aa2ae35 100644 --- a/benchmarks/comparison/bundle-baseline.json +++ b/benchmarks/comparison/bundle-baseline.json @@ -1,8 +1,8 @@ { "schemaVersion": 3, - "generatedAt": "2026-08-02T21:03:04.476Z", + "generatedAt": "2026-08-03T05:36:45.402Z", "packageVersions": { - "tanstack": "0.6.2", + "tanstack": "0.6.3", "chartjs": "4.5.1", "echarts": "6.1.0", "recharts": "3.10.1", @@ -11,7 +11,7 @@ "sources": { "tanstack": { "kind": "workspace", - "revision": "e4b5249ceca9910d990aefc9f8777a8d28e63921" + "revision": "3fdff48a76e4f7ff563bc2c0b182c7cb563b5a95" }, "chartjs": { "kind": "package", @@ -116,16 +116,16 @@ "tanstack-scatter-interactive": { "minifiedBytes": 82948, "gzipBytes": 30973, - "brotliBytes": 27313, + "brotliBytes": 27317, "incrementalGzipBytes": 30973, - "incrementalBrotliBytes": 27313 + "incrementalBrotliBytes": 27317 }, "tanstack-scatter-advanced": { "minifiedBytes": 82964, "gzipBytes": 30979, - "brotliBytes": 27318, + "brotliBytes": 27329, "incrementalGzipBytes": 30979, - "incrementalBrotliBytes": 27318 + "incrementalBrotliBytes": 27329 }, "chartjs-line-basic": { "minifiedBytes": 137909, diff --git a/docs/comparison.md b/docs/comparison.md index bb906819..df23b5d1 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -12,14 +12,14 @@ untested behavior into a checkmark. | Library | Package | Measured source | | -------------------------------------------------------------------------------------- | -------------------- | ------------------- | -| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `e4b5249` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3fdff48` | | [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 `e4b5249`. +at page render time. The measured TanStack workspace revision is `3fdff48`. ## Capability matrix @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-02`. +Baseline date: `2026-08-03`. Controlled ranges cover 12 independently built, minified browser consumers: line, bar, area, and scatter at basic, interactive, and advanced tiers. Only diff --git a/packages/charts-core/docs/comparison.md b/packages/charts-core/docs/comparison.md index bb906819..df23b5d1 100644 --- a/packages/charts-core/docs/comparison.md +++ b/packages/charts-core/docs/comparison.md @@ -12,14 +12,14 @@ untested behavior into a checkmark. | Library | Package | Measured source | | -------------------------------------------------------------------------------------- | -------------------- | ------------------- | -| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `e4b5249` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `3fdff48` | | [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 `e4b5249`. +at page render time. The measured TanStack workspace revision is `3fdff48`. ## Capability matrix @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-02`. +Baseline date: `2026-08-03`. Controlled ranges cover 12 independently built, minified browser consumers: line, bar, area, and scatter at basic, interactive, and advanced tiers. Only diff --git a/packages/charts-core/src/scale-input.ts b/packages/charts-core/src/scale-input.ts index d59d332a..243573e0 100644 --- a/packages/charts-core/src/scale-input.ts +++ b/packages/charts-core/src/scale-input.ts @@ -33,7 +33,7 @@ export function resolveScaleInput( 'A scale factory must return a copyable scale with domain and range methods', ) } - const scale = created.copy() + const scale = created.copy() as ConfiguredScaleLike if (infer) { const domain = inferScaleDomain(scale, options.values, options.includeZero) @@ -47,9 +47,9 @@ export function resolveScaleInput( return scale } -export function isScaleFactory( - source: ChartScaleInput, -): source is ChartScaleFactory { +export function isScaleFactory( + source: Function, +): source is ChartScaleFactory { return typeof source === 'function' && !('copy' in source) } diff --git a/packages/charts-core/src/type-contract.test.ts b/packages/charts-core/src/type-contract.test.ts index efb3a537..c1670a4e 100644 --- a/packages/charts-core/src/type-contract.test.ts +++ b/packages/charts-core/src/type-contract.test.ts @@ -1,5 +1,5 @@ import { describe, expect, expectTypeOf, it } from 'vitest' -import { scaleBand, scaleLinear, scaleUtc } from 'd3-scale' +import { scaleBand, scaleLinear, scaleTime, scaleUtc } from 'd3-scale' import { barX, barY } from './bar' import type { BarYOptions } from './bar' import { mountChart } from './dom' @@ -143,6 +143,12 @@ const temporalDefinition = defineChart({ }, y: { scale: scaleLinear().domain([0, 4]) }, }) +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]) }, +}) const implicitIndexDefinition = defineChart({ marks: [implicitIndexMark], x: { scale: scaleLinear().domain([0, 2]) }, @@ -686,6 +692,16 @@ if (false) { } void invalidTemporalSpec + const invalidUnionPositionSpec: ChartSpec< + 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]) }, + } + void invalidUnionPositionSpec + // @ts-expect-error Static definitions infer and enforce the mark-to-scale contract. defineChart({ marks: [categoricalMark], @@ -893,6 +909,9 @@ describe('public type contracts', () => { expectTypeOf< NonNullable >().toEqualTypeOf() + expectTypeOf< + NonNullable + >().toEqualTypeOf() expectTypeOf< NonNullable >().toEqualTypeOf() diff --git a/packages/charts-core/src/types.ts b/packages/charts-core/src/types.ts index ec68e142..27075e78 100644 --- a/packages/charts-core/src/types.ts +++ b/packages/charts-core/src/types.ts @@ -48,7 +48,9 @@ export type ChartScaleFactory = Function & { } export type ChartScaleInput = - ConfiguredScaleLike | ChartScaleFactory + TValue extends ChartValue + ? ConfiguredScaleLike | ChartScaleFactory + : never export interface ChartNumericScaleOptions { scale: ChartScaleInput