A datum that renders as several leaves cannot be stated once: `states` on a group is never resolved
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- typescript
- Domain
- data-visualization
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.
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
- Resolve
stateson a group node. Ifnode.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. - An opt-out from the prefix-stripping ownership fallback — a way to say that
candles:5:bodyshould not inheritcandles: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.
- Dominant language
- TypeScript
- Stars
- 749
- Forks
- 45
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 26
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TanStack/charts
-
Difficulty 5/5 Over a week Newbie friendliness 45/100
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
-
Difficulty 5/5 Over a week Newbie friendliness 38/100
-
Difficulty 4/5 3-5 days Newbie friendliness 52/100
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
fullcalendar/fullcalendar#8106 ·