use textplots::{Chart, Plot, Shape};
use crate::FigureState;
fn chart_canvas() -> (u32, u32) {
let cols = crate::term_cols();
let rows = crate::term_rows();
((cols * 2) as u32, ((rows * 2).clamp(20, 120)) as u32)
}
fn chart_x_bounds(x: &[f64], state: &FigureState) -> (f32, f32) {
state
.xlim
.map(|(lo, hi)| (lo as f32, hi as f32))
.unwrap_or_else(|| x_bounds(x))
}
fn print_header(state: &FigureState) {
if let Some(t) = &state.title {
println!("{t}");
}
}
pub fn render_line(x: &[f64], y: &[f64], state: FigureState) {
let data = to_f32_pairs(x, y);
let (x_min, x_max) = chart_x_bounds(x, &state);
let (cw, ch) = chart_canvas();
print_header(&state);
Chart::new(cw, ch, x_min, x_max)
.lineplot(&Shape::Lines(&data))
.display();
print_labels(&state);
}
pub fn render_scatter(x: &[f64], y: &[f64], state: FigureState) {
let data = to_f32_pairs(x, y);
let (x_min, x_max) = chart_x_bounds(x, &state);
let (cw, ch) = chart_canvas();
print_header(&state);
Chart::new(cw, ch, x_min, x_max)
.lineplot(&Shape::Points(&data))
.display();
print_labels(&state);
}
pub fn render_bar(x: &[f64], y: &[f64], state: FigureState) {
let data = to_f32_pairs(x, y);
let (x_min, x_max) = chart_x_bounds(x, &state);
let (cw, ch) = chart_canvas();
print_header(&state);
Chart::new(cw, ch, x_min, x_max)
.lineplot(&Shape::Bars(&data))
.display();
print_labels(&state);
}
pub fn render_stem(x: &[f64], y: &[f64], state: FigureState) {
let data = to_f32_pairs(x, y);
let (x_min, x_max) = chart_x_bounds(x, &state);
let (cw, ch) = chart_canvas();
print_header(&state);
Chart::new(cw, ch, x_min, x_max)
.lineplot(&Shape::Bars(&data))
.display();
print_labels(&state);
}
pub fn render_fill(x: &[f64], y: &[f64], state: FigureState) {
if x.is_empty() {
return;
}
let data = to_f32_pairs(x, y);
let (x_min, x_max) = chart_x_bounds(x, &state);
let y_min = y.iter().copied().fold(f64::INFINITY, f64::min) as f32;
let y_max = y.iter().copied().fold(f64::NEG_INFINITY, f64::max) as f32;
print_header(&state);
let cols: usize = crate::term_cols().saturating_sub(2).max(10);
let rows: usize = (crate::term_rows() / 2).max(5);
let col_step = (x_max - x_min) / cols as f32;
let row_step = if (y_max - y_min).abs() > f32::EPSILON {
(y_max - y_min) / rows as f32
} else {
1.0
};
for r in (0..rows).rev() {
let cy = y_min + (r as f32 + 0.5) * row_step;
let mut row_str = String::new();
for c in 0..cols {
let cx = x_min + (c as f32 + 0.5) * col_step;
if point_in_polygon(cx, cy, &data) {
row_str.push('░');
} else {
row_str.push(' ');
}
}
println!("|{row_str}|");
}
print_labels(&state);
}
pub fn render_area(x: &[f64], y: &[f64], state: FigureState) {
if x.is_empty() {
return;
}
let mut poly_x = x.to_vec();
let mut poly_y = y.to_vec();
poly_x.push(*x.last().unwrap());
poly_y.push(0.0);
poly_x.push(x[0]);
poly_y.push(0.0);
render_fill(&poly_x, &poly_y, state);
}
fn point_in_polygon(px: f32, py: f32, polygon: &[(f32, f32)]) -> bool {
let n = polygon.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = polygon[i];
let (xj, yj) = polygon[j];
if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
fn print_labels(state: &FigureState) {
if let Some(xl) = &state.xlabel {
println!("x: {xl}");
}
if let Some(yl) = &state.ylabel {
println!("y: {yl}");
}
}
fn to_f32_pairs(x: &[f64], y: &[f64]) -> Vec<(f32, f32)> {
x.iter()
.zip(y.iter())
.map(|(&xi, &yi)| (xi as f32, yi as f32))
.collect()
}
fn x_bounds(xs: &[f64]) -> (f32, f32) {
let lo = xs.iter().copied().fold(f64::INFINITY, f64::min) as f32;
let hi = xs.iter().copied().fold(f64::NEG_INFINITY, f64::max) as f32;
if (hi - lo).abs() < f32::EPSILON {
(lo - 1.0, lo + 1.0)
} else {
(lo, hi)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_line_no_panic() {
let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
render_line(&x, &y, FigureState::default());
}
#[test]
fn test_render_scatter_no_panic() {
let x: Vec<f64> = (0..5).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| xi * 2.0).collect();
render_scatter(&x, &y, FigureState::default());
}
#[test]
fn test_single_point_no_panic() {
render_line(&[5.0], &[3.0], FigureState::default());
}
#[test]
fn test_figure_state_label_values() {
let state = FigureState {
title: Some("My Plot".into()),
xlabel: Some("time".into()),
ylabel: Some("value".into()),
..FigureState::default()
};
assert_eq!(state.title.as_deref(), Some("My Plot"));
assert_eq!(state.xlabel.as_deref(), Some("time"));
assert_eq!(state.ylabel.as_deref(), Some("value"));
}
}