1use crate::data::point::DataPoint;
4use crate::error::{DataError, DataResult};
5use crate::math::{Math, NumericConversion};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct DataBounds<X, Y>
10where
11 X: PartialOrd + Copy,
12 Y: PartialOrd + Copy,
13{
14 pub min_x: X,
16 pub max_x: X,
18 pub min_y: Y,
20 pub max_y: Y,
22}
23
24impl<X, Y> DataBounds<X, Y>
25where
26 X: PartialOrd + Copy,
27 Y: PartialOrd + Copy,
28{
29 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 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 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 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 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 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
116pub type FloatBounds = DataBounds<f32, f32>;
118
119pub type IntBounds = DataBounds<i32, i32>;
121
122impl FloatBounds {
123 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 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
194pub 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 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 for point in points_iter {
216 bounds.expand_to_include(&point);
217 }
218
219 Ok(bounds)
220}
221
222pub 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 let first_series = series_iter.next().ok_or(DataError::INSUFFICIENT_DATA)?;
235 let mut combined_bounds = calculate_bounds(first_series)?;
236
237 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}