From 69d70960dd6b29f3706a173d348f2fc8271b0dcc Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 14 Oct 2026 13:22:10 -0600 Subject: [PATCH 1/2] feat: add opt-in lightweight SVG motion renderer --- .changeset/lightweight-svg-motion.md | 5 + API-FRICTION.md | 13 ++ benchmarks/conformance/previews/manifest.json | 2 +- docs/reference/index.md | 1 + docs/reference/motion.md | 48 +++++ packages/charts-core/docs/reference/index.md | 1 + packages/charts-core/docs/reference/motion.md | 48 +++++ packages/charts-core/package.json | 5 + packages/charts-core/src/motion-lite.test.ts | 181 ++++++++++++++++++ packages/charts-core/src/motion-lite.ts | 97 ++++++++++ scripts/check-packed-consumers.mjs | 29 +++ 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 .changeset/lightweight-svg-motion.md create mode 100644 packages/charts-core/src/motion-lite.test.ts create mode 100644 packages/charts-core/src/motion-lite.ts diff --git a/.changeset/lightweight-svg-motion.md b/.changeset/lightweight-svg-motion.md new file mode 100644 index 00000000..a744d68d --- /dev/null +++ b/.changeset/lightweight-svg-motion.md @@ -0,0 +1,5 @@ +--- +'@tanstack/charts': minor +--- + +Add the opt-in `motionLite()` renderer from `@tanstack/charts/motion/lite` for whole-chart fade entrances and keyed SVG transitions without the full motion engine. Existing `motion()` behavior is unchanged. diff --git a/API-FRICTION.md b/API-FRICTION.md index 049e9c6e..458dedd8 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -5865,6 +5865,19 @@ Each entry records: integration test mounts a definition containing both legacy `animate` and a conflicting definition-level motion duration, then verifies that the host-level motion renderer owns the transition timing. +- Follow-up from #205: an entrance/basic-update consumer retained the complete + motion engine. After measuring a private prototype, the owner approved a + separate opt-in `motionLite()` entry, not a change to `motion()`. It reuses + the existing SVG reconciler, owns only duration/easing/entrance/resize policy, + and documents destination-based interaction during updates. Definition-local + timing and tooltip spring inheritance remain exclusive to full motion. +- Follow-up verification: focused tests cover static prerender parity, + hydration without replay, keyed interruption, resize policy, reduced motion, + zero duration, invalid durations, and cleanup. Packed imports and declarations + pass, excluding full motion, springs, tooltip implementation, and D3. Three + browsers pass interrupted exits, hydration, reduced motion, line/pie endpoints, + and authored opacity. All 114 existing consumer outputs are byte-identical to + main. Full validation remains the publication gate. ### F-190 — Static conformance sampled active motion diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json index 146154f1..1a36ca56 100644 --- a/benchmarks/conformance/previews/manifest.json +++ b/benchmarks/conformance/previews/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "width": 288, "height": 192, - "sourceHash": "54ad8bcd52292af01eb73439aebd5ca4186c06ece050bb2d0118406cc36c88ec", + "sourceHash": "408fe8d6fb17b71b48ce505eb93dfe8d34afaf2b99dcd855c8b789d78e7cd486", "assets": [ { "id": "01-line-gaps", diff --git a/docs/reference/index.md b/docs/reference/index.md index aa294b54..20047bdc 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -163,6 +163,7 @@ aggregate `/scales` export. | `@tanstack/charts/mark/scale-values` | `createMarkWithScaleValues` | | `@tanstack/charts/motion` | `motion`, `stagger`, `ChartMotionOptions`, and renderer-neutral motion types | | `@tanstack/charts/motion/definition` | Isolated `stagger` and `ChartMotionStaggerOptions` without the renderer or spring solver | +| `@tanstack/charts/motion/lite` | `motionLite` and `ChartMotionLiteOptions` for whole-chart fade entrance and basic keyed SVG updates | | `@tanstack/charts/network/force` | `forceLayout`, built-in descriptors, named D3-compatible force factories, private working-clone context, settled node/link result, and lineage types | | `@tanstack/charts/network/sankey` | `sankeyDiagram`, shorthand and callable alignment types, responsive layout options, immutable node/link values, comparator contexts, and lineage types | | `@tanstack/charts/polar` | `pie`, `polar`, `focusGroupAngle`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides | diff --git a/docs/reference/motion.md b/docs/reference/motion.md index d511bc5b..8ec7b509 100644 --- a/docs/reference/motion.md +++ b/docs/reference/motion.md @@ -8,6 +8,10 @@ and Canvas do not import a clock or physics solver. ## `motion` +For a smaller renderer with whole-chart fade entrance and basic keyed SVG +updates, use [`motionLite`](#motionlite). Keep `motion()` for springs, +definition-local timing, rolling paths, and animated interaction geometry. + ```ts import { motion } from '@tanstack/charts/motion' @@ -474,3 +478,47 @@ option do not currently provide an equivalent supported class hook. - Static SVG and Canvas ignore definition motion and paint final geometry. - The optional renderer is browser SVG only; it is not a Web Animations, View Transitions, Canvas, or native adapter. + +## `motionLite` + +```ts +import { motionLite } from '@tanstack/charts/motion/lite' + +const renderer = motionLite({ duration: 240, easing: 'ease-out' }) +``` + +Pass this renderer to the DOM host or a browser framework adapter. Import it +directly from `/motion/lite`; it is not re-exported from the root or `/motion`. +Existing `motion()` imports and behavior are unchanged. + +### `ChartMotionLiteOptions` + +The optional argument to `motionLite()` has the following fields. + +`motionLite()` fades the entire SVG, including guides, on the first client +paint. Updates reuse keyed SVG nodes and tween compatible numeric attributes +and path command skeletons. New and removed keyed elements fade; incompatible +path skeletons snap to the new path. Adopted server-rendered SVG does not replay +the entrance. Static prerendering always paints final geometry. + +| Option | Default | Meaning | +| ---------------------- | ------------ | -------------------------------------------------------------- | +| `initial` | `true` | Fade the first client paint | +| `duration` | `240` | Finite, non-negative milliseconds; `0` disables animation | +| `easing` | `'ease-out'` | `linear`, `ease`, `ease-in`, `ease-out`, or `ease-in-out` | +| `respectReducedMotion` | `true` | Snap renders when the user's reduced-motion preference matches | +| `resize` | `false` | Tween size changes instead of committing immediately | + +The entrance uses browser CSS easing through Web Animations. Updates use the +SVG reconciler's named easing curves. If Web Animations is unavailable, the +first paint is immediate and supported SVG updates still animate. Updates +during entrance keep the fade running; a reduced-motion render or an immediate +resize cancels it. Destroying the host cancels both entrance and update motion. + +This renderer owns its duration and easing, ignoring `svgAnimation` and +definition-level motion timing. It does not include springs, velocity +continuity, staggering, rolling paths, specialized bar/arc entrances, or +inherited tooltip motion. Default focus and inline states use the existing SVG +renderer. Hit testing and tooltips use destination geometry during updates, +not interpolated presentation points. Use `motion()` when interactions must +follow moving marks, or for definition-local timing and tooltip springs. diff --git a/packages/charts-core/docs/reference/index.md b/packages/charts-core/docs/reference/index.md index aa294b54..20047bdc 100644 --- a/packages/charts-core/docs/reference/index.md +++ b/packages/charts-core/docs/reference/index.md @@ -163,6 +163,7 @@ aggregate `/scales` export. | `@tanstack/charts/mark/scale-values` | `createMarkWithScaleValues` | | `@tanstack/charts/motion` | `motion`, `stagger`, `ChartMotionOptions`, and renderer-neutral motion types | | `@tanstack/charts/motion/definition` | Isolated `stagger` and `ChartMotionStaggerOptions` without the renderer or spring solver | +| `@tanstack/charts/motion/lite` | `motionLite` and `ChartMotionLiteOptions` for whole-chart fade entrance and basic keyed SVG updates | | `@tanstack/charts/network/force` | `forceLayout`, built-in descriptors, named D3-compatible force factories, private working-clone context, settled node/link result, and lineage types | | `@tanstack/charts/network/sankey` | `sankeyDiagram`, shorthand and callable alignment types, responsive layout options, immutable node/link values, comparator contexts, and lineage types | | `@tanstack/charts/polar` | `pie`, `polar`, `focusGroupAngle`, `radialArc`, `radialBarRadius`, `radialBarAngle`, other radial marks, and radial/angle guides | diff --git a/packages/charts-core/docs/reference/motion.md b/packages/charts-core/docs/reference/motion.md index d511bc5b..8ec7b509 100644 --- a/packages/charts-core/docs/reference/motion.md +++ b/packages/charts-core/docs/reference/motion.md @@ -8,6 +8,10 @@ and Canvas do not import a clock or physics solver. ## `motion` +For a smaller renderer with whole-chart fade entrance and basic keyed SVG +updates, use [`motionLite`](#motionlite). Keep `motion()` for springs, +definition-local timing, rolling paths, and animated interaction geometry. + ```ts import { motion } from '@tanstack/charts/motion' @@ -474,3 +478,47 @@ option do not currently provide an equivalent supported class hook. - Static SVG and Canvas ignore definition motion and paint final geometry. - The optional renderer is browser SVG only; it is not a Web Animations, View Transitions, Canvas, or native adapter. + +## `motionLite` + +```ts +import { motionLite } from '@tanstack/charts/motion/lite' + +const renderer = motionLite({ duration: 240, easing: 'ease-out' }) +``` + +Pass this renderer to the DOM host or a browser framework adapter. Import it +directly from `/motion/lite`; it is not re-exported from the root or `/motion`. +Existing `motion()` imports and behavior are unchanged. + +### `ChartMotionLiteOptions` + +The optional argument to `motionLite()` has the following fields. + +`motionLite()` fades the entire SVG, including guides, on the first client +paint. Updates reuse keyed SVG nodes and tween compatible numeric attributes +and path command skeletons. New and removed keyed elements fade; incompatible +path skeletons snap to the new path. Adopted server-rendered SVG does not replay +the entrance. Static prerendering always paints final geometry. + +| Option | Default | Meaning | +| ---------------------- | ------------ | -------------------------------------------------------------- | +| `initial` | `true` | Fade the first client paint | +| `duration` | `240` | Finite, non-negative milliseconds; `0` disables animation | +| `easing` | `'ease-out'` | `linear`, `ease`, `ease-in`, `ease-out`, or `ease-in-out` | +| `respectReducedMotion` | `true` | Snap renders when the user's reduced-motion preference matches | +| `resize` | `false` | Tween size changes instead of committing immediately | + +The entrance uses browser CSS easing through Web Animations. Updates use the +SVG reconciler's named easing curves. If Web Animations is unavailable, the +first paint is immediate and supported SVG updates still animate. Updates +during entrance keep the fade running; a reduced-motion render or an immediate +resize cancels it. Destroying the host cancels both entrance and update motion. + +This renderer owns its duration and easing, ignoring `svgAnimation` and +definition-level motion timing. It does not include springs, velocity +continuity, staggering, rolling paths, specialized bar/arc entrances, or +inherited tooltip motion. Default focus and inline states use the existing SVG +renderer. Hit testing and tooltips use destination geometry during updates, +not interpolated presentation points. Use `motion()` when interactions must +follow moving marks, or for definition-local timing and tooltip springs. diff --git a/packages/charts-core/package.json b/packages/charts-core/package.json index ae429406..2d389d40 100644 --- a/packages/charts-core/package.json +++ b/packages/charts-core/package.json @@ -155,6 +155,7 @@ "./lit": "./src/lit.ts", "./motion": "./src/motion.ts", "./motion/definition": "./src/motion-definition.ts", + "./motion/lite": "./src/motion-lite.ts", "./network/force": "./src/network-force.ts", "./network/sankey": "./src/network-sankey.ts", "./octane": "./src/octane.ts", @@ -411,6 +412,10 @@ "types": "./dist/motion-definition.d.ts", "import": "./dist/motion-definition.js" }, + "./motion/lite": { + "types": "./dist/motion-lite.d.ts", + "import": "./dist/motion-lite.js" + }, "./network/force": { "types": "./dist/network-force.d.ts", "import": "./dist/network-force.js" diff --git a/packages/charts-core/src/motion-lite.test.ts b/packages/charts-core/src/motion-lite.test.ts new file mode 100644 index 00000000..b0ff79cd --- /dev/null +++ b/packages/charts-core/src/motion-lite.test.ts @@ -0,0 +1,181 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { scaleBand, scaleLinear } from 'd3-scale' +import { barY } from './bar' +import { createChartScene, defineChart } from './scene' +import { motionLite } from './motion-lite' +import { createSvgChartRenderer } from './svg-surface' + +const renderOptions = { ariaLabel: 'Values' } +const scene = (value: number, width = 400) => + createChartScene( + defineChart({ + guides: false, + marks: [ + barY([{ id: 'a', x: 'a', y: value }], { key: 'id', x: 'x', y: 'y' }), + ], + scales: { + x: { scale: scaleBand().domain(['a']) }, + y: { scale: scaleLinear().domain([0, 10]) }, + }, + }), + { width, height: 200 }, + ) + +afterEach(() => vi.unstubAllGlobals()) + +describe('lightweight motion', () => { + it('continues the same entrance across updates and cancels on immediate resize', () => { + const previous = Object.getOwnPropertyDescriptor( + Element.prototype, + 'animate', + ) + const cancel = vi.fn() + const animate = vi.fn(() => ({ cancel })) + Object.defineProperty(Element.prototype, 'animate', { + configurable: true, + value: animate, + }) + const container = document.createElement('div') + const surface = motionLite({ duration: 100, easing: 'linear' }).mount( + container, + () => {}, + ) + try { + surface.render(scene(2), renderOptions) + expect(animate).toHaveBeenCalledOnce() + expect(animate.mock.calls[0]).toBeDefined() + surface.render(scene(4), renderOptions) + expect(animate).toHaveBeenCalledOnce() + expect(cancel).not.toHaveBeenCalled() + surface.render(scene(4, 500), renderOptions) + expect(cancel).toHaveBeenCalledOnce() + } finally { + surface.destroy() + if (previous) + Object.defineProperty(Element.prototype, 'animate', previous) + else Reflect.deleteProperty(Element.prototype, 'animate') + } + }) + + it('renders and updates when Web Animations is unavailable', () => { + const container = document.createElement('div') + const surface = motionLite().mount(container, () => {}) + expect(() => surface.render(scene(2), renderOptions)).not.toThrow() + expect(() => surface.render(scene(4), renderOptions)).not.toThrow() + surface.destroy() + }) + + it.each([-1, NaN, Infinity])('rejects invalid duration %s', (duration) => { + expect(() => motionLite({ duration })).toThrow(RangeError) + }) + + it('keeps server output identical to static SVG and has no tooltip motion capability', () => { + const renderer = motionLite() + expect(renderer.prerender(scene(2), renderOptions)).toBe( + createSvgChartRenderer().prerender(scene(2), renderOptions), + ) + expect(renderer.capabilities).toBeUndefined() + }) + + it.each([false, true])( + 'adopts existing SVG without an entrance, initial: %s', + (initial) => { + const container = document.createElement('div') + const renderer = motionLite({ initial }) + container.innerHTML = renderer.prerender(scene(2), renderOptions) + const element = container.firstElementChild! + const animate = vi.fn() + Object.defineProperty(element, 'animate', { value: animate }) + const surface = renderer.mount(container, () => {}) + surface.render(scene(2), renderOptions) + expect(surface.element).toBe(element) + expect(animate).not.toHaveBeenCalled() + surface.destroy() + }, + ) + + it.each([false, true])( + 'retargets from painted geometry and handles resize: %s', + (resize) => { + const callbacks = new Map() + let id = 0 + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((fn) => { + callbacks.set(++id, fn) + return id + }) + vi.spyOn(window, 'cancelAnimationFrame').mockImplementation( + (key) => key != null && callbacks.delete(key), + ) + const tick = (time: number) => { + const pending = [...callbacks.values()] + callbacks.clear() + pending.forEach((fn) => fn(time)) + } + const container = document.createElement('div') + const surface = motionLite({ + initial: false, + duration: 100, + easing: 'linear', + resize, + }).mount(container, () => {}) + surface.render(scene(2), renderOptions) + const bar = container.querySelector('rect')! + const height = () => Number(bar.getAttribute('height')) + const first = height() + surface.render(scene(8), renderOptions) + tick(0) + tick(50) + expect(height()).toBeCloseTo(first * 2.5) + const midway = height() + surface.render(scene(4), renderOptions) + expect(height()).toBe(midway) + tick(60) + tick(160) + expect(height()).toBeCloseTo(first * 2) + surface.render(scene(9, 500), renderOptions) + expect(height()).toBeCloseTo(first * (resize ? 2 : 4.5)) + tick(170) + tick(270) + expect(height()).toBeCloseTo(first * 4.5) + surface.render(scene(1, 500), renderOptions) + tick(280) + tick(300) + surface.destroy() + expect(callbacks.size).toBe(0) + const stopped = height() + tick(500) + expect(height()).toBe(stopped) + }, + ) + + it('snaps reduced-motion updates and ignores conflicting host animation', () => { + vi.stubGlobal('matchMedia', () => ({ matches: true })) + const frame = vi.spyOn(window, 'requestAnimationFrame') + const container = document.createElement('div') + const surface = motionLite().mount(container, () => {}) + surface.render(scene(2), renderOptions) + const first = Number( + container.querySelector('rect')!.getAttribute('height'), + ) + surface.render(scene(8), { + ...renderOptions, + animation: { duration: 1000 }, + }) + expect( + Number(container.querySelector('rect')!.getAttribute('height')), + ).toBe(first * 4) + expect(frame).not.toHaveBeenCalled() + surface.destroy() + }) + + it('treats duration zero as immediate and works without Web Animations', () => { + const frame = vi.spyOn(window, 'requestAnimationFrame') + const container = document.createElement('div') + const surface = motionLite({ duration: 0 }).mount(container, () => {}) + surface.render(scene(2), renderOptions) + surface.render(scene(8), renderOptions) + expect(container.querySelector('rect')!.getAttribute('height')).toBe('160') + expect(frame).not.toHaveBeenCalled() + surface.destroy() + }) +}) diff --git a/packages/charts-core/src/motion-lite.ts b/packages/charts-core/src/motion-lite.ts new file mode 100644 index 00000000..753bd713 --- /dev/null +++ b/packages/charts-core/src/motion-lite.ts @@ -0,0 +1,97 @@ +import { createSvgChartRenderer } from './svg-surface' +import type { ChartSurface, UniversalChartRenderer } from './dom-types' +import type { ChartAnimationOptions, ChartValue } from './types' + +export interface ChartMotionLiteOptions { + /** Fade the whole chart on its first client paint. Adopted SVG does not fade. */ + initial?: boolean + /** Duration in milliseconds for entrance and keyed SVG updates. Default: 240. */ + duration?: number + easing?: Extract + respectReducedMotion?: boolean + /** Animate size changes. Default: false. */ + resize?: boolean +} + +/** Whole-chart entrance and keyed SVG tweens, without the full motion engine. */ +export function motionLite( + options: ChartMotionLiteOptions = {}, +): UniversalChartRenderer { + const duration = options.duration ?? 240 + if (!Number.isFinite(duration) || duration < 0) { + throw new RangeError('motionLite duration must be finite and non-negative.') + } + const animation: ChartAnimationOptions = { + duration, + easing: options.easing ?? 'ease-out', + } + const renderer: UniversalChartRenderer = { + ...createSvgChartRenderer(), + id: 'svg-motion-lite', + mount< + TDatum, + TXValue extends ChartValue = ChartValue, + TYValue extends ChartValue = ChartValue, + >( + container: HTMLElement, + requestRender: (force?: boolean) => void, + ): ChartSurface { + const surface = createSvgChartRenderer().mount( + container, + requestRender, + ) + const adopted = container.firstElementChild?.matches('svg.ts-chart') + const view = container.ownerDocument.defaultView + let initial = true + let width: number | undefined + let height: number | undefined + let entrance: Animation | undefined + return { + renderer, + supportsStateFocus: true, + clientToScene: surface.clientToScene, + paintFocus: surface.paintFocus, + get element() { + return surface.element + }, + render(scene, renderOptions) { + const reduced = + options.respectReducedMotion !== false && + view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches + const resized = + !initial && (width !== scene.width || height !== scene.height) + const animate = + duration > 0 && !reduced && (!resized || options.resize) + if (!animate) entrance?.cancel() + surface.render(scene, { + ...renderOptions, + animation: !initial && animate ? animation : undefined, + }) + if (initial && !adopted && options.initial !== false && animate) { + entrance = surface.element.animate?.( + [ + { opacity: 0 }, + { + opacity: + view?.getComputedStyle(surface.element).opacity || '1', + }, + ], + { + duration, + easing: options.easing ?? 'ease-out', + }, + ) + } + initial = false + width = scene.width + height = scene.height + }, + destroy() { + entrance?.cancel() + surface.destroy() + }, + } + }, + } + return renderer +} diff --git a/scripts/check-packed-consumers.mjs b/scripts/check-packed-consumers.mjs index 68079a4e..b5779582 100644 --- a/scripts/check-packed-consumers.mjs +++ b/scripts/check-packed-consumers.mjs @@ -936,6 +936,7 @@ async function verifyEsmRuntime() { import { interactiveColorLegend } from '@tanstack/charts/legend' import { keyedSelection, whenSelected } from '@tanstack/charts/selection' import { motion } from '@tanstack/charts/motion' + import { motionLite } from '@tanstack/charts/motion/lite' import { createChartSpring } from '@tanstack/charts/spring' import { polar, @@ -978,6 +979,7 @@ async function verifyEsmRuntime() { assert.equal(tooltip.id, 'tooltip') assert.equal(portal.id, 'portal') assert.equal(motion().id, 'svg:svg-motion') + assert.equal(motionLite().id, 'svg-motion-lite') assert.equal(createChartSpring().sample(0).value, 0) const packedBrushChanges = [] const packedBrush = brushX({ @@ -1898,6 +1900,7 @@ async function verifyDeclarations() { type InteractiveColorLegendItemContext, } from '@tanstack/charts/legend' import { motion, type ChartMotionOptions } from '@tanstack/charts/motion' + import { motionLite, type ChartMotionLiteOptions } from '@tanstack/charts/motion/lite' import { createChartSpring, type ChartSpringOptions, @@ -2121,6 +2124,12 @@ async function verifyDeclarations() { }, } const motionRenderer = motion(motionOptions) + const liteOptions: ChartMotionLiteOptions = { initial: false, duration: 200, easing: 'linear' } + const liteRenderer = motionLite(liteOptions) + const typedLiteSurface = liteRenderer.mount(document.createElement('div'), () => {}) + void typedLiteSurface + // @ts-expect-error the lightweight renderer does not accept spring timing + motionLite({ transition: { type: 'spring' } }) const springOptions: ChartSpringOptions = { stiffness: 170, damping: 14 } const spring = createChartSpring(springOptions) void [motionRenderer, spring] @@ -3880,6 +3889,7 @@ async function verifyProductionBundles() { tooltipExtension: ['/@tanstack/charts/dist/tooltip.js'], tooltipPortal: ['/@tanstack/charts/dist/tooltip-portal.js'], motion: ['/@tanstack/charts/dist/motion.js'], + motionLite: ['/@tanstack/charts/dist/motion-lite.js'], spring: ['/@tanstack/charts/dist/spring.js'], focusGuide: ['/@tanstack/charts/dist/focus-guide.js'], focusMark: ['/@tanstack/charts/dist/focus-mark.js'], @@ -5596,6 +5606,25 @@ async function verifyProductionBundles() { export { motion } from '@tanstack/charts/motion' `, }, + { + label: 'Lightweight motion SVG renderer', + filename: 'motion-lite.ts', + external: [], + rendererBoundary: 'svg', + inputBoundary: { + require: ['motionLite'], + forbid: [ + 'motion', + 'spring', + 'tooltipExtension', + 'tooltipPortal', + 'd3Runtime', + ], + }, + source: ` + export { motionLite } from '@tanstack/charts/motion/lite' + `, + }, { label: 'Spring physics kernel', filename: 'spring.ts', From 7b6322c7a0633582282f656df4f37019356edccb Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 14 Oct 2026 13:25:51 -0600 Subject: [PATCH 2/2] chore: refresh approved motion comparison provenance --- benchmarks/comparison/bundle-baseline.json | 4 ++-- docs/comparison.md | 4 ++-- packages/charts-core/docs/comparison.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json index 7b499ddb..bc22e4f0 100644 --- a/benchmarks/comparison/bundle-baseline.json +++ b/benchmarks/comparison/bundle-baseline.json @@ -11,8 +11,8 @@ "sources": { "tanstack": { "kind": "workspace", - "revision": "28a0626e3859d2e0ebd33772bf81ebc2c402b09a", - "inputDigest": "sha256:b0e66aa78ec68bebdd7a9305693d559fd68f0946b82cb1909a851c3704895eb6" + "revision": "69d70960dd6b29f3706a173d348f2fc8271b0dcc", + "inputDigest": "sha256:80f5c5447f39757715be5def33eed28b12869946936369671243436701fb7964" }, "chartjs": { "kind": "package", diff --git a/docs/comparison.md b/docs/comparison.md index 369542af..61c55ce6 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 `28a0626` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `69d7096` | | [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 `28a0626`. +at page render time. The measured TanStack workspace revision is `69d7096`. ## Capability matrix diff --git a/packages/charts-core/docs/comparison.md b/packages/charts-core/docs/comparison.md index 369542af..61c55ce6 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 `28a0626` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `69d7096` | | [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 `28a0626`. +at page render time. The measured TanStack workspace revision is `69d7096`. ## Capability matrix