Skip to main content

damascene_core/plot/
series.rs

1//! The app-owned, versioned data handle a plot mark reads from, and the
2//! sample type it holds.
3//!
4//! A [`SeriesHandle`] is the plot analogue of a
5//! [`GeometryHandle`](crate::scene::GeometryHandle): the app creates it once,
6//! stores it in its state, clones it into the [`PlotSpec`](crate::plot::PlotSpec)
7//! every frame (a cheap `Arc` bump, never a data copy), and mutates it with
8//! [`set`](SeriesHandle::set) / [`append`](SeriesHandle::append). It carries
9//! the data in **data space** — raw `f64` `(x, y)` samples, so epoch
10//! timestamps keep their precision (the plan's decision 7) — and the GPU-side
11//! lowering to scale space happens downstream (see
12//! [`lower`](crate::plot::lower)).
13//!
14//! The handle is the single interface for both data paradigms (the plan's
15//! decision 5): a **virtual** app resamples its source to the visible window
16//! and `set`s a window's worth of points; a **dump-everything** app `set`s the
17//! whole series once and lets the plot decimate. Either way the mark just
18//! reads whatever the handle holds.
19
20#![warn(missing_docs)]
21
22use std::sync::Arc;
23use std::sync::Mutex;
24use std::sync::atomic::{AtomicU64, Ordering};
25
26use crate::scene::geometry::{GeometryId, next_geometry_id};
27
28/// One data point: an `(x, y)` pair in **data space** (`f64`). For a
29/// [`Scale::Time`](crate::plot::Scale::Time) axis, `x` is epoch seconds.
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct Sample {
32    /// Horizontal-axis value, in data space.
33    pub x: f64,
34    /// Vertical-axis value, in data space.
35    pub y: f64,
36}
37
38impl Sample {
39    /// A sample at `(x, y)`.
40    pub fn new(x: f64, y: f64) -> Self {
41        Self { x, y }
42    }
43}
44
45impl From<(f64, f64)> for Sample {
46    fn from((x, y): (f64, f64)) -> Self {
47        Self { x, y }
48    }
49}
50
51/// The data-space extent of a series: the `(min, max)` of each axis, or
52/// `None` per axis when there are no finite samples. Cached at mutation time
53/// and used to auto-frame the initial [`PlotView`](crate::plot::PlotView) and
54/// to size axis tick ranges.
55#[derive(Clone, Copy, Debug, PartialEq, Default)]
56pub struct SeriesBounds {
57    /// `(min, max)` of `x` over all finite samples, or `None` if none.
58    pub x: Option<(f64, f64)>,
59    /// `(min, max)` of `y` over all finite samples, or `None` if none.
60    pub y: Option<(f64, f64)>,
61}
62
63impl SeriesBounds {
64    /// The bounds of `samples`, ignoring non-finite values.
65    pub fn of(samples: &[Sample]) -> Self {
66        let mut x: Option<(f64, f64)> = None;
67        let mut y: Option<(f64, f64)> = None;
68        for s in samples {
69            if s.x.is_finite() {
70                x = Some(match x {
71                    Some((lo, hi)) => (lo.min(s.x), hi.max(s.x)),
72                    None => (s.x, s.x),
73                });
74            }
75            if s.y.is_finite() {
76                y = Some(match y {
77                    Some((lo, hi)) => (lo.min(s.y), hi.max(s.y)),
78                    None => (s.y, s.y),
79                });
80            }
81        }
82        Self { x, y }
83    }
84
85    /// Union with `other` (per axis).
86    pub fn union(self, other: SeriesBounds) -> SeriesBounds {
87        SeriesBounds {
88            x: union_axis(self.x, other.x),
89            y: union_axis(self.y, other.y),
90        }
91    }
92}
93
94fn union_axis(a: Option<(f64, f64)>, b: Option<(f64, f64)>) -> Option<(f64, f64)> {
95    match (a, b) {
96        (Some((alo, ahi)), Some((blo, bhi))) => Some((alo.min(blo), ahi.max(bhi))),
97        (some, None) | (None, some) => some,
98    }
99}
100
101/// Interior, shared by every clone of one [`SeriesHandle`].
102#[derive(Debug)]
103struct SeriesStore {
104    id: GeometryId,
105    samples: Mutex<Arc<Vec<Sample>>>,
106    bounds: Mutex<SeriesBounds>,
107    rev: AtomicU64,
108}
109
110/// App-owned, versioned series data. Create once, clone into the spec each
111/// frame (a cheap `Arc` bump), mutate with [`set`](Self::set) /
112/// [`append`](Self::append). The stable [`id`](Self::id) keys the GPU buffer
113/// cache for the geometry this series lowers to; [`revision`](Self::revision)
114/// advances on every mutation so the backend re-uploads only when the data
115/// actually changed.
116#[derive(Clone, Debug)]
117pub struct SeriesHandle {
118    inner: Arc<SeriesStore>,
119}
120
121impl SeriesHandle {
122    /// A handle owning `samples`, with a fresh stable id.
123    pub fn new(samples: impl Into<Vec<Sample>>) -> Self {
124        let samples = samples.into();
125        let bounds = SeriesBounds::of(&samples);
126        Self {
127            inner: Arc::new(SeriesStore {
128                id: next_geometry_id(),
129                samples: Mutex::new(Arc::new(samples)),
130                bounds: Mutex::new(bounds),
131                rev: AtomicU64::new(0),
132            }),
133        }
134    }
135
136    /// An empty handle.
137    pub fn empty() -> Self {
138        Self::new(Vec::new())
139    }
140
141    /// Replace the samples and advance the revision. The hot path for a
142    /// **virtual** app: as the user pans/zooms, resample the source to the
143    /// visible window and `set` the result.
144    pub fn set(&self, samples: impl Into<Vec<Sample>>) {
145        let samples = samples.into();
146        *self.inner.bounds.lock().unwrap() = SeriesBounds::of(&samples);
147        *self.inner.samples.lock().unwrap() = Arc::new(samples);
148        self.inner.rev.fetch_add(1, Ordering::Relaxed);
149    }
150
151    /// Append samples to the tail and advance the revision — for a live
152    /// series that grows over time. Recomputes bounds over the new tail only.
153    pub fn append(&self, more: &[Sample]) {
154        if more.is_empty() {
155            return;
156        }
157        {
158            let mut guard = self.inner.samples.lock().unwrap();
159            let mut v = guard.as_ref().clone();
160            v.extend_from_slice(more);
161            *guard = Arc::new(v);
162        }
163        {
164            let mut b = self.inner.bounds.lock().unwrap();
165            *b = b.union(SeriesBounds::of(more));
166        }
167        self.inner.rev.fetch_add(1, Ordering::Relaxed);
168    }
169
170    /// The stable id, constant for the handle's life. Backends key the GPU
171    /// buffers for this series' lowered geometry on it.
172    pub fn id(&self) -> GeometryId {
173        self.inner.id
174    }
175
176    /// The current revision; advances on every mutation.
177    pub fn revision(&self) -> u64 {
178        self.inner.rev.load(Ordering::Relaxed)
179    }
180
181    /// The cached data-space [`SeriesBounds`].
182    pub fn bounds(&self) -> SeriesBounds {
183        *self.inner.bounds.lock().unwrap()
184    }
185
186    /// The number of samples currently held.
187    pub fn len(&self) -> usize {
188        self.inner.samples.lock().unwrap().len()
189    }
190
191    /// Whether the series currently holds no samples.
192    pub fn is_empty(&self) -> bool {
193        self.len() == 0
194    }
195
196    /// A snapshot of the samples (a cheap `Arc` clone) plus the revision it
197    /// was taken at — what the lowering and backend read.
198    pub fn snapshot(&self) -> (Arc<Vec<Sample>>, u64) {
199        let rev = self.inner.rev.load(Ordering::Relaxed);
200        (Arc::clone(&self.inner.samples.lock().unwrap()), rev)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn bounds_ignore_non_finite() {
210        let b = SeriesBounds::of(&[
211            Sample::new(1.0, 10.0),
212            Sample::new(f64::NAN, 5.0),
213            Sample::new(3.0, f64::INFINITY),
214        ]);
215        assert_eq!(b.x, Some((1.0, 3.0)));
216        assert_eq!(b.y, Some((5.0, 10.0)));
217    }
218
219    #[test]
220    fn empty_bounds_are_none() {
221        let b = SeriesBounds::of(&[]);
222        assert_eq!(b.x, None);
223        assert_eq!(b.y, None);
224    }
225
226    #[test]
227    fn set_bumps_revision_and_recomputes_bounds() {
228        let h = SeriesHandle::new(vec![Sample::new(0.0, 0.0)]);
229        assert_eq!(h.revision(), 0);
230        assert_eq!(h.bounds().x, Some((0.0, 0.0)));
231        h.set(vec![Sample::new(-1.0, 2.0), Sample::new(4.0, 9.0)]);
232        assert_eq!(h.revision(), 1);
233        assert_eq!(h.bounds().x, Some((-1.0, 4.0)));
234        assert_eq!(h.bounds().y, Some((2.0, 9.0)));
235    }
236
237    #[test]
238    fn append_grows_and_unions_bounds() {
239        let h = SeriesHandle::new(vec![Sample::new(0.0, 0.0)]);
240        h.append(&[Sample::new(5.0, -3.0)]);
241        assert_eq!(h.len(), 2);
242        assert_eq!(h.revision(), 1);
243        assert_eq!(h.bounds().x, Some((0.0, 5.0)));
244        assert_eq!(h.bounds().y, Some((-3.0, 0.0)));
245        // appending nothing is a no-op (no revision bump)
246        h.append(&[]);
247        assert_eq!(h.revision(), 1);
248    }
249
250    #[test]
251    fn clones_share_storage_and_id() {
252        let a = SeriesHandle::new(vec![Sample::new(0.0, 0.0)]);
253        let b = a.clone();
254        assert_eq!(a.id(), b.id());
255        b.set(vec![Sample::new(1.0, 1.0), Sample::new(2.0, 2.0)]);
256        // the mutation is visible through the other clone
257        assert_eq!(a.len(), 2);
258        assert_eq!(a.revision(), 1);
259    }
260
261    #[test]
262    fn distinct_handles_have_distinct_ids() {
263        let a = SeriesHandle::new(Vec::new());
264        let b = SeriesHandle::new(Vec::new());
265        assert_ne!(a.id(), b.id());
266    }
267}