use textplots::{Chart, Plot};
fn determine_scale(max_value: f64) -> (f64, &'static str) {
if max_value >= 1.0 {
(1.0, "")
} else if max_value >= 1e-3 {
(1e3, "m")
} else if max_value >= 1e-6 {
(1e6, "μ")
} else if max_value >= 1e-9 {
(1e9, "n")
} else {
(1e12, "p")
}
}
pub fn plot_values(
values: &[f64],
title: Option<&str>,
width: Option<usize>,
height: Option<usize>,
) -> Result<(), Box<dyn std::error::Error>> {
if values.is_empty() {
return Err("Cannot plot empty data".into());
}
let width = width.unwrap_or(140);
let height = height.unwrap_or(60);
let min_value = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_value = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let max_abs = max_value.abs().max(min_value.abs());
let (value_scale, value_unit) = determine_scale(max_abs);
let frame: Vec<(f32, f32)> = values
.iter()
.enumerate()
.map(|(i, &value)| (i as f32, (value * value_scale) as f32))
.collect();
let max_index = (values.len() - 1) as f32;
let scaled_min = min_value * value_scale;
let scaled_max = max_value * value_scale;
if let Some(title) = title {
println!("{}", title);
} else {
println!("Data Plot");
}
println!("X-axis: Sample Index | Y-axis: {}units", value_unit);
println!(
"Range: {} samples | Values: {:.3} to {:.3} {}units",
values.len(),
scaled_min,
scaled_max,
value_unit
);
println!("{}", "─".repeat(width));
Chart::new(width as u32, height as u32, 0.0, max_index)
.lineplot(&textplots::Shape::Lines(&frame))
.nice();
println!("Sample Index →");
Ok(())
}
pub fn plot_values_with_range(
values: &[f64],
y_min: f64,
y_max: f64,
title: Option<&str>,
width: Option<usize>,
height: Option<usize>,
) -> Result<(), Box<dyn std::error::Error>> {
if values.is_empty() {
return Err("Cannot plot empty data".into());
}
let width = width.unwrap_or(140);
let height = height.unwrap_or(60);
let max_abs = y_max.abs().max(y_min.abs());
let (value_scale, value_unit) = determine_scale(max_abs);
let frame: Vec<(f32, f32)> = values
.iter()
.enumerate()
.map(|(i, &value)| {
let clipped_value = value.max(y_min).min(y_max);
(i as f32, (clipped_value * value_scale) as f32)
})
.collect();
let max_index = (values.len() - 1) as f32;
let scaled_y_min = y_min * value_scale;
let scaled_y_max = y_max * value_scale;
if let Some(title) = title {
println!("{}", title);
} else {
println!("Data Plot (Clipped to Range)");
}
println!("X-axis: Sample Index | Y-axis: {}units", value_unit);
println!(
"Range: {} samples | Y-Range: {:.3} to {:.3} {}units (clipped)",
values.len(),
scaled_y_min,
scaled_y_max,
value_unit
);
println!("{}", "─".repeat(width));
Chart::new(width as u32, height as u32, 0.0, max_index)
.lineplot(&textplots::Shape::Lines(&frame))
.nice();
println!("Sample Index →");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_determine_scale() {
assert_eq!(determine_scale(5.0), (1.0, ""));
assert_eq!(determine_scale(0.005), (1e3, "m"));
assert_eq!(determine_scale(5e-6), (1e6, "μ"));
assert_eq!(determine_scale(5e-9), (1e9, "n"));
assert_eq!(determine_scale(5e-12), (1e12, "p"));
}
#[test]
fn test_plot_values_basic() {
let data = vec![1.0, 2.0, 3.0, 2.0, 1.0];
assert!(plot_values(&data, Some("Test Plot"), None, None).is_ok());
}
#[test]
fn test_plot_empty_data() {
let data: Vec<f64> = vec![];
assert!(plot_values(&data, None, None, None).is_err());
}
#[test]
fn test_plot_small_values() {
let data = vec![1e-12, 2e-12, 1.5e-12, 3e-12];
assert!(plot_values(&data, Some("Picoamp Current"), None, None).is_ok());
}
}