1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Vec2};
10use std::f32::consts::PI;
11
12use super::theme::AccNetTheme;
13
14pub const BENFORD_EXPECTED: [f64; 9] = [
16 0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
17];
18
19pub struct Histogram {
21 pub values: Vec<f64>,
23 pub labels: Vec<String>,
25 pub expected: Option<Vec<f64>>,
27 pub bar_color: Color32,
29 pub expected_color: Color32,
31 pub title: String,
33 pub height: f32,
35}
36
37impl Histogram {
38 pub fn new(title: impl Into<String>) -> Self {
40 Self {
41 values: Vec::new(),
42 labels: Vec::new(),
43 expected: None,
44 bar_color: Color32::from_rgb(100, 150, 230),
45 expected_color: Color32::from_rgb(255, 180, 100),
46 title: title.into(),
47 height: 120.0,
48 }
49 }
50
51 pub fn benford(actual_counts: [usize; 9]) -> Self {
53 let total: usize = actual_counts.iter().sum();
54 let total_f = total.max(1) as f64;
55
56 let values: Vec<f64> = actual_counts.iter().map(|&c| c as f64 / total_f).collect();
57 let labels: Vec<String> = (1..=9).map(|d| d.to_string()).collect();
58 let expected = Some(BENFORD_EXPECTED.to_vec());
59
60 Self {
61 values,
62 labels,
63 expected,
64 bar_color: Color32::from_rgb(100, 180, 130),
65 expected_color: Color32::from_rgb(255, 100, 100),
66 title: "Benford's Law Distribution".to_string(),
67 height: 100.0,
68 }
69 }
70
71 pub fn height(mut self, height: f32) -> Self {
73 self.height = height;
74 self
75 }
76
77 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
79 let width = ui.available_width();
80 let (response, painter) =
81 ui.allocate_painter(Vec2::new(width, self.height + 30.0), Sense::hover());
82 let rect = response.rect;
83
84 painter.text(
86 Pos2::new(rect.left() + 5.0, rect.top()),
87 egui::Align2::LEFT_TOP,
88 &self.title,
89 egui::FontId::proportional(11.0),
90 theme.text_secondary,
91 );
92
93 let chart_rect = Rect::from_min_max(
95 Pos2::new(rect.left() + 20.0, rect.top() + 18.0),
96 Pos2::new(rect.right() - 5.0, rect.bottom() - 15.0),
97 );
98
99 painter.rect_filled(chart_rect, 2.0, Color32::from_rgb(25, 25, 35));
101
102 if self.values.is_empty() {
103 painter.text(
104 chart_rect.center(),
105 egui::Align2::CENTER_CENTER,
106 "No data",
107 egui::FontId::proportional(10.0),
108 theme.text_secondary,
109 );
110 return response;
111 }
112
113 let n = self.values.len();
114 let bar_width = (chart_rect.width() - 10.0) / n as f32;
115 let gap = bar_width * 0.15;
116 let max_val = self
117 .values
118 .iter()
119 .copied()
120 .fold(0.0_f64, f64::max)
121 .max(
122 self.expected
123 .as_ref()
124 .map(|e| e.iter().copied().fold(0.0_f64, f64::max))
125 .unwrap_or(0.0),
126 )
127 .max(0.01);
128
129 for (i, &val) in self.values.iter().enumerate() {
131 let x = chart_rect.left() + 5.0 + i as f32 * bar_width;
132 let bar_height = (val / max_val) as f32 * (chart_rect.height() - 5.0);
133
134 let bar_rect = Rect::from_min_max(
135 Pos2::new(x + gap / 2.0, chart_rect.bottom() - bar_height),
136 Pos2::new(x + bar_width - gap / 2.0, chart_rect.bottom()),
137 );
138
139 painter.rect_filled(bar_rect, 2.0, self.bar_color);
140
141 if let Some(ref expected) = self.expected {
143 if i < expected.len() {
144 let expected_y = chart_rect.bottom()
145 - (expected[i] / max_val) as f32 * (chart_rect.height() - 5.0);
146 painter.line_segment(
147 [
148 Pos2::new(x + gap / 2.0, expected_y),
149 Pos2::new(x + bar_width - gap / 2.0, expected_y),
150 ],
151 Stroke::new(2.0, self.expected_color),
152 );
153 }
154 }
155
156 if i < self.labels.len() {
158 painter.text(
159 Pos2::new(x + bar_width / 2.0, chart_rect.bottom() + 2.0),
160 egui::Align2::CENTER_TOP,
161 &self.labels[i],
162 egui::FontId::proportional(9.0),
163 theme.text_secondary,
164 );
165 }
166 }
167
168 painter.text(
170 Pos2::new(chart_rect.left() - 2.0, chart_rect.top()),
171 egui::Align2::RIGHT_TOP,
172 format!("{:.0}%", max_val * 100.0),
173 egui::FontId::proportional(8.0),
174 theme.text_secondary,
175 );
176
177 response
178 }
179}
180
181pub struct BarChart {
183 pub data: Vec<(String, f64, Color32)>,
185 pub title: String,
187 pub bar_height: f32,
189}
190
191impl BarChart {
192 pub fn new(title: impl Into<String>) -> Self {
194 Self {
195 data: Vec::new(),
196 title: title.into(),
197 bar_height: 18.0,
198 }
199 }
200
201 pub fn add(mut self, label: impl Into<String>, value: f64, color: Color32) -> Self {
203 self.data.push((label.into(), value, color));
204 self
205 }
206
207 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
209 let width = ui.available_width();
210 let total_height = 18.0 + self.data.len() as f32 * (self.bar_height + 4.0);
211
212 let (response, painter) =
213 ui.allocate_painter(Vec2::new(width, total_height), Sense::hover());
214 let rect = response.rect;
215
216 painter.text(
218 Pos2::new(rect.left() + 5.0, rect.top()),
219 egui::Align2::LEFT_TOP,
220 &self.title,
221 egui::FontId::proportional(11.0),
222 theme.text_secondary,
223 );
224
225 if self.data.is_empty() {
226 return response;
227 }
228
229 let max_val = self
230 .data
231 .iter()
232 .map(|(_, v, _)| *v)
233 .fold(0.0_f64, f64::max)
234 .max(1.0);
235 let label_width = 80.0;
236 let bar_area_width = width - label_width - 45.0;
237
238 for (i, (label, value, color)) in self.data.iter().enumerate() {
239 let y = rect.top() + 18.0 + i as f32 * (self.bar_height + 4.0);
240
241 painter.text(
243 Pos2::new(rect.left() + label_width - 5.0, y + self.bar_height / 2.0),
244 egui::Align2::RIGHT_CENTER,
245 label,
246 egui::FontId::proportional(10.0),
247 theme.text_primary,
248 );
249
250 let bar_width = ((*value / max_val) as f32 * bar_area_width).max(2.0);
252 let bar_rect = Rect::from_min_size(
253 Pos2::new(rect.left() + label_width, y),
254 Vec2::new(bar_width, self.bar_height),
255 );
256 painter.rect_filled(bar_rect, 2.0, *color);
257
258 painter.text(
260 Pos2::new(
261 rect.left() + label_width + bar_width + 5.0,
262 y + self.bar_height / 2.0,
263 ),
264 egui::Align2::LEFT_CENTER,
265 format!("{}", *value as usize),
266 egui::FontId::proportional(10.0),
267 theme.text_secondary,
268 );
269 }
270
271 response
272 }
273}
274
275pub struct DonutChart {
277 pub segments: Vec<(String, f64, Color32)>,
279 pub title: String,
281 pub radius: f32,
283 pub inner_radius: f32,
285}
286
287impl DonutChart {
288 pub fn new(title: impl Into<String>) -> Self {
290 Self {
291 segments: Vec::new(),
292 title: title.into(),
293 radius: 45.0,
294 inner_radius: 25.0,
295 }
296 }
297
298 pub fn add(mut self, label: impl Into<String>, value: f64, color: Color32) -> Self {
300 self.segments.push((label.into(), value, color));
301 self
302 }
303
304 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
306 let width = ui.available_width();
307 let height = self.radius * 2.0 + 50.0;
308
309 let (response, painter) = ui.allocate_painter(Vec2::new(width, height), Sense::hover());
310 let rect = response.rect;
311
312 painter.text(
314 Pos2::new(rect.left() + 5.0, rect.top()),
315 egui::Align2::LEFT_TOP,
316 &self.title,
317 egui::FontId::proportional(11.0),
318 theme.text_secondary,
319 );
320
321 if self.segments.is_empty() {
322 return response;
323 }
324
325 let total: f64 = self.segments.iter().map(|(_, v, _)| *v).sum();
326 if total == 0.0 {
327 return response;
328 }
329
330 let center = Pos2::new(
331 rect.left() + self.radius + 10.0,
332 rect.top() + self.radius + 18.0,
333 );
334 let mut start_angle = -PI / 2.0; for (_label, value, color) in &self.segments {
338 let sweep_angle = (*value / total) as f32 * 2.0 * PI;
339
340 if sweep_angle > 0.01 {
341 let steps = (sweep_angle * 30.0).max(8.0) as usize;
343
344 for i in 0..steps {
345 let angle1 = start_angle + sweep_angle * (i as f32 / steps as f32);
346 let angle2 = start_angle + sweep_angle * ((i + 1) as f32 / steps as f32);
347
348 let outer1 = Pos2::new(
350 center.x + self.radius * angle1.cos(),
351 center.y + self.radius * angle1.sin(),
352 );
353 let outer2 = Pos2::new(
354 center.x + self.radius * angle2.cos(),
355 center.y + self.radius * angle2.sin(),
356 );
357 let inner1 = Pos2::new(
358 center.x + self.inner_radius * angle1.cos(),
359 center.y + self.inner_radius * angle1.sin(),
360 );
361 let inner2 = Pos2::new(
362 center.x + self.inner_radius * angle2.cos(),
363 center.y + self.inner_radius * angle2.sin(),
364 );
365
366 painter.add(egui::Shape::convex_polygon(
368 vec![outer1, outer2, inner2, inner1],
369 *color,
370 Stroke::NONE,
371 ));
372 }
373
374 let end_angle = start_angle + sweep_angle;
376 painter.line_segment(
377 [
378 Pos2::new(
379 center.x + self.inner_radius * start_angle.cos(),
380 center.y + self.inner_radius * start_angle.sin(),
381 ),
382 Pos2::new(
383 center.x + self.radius * start_angle.cos(),
384 center.y + self.radius * start_angle.sin(),
385 ),
386 ],
387 Stroke::new(1.0, Color32::from_rgb(50, 50, 60)),
388 );
389 painter.line_segment(
390 [
391 Pos2::new(
392 center.x + self.inner_radius * end_angle.cos(),
393 center.y + self.inner_radius * end_angle.sin(),
394 ),
395 Pos2::new(
396 center.x + self.radius * end_angle.cos(),
397 center.y + self.radius * end_angle.sin(),
398 ),
399 ],
400 Stroke::new(1.0, Color32::from_rgb(50, 50, 60)),
401 );
402 }
403
404 start_angle += sweep_angle;
405 }
406
407 painter.text(
409 center,
410 egui::Align2::CENTER_CENTER,
411 format!("{}", total as usize),
412 egui::FontId::proportional(12.0),
413 theme.text_primary,
414 );
415
416 let legend_x = center.x + self.radius + 20.0;
418 let legend_y_start = rect.top() + 18.0;
419
420 for (i, (label, value, color)) in self.segments.iter().enumerate() {
421 let y = legend_y_start + i as f32 * 14.0;
422
423 painter.circle_filled(Pos2::new(legend_x, y + 5.0), 4.0, *color);
425
426 let pct = if total > 0.0 {
428 (*value / total * 100.0) as usize
429 } else {
430 0
431 };
432 painter.text(
433 Pos2::new(legend_x + 10.0, y),
434 egui::Align2::LEFT_TOP,
435 format!("{} ({}%)", label, pct),
436 egui::FontId::proportional(9.0),
437 theme.text_secondary,
438 );
439 }
440
441 response
442 }
443}
444
445pub struct Sparkline {
447 pub values: Vec<f32>,
449 pub max_points: usize,
451 pub color: Color32,
453 pub title: String,
455 pub height: f32,
457 pub show_labels: bool,
459}
460
461impl Sparkline {
462 pub fn new(title: impl Into<String>) -> Self {
464 Self {
465 values: Vec::new(),
466 max_points: 100,
467 color: Color32::from_rgb(100, 200, 150),
468 title: title.into(),
469 height: 50.0,
470 show_labels: true,
471 }
472 }
473
474 pub fn push(&mut self, value: f32) {
476 self.values.push(value);
477 if self.values.len() > self.max_points {
478 self.values.remove(0);
479 }
480 }
481
482 pub fn color(mut self, color: Color32) -> Self {
484 self.color = color;
485 self
486 }
487
488 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
490 let width = ui.available_width();
491 let (response, painter) =
492 ui.allocate_painter(Vec2::new(width, self.height + 16.0), Sense::hover());
493 let rect = response.rect;
494
495 painter.text(
497 Pos2::new(rect.left() + 5.0, rect.top()),
498 egui::Align2::LEFT_TOP,
499 &self.title,
500 egui::FontId::proportional(11.0),
501 theme.text_secondary,
502 );
503
504 let chart_rect = Rect::from_min_max(
506 Pos2::new(rect.left() + 5.0, rect.top() + 14.0),
507 Pos2::new(rect.right() - 5.0, rect.bottom()),
508 );
509
510 painter.rect_filled(chart_rect, 2.0, Color32::from_rgb(25, 25, 35));
512
513 if self.values.len() < 2 {
514 return response;
515 }
516
517 let min_val = self.values.iter().copied().fold(f32::INFINITY, f32::min);
518 let max_val = self
519 .values
520 .iter()
521 .copied()
522 .fold(f32::NEG_INFINITY, f32::max);
523 let range = (max_val - min_val).max(0.01);
524
525 let n = self.values.len();
526 let points: Vec<Pos2> = self
527 .values
528 .iter()
529 .enumerate()
530 .map(|(i, &val)| {
531 let x = chart_rect.left() + (i as f32 / (n - 1) as f32) * chart_rect.width();
532 let y = chart_rect.bottom()
533 - ((val - min_val) / range) * (chart_rect.height() - 4.0)
534 - 2.0;
535 Pos2::new(x, y)
536 })
537 .collect();
538
539 let mut area_points = points.clone();
541 area_points.push(Pos2::new(chart_rect.right(), chart_rect.bottom()));
542 area_points.push(Pos2::new(chart_rect.left(), chart_rect.bottom()));
543
544 let fill_color =
545 Color32::from_rgba_unmultiplied(self.color.r(), self.color.g(), self.color.b(), 40);
546 painter.add(egui::Shape::convex_polygon(
547 area_points,
548 fill_color,
549 Stroke::NONE,
550 ));
551
552 for i in 0..(points.len() - 1) {
554 painter.line_segment([points[i], points[i + 1]], Stroke::new(2.0, self.color));
555 }
556
557 if let Some(last) = points.last() {
559 painter.circle_filled(*last, 3.0, self.color);
560 }
561
562 if self.show_labels && !self.values.is_empty() {
564 let current = self.values.last().unwrap_or(&0.0);
565 painter.text(
566 Pos2::new(chart_rect.right() - 3.0, chart_rect.top() + 3.0),
567 egui::Align2::RIGHT_TOP,
568 format!("{:.1}", current),
569 egui::FontId::proportional(9.0),
570 self.color,
571 );
572 }
573
574 response
575 }
576}
577
578pub struct MethodDistribution {
580 pub counts: [usize; 5],
582 pub show_explanation: bool,
584}
585
586impl MethodDistribution {
587 pub fn new(counts: [usize; 5]) -> Self {
589 Self {
590 counts,
591 show_explanation: true,
592 }
593 }
594
595 pub fn with_explanation(mut self, show: bool) -> Self {
597 self.show_explanation = show;
598 self
599 }
600
601 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
603 let labels = ["A", "B", "C", "D", "E"];
604 let descriptions = [
605 "1:1 direct mapping", "n:n amount match", "n:m partition", "Higher aggregate", "Decomposition", ];
611 let colors = [
612 Color32::from_rgb(100, 180, 230), Color32::from_rgb(150, 200, 130), Color32::from_rgb(230, 180, 100), Color32::from_rgb(200, 130, 180), Color32::from_rgb(180, 130, 130), ];
618
619 let total: usize = self.counts.iter().sum();
620 let width = ui.available_width();
621 let base_height = if self.show_explanation { 95.0 } else { 60.0 };
622
623 let (response, painter) =
624 ui.allocate_painter(Vec2::new(width, base_height), Sense::hover());
625 let rect = response.rect;
626
627 painter.text(
629 Pos2::new(rect.left() + 5.0, rect.top()),
630 egui::Align2::LEFT_TOP,
631 "Transformation Methods",
632 egui::FontId::proportional(11.0),
633 theme.text_secondary,
634 );
635
636 let bar_y = rect.top() + 18.0;
637 let bar_height = 20.0;
638 let bar_width = width - 10.0;
639
640 painter.rect_filled(
642 Rect::from_min_size(
643 Pos2::new(rect.left() + 5.0, bar_y),
644 Vec2::new(bar_width, bar_height),
645 ),
646 4.0,
647 Color32::from_rgb(40, 40, 50),
648 );
649
650 if total == 0 {
651 return response;
652 }
653
654 let mut x = rect.left() + 5.0;
656 for (i, &count) in self.counts.iter().enumerate() {
657 let segment_width = (count as f32 / total as f32) * bar_width;
658 if segment_width > 1.0 {
659 painter.rect_filled(
660 Rect::from_min_size(Pos2::new(x, bar_y), Vec2::new(segment_width, bar_height)),
661 if i == 0 || i == 4 { 4.0 } else { 0.0 },
662 colors[i],
663 );
664 x += segment_width;
665 }
666 }
667
668 let legend_y = bar_y + bar_height + 5.0;
670 let legend_spacing = bar_width / 5.0;
671
672 for (i, label) in labels.iter().enumerate() {
673 let x = rect.left() + 5.0 + legend_spacing * (i as f32 + 0.5);
674 let pct = (self.counts[i] * 100).checked_div(total).unwrap_or(0);
675
676 painter.circle_filled(Pos2::new(x - 15.0, legend_y + 5.0), 4.0, colors[i]);
677 painter.text(
678 Pos2::new(x - 8.0, legend_y),
679 egui::Align2::LEFT_TOP,
680 format!("{}: {}%", label, pct),
681 egui::FontId::proportional(9.0),
682 theme.text_secondary,
683 );
684 }
685
686 if self.show_explanation {
688 let exp_y = legend_y + 18.0;
689 let exp_spacing = bar_width / 5.0;
690
691 for (i, desc) in descriptions.iter().enumerate() {
692 let x = rect.left() + 5.0 + exp_spacing * (i as f32 + 0.5);
693 painter.text(
694 Pos2::new(x, exp_y),
695 egui::Align2::CENTER_TOP,
696 *desc,
697 egui::FontId::proportional(7.5),
698 Color32::from_rgb(120, 120, 135),
699 );
700 }
701 }
702
703 response
704 }
705}
706
707pub struct BalanceBarChart {
709 pub data: Vec<(String, f64, Color32)>,
711 pub title: String,
713 pub bar_height: f32,
715}
716
717impl BalanceBarChart {
718 pub fn new(title: impl Into<String>) -> Self {
720 Self {
721 data: Vec::new(),
722 title: title.into(),
723 bar_height: 14.0,
724 }
725 }
726
727 pub fn add(mut self, label: impl Into<String>, value: f64, color: Color32) -> Self {
729 self.data.push((label.into(), value, color));
730 self
731 }
732
733 fn format_value(value: f64) -> String {
735 if value >= 1_000_000.0 {
736 format!("{:.1}M", value / 1_000_000.0)
737 } else if value >= 1_000.0 {
738 format!("{:.1}K", value / 1_000.0)
739 } else {
740 format!("{:.0}", value)
741 }
742 }
743
744 pub fn show(&self, ui: &mut egui::Ui, theme: &AccNetTheme) -> Response {
746 let width = ui.available_width();
747 let total_height = 18.0 + self.data.len() as f32 * (self.bar_height + 3.0);
748
749 let (response, painter) =
750 ui.allocate_painter(Vec2::new(width, total_height), Sense::hover());
751 let rect = response.rect;
752
753 painter.text(
755 Pos2::new(rect.left() + 5.0, rect.top()),
756 egui::Align2::LEFT_TOP,
757 &self.title,
758 egui::FontId::proportional(11.0),
759 theme.text_secondary,
760 );
761
762 if self.data.is_empty() {
763 return response;
764 }
765
766 let max_val = self
767 .data
768 .iter()
769 .map(|(_, v, _)| *v)
770 .fold(0.0_f64, f64::max)
771 .max(1.0);
772 let label_width = 55.0;
773 let value_width = 40.0;
774 let bar_area_width = width - label_width - value_width - 15.0;
775
776 for (i, (label, value, color)) in self.data.iter().enumerate() {
777 let y = rect.top() + 18.0 + i as f32 * (self.bar_height + 3.0);
778
779 painter.text(
781 Pos2::new(rect.left() + label_width - 3.0, y + self.bar_height / 2.0),
782 egui::Align2::RIGHT_CENTER,
783 label,
784 egui::FontId::proportional(9.0),
785 theme.text_primary,
786 );
787
788 let bar_bg = Rect::from_min_size(
790 Pos2::new(rect.left() + label_width, y),
791 Vec2::new(bar_area_width, self.bar_height),
792 );
793 painter.rect_filled(bar_bg, 2.0, Color32::from_rgb(35, 35, 45));
794
795 let bar_width = ((*value / max_val) as f32 * bar_area_width).max(2.0);
797 let bar_rect = Rect::from_min_size(
798 Pos2::new(rect.left() + label_width, y),
799 Vec2::new(bar_width, self.bar_height),
800 );
801 painter.rect_filled(bar_rect, 2.0, *color);
802
803 painter.text(
805 Pos2::new(
806 rect.left() + label_width + bar_area_width + 5.0,
807 y + self.bar_height / 2.0,
808 ),
809 egui::Align2::LEFT_CENTER,
810 Self::format_value(*value),
811 egui::FontId::proportional(9.0),
812 theme.text_secondary,
813 );
814 }
815
816 response
817 }
818}
819
820pub struct LiveTicker {
822 pub items: Vec<(String, String, Color32)>,
824}
825
826impl LiveTicker {
827 pub fn new() -> Self {
829 Self { items: Vec::new() }
830 }
831
832 pub fn add(
834 mut self,
835 label: impl Into<String>,
836 value: impl Into<String>,
837 color: Color32,
838 ) -> Self {
839 self.items.push((label.into(), value.into(), color));
840 self
841 }
842
843 pub fn show(&self, ui: &mut egui::Ui, _theme: &AccNetTheme) -> Response {
845 let width = ui.available_width();
846 let (response, painter) = ui.allocate_painter(Vec2::new(width, 30.0), Sense::hover());
847 let rect = response.rect;
848
849 if self.items.is_empty() {
850 return response;
851 }
852
853 let spacing = width / self.items.len() as f32;
854
855 for (i, (label, value, color)) in self.items.iter().enumerate() {
856 let x = rect.left() + spacing * (i as f32 + 0.5);
857
858 painter.text(
860 Pos2::new(x, rect.top()),
861 egui::Align2::CENTER_TOP,
862 value,
863 egui::FontId::proportional(16.0),
864 *color,
865 );
866
867 painter.text(
869 Pos2::new(x, rect.top() + 18.0),
870 egui::Align2::CENTER_TOP,
871 label,
872 egui::FontId::proportional(9.0),
873 Color32::from_rgb(150, 150, 160),
874 );
875 }
876
877 response
878 }
879}
880
881impl Default for LiveTicker {
882 fn default() -> Self {
883 Self::new()
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
892 fn test_benford_histogram() {
893 let counts = [30, 18, 12, 10, 8, 7, 6, 5, 4];
894 let hist = Histogram::benford(counts);
895 assert_eq!(hist.values.len(), 9);
896 assert_eq!(hist.labels.len(), 9);
897 }
898
899 #[test]
900 fn test_sparkline() {
901 let mut spark = Sparkline::new("Test");
902 for i in 0..150 {
903 spark.push(i as f32);
904 }
905 assert_eq!(spark.values.len(), 100);
906 }
907
908 #[test]
909 fn test_method_distribution() {
910 let dist = MethodDistribution::new([100, 50, 30, 15, 5]);
911 assert_eq!(dist.counts.iter().sum::<usize>(), 200);
912 }
913}