Skip to main content

gpui_liveplot/
plot.rs

1//! Plot widget entry points and builders.
2//!
3//! A [`Plot`] owns axis configuration, view mode, and a set of series. All
4//! series in a plot share the same axes and transforms.
5
6use crate::axis::AxisConfig;
7use crate::interaction::Pin;
8use crate::series::Series;
9use crate::style::Theme;
10use crate::view::{Range, View, Viewport};
11
12/// Main plot widget container.
13///
14/// A plot is backend-agnostic and focuses on data, view state, and styling.
15/// Render backends (such as the GPUI backend) drive viewport refreshes and
16/// interaction state.
17#[derive(Debug, Clone)]
18pub struct Plot {
19    theme: Theme,
20    x_axis: AxisConfig,
21    y_axis: AxisConfig,
22    view: View,
23    viewport: Option<Viewport>,
24    series: Vec<Series>,
25    pins: Vec<Pin>,
26}
27
28impl Plot {
29    /// Create a plot with default configuration.
30    ///
31    /// Equivalent to `PlotBuilder::default().build()`.
32    pub fn new() -> Self {
33        Self {
34            theme: Theme::default(),
35            x_axis: AxisConfig::default(),
36            y_axis: AxisConfig::default(),
37            view: View::default(),
38            viewport: None,
39            series: Vec::new(),
40            pins: Vec::new(),
41        }
42    }
43
44    /// Start building a plot with custom configuration.
45    pub fn builder() -> PlotBuilder {
46        PlotBuilder::default()
47    }
48
49    /// Access the current theme.
50    pub fn theme(&self) -> &Theme {
51        &self.theme
52    }
53
54    /// Set the plot theme.
55    pub fn set_theme(&mut self, theme: Theme) {
56        self.theme = theme;
57    }
58
59    /// Access the X axis configuration.
60    pub fn x_axis(&self) -> &AxisConfig {
61        &self.x_axis
62    }
63
64    /// Access the Y axis configuration.
65    pub fn y_axis(&self) -> &AxisConfig {
66        &self.y_axis
67    }
68
69    /// Access the active view mode.
70    pub fn view(&self) -> View {
71        self.view
72    }
73
74    /// Access the current viewport.
75    ///
76    /// The viewport is computed by [`Plot::refresh_viewport`].
77    pub fn viewport(&self) -> Option<Viewport> {
78        self.viewport
79    }
80
81    /// Access all series.
82    pub fn series(&self) -> &[Series] {
83        &self.series
84    }
85
86    /// Access all series mutably.
87    ///
88    /// Returning the backing vector allows callers to add, remove, and reorder
89    /// series at runtime.
90    pub fn series_mut(&mut self) -> &mut Vec<Series> {
91        &mut self.series
92    }
93
94    /// Add a series to the plot.
95    ///
96    /// The plot stores a shared handle instead of taking unique ownership.
97    /// Appends made through other shared handles are visible immediately.
98    pub fn add_series(&mut self, series: &Series) {
99        self.series.push(series.share());
100    }
101
102    /// Access the pinned points.
103    pub fn pins(&self) -> &[Pin] {
104        &self.pins
105    }
106
107    /// Access the pinned points mutably.
108    pub fn pins_mut(&mut self) -> &mut Vec<Pin> {
109        &mut self.pins
110    }
111
112    /// Compute bounds across all visible series.
113    pub fn data_bounds(&self) -> Option<Viewport> {
114        let mut x_range: Option<Range> = None;
115        let mut y_range: Option<Range> = None;
116        for series in &self.series {
117            if !series.is_visible() {
118                continue;
119            }
120            if let Some(bounds) = series.bounds() {
121                x_range = Some(match x_range {
122                    None => bounds.x,
123                    Some(existing) => Range::union(existing, bounds.x)?,
124                });
125                y_range = Some(match y_range {
126                    None => bounds.y,
127                    Some(existing) => Range::union(existing, bounds.y)?,
128                });
129            }
130        }
131        match (x_range, y_range) {
132            (Some(x), Some(y)) => Some(Viewport::new(x, y)),
133            _ => None,
134        }
135    }
136
137    /// Enter manual view with the given viewport.
138    pub fn set_manual_view(&mut self, viewport: Viewport) {
139        self.view = View::Manual;
140        self.viewport = Some(viewport);
141    }
142
143    /// Reset to automatic view.
144    pub fn reset_view(&mut self) {
145        self.view = View::default();
146        self.viewport = None;
147    }
148
149    /// Refresh the viewport based on the current view mode and data.
150    ///
151    /// This updates the cached viewport and applies padding to avoid tight
152    /// bounds during auto-fit.
153    pub fn refresh_viewport(&mut self, padding_frac: f64, min_padding: f64) -> Option<Viewport> {
154        let bounds = self.data_bounds()?;
155        match self.view {
156            View::AutoAll { auto_x, auto_y } => {
157                let mut next = bounds;
158                if let Some(current) = self.viewport {
159                    if !auto_x {
160                        next.x = current.x;
161                    }
162                    if !auto_y {
163                        next.y = current.y;
164                    }
165                }
166                self.viewport = Some(next.padded(padding_frac, min_padding));
167            }
168            View::Manual => {
169                if self.viewport.is_none() {
170                    self.viewport = Some(bounds);
171                }
172            }
173            View::FollowLastN { points } => {
174                self.viewport = self.follow_last(points, false);
175            }
176            View::FollowLastNXY { points } => {
177                self.viewport = self.follow_last(points, true);
178            }
179        }
180        self.viewport
181    }
182
183    fn follow_last(&self, points: usize, follow_y: bool) -> Option<Viewport> {
184        let mut max_series: Option<&Series> = None;
185        let mut max_point: Option<crate::geom::Point> = None;
186        for series in &self.series {
187            if !series.is_visible() {
188                continue;
189            }
190            let last_point = series.with_store(|store| store.data().points().last().copied());
191            if let Some(point) = last_point
192                && max_point.is_none_or(|max| point.x > max.x)
193            {
194                max_point = Some(point);
195                max_series = Some(series);
196            }
197        }
198
199        let max_series = max_series?;
200        let max_point = max_point?;
201        let (len, start_point) = max_series.with_store(|store| {
202            let data = store.data();
203            let len = data.len();
204            let start_index = len.saturating_sub(points);
205            (len, data.point(start_index))
206        });
207        if len == 0 {
208            return None;
209        }
210        let start_point = start_point?;
211        let x_range = Range::new(start_point.x, max_point.x);
212
213        let y_range = if follow_y {
214            let mut y_range: Option<Range> = None;
215            for series in &self.series {
216                if !series.is_visible() {
217                    continue;
218                }
219                series.with_store(|store| {
220                    let series_data = store.data();
221                    let index_range = series_data.range_by_x(x_range);
222                    for index in index_range {
223                        if let Some(point) = series_data.point(index) {
224                            y_range = Some(match y_range {
225                                None => Range::new(point.y, point.y),
226                                Some(mut existing) => {
227                                    existing.expand_to_include(point.y);
228                                    existing
229                                }
230                            });
231                        }
232                    }
233                });
234            }
235            y_range?
236        } else if let Some(current) = self.viewport {
237            current.y
238        } else {
239            self.data_bounds()?.y
240        };
241
242        Some(Viewport::new(x_range, y_range))
243    }
244}
245
246impl Default for Plot {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Builder for configuring a plot before construction.
253///
254/// The builder captures theme, axes, view mode, and any initial series.
255#[derive(Debug, Default)]
256pub struct PlotBuilder {
257    theme: Theme,
258    x_axis: AxisConfig,
259    y_axis: AxisConfig,
260    view: View,
261    series: Vec<Series>,
262}
263
264impl PlotBuilder {
265    /// Set the theme used by the plot.
266    pub fn theme(mut self, theme: Theme) -> Self {
267        self.theme = theme;
268        self
269    }
270
271    /// Set the X axis configuration.
272    pub fn x_axis(mut self, axis: AxisConfig) -> Self {
273        self.x_axis = axis;
274        self
275    }
276
277    /// Set the Y axis configuration.
278    pub fn y_axis(mut self, axis: AxisConfig) -> Self {
279        self.y_axis = axis;
280        self
281    }
282
283    /// Set the initial view mode.
284    pub fn view(mut self, view: View) -> Self {
285        self.view = view;
286        self
287    }
288
289    /// Add a series to the plot.
290    ///
291    /// The builder stores a shared handle to the given series.
292    pub fn series(mut self, series: &Series) -> Self {
293        self.series.push(series.share());
294        self
295    }
296
297    /// Build the plot.
298    pub fn build(self) -> Plot {
299        Plot {
300            theme: self.theme,
301            x_axis: self.x_axis,
302            y_axis: self.y_axis,
303            view: self.view,
304            viewport: None,
305            series: self.series,
306            pins: Vec::new(),
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::series::Series;
315
316    #[test]
317    fn add_series_uses_shared_data_stream() {
318        let mut source = Series::line("shared");
319        let _ = source.extend_y([1.0, 2.0]);
320
321        let mut plot = Plot::new();
322        plot.add_series(&source);
323
324        let initial_bounds = plot.data_bounds().expect("plot bounds");
325        assert_eq!(initial_bounds.y.min, 1.0);
326        assert_eq!(initial_bounds.y.max, 2.0);
327
328        let _ = source.push_y(3.0);
329        let next_bounds = plot.data_bounds().expect("plot bounds");
330        assert_eq!(next_bounds.y.max, 3.0);
331    }
332
333    #[test]
334    fn series_mut_can_remove_series() {
335        let mut first = Series::line("first");
336        let mut second = Series::line("second");
337        let _ = first.push_y(1.0);
338        let _ = second.push_y(9.0);
339
340        let mut plot = Plot::new();
341        plot.add_series(&first);
342        plot.add_series(&second);
343
344        let removed = plot.series_mut().remove(1);
345        assert_eq!(removed.name(), "second");
346        assert_eq!(plot.series().len(), 1);
347        assert_eq!(plot.series()[0].name(), "first");
348    }
349}