Skip to main content

shape_viz_core/
data.rs

1//! Data structures for generic time series visualization
2
3use crate::error::{ChartError, Result};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::any::Any;
7
8/// Represents a time range for chart data
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub struct TimeRange {
11    pub start: DateTime<Utc>,
12    pub end: DateTime<Utc>,
13}
14
15impl TimeRange {
16    pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Self> {
17        if start >= end {
18            return Err(ChartError::data_range("Start time must be before end time"));
19        }
20        Ok(Self { start, end })
21    }
22
23    pub fn duration(&self) -> chrono::Duration {
24        self.end - self.start
25    }
26
27    pub fn contains(&self, time: DateTime<Utc>) -> bool {
28        time >= self.start && time <= self.end
29    }
30
31    pub fn overlaps(&self, other: &TimeRange) -> bool {
32        self.start < other.end && other.start < self.end
33    }
34}
35
36/// Generic data series trait
37pub trait Series: std::fmt::Debug + Send + Sync {
38    fn name(&self) -> &str;
39    fn len(&self) -> usize;
40    fn is_empty(&self) -> bool {
41        self.len() == 0
42    }
43
44    /// Get X value at index (generic scalar, usually timestamp)
45    fn get_x(&self, index: usize) -> f64;
46
47    /// Get Y value at index (primary value, e.g. Close or Value)
48    fn get_y(&self, index: usize) -> f64;
49
50    /// Get min/max X values
51    fn get_x_range(&self) -> (f64, f64);
52
53    /// Get min/max Y values within X range
54    fn get_y_range(&self, x_min: f64, x_max: f64) -> (f64, f64);
55
56    /// Find closest index for a given X value
57    fn find_index(&self, x: f64) -> Option<usize>;
58
59    /// Downcasting
60    fn as_any(&self) -> &dyn Any;
61}
62
63/// Trait for series that support range data (start, max, min, end).
64/// Generic - can represent OHLC candles, temperature ranges, error bars, box plots, etc.
65pub trait RangeSeries: Series {
66    /// Get range values at index: (start, max, min, end)
67    /// - For candlesticks: (open, high, low, close)
68    /// - For temperature: (start_temp, max_temp, min_temp, end_temp)
69    /// - For error bars: (center, upper, lower, center)
70    fn get_range(&self, index: usize) -> (f64, f64, f64, f64);
71
72    /// Optional auxiliary value (volume, sample size, weight, etc.)
73    fn get_auxiliary(&self, index: usize) -> Option<f64> {
74        let _ = index;
75        None
76    }
77}
78
79/// Chart data wrapper that provides efficient access patterns for rendering
80#[derive(Debug)]
81pub struct ChartData {
82    /// The primary series (drives the x-axis)
83    pub main_series: Box<dyn Series>,
84    /// Additional series (indicators, overlays)
85    pub overlays: Vec<Box<dyn Series>>,
86
87    /// Cached visible range for efficient rendering
88    visible_range: Option<TimeRange>,
89    /// Cached Y bounds for the visible range
90    y_bounds: Option<(f64, f64)>,
91}
92
93impl ChartData {
94    /// Create new chart data from generic series
95    pub fn new(series: Box<dyn Series>) -> Self {
96        Self {
97            main_series: series,
98            overlays: Vec::new(),
99            visible_range: None,
100            y_bounds: None,
101        }
102    }
103
104    /// Get the symbol name
105    pub fn symbol(&self) -> &str {
106        self.main_series.name()
107    }
108
109    /// Get the number of points
110    pub fn len(&self) -> usize {
111        self.main_series.len()
112    }
113
114    /// Check if empty
115    pub fn is_empty(&self) -> bool {
116        self.main_series.is_empty()
117    }
118
119    /// Get time range for all data
120    pub fn time_range(&self) -> Option<TimeRange> {
121        if self.is_empty() {
122            return None;
123        }
124
125        let (min_x, max_x) = self.main_series.get_x_range();
126
127        // Assuming X is timestamp for now (compatibility)
128        let start = DateTime::from_timestamp(min_x as i64, 0)?;
129        let end = DateTime::from_timestamp(max_x as i64, 0)?;
130
131        TimeRange::new(start, end).ok()
132    }
133
134    /// Get Y bounds (min, max) for the visible range
135    pub fn y_bounds(&self) -> Option<(f64, f64)> {
136        if self.is_empty() {
137            return None;
138        }
139
140        // Use cached value if available
141        if let Some(bounds) = self.y_bounds {
142            return Some(bounds);
143        }
144
145        // Calculate generic bounds
146        let (min_x, max_x) = if let Some(range) = self.visible_range {
147            (range.start.timestamp() as f64, range.end.timestamp() as f64)
148        } else {
149            self.main_series.get_x_range()
150        };
151
152        Some(self.main_series.get_y_range(min_x, max_x))
153    }
154
155    /// Set the visible time range for efficient rendering
156    pub fn set_visible_range(&mut self, range: TimeRange) {
157        self.visible_range = Some(range);
158        self.y_bounds = None; // Invalidate cached Y bounds
159    }
160
161    /// Clear the visible range (show all data)
162    pub fn clear_visible_range(&mut self) {
163        self.visible_range = None;
164        self.y_bounds = None;
165    }
166
167    /// Get indices for the visible range
168    pub fn visible_indices(&self) -> Option<(usize, usize)> {
169        let range = self.visible_range?;
170        let start_ts = range.start.timestamp() as f64;
171        let end_ts = range.end.timestamp() as f64;
172
173        let start_idx = self.main_series.find_index(start_ts)?;
174        let end_idx = self.main_series.find_index(end_ts)?.min(self.len());
175
176        Some((start_idx, end_idx))
177    }
178}