Skip to content

fix: resize Vega explore charts in place instead of re-embedding on every frame - #9859

Merged
nishantmonu51 merged 3 commits into
mainfrom
measure_width
Sep 5, 2026
Merged

fix: resize Vega explore charts in place instead of re-embedding on every frame#9859
nishantmonu51 merged 3 commits into
mainfrom
measure_width

Conversation

@nishantmonu51

@nishantmonu51 nishantmonu51 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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:

  • Re-embed on every frame. svelte-vega stores the previous options in a Svelte $state proxy, so its === check on nested objects (config, tooltip, loader) never passes and its cheap view.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. VegaLiteRenderer now only includes the size in options when the options are (re)built, applies later size changes directly to the live view via view.width()/view.height() (vega-embed applies options.width the same way), and embeds only after the container is measured. A resize frame now takes ~70ms and keeps the existing canvases.
  • Bars vanish at high density. Vega-Lite's bar.binSpacing defaults to a fixed 1px gap between time-unit bars on top of the proportional width: { 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 sets binSpacing: 0 so 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:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

https://claude.ai/code/session_01G27n7BGbcXjbE3mquSJfim

…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 AdityaHegde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude's review,

  • svelte-vega's own cheap path is genuinely unreachable. VegaEmbed.svelte stores prevOptions in $state (deeply proxied), so shallowEqual(options, prevOptions, WIDTH_HEIGHT) can never pass for nested keys like config/tooltip/loader. Its internal view.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.
  • viewVL is 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
@nishantmonu51 nishantmonu51 added Type:Bug Something isn't working Area:Dashboard Size:M Medium change: 100-499 lines labels Sep 4, 2026
@nishantmonu51

Copy link
Copy Markdown
Collaborator Author

@AdityaHegde : Handled review comment.

@nishantmonu51
nishantmonu51 merged commit 293f53e into main Sep 5, 2026
11 checks passed
@nishantmonu51
nishantmonu51 deleted the measure_width branch September 5, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area:Dashboard Size:M Medium change: 100-499 lines Type:Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants