chart: Add appear motion to Plot and every chart - #3296
Merged
Merged
Conversation
Charts now draw their data in the first time they are painted: line, area, candlestick and sankey charts reveal from the left, bars grow out of the zero line one after another, a pie sweeps clockwise and a radar grows from its center. Axes, grids and labels are there from the first frame, and the tooltip waits until the data is whole. Base adds `PlotMotion::with_appear`, `PlotAppear` and the `Plot::appear` / `Plot::appear_generation` hooks, and splits `Plot::interactive` from `Plot::id` so a chart with `interactive(false)` keeps its id for the appear and its path caches. Charts take `appear(false)` and `appear_key(key)`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gallery is keyed on a generation the button bumps, so every chart gets fresh element state and draws in again. Release notes cover the chart appear motion and the `Plot::interactive` split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ECharts, Chart.js and Highcharts all default to about a second of drawing in, ApexCharts to 800 ms and Recharts to 1500 ms. The design system's enter curve is an exponential ease-out, nine-tenths done in the first quarter, which made the 500 ms appear read as a flash; data now grows on ECharts' `cubicInOut`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button joins `story_toolbar_group`, small and outlined like every other story's toolbar. The toolbar stays put while the gallery scrolls, so it holds the gap above the cards instead of the list's padding, and scrolled cards no longer run up against the button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bars grow out of the zero line together instead of one after another, the way Chart.js, Recharts and ApexCharts draw them, and the appear eases on Chart.js' default `easeOutQuart` over its 1000 ms. Lines and areas keep the reveal from the left that ECharts, Highcharts and Recharts use, which also keeps their cached paths whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges Every new `Plot` method has a default, so existing plots need no changes; the note belongs with the appear motion, not in the list of required migrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
madcodelife
enabled auto-merge (squash)
September 28, 2026 14:07
4 tasks
madcodelife
added a commit
that referenced
this pull request
Sep 29, 2026
…3308) ## Summary A plot's appear (#3296) lives in its element state. GPUI drops element state for anything not painted in a frame, so a plot inside a virtual list — `list`, `uniform_list`, a `List` row — forgets its appear once it scrolls past the overdraw and draws in again every time it comes back. The only workaround so far was `.appear(false)`, which gives up the appear entirely. `PlotAppearScope` wraps the region that repaints its plots on and off: ```rust PlotAppearScope::new(("transcript", conversation_id), list(state, render_row).flex_1()) ``` - A plot inside it that finished appearing is remembered by its global element id and appear generation. Painted again after a gap, it gets `PlotAppear::complete()` straight away: no `Presence` tracking and no frames, and hover is not held back. - A new `appear_generation` still replays, and a plot taken away mid-appear draws in again from the start. - The memory is the scope's own element state, so it lasts exactly while the scope is painted every frame. Closing the view, or giving the scope another id (name it after the content), draws its plots in afresh. - The innermost scope wins. The scope takes no part in layout: it hands on its child's `LayoutId`, so a self-sizing child such as `list(..).flex_1()` keeps working unchanged. - Without a scope, behavior is unchanged. It works for every `Plot`: the built-in charts and hand-written plots, since the check sits in `track_appear`, which `PlotElement` already calls. ## Public API ### `gpui-base` ```rust pub struct PlotAppearScope // remembers finished appears across remounts impl PlotAppearScope { pub fn new(id: impl Into<ElementId>, child: impl IntoElement) -> Self } impl IntoElement for PlotAppearScope impl Element for PlotAppearScope ``` Re-exported as `gpui_component::plot::PlotAppearScope`. ## Breaking Changes None. ## Test plan - [x] `cargo test -p gpui-base -p gpui-component` - [x] New tests in `plot::appear`: - A remount inside a scope after the appear finished is complete at once and asks for no frames. - A new generation still replays. - A remount before the appear finished replays. - A renamed scope, or one that stops being painted, forgets. - Without a scope, a remount replays as before. - [x] `cargo clippy -p gpui-base -p gpui-component --lib --tests -- --deny warnings` - [x] Docs: `website/base/plot.md`, `website/component/chart.md` (en + zh-CN), `release-notes.md` This is a `gpui-base` change: the appear state it extends already lives there. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Charts now draw their data in the first time they are painted, over 1000 ms on Chart.js' default
easeOutQuart. The mainstream defaults are ECharts, Chart.js and Highcharts at 1000 ms, ApexCharts at 800 ms and Recharts at 1500 ms. The design system's enter curve, an exponential ease-out, made the draw-in read as a flash, so the appear uses its own curve. Each effect follows what the mainstream libraries do (checked against their sources):animateRotate, Recharts); leader-line labels fade in at the endAxes, grid lines and tick labels are there from the first frame, and the tooltip waits until the data is whole. New data paints in place, so charts fed live quotes never replay. Reduced motion skips the appear.
The chart story gets a Replay button that re-keys the gallery so every chart draws in again.
Public API
gpui-basegpui-componentOn
LineChart,AreaChart,BarChart,PieChart,RadarChart,CandlestickChartandSankeyChart:The theme projects
PlotMotion::with_appear(1000 ms, easeOutQuart).Breaking Changes
None at the source level. The three new
Plotmethods all have defaults (interactive→true,appear→ no-op,appear_generation→None), so an existing hand-written plot compiles and behaves exactly as before: with an id it tracks hover as it did, and it tracks no appear and asks for no frames.PlotMotiongains a field behind a builder, and its default adds no motion.Behavior changes worth knowing:
.appear(false).interactive(false)charts now return an id.Plot::idon them isSome(it wasNone), andPlot::interactiveisfalse. They still get no hitbox, but they now keep element state under that id. Like interactive charts, siblings built at one site without.id(...)share the appear state and path caches. That stays correct, but it rebuilds geometry more often.Performance
Once the appear is done, it costs nothing extra: no mask, no frame requests, and
PlotAppearreturns early without sampling the easing. The per-frame state key is allocation-free, and the theme is borrowed rather than cloned.A chart with
.appear(false)(or a hand-written plot that doesn't opt in) tracks no state and requests no frames. Covered by tests.Timing harness (not committed) on 100 line + 100 bar + 50 pie charts, 200 points each,
--release, avg CPU per frame:mainNo regression. Per type during the appear: line 140 ms, bar 139 ms, pie 0.86 ms (vs 0.32 ms steady, from re-tessellating while sweeping).
Test plan
cargo test -p gpui-base -p gpui-component -p gpui-kitinteractive(false)keeps its idcargo clippy -p gpui-base -p gpui-component --lib --tests