1use super::Orientation;
2use crate::Bounds;
3
4#[derive(Debug, Clone)]
13pub struct ProgressDimensions {
14 bounds: Bounds,
16 orientation: Orientation,
18 progress: f64,
20 data_range: (f64, f64),
22}
23
24impl ProgressDimensions {
25 pub fn new(bounds: Bounds, orientation: Orientation) -> Self {
27 Self {
28 bounds,
29 orientation,
30 progress: 0.0,
31 data_range: (0.0, 1.0),
32 }
33 }
34
35 pub fn set_progress(&mut self, progress: f64) {
37 self.progress = progress.clamp(0.0, 1.0);
38 }
39
40 pub fn set_data_range(&mut self, min: f64, max: f64) {
42 self.data_range = (min, max);
43 }
44
45 pub fn calculate_fill_size(&self) -> usize {
48 let total_size = match self.orientation {
49 Orientation::Horizontal => self.bounds.width(),
50 Orientation::Vertical => self.bounds.height(),
51 };
52
53 if total_size == 0 {
54 return 0;
55 }
56
57 ((total_size as f64) * self.progress).round() as usize
58 }
59
60 pub fn get_fill_bounds(&self) -> Bounds {
62 let fill_size = self.calculate_fill_size();
63
64 match self.orientation {
65 Orientation::Horizontal => {
66 Bounds::new(
68 self.bounds.x1,
69 self.bounds.y1,
70 self.bounds.x1 + fill_size.saturating_sub(1),
71 self.bounds.y2,
72 )
73 }
74 Orientation::Vertical => {
75 Bounds::new(
77 self.bounds.x1,
78 self.bounds.y2.saturating_sub(fill_size.saturating_sub(1)),
79 self.bounds.x2,
80 self.bounds.y2,
81 )
82 }
83 }
84 }
85
86 pub fn get_empty_bounds(&self) -> Option<Bounds> {
88 let fill_size = self.calculate_fill_size();
89 let total_size = match self.orientation {
90 Orientation::Horizontal => self.bounds.width(),
91 Orientation::Vertical => self.bounds.height(),
92 };
93
94 if fill_size >= total_size {
95 return None; }
97
98 match self.orientation {
99 Orientation::Horizontal => {
100 Some(Bounds::new(
102 self.bounds.x1 + fill_size,
103 self.bounds.y1,
104 self.bounds.x2,
105 self.bounds.y2,
106 ))
107 }
108 Orientation::Vertical => {
109 Some(Bounds::new(
111 self.bounds.x1,
112 self.bounds.y1,
113 self.bounds.x2,
114 self.bounds.y2.saturating_sub(fill_size),
115 ))
116 }
117 }
118 }
119
120 pub fn map_value_to_pixel(&self, value: f64) -> usize {
123 let (min_val, max_val) = self.data_range;
124 let range = max_val - min_val;
125
126 if range <= 0.0 {
127 return match self.orientation {
128 Orientation::Horizontal => self.bounds.x1,
129 Orientation::Vertical => self.bounds.y1,
130 };
131 }
132
133 let normalized = ((value - min_val) / range).clamp(0.0, 1.0);
134 let available_size = match self.orientation {
135 Orientation::Horizontal => self.bounds.width(),
136 Orientation::Vertical => self.bounds.height(),
137 };
138
139 let pixel_offset = (normalized * (available_size - 1) as f64).floor() as usize;
140
141 match self.orientation {
142 Orientation::Horizontal => self.bounds.x1 + pixel_offset,
143 Orientation::Vertical => {
144 self.bounds.y2 - pixel_offset
146 }
147 }
148 }
149
150 pub fn map_pixel_to_value(&self, pixel: usize) -> f64 {
153 let (min_val, max_val) = self.data_range;
154 let range = max_val - min_val;
155
156 let relative_pixel = match self.orientation {
157 Orientation::Horizontal => pixel.saturating_sub(self.bounds.x1),
158 Orientation::Vertical => {
159 self.bounds.y2.saturating_sub(pixel)
161 }
162 };
163
164 let available_size = match self.orientation {
165 Orientation::Horizontal => self.bounds.width(),
166 Orientation::Vertical => self.bounds.height(),
167 };
168
169 if available_size <= 1 {
170 return min_val;
171 }
172
173 let normalized = (relative_pixel as f64) / ((available_size - 1) as f64);
174 min_val + (normalized * range)
175 }
176
177 pub fn calculate_segments(&self, segment_count: usize) -> Vec<ProgressSegment> {
179 if segment_count == 0 {
180 return vec![];
181 }
182
183 let total_size = match self.orientation {
184 Orientation::Horizontal => self.bounds.width(),
185 Orientation::Vertical => self.bounds.height(),
186 };
187
188 let segment_size = total_size / segment_count;
189 let remainder = total_size % segment_count;
190 let mut segments = Vec::new();
191
192 let mut current_pos = match self.orientation {
193 Orientation::Horizontal => self.bounds.x1,
194 Orientation::Vertical => self.bounds.y1,
195 };
196
197 for i in 0..segment_count {
198 let this_segment_size = if i < remainder {
200 segment_size + 1
201 } else {
202 segment_size
203 };
204
205 let filled = (self.progress * segment_count as f64) >= (i + 1) as f64;
206 let partial_fill = if filled {
207 1.0 } else {
209 let segment_progress = (self.progress * segment_count as f64) - i as f64;
211 if segment_progress > 0.0 {
212 segment_progress.min(1.0)
213 } else {
214 0.0
215 }
216 };
217
218 let segment_bounds = match self.orientation {
219 Orientation::Horizontal => Bounds::new(
220 current_pos,
221 self.bounds.y1,
222 current_pos + this_segment_size - 1,
223 self.bounds.y2,
224 ),
225 Orientation::Vertical => Bounds::new(
226 self.bounds.x1,
227 current_pos,
228 self.bounds.x2,
229 current_pos + this_segment_size - 1,
230 ),
231 };
232
233 segments.push(ProgressSegment {
234 bounds: segment_bounds,
235 fill_ratio: partial_fill,
236 is_complete: filled,
237 segment_index: i,
238 });
239
240 current_pos += this_segment_size;
241 }
242
243 segments
244 }
245
246 pub fn calculate_axis_ticks(&self, tick_count: usize) -> Vec<AxisTick> {
249 if tick_count <= 1 {
250 return vec![];
251 }
252
253 let (min_val, max_val) = self.data_range;
254 let value_step = (max_val - min_val) / (tick_count - 1) as f64;
255 let mut ticks = Vec::new();
256
257 for i in 0..tick_count {
258 let value = min_val + (i as f64 * value_step);
259 let pixel = self.map_value_to_pixel(value);
260
261 ticks.push(AxisTick {
262 value,
263 pixel_position: pixel,
264 label: format!("{:.1}", value),
265 is_major: i % 5 == 0, });
267 }
268
269 ticks
270 }
271
272 pub fn calculate_data_points(&self, data: &[f64]) -> Vec<DataPoint> {
274 if data.is_empty() {
275 return vec![];
276 }
277
278 let mut points = Vec::new();
279 let x_step = if data.len() <= 1 {
280 0.0
281 } else {
282 (self.bounds.width() - 1) as f64 / (data.len() - 1) as f64
283 };
284
285 for (i, &value) in data.iter().enumerate() {
286 let x = self.bounds.x1 + (i as f64 * x_step).round() as usize;
287 let y = self.map_value_to_pixel(value);
288
289 points.push(DataPoint {
290 value,
291 x,
292 y,
293 index: i,
294 });
295 }
296
297 points
298 }
299
300 pub fn calculate_bars(&self, data: &[f64]) -> Vec<ChartBar> {
302 if data.is_empty() {
303 return vec![];
304 }
305
306 let bar_count = data.len();
307 let total_width = self.bounds.width();
308 let bar_width = total_width / bar_count;
309 let remainder = total_width % bar_count;
310
311 let mut bars = Vec::new();
312 let mut current_x = self.bounds.x1;
313
314 for (i, &value) in data.iter().enumerate() {
315 let this_bar_width = if i < remainder {
317 bar_width + 1
318 } else {
319 bar_width
320 };
321
322 let bar_height_pixels = self.calculate_bar_height(value);
323 let bar_top_y = self
324 .bounds
325 .y2
326 .saturating_sub(bar_height_pixels.saturating_sub(1));
327
328 let bar_bounds = Bounds::new(
329 current_x,
330 bar_top_y,
331 current_x + this_bar_width - 1,
332 self.bounds.y2,
333 );
334
335 bars.push(ChartBar {
336 value,
337 bounds: bar_bounds,
338 height_pixels: bar_height_pixels,
339 index: i,
340 });
341
342 current_x += this_bar_width;
343 }
344
345 bars
346 }
347
348 fn calculate_bar_height(&self, value: f64) -> usize {
350 let (min_val, max_val) = self.data_range;
351 let range = max_val - min_val;
352 let available_height = self.bounds.height();
353
354 if range <= 0.0 || available_height == 0 {
355 return if value > min_val { available_height } else { 0 };
356 }
357
358 let normalized = ((value - min_val) / range).clamp(0.0, 1.0);
359 (normalized * available_height as f64).round() as usize
360 }
361
362 pub fn calculate_sparkline(&self, data: &[f64]) -> Vec<SparklinePoint> {
364 if data.is_empty() {
365 return vec![];
366 }
367
368 let width = self.bounds.width();
370 let mut points = Vec::new();
371
372 if data.len() <= width {
373 let x_step = if data.len() == 1 {
375 0.0
376 } else {
377 (width - 1) as f64 / (data.len() - 1) as f64
378 };
379
380 for (i, &value) in data.iter().enumerate() {
381 let x = (i as f64 * x_step).round() as usize;
382 let normalized_y = self.map_value_to_pixel(value);
383
384 points.push(SparklinePoint {
385 x: self.bounds.x1 + x,
386 y: normalized_y,
387 value,
388 data_index: i,
389 });
390 }
391
392 if data.len() > 1 {
394 for x_pos in 0..width {
395 let needs_point = !points.iter().any(|p| p.x == self.bounds.x1 + x_pos);
396 if needs_point {
397 let data_pos =
399 (x_pos as f64 / (width - 1) as f64) * (data.len() - 1) as f64;
400 let data_index = data_pos.floor() as usize;
401 let next_index = (data_index + 1).min(data.len() - 1);
402
403 let frac = data_pos - data_index as f64;
404 let value = data[data_index] + frac * (data[next_index] - data[data_index]);
405 let normalized_y = self.map_value_to_pixel(value);
406
407 points.push(SparklinePoint {
408 x: self.bounds.x1 + x_pos,
409 y: normalized_y,
410 value,
411 data_index,
412 });
413 }
414 }
415 }
416 } else {
417 for x_pos in 0..width {
419 let data_pos = (x_pos as f64 / (width - 1) as f64) * (data.len() - 1) as f64;
420 let data_index = data_pos.round() as usize;
421 let value = data[data_index];
422 let normalized_y = self.map_value_to_pixel(value);
423
424 points.push(SparklinePoint {
425 x: self.bounds.x1 + x_pos,
426 y: normalized_y,
427 value,
428 data_index,
429 });
430 }
431 }
432
433 points.sort_by_key(|p| p.x);
435 points
436 }
437
438 pub fn validate(&self) -> Result<(), ProgressDimensionError> {
440 if self.bounds.x1 == self.bounds.x2 && self.bounds.y1 == self.bounds.y2 {
442 return Err(ProgressDimensionError::ZeroDimensions);
443 }
444
445 let (min_val, max_val) = self.data_range;
446 if min_val >= max_val {
447 return Err(ProgressDimensionError::InvalidDataRange {
448 min: min_val,
449 max: max_val,
450 });
451 }
452
453 Ok(())
454 }
455}
456
457#[derive(Debug, Clone, PartialEq)]
458pub struct ProgressSegment {
459 pub bounds: Bounds,
460 pub fill_ratio: f64, pub is_complete: bool,
462 pub segment_index: usize,
463}
464
465#[derive(Debug, Clone, PartialEq)]
466pub struct AxisTick {
467 pub value: f64,
468 pub pixel_position: usize,
469 pub label: String,
470 pub is_major: bool,
471}
472
473#[derive(Debug, Clone, PartialEq)]
474pub struct DataPoint {
475 pub value: f64,
476 pub x: usize,
477 pub y: usize,
478 pub index: usize,
479}
480
481#[derive(Debug, Clone, PartialEq)]
482pub struct ChartBar {
483 pub value: f64,
484 pub bounds: Bounds,
485 pub height_pixels: usize,
486 pub index: usize,
487}
488
489#[derive(Debug, Clone, PartialEq)]
490pub struct SparklinePoint {
491 pub x: usize,
492 pub y: usize,
493 pub value: f64,
494 pub data_index: usize,
495}
496
497#[derive(Debug, Clone, PartialEq)]
498pub enum ProgressDimensionError {
499 ZeroDimensions,
500 InvalidDataRange { min: f64, max: f64 },
501 InvalidProgress { value: f64 },
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn test_fill_size_calculation() {
510 let bounds = Bounds::new(0, 0, 19, 5); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
512
513 progress_dims.set_progress(0.5); let fill_size = progress_dims.calculate_fill_size();
515 assert_eq!(fill_size, 10); progress_dims.set_progress(0.75); let fill_size = progress_dims.calculate_fill_size();
519 assert_eq!(fill_size, 15); }
521
522 #[test]
523 fn test_fill_bounds() {
524 let bounds = Bounds::new(5, 5, 14, 7); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
526
527 progress_dims.set_progress(0.6); let fill_bounds = progress_dims.get_fill_bounds();
529
530 assert_eq!(fill_bounds.x1, 5); assert_eq!(fill_bounds.x2, 10); assert_eq!(fill_bounds.y1, 5);
533 assert_eq!(fill_bounds.y2, 7);
534 }
535
536 #[test]
537 fn test_empty_bounds() {
538 let bounds = Bounds::new(0, 0, 9, 3); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
540
541 progress_dims.set_progress(0.3); let empty_bounds = progress_dims.get_empty_bounds().unwrap();
543
544 assert_eq!(empty_bounds.x1, 3); assert_eq!(empty_bounds.x2, 9); progress_dims.set_progress(1.0);
549 assert!(progress_dims.get_empty_bounds().is_none());
550 }
551
552 #[test]
553 fn test_value_to_pixel_mapping() {
554 let bounds = Bounds::new(0, 0, 99, 19); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
556 progress_dims.set_data_range(0.0, 100.0); let pixel = progress_dims.map_value_to_pixel(50.0);
560 assert_eq!(pixel, 49); let pixel = progress_dims.map_value_to_pixel(0.0);
564 assert_eq!(pixel, 0);
565
566 let pixel = progress_dims.map_value_to_pixel(100.0);
568 assert_eq!(pixel, 99);
569 }
570
571 #[test]
572 fn test_pixel_to_value_mapping() {
573 let bounds = Bounds::new(0, 0, 99, 19); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
575 progress_dims.set_data_range(0.0, 100.0);
576
577 let value = progress_dims.map_pixel_to_value(49);
579 assert!((value - 49.5).abs() < 1.0); let value = progress_dims.map_pixel_to_value(0);
583 assert!((value - 0.0).abs() < 0.1);
584
585 let value = progress_dims.map_pixel_to_value(99);
587 assert!((value - 100.0).abs() < 0.1);
588 }
589
590 #[test]
591 fn test_segments_calculation() {
592 let bounds = Bounds::new(0, 0, 9, 2); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
594 progress_dims.set_progress(0.35); let segments = progress_dims.calculate_segments(5); assert_eq!(segments.len(), 5);
598
599 for segment in &segments {
601 assert_eq!(segment.bounds.width(), 2);
602 }
603
604 assert!(segments[0].is_complete);
606
607 assert!(!segments[1].is_complete);
609 assert!(segments[1].fill_ratio > 0.0 && segments[1].fill_ratio < 1.0);
610
611 assert_eq!(segments[3].fill_ratio, 0.0);
613 assert_eq!(segments[4].fill_ratio, 0.0);
614 }
615
616 #[test]
617 fn test_axis_ticks() {
618 let bounds = Bounds::new(0, 0, 99, 19);
619 let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
620 progress_dims.set_data_range(0.0, 100.0);
621
622 let ticks = progress_dims.calculate_axis_ticks(6); assert_eq!(ticks.len(), 6);
624
625 assert_eq!(ticks[0].value, 0.0);
627 assert_eq!(ticks[0].pixel_position, 0);
628
629 assert_eq!(ticks[5].value, 100.0);
631 assert_eq!(ticks[5].pixel_position, 99);
632
633 let expected_step = 100.0 / 5.0; for i in 0..ticks.len() {
636 let expected_value = i as f64 * expected_step;
637 assert!((ticks[i].value - expected_value).abs() < 0.1);
638 }
639 }
640
641 #[test]
642 fn test_bar_chart_calculation() {
643 let bounds = Bounds::new(0, 5, 9, 15); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Vertical);
645 progress_dims.set_data_range(0.0, 10.0);
646
647 let data = vec![5.0, 10.0, 2.5]; let bars = progress_dims.calculate_bars(&data);
649
650 assert_eq!(bars.len(), 3);
651
652 assert!(bars[0].bounds.width() >= 3 && bars[0].bounds.width() <= 4);
654
655 let max_bar = bars
657 .iter()
658 .max_by(|a, b| a.height_pixels.cmp(&b.height_pixels))
659 .unwrap();
660 assert_eq!(max_bar.value, 10.0);
661 assert_eq!(max_bar.height_pixels, 11); let half_bar = bars.iter().find(|b| b.value == 5.0).unwrap();
665 assert_eq!(half_bar.height_pixels, 6); }
667
668 #[test]
669 fn test_sparkline_calculation() {
670 let bounds = Bounds::new(0, 0, 9, 3); let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
672 progress_dims.set_data_range(0.0, 10.0);
673
674 let data = vec![1.0, 5.0, 8.0, 3.0, 9.0]; let points = progress_dims.calculate_sparkline(&data);
676
677 assert_eq!(points.len(), 10); assert_eq!(points[0].x, 0);
681 assert_eq!(points[9].x, 9);
682
683 for point in &points {
685 assert!(point.value >= 0.0 && point.value <= 10.0);
686 }
687 }
688
689 #[test]
690 fn test_validation() {
691 let bounds = Bounds::new(0, 0, 10, 5);
692 let mut progress_dims = ProgressDimensions::new(bounds, Orientation::Horizontal);
693
694 progress_dims.set_data_range(0.0, 100.0);
696 assert!(progress_dims.validate().is_ok());
697
698 progress_dims.set_data_range(100.0, 50.0); assert!(progress_dims.validate().is_err());
701
702 let zero_bounds = Bounds::new(0, 0, 0, 0); let zero_progress_dims = ProgressDimensions::new(zero_bounds, Orientation::Horizontal);
705 assert!(zero_progress_dims.validate().is_err());
706 }
707}