Skip to main content

gpui_liveplot/
series.rs

1//! Data series configuration and storage.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, RwLock};
5
6use crate::datasource::{AppendError, AppendOnlyData, SeriesStore};
7use crate::geom::Point;
8use crate::render::{LineStyle, MarkerStyle};
9use crate::view::Viewport;
10
11static SERIES_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
12
13/// Unique identifier for a series.
14///
15/// Series IDs are stable within a process and are used to bind pins to
16/// specific series and point indices.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct SeriesId(u64);
19
20impl SeriesId {
21    fn next() -> Self {
22        Self(SERIES_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
23    }
24}
25
26/// Series rendering kind.
27///
28/// A series always has exactly one rendering kind.
29#[derive(Debug, Clone)]
30pub enum SeriesKind {
31    /// Line series with styling.
32    Line(LineStyle),
33    /// Scatter series with styling.
34    Scatter(MarkerStyle),
35}
36
37/// Plot series with data storage and styling.
38///
39/// Series own their data and provide append-only methods for streaming
40/// workloads. All axes and transforms are handled at the plot level.
41///
42/// Use [`Series::share`] when multiple plots should observe the same live
43/// append-only stream. By contrast, [`Clone`] creates an independent copy.
44#[derive(Debug)]
45pub struct Series {
46    id: SeriesId,
47    name: String,
48    kind: SeriesKind,
49    data: Arc<RwLock<SeriesStore>>,
50    visible: bool,
51}
52
53impl Series {
54    /// Create a line series with indexed data.
55    ///
56    /// Indexed data uses implicit X values (0, 1, 2, ...).
57    pub fn line(name: impl Into<String>) -> Self {
58        Self {
59            id: SeriesId::next(),
60            name: name.into(),
61            kind: SeriesKind::Line(LineStyle::default()),
62            data: Arc::new(RwLock::new(SeriesStore::indexed())),
63            visible: true,
64        }
65    }
66
67    /// Create a scatter series with indexed data.
68    ///
69    /// Indexed data uses implicit X values (0, 1, 2, ...).
70    pub fn scatter(name: impl Into<String>) -> Self {
71        Self {
72            id: SeriesId::next(),
73            name: name.into(),
74            kind: SeriesKind::Scatter(MarkerStyle::default()),
75            data: Arc::new(RwLock::new(SeriesStore::indexed())),
76            visible: true,
77        }
78    }
79
80    /// Create a series from existing append-only data.
81    pub(crate) fn with_data(
82        name: impl Into<String>,
83        data: AppendOnlyData,
84        kind: SeriesKind,
85    ) -> Self {
86        Self {
87            id: SeriesId::next(),
88            name: name.into(),
89            kind,
90            data: Arc::new(RwLock::new(SeriesStore::with_base_chunk(data, 64))),
91            visible: true,
92        }
93    }
94
95    /// Build a series from an iterator of Y values.
96    ///
97    /// X values are assigned as implicit indices.
98    pub fn from_iter_y<I, T>(name: impl Into<String>, iter: I, kind: SeriesKind) -> Self
99    where
100        I: IntoIterator<Item = T>,
101        T: Into<f64>,
102    {
103        let data = AppendOnlyData::from_iter_y(iter);
104        Self::with_data(name, data, kind)
105    }
106
107    /// Build a series from an iterator of points.
108    ///
109    /// X values are taken from each [`Point`](crate::geom::Point).
110    pub fn from_iter_points<I>(name: impl Into<String>, iter: I, kind: SeriesKind) -> Self
111    where
112        I: IntoIterator<Item = Point>,
113    {
114        let data = AppendOnlyData::from_iter_points(iter);
115        Self::with_data(name, data, kind)
116    }
117
118    /// Build a series by sampling a callback function.
119    ///
120    /// The callback is sampled uniformly across `x_range`.
121    pub fn from_explicit_callback(
122        name: impl Into<String>,
123        function: impl Fn(f64) -> f64,
124        x_range: crate::view::Range,
125        points: usize,
126        kind: SeriesKind,
127    ) -> Self {
128        let data = AppendOnlyData::from_explicit_callback(function, x_range, points);
129        Self::with_data(name, data, kind)
130    }
131
132    /// Access the series identifier.
133    pub fn id(&self) -> SeriesId {
134        self.id
135    }
136
137    /// Access the series name.
138    pub fn name(&self) -> &str {
139        &self.name
140    }
141
142    /// Access the series kind.
143    pub fn kind(&self) -> &SeriesKind {
144        &self.kind
145    }
146
147    /// Replace the series kind.
148    pub fn with_kind(mut self, kind: SeriesKind) -> Self {
149        self.kind = kind;
150        self
151    }
152
153    /// Create another series handle that shares the same append-only data.
154    ///
155    /// The returned series receives a new [`SeriesId`], so it can coexist with
156    /// the source series in the same plot. Data appends through either series
157    /// are immediately visible to all shared handles.
158    pub fn share(&self) -> Self {
159        Self {
160            id: SeriesId::next(),
161            name: self.name.clone(),
162            kind: self.kind.clone(),
163            data: Arc::clone(&self.data),
164            visible: self.visible,
165        }
166    }
167
168    /// Access the underlying series store.
169    pub(crate) fn with_store<R>(&self, f: impl FnOnce(&SeriesStore) -> R) -> R {
170        let data = self.data.read().expect("series data lock");
171        f(&data)
172    }
173
174    /// Append a Y value to an indexed series.
175    pub fn push_y(&mut self, y: f64) -> Result<usize, AppendError> {
176        self.with_store_mut(|data| data.push_y(y))
177    }
178
179    /// Append multiple Y values to an indexed series.
180    ///
181    /// Returns the number of appended points.
182    pub fn extend_y<I, T>(&mut self, values: I) -> Result<usize, AppendError>
183    where
184        I: IntoIterator<Item = T>,
185        T: Into<f64>,
186    {
187        self.with_store_mut(|data| data.extend_y(values))
188    }
189
190    /// Append a point to an explicit series.
191    pub fn push_point(&mut self, point: Point) -> Result<usize, AppendError> {
192        self.with_store_mut(|data| data.push_point(point))
193    }
194
195    /// Append multiple explicit points to a series.
196    ///
197    /// Returns the number of appended points when X values stay monotonic.
198    /// If any new point has a smaller X than the previous point, all points are
199    /// still appended and [`AppendError::NonMonotonicX`] is returned.
200    pub fn extend_points<I>(&mut self, points: I) -> Result<usize, AppendError>
201    where
202        I: IntoIterator<Item = Point>,
203    {
204        self.with_store_mut(|data| data.extend_points(points))
205    }
206
207    /// Access the series bounds.
208    pub fn bounds(&self) -> Option<Viewport> {
209        self.with_store(SeriesStore::bounds)
210    }
211
212    /// Access the series generation.
213    ///
214    /// This monotonically increasing value is used for render cache invalidation.
215    pub fn generation(&self) -> u64 {
216        self.with_store(SeriesStore::generation)
217    }
218
219    /// Check if the series is visible.
220    pub fn is_visible(&self) -> bool {
221        self.visible
222    }
223
224    /// Toggle series visibility.
225    pub fn set_visible(&mut self, visible: bool) {
226        self.visible = visible;
227    }
228
229    fn with_store_mut<R>(&self, f: impl FnOnce(&mut SeriesStore) -> R) -> R {
230        let mut data = self.data.write().expect("series data lock");
231        f(&mut data)
232    }
233}
234
235impl Clone for Series {
236    fn clone(&self) -> Self {
237        let data = self.data.read().expect("series data lock").clone();
238        Self {
239            id: self.id,
240            name: self.name.clone(),
241            kind: self.kind.clone(),
242            data: Arc::new(RwLock::new(data)),
243            visible: self.visible,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn share_observes_appends_from_source() {
254        let mut source = Series::line("shared");
255        let mut shared = source.share();
256
257        let _ = source.extend_y([1.0, 2.0, 3.0]);
258        assert_eq!(shared.generation(), 3);
259
260        let _ = shared.push_y(4.0);
261        assert_eq!(source.generation(), 4);
262        assert_eq!(source.bounds(), shared.bounds());
263    }
264
265    #[test]
266    fn clone_is_independent_copy() {
267        let mut source = Series::line("sensor");
268        let mut cloned = source.clone();
269
270        let _ = source.push_y(1.0);
271        assert_eq!(source.generation(), 1);
272        assert_eq!(cloned.generation(), 0);
273
274        let _ = cloned.push_y(2.0);
275        assert_eq!(source.generation(), 1);
276        assert_eq!(cloned.generation(), 1);
277    }
278}