embedded_charts/data/
bounds.rs

1//! Data bounds calculation and management for chart scaling.
2
3use crate::data::point::DataPoint;
4use crate::error::{DataError, DataResult};
5use crate::math::{Math, NumericConversion};
6
7/// Represents the bounds of a dataset in 2D space
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct DataBounds<X, Y>
10where
11    X: PartialOrd + Copy,
12    Y: PartialOrd + Copy,
13{
14    /// Minimum X value
15    pub min_x: X,
16    /// Maximum X value
17    pub max_x: X,
18    /// Minimum Y value
19    pub min_y: Y,
20    /// Maximum Y value
21    pub max_y: Y,
22}
23
24impl<X, Y> DataBounds<X, Y>
25where
26    X: PartialOrd + Copy,
27    Y: PartialOrd + Copy,
28{
29    /// Create new data bounds
30    pub fn new(min_x: X, max_x: X, min_y: Y, max_y: Y) -> DataResult<Self> {
31        if min_x > max_x || min_y > max_y {
32            return Err(DataError::INVALID_DATA_POINT);
33        }
34
35        Ok(Self {
36            min_x,
37            max_x,
38            min_y,
39            max_y,
40        })
41    }
42
43    /// Get the width of the X range
44    pub fn width(&self) -> X
45    where
46        X: core::ops::Sub<Output = X>,
47    {
48        self.max_x - self.min_x
49    }
50
51    /// Get the height of the Y range
52    pub fn height(&self) -> Y
53    where
54        Y: core::ops::Sub<Output = Y>,
55    {
56        self.max_y - self.min_y
57    }
58
59    /// Check if a point is within these bounds
60    pub fn contains<P>(&self, point: &P) -> bool
61    where
62        P: DataPoint<X = X, Y = Y>,
63    {
64        point.x() >= self.min_x
65            && point.x() <= self.max_x
66            && point.y() >= self.min_y
67            && point.y() <= self.max_y
68    }
69
70    /// Expand bounds to include a new point
71    pub fn expand_to_include<P>(&mut self, point: &P)
72    where
73        P: DataPoint<X = X, Y = Y>,
74    {
75        if point.x() < self.min_x {
76            self.min_x = point.x();
77        }
78        if point.x() > self.max_x {
79            self.max_x = point.x();
80        }
81        if point.y() < self.min_y {
82            self.min_y = point.y();
83        }
84        if point.y() > self.max_y {
85            self.max_y = point.y();
86        }
87    }
88
89    /// Merge with another bounds, creating a bounds that contains both
90    pub fn merge(&self, other: &Self) -> Self {
91        Self {
92            min_x: if self.min_x < other.min_x {
93                self.min_x
94            } else {
95                other.min_x
96            },
97            max_x: if self.max_x > other.max_x {
98                self.max_x
99            } else {
100                other.max_x
101            },
102            min_y: if self.min_y < other.min_y {
103                self.min_y
104            } else {
105                other.min_y
106            },
107            max_y: if self.max_y > other.max_y {
108                self.max_y
109            } else {
110                other.max_y
111            },
112        }
113    }
114}
115
116/// Specialized bounds for floating point data
117pub type FloatBounds = DataBounds<f32, f32>;
118
119/// Specialized bounds for integer data
120pub type IntBounds = DataBounds<i32, i32>;
121
122impl FloatBounds {
123    /// Create bounds with some padding around the data
124    pub fn with_padding(&self, padding_percent: f32) -> Self {
125        let x_padding = self.width() * padding_percent / 100.0;
126        let y_padding = self.height() * padding_percent / 100.0;
127
128        Self {
129            min_x: self.min_x - x_padding,
130            max_x: self.max_x + x_padding,
131            min_y: self.min_y - y_padding,
132            max_y: self.max_y + y_padding,
133        }
134    }
135
136    /// Create bounds that are nice for display (rounded to nice numbers)
137    pub fn nice_bounds(&self) -> Self {
138        fn nice_number(value: f32, round: bool) -> f32 {
139            if value == 0.0 {
140                return 0.0;
141            }
142
143            let value_num = value.to_number();
144            let abs_val = Math::abs(value_num);
145            let exp = Math::floor(Math::log10(abs_val));
146            let ten = 10.0f32.to_number();
147            let divisor = Math::pow(ten, exp);
148            let divisor_f32 = f32::from_number(divisor);
149            let f = value / divisor_f32;
150
151            let nice_f = if round {
152                if f < 1.5 {
153                    1.0
154                } else if f < 3.0 {
155                    2.0
156                } else if f < 7.0 {
157                    5.0
158                } else {
159                    10.0
160                }
161            } else if f <= 1.0 {
162                1.0
163            } else if f <= 2.0 {
164                2.0
165            } else if f <= 5.0 {
166                5.0
167            } else {
168                10.0
169            };
170
171            let _exp_f32 = f32::from_number(exp);
172            let ten_pow_exp = f32::from_number(Math::pow(10.0f32.to_number(), exp));
173            nice_f * ten_pow_exp
174        }
175
176        let x_range = self.width();
177        let y_range = self.height();
178
179        let nice_x_range = nice_number(x_range, false);
180        let nice_y_range = nice_number(y_range, false);
181
182        let x_center = (self.min_x + self.max_x) / 2.0;
183        let y_center = (self.min_y + self.max_y) / 2.0;
184
185        Self {
186            min_x: x_center - nice_x_range / 2.0,
187            max_x: x_center + nice_x_range / 2.0,
188            min_y: y_center - nice_y_range / 2.0,
189            max_y: y_center + nice_y_range / 2.0,
190        }
191    }
192}
193
194/// Calculate bounds for a collection of data points
195pub fn calculate_bounds<P, I>(points: I) -> DataResult<DataBounds<P::X, P::Y>>
196where
197    P: DataPoint,
198    P::X: PartialOrd + Copy,
199    P::Y: PartialOrd + Copy,
200    I: Iterator<Item = P>,
201{
202    let mut points_iter = points;
203
204    // Get the first point to initialize bounds
205    let first_point = points_iter.next().ok_or(DataError::INSUFFICIENT_DATA)?;
206
207    let mut bounds = DataBounds {
208        min_x: first_point.x(),
209        max_x: first_point.x(),
210        min_y: first_point.y(),
211        max_y: first_point.y(),
212    };
213
214    // Expand bounds to include all other points
215    for point in points_iter {
216        bounds.expand_to_include(&point);
217    }
218
219    Ok(bounds)
220}
221
222/// Calculate bounds for multiple data series
223pub fn calculate_multi_series_bounds<P, I, S>(series: S) -> DataResult<DataBounds<P::X, P::Y>>
224where
225    P: DataPoint,
226    P::X: PartialOrd + Copy,
227    P::Y: PartialOrd + Copy,
228    I: Iterator<Item = P>,
229    S: Iterator<Item = I>,
230{
231    let mut series_iter = series;
232
233    // Calculate bounds for the first series
234    let first_series = series_iter.next().ok_or(DataError::INSUFFICIENT_DATA)?;
235    let mut combined_bounds = calculate_bounds(first_series)?;
236
237    // Merge bounds from all other series
238    for series_data in series_iter {
239        let series_bounds = calculate_bounds(series_data)?;
240        combined_bounds = combined_bounds.merge(&series_bounds);
241    }
242
243    Ok(combined_bounds)
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::data::point::Point2D;
250
251    #[test]
252    fn test_bounds_creation() {
253        let bounds = DataBounds::new(0.0, 10.0, 0.0, 20.0).unwrap();
254        assert_eq!(bounds.min_x, 0.0);
255        assert_eq!(bounds.max_x, 10.0);
256        assert_eq!(bounds.min_y, 0.0);
257        assert_eq!(bounds.max_y, 20.0);
258    }
259
260    #[test]
261    fn test_invalid_bounds() {
262        let result = DataBounds::new(10.0, 0.0, 0.0, 20.0);
263        assert!(result.is_err());
264    }
265
266    #[test]
267    fn test_bounds_contains() {
268        let bounds = DataBounds::new(0.0, 10.0, 0.0, 20.0).unwrap();
269        let point = Point2D::new(5.0, 10.0);
270        assert!(bounds.contains(&point));
271
272        let outside_point = Point2D::new(15.0, 10.0);
273        assert!(!bounds.contains(&outside_point));
274    }
275
276    #[test]
277    fn test_bounds_expansion() {
278        let mut bounds = DataBounds::new(0.0, 10.0, 0.0, 20.0).unwrap();
279        let point = Point2D::new(15.0, 25.0);
280        bounds.expand_to_include(&point);
281
282        assert_eq!(bounds.max_x, 15.0);
283        assert_eq!(bounds.max_y, 25.0);
284    }
285
286    #[test]
287    fn test_calculate_bounds() {
288        let mut points = heapless::Vec::<Point2D, 8>::new();
289        points.push(Point2D::new(1.0, 2.0)).unwrap();
290        points.push(Point2D::new(5.0, 8.0)).unwrap();
291        points.push(Point2D::new(3.0, 4.0)).unwrap();
292
293        let bounds = calculate_bounds(points.into_iter()).unwrap();
294        assert_eq!(bounds.min_x, 1.0);
295        assert_eq!(bounds.max_x, 5.0);
296        assert_eq!(bounds.min_y, 2.0);
297        assert_eq!(bounds.max_y, 8.0);
298    }
299
300    #[test]
301    fn test_bounds_merge() {
302        let bounds1 = DataBounds::new(0.0, 5.0, 0.0, 10.0).unwrap();
303        let bounds2 = DataBounds::new(3.0, 8.0, 5.0, 15.0).unwrap();
304
305        let merged = bounds1.merge(&bounds2);
306        assert_eq!(merged.min_x, 0.0);
307        assert_eq!(merged.max_x, 8.0);
308        assert_eq!(merged.min_y, 0.0);
309        assert_eq!(merged.max_y, 15.0);
310    }
311}