Skip to main content

astrelis_geometry/chart/
data.rs

1//! Data storage backends for chart series.
2//!
3//! This module provides efficient data storage options:
4//! - `Vec<DataPoint>` - Standard vector for static data
5//! - `RingBuffer<T>` - Fixed-capacity circular buffer for streaming data
6//! - `TimeSeries` - Specialized ring buffer for time-series data with OHLC support
7
8use super::types::DataPoint;
9
10/// Efficient ring buffer for streaming data.
11///
12/// A fixed-capacity circular buffer that overwrites the oldest elements
13/// when full. Provides O(1) push operations and maintains temporal ordering.
14///
15/// # Example
16///
17/// ```ignore
18/// let mut buffer = RingBuffer::<f64>::new(100);
19/// for i in 0..150 {
20///     buffer.push(i as f64);
21/// }
22/// // Buffer now contains 50..150, oldest data was overwritten
23/// assert_eq!(buffer.len(), 100);
24/// assert_eq!(buffer.get(0), Some(&50.0)); // Oldest remaining
25/// ```
26#[derive(Debug, Clone)]
27pub struct RingBuffer<T> {
28    /// Internal data storage
29    data: Vec<T>,
30    /// Maximum capacity
31    capacity: usize,
32    /// Current write position
33    write_pos: usize,
34    /// Current number of elements (capped at capacity)
35    len: usize,
36    /// Total number of elements ever written (for tracking)
37    total_written: u64,
38}
39
40impl<T: Clone + Default> RingBuffer<T> {
41    /// Create a new ring buffer with the specified capacity.
42    ///
43    /// # Panics
44    ///
45    /// Panics if capacity is 0.
46    pub fn new(capacity: usize) -> Self {
47        assert!(capacity > 0, "RingBuffer capacity must be > 0");
48        Self {
49            data: vec![T::default(); capacity],
50            capacity,
51            write_pos: 0,
52            len: 0,
53            total_written: 0,
54        }
55    }
56
57    /// Create a new ring buffer with initial data.
58    ///
59    /// If the data exceeds capacity, only the last `capacity` elements are kept.
60    pub fn from_vec(mut data: Vec<T>, capacity: usize) -> Self {
61        assert!(capacity > 0, "RingBuffer capacity must be > 0");
62
63        let total_written = data.len() as u64;
64
65        if data.len() > capacity {
66            // Keep only the last `capacity` elements
67            let excess = data.len() - capacity;
68            data.drain(..excess);
69        }
70
71        let len = data.len();
72        let write_pos = len % capacity;
73
74        // Pad to capacity
75        data.resize_with(capacity, T::default);
76
77        Self {
78            data,
79            capacity,
80            write_pos,
81            len,
82            total_written,
83        }
84    }
85
86    /// Push a single item to the buffer.
87    ///
88    /// O(1) operation. If the buffer is full, the oldest element is overwritten.
89    #[inline]
90    pub fn push(&mut self, item: T) {
91        self.data[self.write_pos] = item;
92        self.write_pos = (self.write_pos + 1) % self.capacity;
93        self.len = (self.len + 1).min(self.capacity);
94        self.total_written = self.total_written.wrapping_add(1);
95    }
96
97    /// Extend the buffer with multiple items.
98    ///
99    /// More efficient than calling `push` repeatedly.
100    pub fn extend<I: IntoIterator<Item = T>>(&mut self, items: I) {
101        for item in items {
102            self.push(item);
103        }
104    }
105
106    /// Get an item by logical index (0 = oldest item).
107    ///
108    /// Returns `None` if the index is out of bounds.
109    #[inline]
110    pub fn get(&self, index: usize) -> Option<&T> {
111        if index >= self.len {
112            return None;
113        }
114
115        // Calculate actual index in the circular buffer
116        let start = if self.len < self.capacity {
117            0
118        } else {
119            self.write_pos
120        };
121        let actual_idx = (start + index) % self.capacity;
122        Some(&self.data[actual_idx])
123    }
124
125    /// Get a mutable reference to an item by logical index.
126    #[inline]
127    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
128        if index >= self.len {
129            return None;
130        }
131
132        let start = if self.len < self.capacity {
133            0
134        } else {
135            self.write_pos
136        };
137        let actual_idx = (start + index) % self.capacity;
138        Some(&mut self.data[actual_idx])
139    }
140
141    /// Get the most recent item (newest).
142    #[inline]
143    pub fn last(&self) -> Option<&T> {
144        if self.len == 0 {
145            None
146        } else {
147            self.get(self.len - 1)
148        }
149    }
150
151    /// Get the oldest item.
152    #[inline]
153    pub fn first(&self) -> Option<&T> {
154        if self.len == 0 { None } else { self.get(0) }
155    }
156
157    /// Get the current number of elements.
158    #[inline]
159    pub fn len(&self) -> usize {
160        self.len
161    }
162
163    /// Check if the buffer is empty.
164    #[inline]
165    pub fn is_empty(&self) -> bool {
166        self.len == 0
167    }
168
169    /// Get the maximum capacity.
170    #[inline]
171    pub fn capacity(&self) -> usize {
172        self.capacity
173    }
174
175    /// Check if the buffer has wrapped (oldest data was overwritten).
176    #[inline]
177    pub fn has_wrapped(&self) -> bool {
178        self.total_written > self.capacity as u64
179    }
180
181    /// Get the total number of items ever written.
182    #[inline]
183    pub fn total_written(&self) -> u64 {
184        self.total_written
185    }
186
187    /// Check if the buffer is full.
188    #[inline]
189    pub fn is_full(&self) -> bool {
190        self.len == self.capacity
191    }
192
193    /// Clear all elements from the buffer.
194    pub fn clear(&mut self) {
195        self.len = 0;
196        self.write_pos = 0;
197        // Don't reset total_written - useful for tracking
198    }
199
200    /// Iterate over elements in order (oldest to newest).
201    pub fn iter(&self) -> RingBufferIter<'_, T> {
202        RingBufferIter {
203            buffer: self,
204            index: 0,
205        }
206    }
207
208    /// Get the data as one or two contiguous slices.
209    ///
210    /// This is useful for efficient GPU uploads. If the buffer hasn't wrapped,
211    /// returns a single slice. If wrapped, returns two slices that together
212    /// contain all data in order (oldest to newest).
213    pub fn as_slices(&self) -> (&[T], Option<&[T]>) {
214        if self.len == 0 {
215            return (&[], None);
216        }
217
218        if self.len < self.capacity || self.write_pos == 0 {
219            // Haven't wrapped yet, or write position is at start
220            (&self.data[..self.len], None)
221        } else {
222            // Wrapped: data from write_pos to end, then from 0 to write_pos
223            let first = &self.data[self.write_pos..];
224            let second = &self.data[..self.write_pos];
225            (first, Some(second))
226        }
227    }
228
229    /// Get a contiguous slice containing all data (may allocate).
230    ///
231    /// If the buffer hasn't wrapped, this returns a reference to the internal slice.
232    /// Otherwise, it allocates and returns a new Vec.
233    pub fn to_vec(&self) -> Vec<T> {
234        let (first, second) = self.as_slices();
235        if let Some(second) = second {
236            let mut result = Vec::with_capacity(self.len);
237            result.extend_from_slice(first);
238            result.extend_from_slice(second);
239            result
240        } else {
241            first.to_vec()
242        }
243    }
244}
245
246impl<T> Default for RingBuffer<T>
247where
248    T: Clone + Default,
249{
250    fn default() -> Self {
251        Self::new(1024)
252    }
253}
254
255/// Iterator over ring buffer elements.
256pub struct RingBufferIter<'a, T> {
257    buffer: &'a RingBuffer<T>,
258    index: usize,
259}
260
261impl<'a, T: Clone + Default> Iterator for RingBufferIter<'a, T> {
262    type Item = &'a T;
263
264    fn next(&mut self) -> Option<Self::Item> {
265        if self.index >= self.buffer.len {
266            None
267        } else {
268            let item = self.buffer.get(self.index);
269            self.index += 1;
270            item
271        }
272    }
273
274    fn size_hint(&self) -> (usize, Option<usize>) {
275        let remaining = self.buffer.len - self.index;
276        (remaining, Some(remaining))
277    }
278}
279
280impl<T: Clone + Default> ExactSizeIterator for RingBufferIter<'_, T> {}
281
282/// Time series point with optional OHLC (Open-High-Low-Close) data.
283///
284/// Used for downsampled or aggregated time series data where multiple
285/// samples are combined into a single point.
286#[derive(Debug, Clone, Copy, PartialEq, Default)]
287pub struct TimeSeriesPoint {
288    /// Timestamp (typically in seconds or milliseconds)
289    pub time: f64,
290    /// Primary value (close price, average, etc.)
291    pub value: f64,
292    /// Optional minimum value in the aggregation period
293    pub min: Option<f64>,
294    /// Optional maximum value in the aggregation period
295    pub max: Option<f64>,
296    /// Optional count of samples aggregated into this point
297    pub count: Option<u32>,
298}
299
300impl TimeSeriesPoint {
301    /// Create a simple time series point.
302    pub fn new(time: f64, value: f64) -> Self {
303        Self {
304            time,
305            value,
306            min: None,
307            max: None,
308            count: None,
309        }
310    }
311
312    /// Create a point with min/max range.
313    pub fn with_range(time: f64, value: f64, min: f64, max: f64) -> Self {
314        Self {
315            time,
316            value,
317            min: Some(min),
318            max: Some(max),
319            count: None,
320        }
321    }
322
323    /// Create a fully aggregated point.
324    pub fn aggregated(time: f64, value: f64, min: f64, max: f64, count: u32) -> Self {
325        Self {
326            time,
327            value,
328            min: Some(min),
329            max: Some(max),
330            count: Some(count),
331        }
332    }
333
334    /// Convert to a simple DataPoint (ignores OHLC data).
335    pub fn to_data_point(&self) -> DataPoint {
336        DataPoint::new(self.time, self.value)
337    }
338}
339
340impl From<TimeSeriesPoint> for DataPoint {
341    fn from(point: TimeSeriesPoint) -> Self {
342        DataPoint::new(point.time, point.value)
343    }
344}
345
346impl From<(f64, f64)> for TimeSeriesPoint {
347    fn from((time, value): (f64, f64)) -> Self {
348        Self::new(time, value)
349    }
350}
351
352/// Data storage backend for chart series.
353///
354/// Provides multiple storage options optimized for different use cases:
355/// - `Vec` - Standard vector for static or infrequently updated data
356/// - `Ring` - Ring buffer for high-frequency streaming data
357/// - `TimeSeries` - Ring buffer with OHLC support for time series
358/// - `Downsampled` - Downsampled view of another data source
359#[derive(Debug, Clone)]
360pub enum SeriesData {
361    /// Standard vector storage.
362    Vec(Vec<DataPoint>),
363    /// Ring buffer for streaming data.
364    Ring(RingBuffer<DataPoint>),
365    /// Time series with OHLC support.
366    TimeSeries(RingBuffer<TimeSeriesPoint>),
367    /// Downsampled view of source data.
368    Downsampled {
369        /// Source data (boxed to prevent infinite size)
370        source: Box<SeriesData>,
371        /// Downsampling factor
372        factor: usize,
373        /// Cached downsampled data
374        cache: Vec<DataPoint>,
375        /// Version of source data when cache was built
376        source_version: u64,
377    },
378}
379
380impl Default for SeriesData {
381    fn default() -> Self {
382        Self::Vec(Vec::new())
383    }
384}
385
386impl SeriesData {
387    /// Create from a vector of data points.
388    pub fn from_vec(data: Vec<DataPoint>) -> Self {
389        Self::Vec(data)
390    }
391
392    /// Create a ring buffer with the specified capacity.
393    pub fn ring(capacity: usize) -> Self {
394        Self::Ring(RingBuffer::new(capacity))
395    }
396
397    /// Create a time series ring buffer with the specified capacity.
398    pub fn time_series(capacity: usize) -> Self {
399        Self::TimeSeries(RingBuffer::new(capacity))
400    }
401
402    /// Get the number of data points.
403    pub fn len(&self) -> usize {
404        match self {
405            Self::Vec(v) => v.len(),
406            Self::Ring(r) => r.len(),
407            Self::TimeSeries(r) => r.len(),
408            Self::Downsampled { cache, .. } => cache.len(),
409        }
410    }
411
412    /// Check if the data is empty.
413    pub fn is_empty(&self) -> bool {
414        self.len() == 0
415    }
416
417    /// Get a data point by index.
418    pub fn get(&self, index: usize) -> Option<DataPoint> {
419        match self {
420            Self::Vec(v) => v.get(index).copied(),
421            Self::Ring(r) => r.get(index).copied(),
422            Self::TimeSeries(r) => r.get(index).map(|p| p.to_data_point()),
423            Self::Downsampled { cache, .. } => cache.get(index).copied(),
424        }
425    }
426
427    /// Get the first (oldest) data point.
428    pub fn first(&self) -> Option<DataPoint> {
429        self.get(0)
430    }
431
432    /// Get the last (newest) data point.
433    pub fn last(&self) -> Option<DataPoint> {
434        if self.is_empty() {
435            None
436        } else {
437            self.get(self.len() - 1)
438        }
439    }
440
441    /// Push a data point (only works for Vec and Ring variants).
442    pub fn push(&mut self, point: DataPoint) {
443        match self {
444            Self::Vec(v) => v.push(point),
445            Self::Ring(r) => r.push(point),
446            Self::TimeSeries(r) => r.push(TimeSeriesPoint::new(point.x, point.y)),
447            Self::Downsampled { .. } => {
448                // Cannot push to downsampled - push to source instead
449            }
450        }
451    }
452
453    /// Push a time series point (only for TimeSeries variant).
454    pub fn push_time_point(&mut self, point: TimeSeriesPoint) {
455        if let Self::TimeSeries(r) = self {
456            r.push(point);
457        }
458    }
459
460    /// Clear all data.
461    pub fn clear(&mut self) {
462        match self {
463            Self::Vec(v) => v.clear(),
464            Self::Ring(r) => r.clear(),
465            Self::TimeSeries(r) => r.clear(),
466            Self::Downsampled { cache, .. } => cache.clear(),
467        }
468    }
469
470    /// Iterate over data points.
471    pub fn iter(&self) -> SeriesDataIter<'_> {
472        SeriesDataIter {
473            data: self,
474            index: 0,
475            len: self.len(),
476        }
477    }
478
479    /// Get the X range (min, max) of the data.
480    pub fn x_range(&self) -> Option<(f64, f64)> {
481        if self.is_empty() {
482            return None;
483        }
484
485        let mut min = f64::INFINITY;
486        let mut max = f64::NEG_INFINITY;
487
488        for point in self.iter() {
489            min = min.min(point.x);
490            max = max.max(point.x);
491        }
492
493        Some((min, max))
494    }
495
496    /// Get the Y range (min, max) of the data.
497    pub fn y_range(&self) -> Option<(f64, f64)> {
498        if self.is_empty() {
499            return None;
500        }
501
502        let mut min = f64::INFINITY;
503        let mut max = f64::NEG_INFINITY;
504
505        for point in self.iter() {
506            min = min.min(point.y);
507            max = max.max(point.y);
508        }
509
510        Some((min, max))
511    }
512
513    /// Get the bounds (min_x, min_y, max_x, max_y) of the data.
514    pub fn bounds(&self) -> Option<(DataPoint, DataPoint)> {
515        if self.is_empty() {
516            return None;
517        }
518
519        let mut min = DataPoint::new(f64::INFINITY, f64::INFINITY);
520        let mut max = DataPoint::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
521
522        for point in self.iter() {
523            min.x = min.x.min(point.x);
524            min.y = min.y.min(point.y);
525            max.x = max.x.max(point.x);
526            max.y = max.y.max(point.y);
527        }
528
529        Some((min, max))
530    }
531
532    /// Check if this is a ring buffer variant.
533    pub fn is_ring(&self) -> bool {
534        matches!(self, Self::Ring(_) | Self::TimeSeries(_))
535    }
536
537    /// Check if the ring buffer has wrapped (data was overwritten).
538    pub fn has_wrapped(&self) -> bool {
539        match self {
540            Self::Ring(r) => r.has_wrapped(),
541            Self::TimeSeries(r) => r.has_wrapped(),
542            _ => false,
543        }
544    }
545
546    /// Get the capacity (for ring buffers) or current length (for Vec).
547    pub fn capacity(&self) -> usize {
548        match self {
549            Self::Vec(v) => v.capacity(),
550            Self::Ring(r) => r.capacity(),
551            Self::TimeSeries(r) => r.capacity(),
552            Self::Downsampled { source, .. } => source.capacity(),
553        }
554    }
555
556    /// Convert to a Vec of DataPoints.
557    pub fn to_vec(&self) -> Vec<DataPoint> {
558        self.iter().collect()
559    }
560}
561
562/// Iterator over SeriesData.
563pub struct SeriesDataIter<'a> {
564    data: &'a SeriesData,
565    index: usize,
566    len: usize,
567}
568
569impl Iterator for SeriesDataIter<'_> {
570    type Item = DataPoint;
571
572    fn next(&mut self) -> Option<Self::Item> {
573        if self.index >= self.len {
574            None
575        } else {
576            let item = self.data.get(self.index);
577            self.index += 1;
578            item
579        }
580    }
581
582    fn size_hint(&self) -> (usize, Option<usize>) {
583        let remaining = self.len - self.index;
584        (remaining, Some(remaining))
585    }
586}
587
588impl ExactSizeIterator for SeriesDataIter<'_> {}
589
590/// LTTB (Largest Triangle Three Buckets) downsampling algorithm.
591///
592/// Preserves visual features while reducing data density for rendering.
593/// Returns indices of points to keep.
594pub fn lttb_downsample(data: &[DataPoint], target_count: usize) -> Vec<usize> {
595    let n = data.len();
596
597    if n <= target_count {
598        return (0..n).collect();
599    }
600
601    if target_count < 3 {
602        if target_count == 0 {
603            return vec![];
604        } else if target_count == 1 {
605            return vec![0];
606        } else {
607            return vec![0, n - 1];
608        }
609    }
610
611    let mut result = Vec::with_capacity(target_count);
612
613    // Always include first point
614    result.push(0);
615
616    // Bucket size (excluding first and last points)
617    let bucket_size = (n - 2) as f64 / (target_count - 2) as f64;
618
619    let mut a = 0usize; // Previous selected point
620
621    for i in 0..(target_count - 2) {
622        // Calculate bucket boundaries
623        let bucket_start = ((i as f64 * bucket_size) as usize + 1).min(n - 1);
624        let bucket_end = (((i + 1) as f64 * bucket_size) as usize + 1).min(n - 1);
625
626        // Calculate average point in next bucket (for triangle area calculation)
627        let next_bucket_start = bucket_end;
628        let next_bucket_end = (((i + 2) as f64 * bucket_size) as usize + 1).min(n);
629
630        let mut avg_x = 0.0;
631        let mut avg_y = 0.0;
632        let count = next_bucket_end - next_bucket_start;
633
634        if count > 0 {
635            for point in &data[next_bucket_start..next_bucket_end] {
636                avg_x += point.x;
637                avg_y += point.y;
638            }
639            avg_x /= count as f64;
640            avg_y /= count as f64;
641        }
642
643        // Find point in current bucket that creates largest triangle
644        let mut max_area = 0.0;
645        let mut max_area_idx = bucket_start;
646
647        let point_a = &data[a];
648
649        for (offset, point) in data[bucket_start..bucket_end].iter().enumerate() {
650            // Calculate triangle area using cross product
651            let area = ((point_a.x - avg_x) * (point.y - point_a.y)
652                - (point_a.x - point.x) * (avg_y - point_a.y))
653                .abs();
654
655            if area > max_area {
656                max_area = area;
657                max_area_idx = bucket_start + offset;
658            }
659        }
660
661        result.push(max_area_idx);
662        a = max_area_idx;
663    }
664
665    // Always include last point
666    result.push(n - 1);
667
668    result
669}
670
671/// Downsample data using LTTB algorithm.
672pub fn downsample_data(data: &[DataPoint], target_count: usize) -> Vec<DataPoint> {
673    let indices = lttb_downsample(data, target_count);
674    indices.into_iter().map(|i| data[i]).collect()
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    #[test]
682    fn test_ring_buffer_basic() {
683        let mut buffer = RingBuffer::<i32>::new(5);
684
685        // Push fewer than capacity
686        buffer.push(1);
687        buffer.push(2);
688        buffer.push(3);
689
690        assert_eq!(buffer.len(), 3);
691        assert!(!buffer.has_wrapped());
692        assert_eq!(buffer.get(0), Some(&1));
693        assert_eq!(buffer.get(2), Some(&3));
694    }
695
696    #[test]
697    fn test_ring_buffer_wrap() {
698        let mut buffer = RingBuffer::<i32>::new(3);
699
700        // Push more than capacity
701        for i in 0..5 {
702            buffer.push(i);
703        }
704
705        assert_eq!(buffer.len(), 3);
706        assert!(buffer.has_wrapped());
707        assert_eq!(buffer.get(0), Some(&2)); // Oldest remaining
708        assert_eq!(buffer.get(1), Some(&3));
709        assert_eq!(buffer.get(2), Some(&4)); // Newest
710    }
711
712    #[test]
713    fn test_ring_buffer_iter() {
714        let mut buffer = RingBuffer::<i32>::new(4);
715
716        for i in 0..6 {
717            buffer.push(i);
718        }
719
720        let items: Vec<_> = buffer.iter().copied().collect();
721        assert_eq!(items, vec![2, 3, 4, 5]);
722    }
723
724    #[test]
725    fn test_ring_buffer_slices() {
726        let mut buffer = RingBuffer::<i32>::new(4);
727
728        // Before wrap
729        buffer.push(1);
730        buffer.push(2);
731        let (first, second) = buffer.as_slices();
732        assert_eq!(first, &[1, 2]);
733        assert!(second.is_none());
734
735        // After wrap
736        buffer.push(3);
737        buffer.push(4);
738        buffer.push(5);
739        buffer.push(6);
740        let (first, second) = buffer.as_slices();
741        // Should contain 3, 4, 5, 6 in order
742        assert_eq!(first.len() + second.map_or(0, |s| s.len()), 4);
743    }
744
745    #[test]
746    fn test_series_data_vec() {
747        let mut data =
748            SeriesData::from_vec(vec![DataPoint::new(0.0, 1.0), DataPoint::new(1.0, 2.0)]);
749
750        assert_eq!(data.len(), 2);
751        data.push(DataPoint::new(2.0, 3.0));
752        assert_eq!(data.len(), 3);
753        assert_eq!(data.get(2), Some(DataPoint::new(2.0, 3.0)));
754    }
755
756    #[test]
757    fn test_series_data_ring() {
758        let mut data = SeriesData::ring(3);
759
760        for i in 0..5 {
761            data.push(DataPoint::new(i as f64, i as f64 * 2.0));
762        }
763
764        assert_eq!(data.len(), 3);
765        assert!(data.has_wrapped());
766        assert_eq!(data.first(), Some(DataPoint::new(2.0, 4.0)));
767    }
768
769    #[test]
770    fn test_lttb_downsample() {
771        // Create a simple dataset
772        let data: Vec<DataPoint> = (0..100)
773            .map(|i| DataPoint::new(i as f64, (i as f64 * 0.1).sin()))
774            .collect();
775
776        let downsampled = downsample_data(&data, 20);
777
778        assert_eq!(downsampled.len(), 20);
779        // First and last points should be preserved
780        assert_eq!(downsampled[0], data[0]);
781        assert_eq!(downsampled[19], data[99]);
782    }
783}