#[cfg(feature = "plot-svg")]
use plotters::prelude::*;
#[cfg(feature = "plot-svg")]
use plotters::series::LineSeries;
#[cfg(any(feature = "plot", feature = "plot-svg"))]
use crate::FigureState;
#[cfg(any(feature = "plot", feature = "plot-svg"))]
use crate::colormap::data_range;
#[cfg(any(feature = "plot", feature = "plot-svg"))]
use crate::colormap::{ColormapSpec, apply_colormap_spec};
#[cfg(feature = "plot")]
pub fn render_surf_ascii(
x_vals: &[f64],
z: &[f64],
nrows: usize,
ncols: usize,
state: &FigureState,
) {
let chart_height: usize = (crate::term_rows() / 2).max(10);
if nrows == 0 || ncols == 0 {
return;
}
let col_max: Vec<f64> = (0..ncols)
.map(|c| {
(0..nrows)
.map(|r| z[r * ncols + c])
.filter(|v| v.is_finite())
.fold(f64::NEG_INFINITY, f64::max)
})
.collect();
let (z_min, z_max) = data_range(z);
let z_range = z_max - z_min;
if let Some(t) = &state.title {
println!("{t}");
}
for row in (0..chart_height).rev() {
let threshold = z_min + z_range * (row as f64 / chart_height as f64);
let line: String = col_max
.iter()
.map(|&v| if v >= threshold { '#' } else { ' ' })
.collect();
println!("{line}");
}
if !x_vals.is_empty() {
let first = x_vals[0];
let last = x_vals[x_vals.len() - 1];
let width = ncols.saturating_sub(16);
println!("{first:<8.4}{:>width$}{last:>8.4}", "");
}
if let Some(xl) = &state.xlabel {
println!("x: {xl}");
}
if let Some(yl) = &state.ylabel {
println!("y: {yl}");
}
if let Some(zl) = &state.zlabel {
println!("z: {zl}");
}
}
#[cfg(feature = "plot-svg")]
#[allow(clippy::too_many_arguments)]
pub fn render_surf_file(
x_vals: &[f64],
y_vals: &[f64],
z: &[f64],
nrows: usize,
ncols: usize,
path: &str,
state: FigureState,
) -> Result<(), String> {
let canvas = state.canvas_size();
if path.ends_with(".svg") {
let root = SVGBackend::new(path, canvas).into_drawing_area();
draw_surface(x_vals, y_vals, z, nrows, ncols, &state, root, false)
} else if path.ends_with(".png") {
let root = BitMapBackend::new(path, canvas).into_drawing_area();
draw_surface(x_vals, y_vals, z, nrows, ncols, &state, root, false)
} else {
Err(format!("surf: unsupported format '{path}'"))
}
}
#[cfg(feature = "plot-svg")]
#[allow(clippy::too_many_arguments)]
pub fn render_mesh_file(
x_vals: &[f64],
y_vals: &[f64],
z: &[f64],
nrows: usize,
ncols: usize,
path: &str,
state: FigureState,
) -> Result<(), String> {
let canvas = state.canvas_size();
if path.ends_with(".svg") {
let root = SVGBackend::new(path, canvas).into_drawing_area();
draw_surface(x_vals, y_vals, z, nrows, ncols, &state, root, true)
} else if path.ends_with(".png") {
let root = BitMapBackend::new(path, canvas).into_drawing_area();
draw_surface(x_vals, y_vals, z, nrows, ncols, &state, root, true)
} else {
Err(format!("mesh: unsupported format '{path}'"))
}
}
#[cfg(feature = "plot-svg")]
#[allow(clippy::too_many_arguments)]
fn draw_surface<DB: DrawingBackend>(
x_vals: &[f64],
y_vals: &[f64],
z: &[f64],
nrows: usize,
ncols: usize,
state: &FigureState,
root: DrawingArea<DB, plotters::coord::Shift>,
wireframe: bool,
) -> Result<(), String>
where
DB::ErrorType: std::fmt::Display,
{
let (r, g, b) = state.effective_bg_rgb();
root.fill(&RGBColor(r, g, b)).map_err(|e| e.to_string())?;
if nrows == 0 || ncols == 0 {
return root.present().map_err(|e| e.to_string());
}
let (z_min, z_max) = data_range(z);
let x_lo = x_vals.iter().copied().fold(f64::INFINITY, f64::min);
let x_hi = x_vals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let y_lo = y_vals.iter().copied().fold(f64::INFINITY, f64::min);
let y_hi = y_vals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let (x_lo, x_hi) = state.xlim.unwrap_or((x_lo, x_hi));
let (y_lo, y_hi) = state.ylim.unwrap_or((y_lo, y_hi));
let (z_lo, z_hi) = state.zlim.unwrap_or((z_min, z_max));
let title = state.title.as_deref().unwrap_or("");
let default_spec = ColormapSpec::Named("viridis".to_string());
let cmap_spec = state.colormap.as_ref().unwrap_or(&default_spec);
let z_range = (z_hi - z_lo).max(f64::EPSILON);
let mut chart = ChartBuilder::on(&root)
.caption(title, ("sans-serif", 20))
.margin(30)
.build_cartesian_3d(x_lo..x_hi, z_lo..z_hi, y_lo..y_hi)
.map_err(|e| e.to_string())?;
chart.configure_axes().draw().map_err(|e| e.to_string())?;
for r in 0..nrows {
let y_val = y_vals[r];
let points: Vec<(f64, f64, f64)> = (0..ncols)
.map(|c| (x_vals[c], z[r * ncols + c], y_val))
.collect();
let z_avg = z_row_avg(z, r, ncols);
let t = ((z_avg - z_lo) / z_range).clamp(0.0, 1.0);
let (rr, gg, bb) = apply_colormap_spec(t, cmap_spec);
chart
.draw_series(LineSeries::new(points, RGBColor(rr, gg, bb)))
.map_err(|e| e.to_string())?;
}
if !wireframe {
for c in 0..ncols {
let x_val = x_vals[c];
let points: Vec<(f64, f64, f64)> = (0..nrows)
.map(|r| (x_val, z[r * ncols + c], y_vals[r]))
.collect();
let z_avg = z_col_avg(z, c, nrows, ncols);
let t = ((z_avg - z_lo) / z_range).clamp(0.0, 1.0);
let (rr, gg, bb) = apply_colormap_spec(t, cmap_spec);
chart
.draw_series(LineSeries::new(points, RGBColor(rr, gg, bb)))
.map_err(|e| e.to_string())?;
}
}
root.present().map_err(|e| e.to_string())?;
Ok(())
}
#[cfg(feature = "plot-svg")]
fn z_row_avg(z: &[f64], r: usize, ncols: usize) -> f64 {
let sum: f64 = (0..ncols).map(|c| z[r * ncols + c]).sum();
sum / ncols.max(1) as f64
}
#[cfg(feature = "plot-svg")]
fn z_col_avg(z: &[f64], c: usize, nrows: usize, ncols: usize) -> f64 {
let sum: f64 = (0..nrows).map(|r| z[r * ncols + c]).sum();
sum / nrows.max(1) as f64
}