fix: resize Vega explore charts in place instead of re-embedding on every frame - #9859
Merged
Conversation
…very frame With a dimension comparison active, the explore measure charts are rendered by Vega. Dragging the divider between the charts and the leaderboards made svelte-vega tear down and re-embed every view (a full Vega-Lite compile, parse and render) on each frame: it keeps the previous options in a Svelte `$state` proxy, so its `===` comparison of nested objects such as `config` and `tooltip` never passes and its cheap `view.width()` path is unreachable. With a YTD range at day grain and several dimension values, that was ~570ms per frame across three charts, which stalled the page and flashed blank or stale canvases. `VegaLiteRenderer` now only puts the size into `options` when the options are (re)built, and applies later size changes straight to the live view with `view.width()`/`view.height()`; vega-embed applies `options.width` the same way, so the initial embed is unchanged. The view is also embedded only after the container has been measured, which removes the extra embed at width 0 on mount. A resize frame now takes ~70ms and keeps the existing canvases. Claude-Session: https://claude.ai/code/session_01G27n7BGbcXjbE3mquSJfim
…visible
Vega-Lite's `bar.binSpacing` defaults to a 1px gap between bars on binned or
time-unit axes, in addition to the proportional gap from `width: { band: 0.9 }`.
At day grain over a year each bucket is ~1.2px wide, so the 1px gap left ~0.04px
of bar and the explore stacked-bar charts rendered as faint hairlines or nothing;
dragging the divider made bars appear and disappear as the bucket width crossed
the threshold.
Set `binSpacing: 0` in the shared Vega theme so only the band gap remains and it
scales with the bucket width. Bars are now ~1px at that density and low-density
charts keep a 10% gap.
Claude-Session: https://claude.ai/code/session_01G27n7BGbcXjbE3mquSJfim
AdityaHegde
reviewed
Sep 4, 2026
AdityaHegde
left a comment
Collaborator
There was a problem hiding this comment.
Claude's review,
- svelte-vega's own cheap path is genuinely unreachable.
VegaEmbed.sveltestoresprevOptionsin$state(deeply proxied), soshallowEqual(options, prevOptions, WIDTH_HEIGHT)can never pass for nested keys like config/tooltip/loader. Its internalview.width()branch never runs. The PR is right to stop relying on it. - The intent works. Generated code shows
$.legacy_pre_effect(() => ($.get(baseOptions), $.get(measured)), …)— width/height are not tracked, because they're read inside withEmbedSize. A resize no longer changes options identity, so no re-embed during a drag. viewVLis tracked in the new resize effect($.deep_read_state(viewVL())), so it correctly re-fires after each new embed.view.width()post-embed does stick. The specs use width: "container", which compiles to an initonly signal, and vega-dataflow's marshall() nulls _update after the first evaluation. vega-embed applies opts.width/opts.height the same way (embed.js:2836-2841), so the approach matches what the initial embed does.
The cost is, options now carries whatever size was current when baseOptions/measured last changed, so a re-embed caused by a spec change after a resize embeds at the stale size and then snaps — a visible flash plus a wasted layout pass. Previously the re-embed always used the current size. Reading width/height untracked at embed time (e.g. via untrack, or reading the live values inside the createView path rather than baking them into the memo) would keep the drag win without the regression.
Addresses review feedback on the in-place resize change: - `options` froze its width/height at the last rebuild, but svelte-vega still re-embeds from `options` whenever the spec changes, so a chart resized in place re-embedded at its pre-resize size. `applySize` now updates the options object's size fields through a local alias, which compiles to a plain member assignment and so does not invalidate `options` or trigger the re-embed this avoids. - Clamp height to 0: a container can report its width before its height, and `contentRect.height - 10` then gave Vega a negative height signal. - `createBaseEmbedOptions` no longer documents svelte-vega's cheap `view.width()` path as reachable; it is not, because svelte-vega keeps the previous options in a Svelte `$state` proxy. - Apply the same pattern to `VegaRenderer`, which still sized the view through `options` and would have reintroduced the per-frame re-embed. Claude-Session: https://claude.ai/code/session_013T32Ax4DBwYVPv6R7Cd7LM
Collaborator
Author
|
@AdityaHegde : Handled review comment. |
AdityaHegde
approved these changes
Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent problems made the Vega-rendered explore charts (any chart type with a dimension comparison, or a non-line chart type) misbehave when the divider between the charts and the leaderboards is dragged:
$stateproxy, so its===check on nested objects (config,tooltip,loader) never passes and its cheapview.width()path is never taken; every options change tore down and re-embedded the view. With YTD at day grain and several dimension values that was ~570ms per frame across three charts, which stalled the page and showed blank or stale clipped charts while dragging.VegaLiteRenderernow only includes the size inoptionswhen the options are (re)built, applies later size changes directly to the live view viaview.width()/view.height()(vega-embed appliesoptions.widththe same way), and embeds only after the container is measured. A resize frame now takes ~70ms and keeps the existing canvases.bar.binSpacingdefaults to a fixed 1px gap between time-unit bars on top of the proportionalwidth: { band: 0.9 }gap. At day grain over 12 months a bucket is ~1.2px wide, leaving ~0.04px of bar, so the charts rendered as faint hairlines or looked empty, and appeared or disappeared with the width. The shared Vega theme now setsbinSpacing: 0so only the band gap remains and it scales with the bucket.Verified on the dev project widened to ~50M rows across 12 months: explore stacked bars at 255px, 605px and 805px, canvas bar charts, and the chart export dialog.
Checklist:
https://claude.ai/code/session_01G27n7BGbcXjbE3mquSJfim