TanStack / TanStack/charts

A datum that renders as several leaves cannot be stated once: `states` on a group is never resolved

Open
#135 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
749
Forks
45
Avg merge
1d 18h
Merged PRs (30d)
26

Description

Summary

A mark that emits several SVG leaves per datum (a candlestick's upper wick, lower
wick and body; anything with a similar internal composition) has no way to express
"this datum is dimmed" once. Every leaf resolves its own state and runs its own
tween. Wrapping the leaves in a group and putting states on the group does not
help, because mark-state.js never resolves state for a group node.

I am not reporting a rendering bug — the output is correct. The cost is per-hover
animation work, and the absence of any way for a consumer to opt out of it.

Version

@tanstack/charts@0.16.0

What happens

dist/mark-state.js:17, the single path used by the motion, SVG, canvas and native
renderers:

const state = node.kind === "group" ? node.states : void 0;
...
const resolved = node.kind !== "group" && nodeDefinitions && nodeData && candidates.length
  ? resolveNodeState(node, candidates, nodeData, nodeDefinitions, focus, pointer)
  : { node };

A group's states only supply definitions and points to its descendants; the group
itself falls through to { node }, unresolved. So a group cannot carry the dim.

The descendants then each resolve anyway, because ownership strips key prefixes —
sceneKeyOwnedPoints walks while (candidate.includes(":")) back through the last
: (dist/scene-point-ownership-internal.js:40-58), so candles:5:body matches
point candles:5. Regrouped or not, three leaves resolve three times and three
tweens run.

Why it matters

Measured on a 1000-candle candlestick, hovering to dim the non-hovered series:

dim latency
hand-written SVG (one <g opacity> per candle, CSS transition) 75.5 ms
same chart on @tanstack/charts (three stateful leaves per candle) 243 ms

~3.2×, on the same scene, same data, same 150 ms intended transition. The difference
is the number of state resolutions and WAAPI tweens: ~1000 against ~3000.

Both sides settle to the same visual result and the same number of visually-dimmed
elements (3009 vs 2999 — a group's opacity composes onto its leaves, so the rendered
element counts match). The gap is entirely the work done to get there.

What would help, in preference order
  1. Resolve states on a group node. If node.kind === "group" resolved like any
    other node, a consumer could wrap the leaves of one datum and dim them in one tween.
    The composed-opacity semantics already do the right thing at paint time.
  2. An opt-out from the prefix-stripping ownership fallback — a way to say that
    candles:5:body should not inherit candles:5's state, so a consumer can put
    the state exactly where they want it and nowhere else.

Either one is enough. (1) is the smaller change for a consumer to adopt.

What we are not asking for

We are not asking for the leaves to be merged or the mark to emit different SVG. The
composition is ours and it is correct.

Workaround, and why we rejected it

The only mechanism that gives one tween per datum is to bake style.opacity on a
per-candle <g> ourselves: drop the mark states, plumb hover through React state,
re-render, and hand-write a replacement fade for the 150 ms tween that dies with them.
That is a second dim implementation running alongside the engine's, with focus,
tooltip and reveal blast radius. We would rather wait for the engine to be able to
say it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in dist/mark-state.js, the shared path used by the motion, SVG, canvas, and native renderers, then read dist/scene-point-ownership-internal.js:40-58 to understand prefix ownership. Trace how group states and descendant states are resolved. Done means a grouped datum can express its state once without unnecessary per-leaf state resolutions and tweens, while preserving the existing rendered result.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
data-visualization
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.