Ship animation identity planes only when the client can key-match - #330
Conversation
`match` defaults to "index", so `key=` combined with a bare `xy.animation(...)`, with `enabled=False`, or with no animation spec at all was putting two u32 identity columns into the payload that no client code reads — 8 B/row retained for the widget lifetime and 8 B/row on the wire. At 50k rows the payload was literally half dead key columns: 800,000 -> 400,000 bytes. Encoding still runs in every case. Duplicate-key and row-count errors are construction contract, not animation policy, and `_encode_transition _keys` materializes as part of validating, so the saving is retention and transport rather than CPU. The line-like geometry sort is skipped along with the planes. Payloads that do key-match are byte-identical — verified by hashing spec and blob across the merge base and this branch: match=key spec 0d46cee54989e43a blob 2e3a2ed38bad6748 (both) match=index blob 800000 -> 400000 bytes, retained 400000 -> 0 bare animation()/enabled=False/no animation: same as match=index This surfaced a separate pre-existing bug, filed as #329 and pinned by test_mark_animation_spec_clobbers_chart_level_fields: `Animation .to_spec()` emits every field, and both the Python merge and the client's `{...spec.animation, ...trace.animation}` are plain dict spreads, so a mark-level `xy.animation(duration=90)` resets `match`, `easing`, `enter`, `update`, and `interpolate` to defaults. That silently turns off a chart-level `match="key"` and suppresses the `match='key' requires key=` guard. It was invisible before this change because keys shipped regardless of whether the resolved spec could use them; test_aggregate_tier_records_key_matching_fallback was written in exactly that shape and now restates `match="key"` to express its intent. The two fixes compose: `key_matching` reads the same `effective` dict that #329 would correct, so fixing the merge restores plane shipping wherever key matching starts working again. Verified: 2574 passed / 4 skipped; ruff check and format clean.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughKey identity planes are now retained and transmitted only when the resolved animation performs key matching. Key validation remains active, while line-like key sorting is skipped for non-key-matching animations. Tests and documentation cover the updated behavior. ChangesAnimation key-plane retention
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
…ng it (#331) Fixes #329. `Animation.to_spec()` emits every field, and both the Python merge in `_attach_keys` and the client's `_resolvedAnimation` are plain dict spreads. So a mark-level `xy.animation(duration=90)` was a complete spec spread over the chart's, resetting `match`, `easing`, `enter`, `update`, and `interpolate` to their defaults. Chart-level `match="key"` silently became index matching — no error, no fallback string — and the `match='key' requires key=` guard stopped firing in that configuration because it reads the same clobbered dict. `animation()` now records the caller's exact argument names behind sentinel defaults, so "set to the default" is distinguishable from "not set": a mark that passes `match="index"` against a chart's `match="key"` still wins. `to_override_spec()` returns just those fields. Direct `Animation(...)` construction is public too and falls back to diffing the defaults, which is right except for that same explicit-default case. The cascade resolves once, in Python, over a defaults base: {**defaults, **chart_spec, **mark_override} and a trace that carries an override ships the complete resolved policy. Shipping the sparse override instead would break a mark-level animation on a chart with no chart-level one — the client would read `undefined` for duration and easing. Resolving here means the client's spread stays a no-op rather than a second merge needing its own copy of the defaults. A trace with no override still ships no animation dict, and a chart with neither ships none, which is what keeps an unanimated chart static. Verified with a 59-combination cascade matrix (6 chart specs x 10 mark overrides including bools and None), each replaying the client's exact spread and checking all 8 fields plus whether identity planes ship. Identity-plane shipping tracks the resolved `match`, so this composes with #330: key matching that starts working again also gets its planes back. Three tests asserted the old behavior as a literal and now assert the cascade. test_mark_animation_spec_clobbers_chart_level_fields, added in #330 to pin the bug, becomes the regression test for the fix. Verified: 2578 passed / 4 skipped; ruff check and format clean; ty at the 14-diagnostic baseline with an identical diagnostic set; public API verification OK.
The remaining item from the #319 review's perf-audit residual — but a different, more tractable half of it than the one I originally recorded.
What was wrong
matchdefaults to"index". Sokey=combined with a barexy.animation(...), withenabled=False, or with no animation spec at all was putting twou32identity columns into the payload that no client code reads.animation(match="key")animation(match="index")animation(duration=250)— match defaultedanimation(enabled=False)animation(...)at allAt 50k rows the payload was half dead key columns: 800,000 → 400,000 bytes, plus 400 KB retained for the widget lifetime.
What this does not change
Encoding still runs in every case. Duplicate-key and row-count errors are construction contract, not animation policy, and
_encode_transition_keysmaterializes as part of validating — so the saving is retention and transport, not CPU. Tests assert both error classes still fire in all three now-skipped shapes.Payloads that key-match are byte-identical. Verified by hashing spec and blob across the merge base and this branch:
It surfaced a worse bug — filed as #329
Animation.to_spec()emits every field, and both the Python merge and the client's{...spec.animation, ...trace.animation}are plain dict spreads. So a mark-levelxy.animation(duration=90)resetsmatch,easing,enter,update, andinterpolateto defaults:That silently turns off chart-level
match="key", and suppresses thematch='key' requires key=guard entirely. It was invisible before this change because keys shipped whether or not the resolved spec could use them.test_aggregate_tier_records_key_matching_fallbackwas written in exactly that shape and passed for that reason; it now restatesmatch="key"to express its intent, with a pointer to #329.test_mark_animation_spec_clobbers_chart_level_fieldspins the buggy behavior so a fix is deliberate rather than silent.The two compose safely.
key_matchingreads the sameeffectivedict #329 would correct, so fixing the merge automatically restores plane shipping wherever key matching starts working again.Verification
pytest tests/— 2574 passed, 4 skippedruff@0.15.8 check/format --checkcleananimation_fallbackall operate onmatch="key"payloads, so they are untouchedNot done, deliberately
The other half of the perf-audit residual — keys encoded for rows discarded above
MAX_ANIMATION_MATCH_ROWS— stays open. Finite-row filtering meansn_points > 200kdoes not guarantee the shipped count exceeds the limit, so it genuinely cannot be decided at attach time. Parallelizing the kernel would optimize exactly that discarded work, so it should wait on this being resolved.Summary by CodeRabbit
Bug Fixes
Documentation