1use crate::theme::Gradient;
6use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, Default)]
15pub enum BinStrategy {
16 Count(usize),
18 Width(f64),
20 #[default]
22 Sturges,
23 Scott,
25 FreedmanDiaconis,
27}
28
29#[derive(Debug, Clone, Copy, Default)]
31pub enum HistogramOrientation {
32 #[default]
34 Vertical,
35 Horizontal,
37}
38
39#[derive(Debug, Clone, Copy, Default)]
41pub enum BarStyle {
42 #[default]
44 Solid,
45 Blocks,
47 Ascii,
49}
50
51#[derive(Debug, Clone)]
53pub struct Histogram {
54 data: Vec<f64>,
55 bins: BinStrategy,
56 orientation: HistogramOrientation,
57 bar_style: BarStyle,
58 color: Color,
59 gradient: Option<Gradient>,
60 show_labels: bool,
61 bounds: Rect,
62 computed_bins: Vec<(f64, f64, usize)>, }
65
66impl Histogram {
67 #[must_use]
69 pub fn new(data: Vec<f64>) -> Self {
70 let mut hist = Self {
71 data,
72 bins: BinStrategy::default(),
73 orientation: HistogramOrientation::default(),
74 bar_style: BarStyle::default(),
75 color: Color::new(0.3, 0.7, 1.0, 1.0),
76 gradient: None,
77 show_labels: true,
78 bounds: Rect::default(),
79 computed_bins: Vec::new(),
80 };
81 hist.compute_bins();
82 hist
83 }
84
85 #[must_use]
87 pub fn with_bins(mut self, strategy: BinStrategy) -> Self {
88 self.bins = strategy;
89 self.compute_bins();
90 self
91 }
92
93 #[must_use]
95 pub fn with_orientation(mut self, orientation: HistogramOrientation) -> Self {
96 self.orientation = orientation;
97 self
98 }
99
100 #[must_use]
102 pub fn with_bar_style(mut self, style: BarStyle) -> Self {
103 self.bar_style = style;
104 self
105 }
106
107 #[must_use]
109 pub fn with_color(mut self, color: Color) -> Self {
110 self.color = color;
111 self
112 }
113
114 #[must_use]
116 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
117 self.gradient = Some(gradient);
118 self
119 }
120
121 #[must_use]
123 pub fn with_labels(mut self, show: bool) -> Self {
124 self.show_labels = show;
125 self
126 }
127
128 pub fn set_data(&mut self, data: Vec<f64>) {
130 self.data = data;
131 self.compute_bins();
132 }
133
134 #[allow(clippy::manual_clamp)]
136 fn compute_bin_count(&self) -> usize {
137 let n = self.data.len();
138 if n == 0 {
139 return 1;
140 }
141
142 match self.bins {
143 BinStrategy::Count(k) => k.max(1),
144 BinStrategy::Width(w) => {
145 let (min, max) = self.data_range();
146 ((max - min) / w).ceil() as usize
147 }
148 BinStrategy::Sturges => {
149 ((n as f64).log2().ceil() as usize + 1).max(1)
151 }
152 BinStrategy::Scott => {
153 let std = self.std_dev();
155 if std < 1e-10 {
156 return 1;
157 }
158 let (min, max) = self.data_range();
159 let width = 3.49 * std / (n as f64).cbrt();
160 ((max - min) / width).ceil() as usize
161 }
162 BinStrategy::FreedmanDiaconis => {
163 let iqr = self.iqr();
165 if iqr < 1e-10 {
166 return 1;
167 }
168 let (min, max) = self.data_range();
169 let width = 2.0 * iqr / (n as f64).cbrt();
170 ((max - min) / width).ceil() as usize
171 }
172 }
173 .max(1)
174 .min(100) }
176
177 fn data_range(&self) -> (f64, f64) {
179 let mut min = f64::INFINITY;
180 let mut max = f64::NEG_INFINITY;
181
182 for &v in &self.data {
183 if v.is_finite() {
184 min = min.min(v);
185 max = max.max(v);
186 }
187 }
188
189 if min == f64::INFINITY {
190 (0.0, 1.0)
191 } else if (max - min).abs() < 1e-10 {
192 (min - 0.5, max + 0.5)
193 } else {
194 (min, max)
195 }
196 }
197
198 fn std_dev(&self) -> f64 {
200 let n = self.data.len();
201 if n < 2 {
202 return 0.0;
203 }
204
205 let mean: f64 = self.data.iter().filter(|x| x.is_finite()).sum::<f64>()
206 / self.data.iter().filter(|x| x.is_finite()).count() as f64;
207
208 let variance: f64 = self
209 .data
210 .iter()
211 .filter(|x| x.is_finite())
212 .map(|x| (x - mean).powi(2))
213 .sum::<f64>()
214 / (n - 1) as f64;
215
216 variance.sqrt()
217 }
218
219 fn iqr(&self) -> f64 {
221 let mut sorted: Vec<f64> = self
222 .data
223 .iter()
224 .filter(|x| x.is_finite())
225 .copied()
226 .collect();
227 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
228
229 if sorted.len() < 4 {
230 return self.std_dev(); }
232
233 let q1_idx = sorted.len() / 4;
234 let q3_idx = 3 * sorted.len() / 4;
235
236 sorted[q3_idx] - sorted[q1_idx]
237 }
238
239 fn compute_bins(&mut self) {
241 let n_bins = self.compute_bin_count();
242 let (min, max) = self.data_range();
243 let bin_width = (max - min) / n_bins as f64;
244
245 self.computed_bins = (0..n_bins)
246 .map(|i| {
247 let start = min + i as f64 * bin_width;
248 let end = start + bin_width;
249 let count = self
250 .data
251 .iter()
252 .filter(|&&v| {
253 if i == n_bins - 1 {
254 v >= start && v <= end
255 } else {
256 v >= start && v < end
257 }
258 })
259 .count();
260 (start, end, count)
261 })
262 .collect();
263 }
264}
265
266impl Default for Histogram {
267 fn default() -> Self {
268 Self::new(Vec::new())
269 }
270}
271
272impl Widget for Histogram {
273 fn type_id(&self) -> TypeId {
274 TypeId::of::<Self>()
275 }
276
277 fn measure(&self, constraints: Constraints) -> Size {
278 Size::new(
279 constraints.max_width.min(60.0),
280 constraints.max_height.min(15.0),
281 )
282 }
283
284 fn layout(&mut self, bounds: Rect) -> LayoutResult {
285 self.bounds = bounds;
286 LayoutResult {
287 size: Size::new(bounds.width, bounds.height),
288 }
289 }
290
291 fn paint(&self, canvas: &mut dyn Canvas) {
292 if self.bounds.width < 5.0 || self.bounds.height < 3.0 || self.computed_bins.is_empty() {
293 return;
294 }
295
296 let max_count = self
297 .computed_bins
298 .iter()
299 .map(|(_, _, c)| *c)
300 .max()
301 .unwrap_or(1)
302 .max(1);
303
304 match self.orientation {
305 HistogramOrientation::Vertical => self.paint_vertical(canvas, max_count),
306 HistogramOrientation::Horizontal => self.paint_horizontal(canvas, max_count),
307 }
308 }
309
310 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
311 None
312 }
313
314 fn children(&self) -> &[Box<dyn Widget>] {
315 &[]
316 }
317
318 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
319 &mut []
320 }
321}
322
323impl Histogram {
324 fn paint_vertical(&self, canvas: &mut dyn Canvas, max_count: usize) {
325 let label_height = if self.show_labels { 1.0 } else { 0.0 };
326 let label_width = if self.show_labels { 5.0 } else { 0.0 };
327
328 let plot_x = self.bounds.x + label_width;
329 let plot_y = self.bounds.y;
330 let plot_width = self.bounds.width - label_width;
331 let plot_height = self.bounds.height - label_height;
332
333 let n_bins = self.computed_bins.len();
334 let bar_width = (plot_width / n_bins as f32).max(1.0);
335
336 if self.show_labels {
338 let label_style = TextStyle {
339 color: Color::new(0.6, 0.6, 0.6, 1.0),
340 ..Default::default()
341 };
342
343 canvas.draw_text(
344 &format!("{max_count:>4}"),
345 Point::new(self.bounds.x, plot_y),
346 &label_style,
347 );
348 canvas.draw_text(
349 " 0",
350 Point::new(self.bounds.x, plot_y + plot_height - 1.0),
351 &label_style,
352 );
353 }
354
355 for (i, &(start, _end, count)) in self.computed_bins.iter().enumerate() {
357 let bar_height = if max_count > 0 {
358 (count as f32 / max_count as f32) * plot_height
359 } else {
360 0.0
361 };
362
363 let x = plot_x + i as f32 * bar_width;
364 let y = plot_y + plot_height - bar_height;
365
366 let color = if let Some(ref gradient) = self.gradient {
368 gradient.sample(count as f64 / max_count as f64)
369 } else {
370 self.color
371 };
372
373 let style = TextStyle {
374 color,
375 ..Default::default()
376 };
377
378 match self.bar_style {
380 BarStyle::Solid => {
381 for row in 0..(bar_height.ceil() as usize) {
382 let bar_chars: String =
383 (0..(bar_width as usize).max(1)).map(|_| '█').collect();
384 canvas.draw_text(&bar_chars, Point::new(x, y + row as f32), &style);
385 }
386 }
387 BarStyle::Blocks => {
388 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
389 let full_rows = bar_height as usize;
390 let frac = bar_height.fract();
391 let frac_idx = ((frac * 8.0) as usize).min(7);
392
393 for row in 0..full_rows {
394 let bar_chars: String =
395 (0..(bar_width as usize).max(1)).map(|_| '█').collect();
396 canvas.draw_text(&bar_chars, Point::new(x, y + row as f32), &style);
397 }
398
399 if frac > 0.1 {
400 let bar_chars: String = (0..(bar_width as usize).max(1))
401 .map(|_| BLOCKS[frac_idx])
402 .collect();
403 canvas.draw_text(&bar_chars, Point::new(x, y + full_rows as f32), &style);
404 }
405 }
406 BarStyle::Ascii => {
407 for row in 0..(bar_height.ceil() as usize) {
408 let bar_chars: String =
409 (0..(bar_width as usize).max(1)).map(|_| '#').collect();
410 canvas.draw_text(&bar_chars, Point::new(x, y + row as f32), &style);
411 }
412 }
413 }
414
415 if self.show_labels && i % 2 == 0 {
417 let label = format!("{start:.0}");
418 let label_x = x + bar_width / 2.0 - label.len() as f32 / 2.0;
419 canvas.draw_text(
420 &label,
421 Point::new(label_x, plot_y + plot_height),
422 &TextStyle {
423 color: Color::new(0.6, 0.6, 0.6, 1.0),
424 ..Default::default()
425 },
426 );
427 }
428 }
429 }
430
431 fn paint_horizontal(&self, canvas: &mut dyn Canvas, max_count: usize) {
432 let label_width = if self.show_labels { 6.0 } else { 0.0 };
433
434 let plot_x = self.bounds.x + label_width;
435 let plot_y = self.bounds.y;
436 let plot_width = self.bounds.width - label_width;
437 let plot_height = self.bounds.height;
438
439 let n_bins = self.computed_bins.len();
440 let bar_height = (plot_height / n_bins as f32).max(1.0);
441
442 for (i, &(start, _end, count)) in self.computed_bins.iter().enumerate() {
443 let bar_width = if max_count > 0 {
444 (count as f32 / max_count as f32) * plot_width
445 } else {
446 0.0
447 };
448
449 let x = plot_x;
450 let y = plot_y + i as f32 * bar_height;
451
452 let color = if let Some(ref gradient) = self.gradient {
454 gradient.sample(count as f64 / max_count as f64)
455 } else {
456 self.color
457 };
458
459 let style = TextStyle {
460 color,
461 ..Default::default()
462 };
463
464 if self.show_labels {
466 let label = format!("{start:>5.0}");
467 canvas.draw_text(
468 &label,
469 Point::new(self.bounds.x, y),
470 &TextStyle {
471 color: Color::new(0.6, 0.6, 0.6, 1.0),
472 ..Default::default()
473 },
474 );
475 }
476
477 let bar_chars: String = (0..(bar_width.ceil() as usize).max(0))
479 .map(|_| '█')
480 .collect();
481 if !bar_chars.is_empty() {
482 canvas.draw_text(&bar_chars, Point::new(x, y), &style);
483 }
484 }
485 }
486}
487
488impl Brick for Histogram {
489 fn brick_name(&self) -> &'static str {
490 "Histogram"
491 }
492
493 fn assertions(&self) -> &[BrickAssertion] {
494 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
495 ASSERTIONS
496 }
497
498 fn budget(&self) -> BrickBudget {
499 BrickBudget::uniform(8)
500 }
501
502 fn verify(&self) -> BrickVerification {
503 let mut passed = Vec::new();
504 let mut failed = Vec::new();
505
506 if self.bounds.width >= 5.0 && self.bounds.height >= 3.0 {
507 passed.push(BrickAssertion::max_latency_ms(8));
508 } else {
509 failed.push((
510 BrickAssertion::max_latency_ms(8),
511 "Size too small".to_string(),
512 ));
513 }
514
515 BrickVerification {
516 passed,
517 failed,
518 verification_time: Duration::from_micros(5),
519 }
520 }
521
522 fn to_html(&self) -> String {
523 String::new()
524 }
525
526 fn to_css(&self) -> String {
527 String::new()
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_histogram_creation() {
537 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
538 let hist = Histogram::new(data);
539 assert!(!hist.computed_bins.is_empty());
540 }
541
542 #[test]
543 fn test_bin_strategies() {
544 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
545
546 let sturges = Histogram::new(data.clone()).with_bins(BinStrategy::Sturges);
547 assert!(!sturges.computed_bins.is_empty());
548
549 let scott = Histogram::new(data.clone()).with_bins(BinStrategy::Scott);
550 assert!(!scott.computed_bins.is_empty());
551
552 let fd = Histogram::new(data).with_bins(BinStrategy::FreedmanDiaconis);
553 assert!(!fd.computed_bins.is_empty());
554 }
555
556 #[test]
557 fn test_empty_data() {
558 let hist = Histogram::new(vec![]);
559 assert_eq!(hist.computed_bins.len(), 1);
560 }
561
562 #[test]
563 fn test_single_value() {
564 let hist = Histogram::new(vec![5.0, 5.0, 5.0]);
565 assert!(!hist.computed_bins.is_empty());
566 }
567
568 #[test]
569 fn test_histogram_assertions() {
570 let hist = Histogram::default();
571 assert!(!hist.assertions().is_empty());
572 }
573
574 #[test]
575 #[allow(clippy::field_reassign_with_default)]
576 fn test_histogram_verify() {
577 let mut hist = Histogram::default();
578 hist.bounds = Rect::new(0.0, 0.0, 60.0, 15.0);
579 assert!(hist.verify().is_valid());
580 }
581
582 #[test]
583 fn test_histogram_children() {
584 let hist = Histogram::default();
585 assert!(hist.children().is_empty());
586 }
587
588 #[test]
589 fn test_histogram_children_mut() {
590 let mut hist = Histogram::default();
591 assert!(hist.children_mut().is_empty());
592 }
593
594 #[test]
595 fn test_histogram_type_id() {
596 let hist = Histogram::default();
597 let tid = Widget::type_id(&hist);
598 assert_eq!(tid, TypeId::of::<Histogram>());
599 }
600
601 #[test]
602 fn test_histogram_measure() {
603 let hist = Histogram::new(vec![1.0, 2.0, 3.0]);
604 let size = hist.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
605 assert!(size.width > 0.0);
606 assert!(size.height > 0.0);
607 }
608
609 #[test]
610 fn test_histogram_layout() {
611 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0]);
612 let result = hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
613 assert_eq!(result.size.width, 60.0);
614 assert_eq!(result.size.height, 15.0);
615 }
616
617 #[test]
618 fn test_histogram_event() {
619 let mut hist = Histogram::default();
620 let event = Event::Resize {
621 width: 80.0,
622 height: 24.0,
623 };
624 assert!(hist.event(&event).is_none());
625 }
626
627 #[test]
628 fn test_histogram_brick_name() {
629 let hist = Histogram::default();
630 assert_eq!(hist.brick_name(), "Histogram");
631 }
632
633 #[test]
634 fn test_histogram_budget() {
635 let hist = Histogram::default();
636 let budget = hist.budget();
637 assert!(budget.layout_ms > 0);
638 }
639
640 #[test]
641 fn test_histogram_to_html() {
642 let hist = Histogram::default();
643 assert!(hist.to_html().is_empty());
644 }
645
646 #[test]
647 fn test_histogram_to_css() {
648 let hist = Histogram::default();
649 assert!(hist.to_css().is_empty());
650 }
651
652 #[test]
653 fn test_histogram_with_orientation() {
654 let hist =
655 Histogram::new(vec![1.0, 2.0]).with_orientation(HistogramOrientation::Horizontal);
656 assert!(matches!(hist.orientation, HistogramOrientation::Horizontal));
657 }
658
659 #[test]
660 fn test_histogram_with_bar_style() {
661 let hist = Histogram::new(vec![1.0, 2.0]).with_bar_style(BarStyle::Blocks);
662 assert!(matches!(hist.bar_style, BarStyle::Blocks));
663 }
664
665 #[test]
666 fn test_histogram_with_color() {
667 let hist = Histogram::new(vec![1.0, 2.0]).with_color(Color::RED);
668 assert_eq!(hist.color, Color::RED);
669 }
670
671 #[test]
672 fn test_histogram_with_gradient() {
673 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
674 let hist = Histogram::new(vec![1.0, 2.0]).with_gradient(gradient);
675 assert!(hist.gradient.is_some());
676 }
677
678 #[test]
679 fn test_histogram_with_labels() {
680 let hist = Histogram::new(vec![1.0, 2.0]).with_labels(false);
681 assert!(!hist.show_labels);
682 }
683
684 #[test]
685 fn test_histogram_set_data() {
686 let mut hist = Histogram::new(vec![1.0, 2.0]);
687 hist.set_data(vec![10.0, 20.0, 30.0, 40.0, 50.0]);
688 assert!(!hist.computed_bins.is_empty());
689 }
690
691 #[test]
692 fn test_histogram_bin_count() {
693 let hist = Histogram::new(vec![1.0, 2.0, 3.0]).with_bins(BinStrategy::Count(5));
694 assert!(!hist.computed_bins.is_empty());
695 }
696
697 #[test]
698 fn test_histogram_bin_width() {
699 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
700 let hist = Histogram::new(data).with_bins(BinStrategy::Width(2.0));
701 assert!(!hist.computed_bins.is_empty());
702 }
703
704 #[test]
705 fn test_histogram_paint_vertical() {
706 use crate::{CellBuffer, DirectTerminalCanvas};
707
708 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
709 let mut buffer = CellBuffer::new(60, 15);
710 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
711
712 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
713 hist.paint(&mut canvas);
714 }
715
716 #[test]
717 fn test_histogram_paint_horizontal() {
718 use crate::{CellBuffer, DirectTerminalCanvas};
719
720 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0])
721 .with_orientation(HistogramOrientation::Horizontal);
722 let mut buffer = CellBuffer::new(60, 15);
723 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
724
725 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
726 hist.paint(&mut canvas);
727 }
728
729 #[test]
730 fn test_histogram_paint_blocks() {
731 use crate::{CellBuffer, DirectTerminalCanvas};
732
733 let mut hist =
734 Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]).with_bar_style(BarStyle::Blocks);
735 let mut buffer = CellBuffer::new(60, 15);
736 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
737
738 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
739 hist.paint(&mut canvas);
740 }
741
742 #[test]
743 fn test_histogram_paint_ascii() {
744 use crate::{CellBuffer, DirectTerminalCanvas};
745
746 let mut hist =
747 Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]).with_bar_style(BarStyle::Ascii);
748 let mut buffer = CellBuffer::new(60, 15);
749 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
750
751 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
752 hist.paint(&mut canvas);
753 }
754
755 #[test]
756 fn test_histogram_paint_with_gradient() {
757 use crate::{CellBuffer, DirectTerminalCanvas};
758
759 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
760 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]).with_gradient(gradient);
761 let mut buffer = CellBuffer::new(60, 15);
762 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
763
764 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
765 hist.paint(&mut canvas);
766 }
767
768 #[test]
769 fn test_histogram_paint_without_labels() {
770 use crate::{CellBuffer, DirectTerminalCanvas};
771
772 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]).with_labels(false);
773 let mut buffer = CellBuffer::new(60, 15);
774 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
775
776 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
777 hist.paint(&mut canvas);
778 }
779
780 #[test]
781 fn test_histogram_paint_small_bounds() {
782 use crate::{CellBuffer, DirectTerminalCanvas};
783
784 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0]);
785 let mut buffer = CellBuffer::new(4, 2);
786 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
787
788 hist.layout(Rect::new(0.0, 0.0, 4.0, 2.0));
789 hist.paint(&mut canvas);
790 }
792
793 #[test]
794 #[allow(clippy::field_reassign_with_default)]
795 fn test_histogram_verify_small_bounds() {
796 let mut hist = Histogram::default();
797 hist.bounds = Rect::new(0.0, 0.0, 2.0, 1.0);
798 assert!(!hist.verify().is_valid());
799 }
800
801 #[test]
802 fn test_histogram_data_with_nan() {
803 let hist = Histogram::new(vec![1.0, f64::NAN, 3.0, f64::INFINITY, 5.0]);
804 assert!(!hist.computed_bins.is_empty());
805 }
806
807 #[test]
808 fn test_histogram_iqr_small_data() {
809 let hist = Histogram::new(vec![1.0, 2.0]); assert!(!hist.computed_bins.is_empty());
811 }
812
813 #[test]
814 fn test_histogram_std_dev_single() {
815 let hist = Histogram::new(vec![5.0]);
816 assert!(!hist.computed_bins.is_empty());
817 }
818
819 #[test]
820 fn test_histogram_clone() {
821 let hist = Histogram::new(vec![1.0, 2.0, 3.0]);
822 let cloned = hist.clone();
823 assert_eq!(cloned.computed_bins.len(), hist.computed_bins.len());
824 }
825
826 #[test]
827 fn test_histogram_debug() {
828 let hist = Histogram::new(vec![1.0, 2.0, 3.0]);
829 let debug = format!("{hist:?}");
830 assert!(debug.contains("Histogram"));
831 }
832
833 #[test]
834 fn test_bin_strategy_debug() {
835 let strategy = BinStrategy::Sturges;
836 let debug = format!("{strategy:?}");
837 assert!(debug.contains("Sturges"));
838 }
839
840 #[test]
841 fn test_histogram_orientation_debug() {
842 let orientation = HistogramOrientation::Vertical;
843 let debug = format!("{orientation:?}");
844 assert!(debug.contains("Vertical"));
845 }
846
847 #[test]
848 fn test_bar_style_debug() {
849 let style = BarStyle::Solid;
850 let debug = format!("{style:?}");
851 assert!(debug.contains("Solid"));
852 }
853
854 #[test]
855 fn test_histogram_horizontal_with_gradient() {
856 use crate::{CellBuffer, DirectTerminalCanvas};
857
858 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
859 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0])
860 .with_orientation(HistogramOrientation::Horizontal)
861 .with_gradient(gradient);
862 let mut buffer = CellBuffer::new(60, 15);
863 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
864
865 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
866 hist.paint(&mut canvas);
867 }
868
869 #[test]
870 fn test_histogram_horizontal_without_labels() {
871 use crate::{CellBuffer, DirectTerminalCanvas};
872
873 let mut hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0])
874 .with_orientation(HistogramOrientation::Horizontal)
875 .with_labels(false);
876 let mut buffer = CellBuffer::new(60, 15);
877 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
878
879 hist.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
880 hist.paint(&mut canvas);
881 }
882
883 #[test]
884 fn test_histogram_large_data() {
885 let data: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.37) % 100.0).collect();
886 let hist = Histogram::new(data);
887 assert!(!hist.computed_bins.is_empty());
888 }
889}