From 4134429b49973cf64df1f36123ba8392571562eb Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 4 Aug 2026 17:44:53 -0600 Subject: [PATCH 1/3] feat: add deterministic live chart motion and viewports --- .changeset/live-paths-shift.md | 8 + .gitignore | 3 + API-FRICTION.md | 176 ++- benchmarks/bundle-size/README.md | 13 +- .../bundle-size/universal-baseline.json | 40 +- docs/concepts/layout-axes-and-coordinates.md | 64 + docs/framework/octane/adapter.md | 5 +- docs/framework/react/adapter.md | 6 +- docs/guides/custom-marks-and-renderers.md | 18 +- docs/guides/dynamic-data-and-animation.md | 33 + docs/guides/interactions-and-selections.md | 61 +- docs/guides/themes-and-styling.md | 17 +- docs/guides/tooltips-and-focus.md | 22 +- docs/reference/chart-definitions.md | 6 +- docs/reference/chart-spec.md | 16 +- docs/reference/custom-extensions.md | 24 +- docs/reference/dom-host.md | 30 +- docs/reference/focus-and-interaction.md | 43 +- docs/reference/index.md | 2 +- docs/reference/marks/bar-and-rect.md | 6 +- docs/reference/motion.md | 96 ++ docs/reference/rendering-and-export.md | 84 +- docs/reference/runtime-and-scene.md | 55 +- docs/reference/scales-guides-and-color.md | 66 +- docs/reference/types.md | 33 +- examples/charts-react/live/index.html | 12 + examples/charts-react/live/main.tsx | 15 + examples/charts-react/package.json | 2 + examples/charts-react/paged/index.html | 12 + examples/charts-react/paged/main.tsx | 15 + examples/charts-react/src/App.tsx | 6 + examples/charts-react/src/LiveCharts.tsx | 388 +++++++ .../charts-react/src/PagedHistoryChart.tsx | 530 +++++++++ examples/charts-react/src/styles.css | 197 ++++ examples/charts-react/vite.live.config.ts | 13 + examples/charts-react/vite.paged.config.ts | 13 + packages/alpine-charts/src/index.ts | 1 + packages/angular-charts/src/Chart.ts | 1 + .../concepts/layout-axes-and-coordinates.md | 64 + .../docs/framework/octane/adapter.md | 5 +- .../docs/framework/react/adapter.md | 6 +- .../docs/guides/custom-marks-and-renderers.md | 18 +- .../docs/guides/dynamic-data-and-animation.md | 33 + .../guides/interactions-and-selections.md | 61 +- .../docs/guides/themes-and-styling.md | 17 +- .../docs/guides/tooltips-and-focus.md | 22 +- .../docs/reference/chart-definitions.md | 6 +- .../charts-core/docs/reference/chart-spec.md | 16 +- .../docs/reference/custom-extensions.md | 24 +- .../charts-core/docs/reference/dom-host.md | 30 +- .../docs/reference/focus-and-interaction.md | 43 +- packages/charts-core/docs/reference/index.md | 2 +- .../docs/reference/marks/bar-and-rect.md | 6 +- packages/charts-core/docs/reference/motion.md | 96 ++ .../docs/reference/rendering-and-export.md | 84 +- .../docs/reference/runtime-and-scene.md | 55 +- .../docs/reference/scales-guides-and-color.md | 66 +- packages/charts-core/docs/reference/types.md | 33 +- packages/charts-core/src/band.ts | 16 +- packages/charts-core/src/canvas.ts | 3 + .../charts-core/src/configured-scale.test.ts | 439 ++++++- packages/charts-core/src/configured-scale.ts | 210 +++- packages/charts-core/src/dom-types.ts | 59 +- packages/charts-core/src/dom.ts | 5 +- packages/charts-core/src/focus-mark.test.ts | 128 +- packages/charts-core/src/index.ts | 10 + packages/charts-core/src/motion-path.test.ts | 341 ++++++ packages/charts-core/src/motion-path.ts | 254 ++++ packages/charts-core/src/motion.test.ts | 1028 +++++++++++++++++ packages/charts-core/src/motion.ts | 683 +++++++++-- packages/charts-core/src/nearest.ts | 35 +- packages/charts-core/src/renderer.test.ts | 586 ++++++++++ packages/charts-core/src/renderer.ts | 312 ++++- packages/charts-core/src/runtime.test.ts | 6 +- packages/charts-core/src/scene-point-map.ts | 52 + packages/charts-core/src/scene.test.ts | 510 +++++++- packages/charts-core/src/scene.ts | 225 +++- packages/charts-core/src/svg-coordinates.ts | 31 + packages/charts-core/src/svg-resources.ts | 81 +- packages/charts-core/src/svg-surface.test.ts | 98 +- packages/charts-core/src/svg-surface.ts | 39 +- packages/charts-core/src/svg.ts | 77 +- packages/charts-core/src/tooltip.ts | 3 +- .../charts-core/src/type-contract.test.ts | 37 +- packages/charts-core/src/types.ts | 57 + packages/charts-core/src/universal-types.ts | 6 + packages/charts-core/src/universal.ts | 1 + packages/lit-charts/src/Chart.ts | 1 + packages/octane-charts/src/Chart.tsrx | 1 + packages/preact-charts/src/Chart.tsx | 1 + packages/react-charts/src/Chart.test.tsx | 91 +- packages/react-charts/src/Chart.tsx | 1 + .../react-native-charts/src/Chart.test.tsx | 70 +- packages/react-native-charts/src/Chart.tsx | 5 +- .../src/interaction.test.ts | 52 + .../react-native-charts/src/interaction.ts | 22 +- packages/solid-charts/src/Chart.tsx | 1 + packages/svelte-charts/src/Chart.svelte | 1 + packages/vue-charts/src/Chart.ts | 1 + scripts/measure-bundles.mjs | 56 +- 100 files changed, 7810 insertions(+), 644 deletions(-) create mode 100644 .changeset/live-paths-shift.md create mode 100644 examples/charts-react/live/index.html create mode 100644 examples/charts-react/live/main.tsx create mode 100644 examples/charts-react/paged/index.html create mode 100644 examples/charts-react/paged/main.tsx create mode 100644 examples/charts-react/src/LiveCharts.tsx create mode 100644 examples/charts-react/src/PagedHistoryChart.tsx create mode 100644 examples/charts-react/vite.live.config.ts create mode 100644 examples/charts-react/vite.paged.config.ts create mode 100644 packages/charts-core/src/motion-path.test.ts create mode 100644 packages/charts-core/src/motion-path.ts create mode 100644 packages/charts-core/src/scene-point-map.ts create mode 100644 packages/charts-core/src/svg-coordinates.ts diff --git a/.changeset/live-paths-shift.md b/.changeset/live-paths-shift.md new file mode 100644 index 00000000..b2d40e31 --- /dev/null +++ b/.changeset/live-paths-shift.md @@ -0,0 +1,8 @@ +--- +'@tanstack/charts': patch +--- + +Add validated rolling path transforms with dynamic y-domain reprojection, +continuous translated viewports with stationary guides, and a controlled focus +controller that follows presentation geometry. Default SVG rendering now +honors scene clips and gradients. diff --git a/.gitignore b/.gitignore index 0c18d993..c55a1b71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules .pnpm-store dist +dist-live +dist-paged +.netlify/ .svelte-kit .bundle-output .benchmark-output diff --git a/API-FRICTION.md b/API-FRICTION.md index 32fe1d3c..b7a5e8f2 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -5,7 +5,7 @@ observed difficulty from examples, production migrations, tests, and agent evaluations so later API, documentation, and TanStack Intent skill work is based on evidence. -Last updated: 2026-08-02 +Last updated: 2026-08-04 ## Triage rule @@ -234,6 +234,11 @@ Each entry records: | 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 | +| F-199 | Rolling paths morphed samples instead of shifting them | API | resolved | +| F-200 | Motion ignored authored SVG clips | API | resolved | +| F-201 | Paged history required overlaid chart hosts | API | resolved | +| F-202 | Long-press focus duplicated host pointer geometry | API | resolved | +| F-203 | Focus cursor width depended on private band inference | API | resolved | ## Findings @@ -4925,3 +4930,172 @@ Each entry records: - 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. + +### F-199 — Rolling paths morphed samples instead of shifting them + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: Liveline-inspired streaming React examples +- Friction: a fixed-length rolling line retained keyed samples, but SVG path + interpolation matched commands by array position. Each old y-value therefore + bent toward the following sample instead of the trace translating left. + Stable datum keys already preserved interaction points but could not change + the single path element's interpolation strategy. +- Decision: add one first-principles rolling object contract, + `motion.path: { update: 'rolling', x: 'shift', y, fallback }`, with fixed or + affine-reprojected y geometry and an explicit snap-or-morph fallback. Validate + the retained key window, balanced batch, stable semantic values, uniform x + displacement, stable primitive kind and plot bounds, required clipping, + structured path geometry, clip-edge coverage, and the absence of transient x + or y viewport translation before installing the target path and animating one + matrix to identity. Invalid rolling updates snap by default instead of + silently becoming a different interpolation. +- Follow-up evidence: while a rolling path translated correctly, its SVG focus + circles snapped to destination coordinates and an active tooltip received no + updates as presentation points advanced. The surface could expose current + presentation geometry but had no notification contract for the shared host. +- Follow-up decision: animate keyed focus-layer geometry with its owning data + points and add optional `ChartSurface.subscribePresentationPoints()`. The + shared host now re-resolves a stationary pointer or restores pinned and + keyboard focus against each published presentation frame, keeping tooltip + anchors and focus markers aligned without chart-specific wiring. +- Follow-up evidence: entering and exiting point dots still used independent + fades, snap fallback retained removed presentation points until its nominal + duration, and applying an inline focus state cancelled the active data + transform. +- Follow-up decision: associate both previous and target semantic points with + each rolling plan. Retained and entering dots, default focus circles, + exiting dots, and presentation points now share the path transform and + timing; snap removes stale geometry and points in the same commit. Focus + layers remain live during data motion, while inline mark-state geometry and + style retain the latest request across back-to-back updates and reconcile + when data motion becomes idle instead of taking over its transform. +- Verification: pure planner tests cover valid batches and every rejected + invariant, including nonzero x or y viewport translation. Integration tests + hold target line and area path data constant while x translation and affine y + reprojection animate through one matrix, verify matching presentation points, + compose an interrupted A-to-B-to-C update from the currently painted + transform, keep retained, entering, and exiting point and focus decorations + aligned, keep an authored focus band on the same trajectory, retain a + deferred inline focus state across back-to-back rolls, and verify snap removes + old DOM and presentation points without scheduling a frame. The live + dynamic-y example uses the rolling object contract, clipped overscan, a stable + semantic area baseline, and fixed plot margins. The complete motion SVG + renderer measures 14.17 KiB gzip under its reviewed 14.4 KiB ceiling. + +### F-200 — Motion ignored authored SVG clips + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: Liveline-inspired streaming React examples +- Friction: the definitions correctly set `clip: true`, but `motion()` used the + resource-free SVG serializer. Translated line and area geometry therefore + painted through the y-axis labels and beyond the plot instead of being + clipped to `scene.chart`. There was no public way to combine the motion + renderer with the resource-aware serializer. +- Decision: make resource-aware SVG serialization the default and use it from + `motion()` for both prerendering and updates. SVG hosts now consume clips and + gradients already declared in the renderer-neutral scene. +- Verification: the streaming motion regression asserts that the marks group + references a generated clip path whose rectangle exactly matches the + resolved chart x, y, width, and height. The running React examples expose + three distinct scoped clip paths and keep linear shifted marks inside each + plot while guides remain outside. The static SVG line consumer measures + 17.75 KiB gzip; the reviewed universal baseline records its 1,743-byte gzip + increase from making scene resources part of the default renderer. + +### F-201 — Paged history required overlaid chart hosts + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: iOS-style paged history React example +- Friction: stationary guides over a continuously swiped line required two + overlaid chart hosts, duplicated scale definitions, manual width measurement, + CSS clipping, and pixel offsets. Focus geometry belonged to one host while + the visible coordinates belonged to the other. +- Decision: add a continuous axis `viewport` with a committed semantic domain + and transient scene-pixel translation. Resolve guides against the viewport + domain and place viewport content in clipped layers per mark and per owned + axis. Infer ownership from materialized channels and let custom marks override + each axis as `content` or `fixed`, keeping guides and unrelated annotations + stationary. Remap scene, node-interaction, focus-layer, and mark-state point + references to presented coordinates. Expose the full content domain and + presented mapper on the resolved scale. Preserve complete scene points for + rendering and diagnostics while `viewportInteractionPoints` and the optional + `findNearestPoint` candidate list limit focus and keyboard navigation to + clipped content anchors inside the plot while retaining points from + fixed-ownership marks outside it. +- Verification: type tests constrain `ChartContinuousDomain` to homogeneous + numeric or Date endpoints. Configured-scale tests require continuous, + invertible, unclamped scales with independently configurable domain and range, + reject categorical, quantize, getter-only, and clamped configured scales, + reject an authored viewport on an opaque custom resolver, accept a custom + resolver that returns its own complete viewport, and cover positive, + reversed, and negative same-sign logarithmic domains. Scene, + SVG, focus, and renderer tests assert screen-direction translation, + per-mark/per-axis clips, fixed guides and annotations, explicit custom-mark + ownership, presented point references, candidate filtering, and tooltip/focus + continuity. The paged example renders its complete history as one line and + area in one chart host while the application owns only drag policy and page + settling. The locked D3-scale line scene measures 16.18 KiB gzip, a reviewed + 1,233-byte increase for the default viewport-capable scene contract. + +### F-202 — Long-press focus duplicated host pointer geometry + +- Status: resolved +- Severity: medium +- Owner: API +- Observed in: iOS-style paged history React example +- Friction: delaying focus until a touch hold required the application to read + SVG bounds, convert client coordinates, search scene points, position a + cursor, and render a second tooltip. The chart already owned all of that + logic, but its pointer handling was all-or-nothing and not callable. +- Decision: expose one stable `ChartInteractionController` on the host and + render context. `clientToScene()` exposes renderer-correct drag geometry, + `resolvePointer()` applies the current presentation and configured focus + strategy, and `setControlledFocus()` paints or clears the same focus marks + and tooltip as native input. Definition `pointer: false` disables automatic + pointer move, leave, and click without disabling keyboard focus, and + controlled focus has separate ownership from pointer and keyboard focus. + Share SVG client-to-scene conversion between normal and motion surfaces. + Keep that surface capability optional for existing custom renderers; the + controller returns `null` when it is absent. Passing a pointer resolution to + `setControlledFocus()` infers pointer source unless explicitly overridden, + while a raw point defaults to programmatic source. +- Follow-up evidence: the host rebuilt a configured spatial index from the + transition-start presentation points immediately after a data update. Motion + correctly bypassed that index while presentation points were active, but + re-enabled the stale index when the transition settled. +- Follow-up decision: build spatial indexes from the destination scene's + visible points. Presentation points remain authoritative during motion and + the destination index becomes authoritative only after they settle. +- Verification: renderer tests cover resolution, focus groups, clearing, + pinning, pointer opt-out, ownership boundaries, presentation updates, and + controller identity, including inferred pointer source across a scene update + and destination spatial-index resolution after presentation geometry clears. + Every DOM framework adapter forwards the controller in `onRender`, and the + paged example delegates its long-press cursor and tooltip to the definition + without application SVG math. The locked DOM host measures 18.39 KiB gzip; + its reviewed 2,583-byte increase includes the default viewport and controlled + interaction contracts. + +### F-203 — Focus cursor width depended on private band inference + +- Status: resolved +- Severity: low +- Owner: API +- Observed in: iOS-style paged history React example +- Friction: a one-pixel definition-owned cursor could use a focused `bandX`, + but its width was always inferred from sample spacing. Producing a precise + cursor with `inset` required the application to duplicate the mark's private + `0.8` bandwidth factor. `ruleX` could paint the right geometry but emits no + focus-match points. +- Decision: add explicit scene-pixel `width` to `bandX` and `height` to + `bandY`. Explicit dimensions replace scale or inferred bandwidth before + applying `inset`; existing definitions retain inferred sizing. +- Verification: mark tests assert fixed one- and two-pixel continuous bands, + and the paged history definition expresses its cursor as `width: 1` without + responsive sample-spacing math. diff --git a/benchmarks/bundle-size/README.md b/benchmarks/bundle-size/README.md index 0756029c..317e3ca0 100644 --- a/benchmarks/bundle-size/README.md +++ b/benchmarks/bundle-size/README.md @@ -25,18 +25,25 @@ may add only its transport module over the tooltip consumer. Ordinary line, compact-scale, and tooltip kernels also reject all transform modules. The compact linear scene and React consumer are both locked and budgeted. The -scene has an 8.1 KiB gzip ceiling. The React compact-scale line consumer has an -18.6 KiB ceiling with React and React DOM external. `d3-array` tick helpers are +scene has a 9.5 KiB gzip ceiling. The React compact-scale line consumer has a +21.2 KiB ceiling with React and React DOM external. `d3-array` tick helpers are allowed only in the compact linear path; categorical compact-scale kernels reject every D3 runtime input. All compact fixtures reject `d3-scale`, `d3-format`, `d3-interpolate`, `d3-color`, and `internmap`. Painted-geometry interaction is part of the default scene and host contract -across DOM, Canvas, and native rendering. Its isolated resolver has a 2 KiB +across DOM, Canvas, and native rendering. Its isolated resolver has a 2.2 KiB gzip ceiling. The locked shared-host entries record the reviewed integration cost, while noninteractive consumers retain only the small scene-compiler portion of that contract. +Continuous viewports and the controlled interaction controller are also part +of the default scene and host contracts. Default static SVG consumes scene +clips and gradients. Their reviewed shared-path cost is recorded in the locked +entries and the corresponding complete-consumer budgets. Rolling path planning +remains confined to the opt-in motion renderer, whose complete SVG budget is +14.4 KiB gzip. + Every public transform family has an isolated budget and retained-input allowlist. Numeric and 2D bins may retain `d3-array`, and row stacks may retain `d3-shape`. Other transform entries reject those dependencies, while every diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json index 35b29cc5..ef410580 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": 39446, - "gzip": 15338 + "bytes": 43322, + "gzip": 16571 }, "D3-scale line + static SVG": { - "bytes": 42387, - "gzip": 16429 + "bytes": 47744, + "gzip": 18172 }, "Representative marks": { - "bytes": 58176, - "gzip": 21599 + "bytes": 63515, + "gzip": 23326 }, "TanStack DOM host": { - "bytes": 44461, - "gzip": 16246 + "bytes": 52231, + "gzip": 18829 }, "React adapter": { - "bytes": 46629, - "gzip": 16949 + "bytes": 54373, + "gzip": 19650 }, "React line consumer": { - "bytes": 69295, - "gzip": 26026 + "bytes": 77038, + "gzip": 28621 }, "Compact-scale line scene": { - "bytes": 21910, - "gzip": 8283 + "bytes": 25782, + "gzip": 9480 }, "React compact-scale line consumer": { - "bytes": 51805, - "gzip": 18924 + "bytes": 59548, + "gzip": 21511 }, "Custom-scale line scene": { - "bytes": 20094, - "gzip": 7544 + "bytes": 23966, + "gzip": 8751 }, "D3 linear-scale line scene": { - "bytes": 39378, - "gzip": 15301 + "bytes": 43254, + "gzip": 16536 } } } diff --git a/docs/concepts/layout-axes-and-coordinates.md b/docs/concepts/layout-axes-and-coordinates.md index ce2f4cc7..9241feec 100644 --- a/docs/concepts/layout-axes-and-coordinates.md +++ b/docs/concepts/layout-axes-and-coordinates.md @@ -225,6 +225,70 @@ For a normal cartesian chart: See [Scales and D3](./scales-and-d3.md) for the complete ownership boundary and pixel-to-value inversion. +## Continuous viewports + +A continuous axis can present one semantic window of a larger content domain: + +```ts +const x = { + scale: scaleUtc().domain([historyStart, historyEnd]), + viewport: { + domain: [visibleStart, visibleEnd], + translate: dragOffset, + }, +} +``` + +The configured or inferred scale domain describes the complete content. +`viewport.domain` is the committed semantic window used for mapping, axes, and +grid lines. `viewport.translate` is a transient output-space offset applied +after that mapping. + +Viewport ownership is resolved for each mark and each axis. A mark that +materializes an active viewport axis is content on that axis by default. The +compiler gives each such mark its own plot-bounded clip layer and applies only +the translations for axes it owns. Axes and grid lines stay fixed. Marks that +do not depend on the translated axis also stay fixed, such as a frame or a +y-only annotation during an x drag. Custom marks can override either axis as +`'content'` or `'fixed'` through `InitializedMark.viewport`. + +Marks, focus layers, interaction points, and tooltip anchors use the same +presented coordinates. `scene.points` retains every content point, including +off-window points, for rendering and diagnostics. The interaction host limits +pointer strategies and keyboard navigation to clipped content points whose +presented anchors are inside the plot clip. Points from marks with fixed +viewport ownership remain candidates outside the plot. +`viewportInteractionPoints(scene)` returns that subset without changing +`scene.points`. + +Translation is expressed in screen-direction scene pixels: positive x moves +content right, negative x moves it left, positive y moves it down, and negative +y moves it up. Domain order and `reverse` do not change those directions. + +This makes paged history one chart and one continuous line rather than a guide +chart overlaid with several plot charts. During a drag, keep the committed +domain fixed and update only `translate`. To settle one page, animate the +translation to one plot width, then update the semantic domain and reset the +translation to zero in the same application commit. + +Viewport domains accept two distinct finite numbers or two distinct finite +Dates. The scale must be configured or inferable, continuous, invertible, +unclamped, and independently accept domain and range assignment. Band, +ordinal, quantize, clamped, and getter-only scales are rejected. An authored +`axis.viewport` cannot be applied to an opaque custom `ChartScale`; a custom +resolver can instead return a complete `ResolvedScale.viewport` that it owns. +A logarithmic content domain and viewport domain must contain finite, nonzero +numbers and remain on the same side of zero. + +The resolved scale exposes both coordinate systems: + +```ts +const { contentDomain, domain, translate, map } = scene.scales.x.viewport! +``` + +`scene.scales.x.map(value)` is the committed, untranslated coordinate used to +construct geometry. `viewport.map(value)` returns its presented coordinate. + ## Non-cartesian coordinates Polar and geographic marks resolve geometry from the same final diff --git a/docs/framework/octane/adapter.md b/docs/framework/octane/adapter.md index e43766aa..b0e18db7 100644 --- a/docs/framework/octane/adapter.md +++ b/docs/framework/octane/adapter.md @@ -70,8 +70,9 @@ resource prefix from Octane's `useId()` when `idPrefix` is absent. `tabIndex` defaults to `0` on both targets. `keyboard: false` forces it to `-1`. -Pass `renderChartSvgWithResources` on both targets for gradients and clipping; -see [Rendering and export](../../reference/rendering-and-export.md#resource-aware-svg). +The default SVG renderer emits gradients and clipping on both targets. Custom +serializers must preserve the same resources; see +[Rendering and export](../../reference/rendering-and-export.md#svg-resources). ## Sizing and layout diff --git a/docs/framework/react/adapter.md b/docs/framework/react/adapter.md index d6eeabb9..8c8f9bbb 100644 --- a/docs/framework/react/adapter.md +++ b/docs/framework/react/adapter.md @@ -97,9 +97,9 @@ stable through hydration. `tabIndex` defaults to `0` on both server and client. `keyboard: false` forces it to `-1`. -For resource-aware gradients or clipping, pass the same renderer on both -server and client. See -[Rendering and export](../../reference/rendering-and-export.md#resource-aware-svg). +The default SVG renderer emits gradients and clipping on both server and +client. Custom serializers must preserve the same resources. See +[Rendering and export](../../reference/rendering-and-export.md#svg-resources). ## Sizing and layout diff --git a/docs/guides/custom-marks-and-renderers.md b/docs/guides/custom-marks-and-renderers.md index 06413984..ed0ee4fc 100644 --- a/docs/guides/custom-marks-and-renderers.md +++ b/docs/guides/custom-marks-and-renderers.md @@ -199,14 +199,20 @@ const host = mountChartRenderer(container, { ``` The renderer owns server shell markup, its mounted element, scene painting, -coordinate conversion, focus painting, and cleanup. The host retains sizing, -runtime, keyboard, tooltip, selection, and focus-strategy behavior. Keep -`prerender` deterministic and make `mount` adopt compatible server markup. +focus painting, and cleanup. It can implement `clientToScene` when controlled +pointer gestures need client-coordinate conversion; the interaction controller +returns `null` when that optional capability is absent. The host retains +sizing, runtime, keyboard, tooltip, selection, and focus-strategy behavior. +Keep `prerender` deterministic and make `mount` adopt compatible server markup. If `paintFocus` resolves and paints inline mark-state geometry, return that destination `ChartScene`. The host will use it for subsequent pointer hits; returning nothing preserves base-scene interaction for simpler renderers. +If the renderer animates point geometry, implement `getPresentationPoints` +and `subscribePresentationPoints`. This keeps stationary pointer focus, +keyboard focus, and tooltip anchors aligned with the painted frame. + Use `ChartRendererRenderContext.surface` instead of assuming `onRender` exposes an SVG element. Framework consumers pass `renderer` through `@tanstack/react-charts/core` or `@tanstack/octane-charts/core`. @@ -231,9 +237,9 @@ adapter. Preserve: - scoped IDs through `idPrefix`; - deterministic server output. -Use `renderChartSvgWithResources` from -`@tanstack/charts/svg/resources` when the only missing behavior is gradients or -clipping. +The default `renderChartSvg` already emits declared gradients and group clips. +The compatible `renderChartSvgWithResources` export remains available when an +explicit resource serializer name is useful. ## Custom focus and spatial indexes diff --git a/docs/guides/dynamic-data-and-animation.md b/docs/guides/dynamic-data-and-animation.md index 197169eb..44a02069 100644 --- a/docs/guides/dynamic-data-and-animation.md +++ b/docs/guides/dynamic-data-and-animation.md @@ -193,4 +193,37 @@ For high-rate data: 4. Keep viewport state controlled. 5. Coalesce upstream work when only the latest state matters. +For a scrolling trace, keep enough overscan before the visible x-domain to +cover the largest expected update batch, enable `clip`, and use a rolling path +contract: + +```ts +const definition = defineChart({ + motion: { + path: { + update: 'rolling', + x: 'shift', + y: 'reproject', + fallback: 'snap', + }, + transition: { type: 'tween', duration: sampleInterval, easing: 'linear' }, + }, + marks, +}) +``` + +The keyed retained window moves as one affine path. `y: 'reproject'` keeps that +motion valid while a continuous y-domain changes. A failed rolling invariant +snaps instead of occasionally becoming a different interpolation. Set +`fallback: 'morph'` only when path interpolation is intentional. A valid update +that arrives during another roll composes from the transform currently painted +on screen. + +Keep `viewport.translate` at zero on both the previous and target scene during +a rolling update. A nonzero transient viewport translation makes the rolling +contract fail and uses its configured fallback. Commit the viewport domain and +reset the translation before applying the next live-data window. Keep plot +margins fixed and prefer linear segments so appending a sample cannot recompute +a visible curve tangent. + The final definition passed to `host.update` is applied synchronously. diff --git a/docs/guides/interactions-and-selections.md b/docs/guides/interactions-and-selections.md index 2f3bcf88..b228277e 100644 --- a/docs/guides/interactions-and-selections.md +++ b/docs/guides/interactions-and-selections.md @@ -46,6 +46,58 @@ Every application-owned gesture follows the same loop: Do not mutate SVG geometry directly and then attempt to reconcile application state afterward. +## Controlled point inspection + +Use the chart's interaction controller when the application owns pointer +timing but still wants the definition's focus strategy, focus marks, and +tooltip. Long-press inspection is one example: + +```tsx +let interaction: ChartInteractionController | undefined + +const definition = defineChart({ + marks: [lineY(rows, { x: 'date', y: 'value', key: 'id' })], + x: { scale: scaleUtc() }, + y: { scale: scaleLinear() }, + focus: 'nearest-x', + pointer: false, + tooltip, +}) + +const chart = ( + { + interaction = context.interaction + }} + /> +) + +function inspect(clientX: number, clientY: number) { + interaction?.setControlledFocus(interaction.resolvePointer(clientX, clientY)) +} + +function stopInspecting() { + interaction?.setControlledFocus(null) +} +``` + +`resolvePointer` uses the current renderer presentation, including an active +motion or viewport transform, and returns the scene position, primary point, +and complete focus group. `setControlledFocus` paints the same definition-owned +focus and tooltip as native pointer input. Pass `{ pinned: true }` when the +configured sticky tooltip should accept interaction. + +For a drag that does not require a nearby datum, use +`interaction.clientToScene(clientX, clientY)`. It applies the renderer's full +client-to-scene transform without coupling viewport movement to point focus. + +`pointer: false` disables automatic pointer move, leave, and click handling. It +does not disable keyboard navigation. Controlled focus has separate ownership, +so unrelated mouse-leave and focus-out events cannot clear it. The stable +controller is available as `host.interaction` and in every `onRender` context. + ## Invert configured scales Copy the same configured D3 scale onto the resolved plot range: @@ -74,7 +126,7 @@ D3 ownership and official interaction-module links. ## Disable competing datum focus -When a gesture owns the chart surface, disable native focus explicitly: +When a gesture has no datum inspection at all, disable native focus explicitly: ```ts import { focusDisabled } from '@tanstack/charts/focus/disabled' @@ -91,9 +143,10 @@ mountChart(element, { }) ``` -This prevents a brush or free cursor from competing with the host's point -marker and tooltip. It does not remove keyboard accessibility from the -application-owned controls. +This prevents a brush from competing with the host's point marker and tooltip. +Use `pointer: false` plus the interaction controller when the application owns +the gesture but the chart should still own datum focus. `focusDisabled` does +not remove keyboard accessibility from application-owned controls. ## Brush selection diff --git a/docs/guides/themes-and-styling.md b/docs/guides/themes-and-styling.md index 358b1acf..fbf26f39 100644 --- a/docs/guides/themes-and-styling.md +++ b/docs/guides/themes-and-styling.md @@ -104,12 +104,9 @@ container CSS for palette variables, inherited color, and typography. ## Gradients and clipping -Gradients are opt-in SVG resources. Declare them on the chart and render with -the resource-aware SVG renderer: +Gradients are opt-in resources. Declare them on the chart: ```ts -import { renderChartSvgWithResources } from '@tanstack/charts/svg/resources' - const definition = defineChart({ marks, x, @@ -130,16 +127,16 @@ const definition = defineChart({ }) ``` -Use `url(#area-fill)` as the mark paint and pass -`renderSvg: renderChartSvgWithResources` to the host or adapter. `idPrefix` -scopes resource and clip IDs when several charts share a document. +Use `url(#area-fill)` as the mark paint. Default SVG hosts emit and scope the +resource; `idPrefix` keeps resource and clip IDs distinct when several charts +share a document. Set `clip: true` when marks should be clipped to the resolved plot rectangle. Clipping is a geometry policy, not a substitute for correct scale domains. -Canvas consumes the same declared gradients and group clips without the -resource-aware SVG serializer. A Canvas gradient needs measurable node bounds; -path-only geometry with no point bounds should use an explicit paint instead. +Canvas consumes the same declared gradients and group clips. A Canvas gradient +needs measurable node bounds; path-only geometry with no point bounds should +use an explicit paint instead.