Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/live-paths-shift.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
node_modules
.pnpm-store
dist
dist-live
dist-paged
.netlify/
.svelte-kit
.bundle-output
.benchmark-output
Expand Down
188 changes: 184 additions & 4 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ Each entry records:
| F-213 | Focused rules had no matchable presentation points | API | resolved |
| F-214 | Callback parameter shapes were inconsistent | API/Tooling | resolved |
| F-215 | Example keys collapsed distinct source rows | Application | resolved |
| F-216 | Rolling paths morphed samples instead of shifting them | API | resolved |
| F-217 | Motion ignored authored SVG clips | API | resolved |
| F-218 | Paged history required overlaid chart hosts | API | resolved |
| F-219 | Long-press focus duplicated host pointer geometry | API | resolved |
| F-220 | Focus cursor width depended on private band inference | API | resolved |

## Findings

Expand Down Expand Up @@ -5315,10 +5320,12 @@ Each entry records:
exact upstream protocols such as D3 threshold generators, and
consumer-called service methods as classified exceptions.
- Verification: the public callback inventory follows exported types,
functions, and values into nested package-owned types. It classifies all 527
reachable callable surfaces, including Alpine's external directive protocol
and Vue's nested tooltip slot, and rejects unclassified surfaces, callback
arity above two, or a non-object second callback argument. Failed parameter
functions, and values into nested package-owned types. It classifies all 534
reachable callable surfaces, including Alpine's external directive protocol,
Vue's nested tooltip slot, live-chart interaction and presentation service
handles, the presentation-points listener callback, and viewport mapping. It
rejects unclassified surfaces, callback arity above two, or a non-object
second callback argument. Failed parameter
type resolution preserves the parameter as a fail-closed non-object bag
instead of aborting or undercounting the inventory. Focused core, React
Native, React, Octane, channel, facet, focus, legend, tooltip, and contract
Expand Down Expand Up @@ -5351,3 +5358,176 @@ Each entry records:
many-point example retains 300 unique keys for their initial and revised
windows. Focused standard conformance passes each example's full 320/640/960
light/dark visual matrix.

### F-216 — 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-217 — 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-218 — 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-219 — 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-220 — 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. At the time, `ruleX` could paint the right geometry
but emitted 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.
- Follow-up: F-213 later added presentation-only focus points to rules, so a
focused `ruleX` can now express a one-pixel cursor too. Explicit band
dimensions remain useful when authored band geometry needs an exact scene-
pixel width or height.
13 changes: 10 additions & 3 deletions benchmarks/bundle-size/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 20 additions & 20 deletions benchmarks/bundle-size/universal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": 39569,
"gzip": 15382
"bytes": 43460,
"gzip": 16624
},
"D3-scale line + static SVG": {
"bytes": 42510,
"gzip": 16473
"bytes": 47882,
"gzip": 18219
},
"Representative marks": {
"bytes": 58666,
"gzip": 21703
"bytes": 64027,
"gzip": 23441
},
"TanStack DOM host": {
"bytes": 44879,
"gzip": 16403
"bytes": 52638,
"gzip": 18983
},
"React adapter": {
"bytes": 47104,
"gzip": 17127
"bytes": 54837,
"gzip": 19813
},
"React line consumer": {
"bytes": 69840,
"gzip": 26233
"bytes": 77572,
"gzip": 28813
},
"Compact-scale line scene": {
"bytes": 22033,
"gzip": 8322
"bytes": 25919,
"gzip": 9529
},
"React compact-scale line consumer": {
"bytes": 52348,
"gzip": 19127
"bytes": 60079,
"gzip": 21717
},
"Custom-scale line scene": {
"bytes": 20217,
"gzip": 7583
"bytes": 24103,
"gzip": 8801
},
"D3 linear-scale line scene": {
"bytes": 39501,
"gzip": 15346
"bytes": 43392,
"gzip": 16588
}
}
}
Loading