1use textplots::{Chart, Plot};
2
3fn determine_scale(max_value: f64) -> (f64, &'static str) {
5 if max_value >= 1.0 {
6 (1.0, "")
7 } else if max_value >= 1e-3 {
8 (1e3, "m")
9 } else if max_value >= 1e-6 {
10 (1e6, "μ")
11 } else if max_value >= 1e-9 {
12 (1e9, "n")
13 } else {
14 (1e12, "p")
15 }
16}
17
18pub fn plot_values(
34 values: &[f64],
35 title: Option<&str>,
36 width: Option<usize>,
37 height: Option<usize>,
38) -> Result<(), Box<dyn std::error::Error>> {
39 if values.is_empty() {
40 return Err("Cannot plot empty data".into());
41 }
42
43 let width = width.unwrap_or(140);
44 let height = height.unwrap_or(60);
45
46 let min_value = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
48 let max_value = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
49 let max_abs = max_value.abs().max(min_value.abs());
50
51 let (value_scale, value_unit) = determine_scale(max_abs);
53
54 let frame: Vec<(f32, f32)> = values
56 .iter()
57 .enumerate()
58 .map(|(i, &value)| (i as f32, (value * value_scale) as f32))
59 .collect();
60
61 let max_index = (values.len() - 1) as f32;
62 let scaled_min = min_value * value_scale;
63 let scaled_max = max_value * value_scale;
64
65 if let Some(title) = title {
67 println!("{}", title);
68 } else {
69 println!("Data Plot");
70 }
71 println!("X-axis: Sample Index | Y-axis: {}units", value_unit);
72 println!(
73 "Range: {} samples | Values: {:.3} to {:.3} {}units",
74 values.len(),
75 scaled_min,
76 scaled_max,
77 value_unit
78 );
79 println!("{}", "─".repeat(width));
80
81 Chart::new(width as u32, height as u32, 0.0, max_index)
83 .lineplot(&textplots::Shape::Lines(&frame))
84 .nice();
85
86 println!("Sample Index →");
87
88 Ok(())
89}
90
91pub fn plot_values_with_range(
94 values: &[f64],
95 y_min: f64,
96 y_max: f64,
97 title: Option<&str>,
98 width: Option<usize>,
99 height: Option<usize>,
100) -> Result<(), Box<dyn std::error::Error>> {
101 if values.is_empty() {
102 return Err("Cannot plot empty data".into());
103 }
104
105 let width = width.unwrap_or(140);
106 let height = height.unwrap_or(60);
107 let max_abs = y_max.abs().max(y_min.abs());
108
109 let (value_scale, value_unit) = determine_scale(max_abs);
111
112 let frame: Vec<(f32, f32)> = values
114 .iter()
115 .enumerate()
116 .map(|(i, &value)| {
117 let clipped_value = value.max(y_min).min(y_max);
118 (i as f32, (clipped_value * value_scale) as f32)
119 })
120 .collect();
121
122 let max_index = (values.len() - 1) as f32;
123 let scaled_y_min = y_min * value_scale;
124 let scaled_y_max = y_max * value_scale;
125
126 if let Some(title) = title {
128 println!("{}", title);
129 } else {
130 println!("Data Plot (Clipped to Range)");
131 }
132 println!("X-axis: Sample Index | Y-axis: {}units", value_unit);
133 println!(
134 "Range: {} samples | Y-Range: {:.3} to {:.3} {}units (clipped)",
135 values.len(),
136 scaled_y_min,
137 scaled_y_max,
138 value_unit
139 );
140 println!("{}", "─".repeat(width));
141
142 Chart::new(width as u32, height as u32, 0.0, max_index)
144 .lineplot(&textplots::Shape::Lines(&frame))
145 .nice();
146
147 println!("Sample Index →");
148
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_determine_scale() {
158 assert_eq!(determine_scale(5.0), (1.0, ""));
160 assert_eq!(determine_scale(0.005), (1e3, "m"));
161 assert_eq!(determine_scale(5e-6), (1e6, "μ"));
162 assert_eq!(determine_scale(5e-9), (1e9, "n"));
163 assert_eq!(determine_scale(5e-12), (1e12, "p"));
164 }
165
166 #[test]
167 fn test_plot_values_basic() {
168 let data = vec![1.0, 2.0, 3.0, 2.0, 1.0];
169 assert!(plot_values(&data, Some("Test Plot"), None, None).is_ok());
171 }
172
173 #[test]
174 fn test_plot_empty_data() {
175 let data: Vec<f64> = vec![];
176 assert!(plot_values(&data, None, None, None).is_err());
177 }
178
179 #[test]
180 fn test_plot_small_values() {
181 let data = vec![1e-12, 2e-12, 1.5e-12, 3e-12];
182 assert!(plot_values(&data, Some("Picoamp Current"), None, None).is_ok());
184 }
185}