use steel::SteelVal;
pub(crate) struct PlotParams {
pub style: gantz_ui::PlotStyle,
pub color: Option<[u8; 4]>,
pub grid: bool,
pub axes: bool,
pub interactive: bool,
pub y_min: Option<f32>,
pub y_max: Option<f32>,
}
pub(crate) fn plot_body(
params: &PlotParams,
channels: &[Vec<f64>],
plot_id: egui::Id,
size: egui::Vec2,
ui: &mut egui::Ui,
) -> egui::Response {
if channels.len() <= 1 {
let ys = channels.first().map(Vec::as_slice).unwrap_or(&[]);
return plot_channel(params, ys, plot_id, size, ui);
}
let sub_h = size.y / channels.len() as f32;
ui.vertical(|ui| {
let mut resp: Option<egui::Response> = None;
for (i, ch) in channels.iter().enumerate() {
let r = plot_channel(params, ch, plot_id.with(i), egui::vec2(size.x, sub_h), ui);
resp = Some(match resp.take() {
Some(prev) => prev.union(r),
None => r,
});
}
resp.expect("at least two channels")
})
.inner
}
fn plot_channel(
params: &PlotParams,
ys: &[f64],
plot_id: egui::Id,
size: egui::Vec2,
ui: &mut egui::Ui,
) -> egui::Response {
let color = resolve_color(params.color, ui);
let plot_style = params.style;
let interactive = params.interactive;
let bounds = value_bounds(ys, plot_style, params.y_min, params.y_max);
let mut plot = egui_plot::Plot::new(plot_id)
.width(size.x)
.height(size.y)
.show_background(false)
.show_axes(egui::Vec2b::new(params.axes, params.axes))
.show_grid(egui::Vec2b::new(params.grid, params.grid))
.allow_drag(false)
.allow_zoom(false)
.allow_scroll(false)
.allow_boxed_zoom(false)
.sense(egui::Sense::hover());
if !interactive {
plot = plot.cursor_color(egui::Color32::TRANSPARENT);
}
let plot_resp = plot
.show(ui, |plot_ui| {
match plot_style {
gantz_ui::PlotStyle::Bars => {
let bars = ys
.iter()
.enumerate()
.map(|(i, &y)| {
egui_plot::Bar::new(i as f64, y)
.width(1.0)
.fill(color)
.stroke(egui::Stroke::NONE)
})
.collect();
plot_ui.bar_chart(egui_plot::BarChart::new("", bars).allow_hover(interactive));
}
gantz_ui::PlotStyle::Line => {
let points = egui_plot::PlotPoints::from_ys_f64(ys);
plot_ui.line(
egui_plot::Line::new("", points)
.color(color)
.allow_hover(interactive),
);
}
}
let ([xlo, ylo], [xhi, yhi]) = bounds;
plot_ui.set_plot_bounds_x(xlo..=xhi);
plot_ui.set_plot_bounds_y(ylo..=yhi);
})
.response;
if !interactive && plot_resp.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
}
plot_resp
}
fn value_bounds(
ys: &[f64],
style: gantz_ui::PlotStyle,
y_min: Option<f32>,
y_max: Option<f32>,
) -> ([f64; 2], [f64; 2]) {
let n = ys.len() as f64;
let (xlo, xhi) = match style {
gantz_ui::PlotStyle::Bars => (-0.5, (n - 0.5).max(0.5)),
gantz_ui::PlotStyle::Line => (0.0, (n - 1.0).max(1.0)),
};
let (dmin, dmax) = ys
.iter()
.copied()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
(lo.min(v), hi.max(v))
});
let (mut ylo, mut yhi) = if dmin <= dmax {
match style {
gantz_ui::PlotStyle::Bars => (dmin.min(0.0), dmax.max(0.0)),
gantz_ui::PlotStyle::Line => (dmin, dmax),
}
} else {
(0.0, 1.0)
};
if (yhi - ylo).abs() < 1e-9 {
ylo -= 1.0;
yhi += 1.0;
}
if let Some(v) = y_min {
ylo = v as f64;
}
if let Some(v) = y_max {
yhi = v as f64;
}
([xlo, ylo], [xhi, yhi])
}
pub(crate) fn resolve_color(color: Option<[u8; 4]>, ui: &egui::Ui) -> egui::Color32 {
match color {
Some([r, g, b, a]) => egui::Color32::from_rgba_unmultiplied(r, g, b, a),
None => ui.visuals().strong_text_color(),
}
}
pub(crate) fn split_channels(val: &SteelVal) -> Vec<Vec<f64>> {
let elems: Option<Vec<&SteelVal>> = match val {
SteelVal::ListV(list) => Some(list.iter().collect()),
SteelVal::VectorV(vec) => Some(vec.iter().collect()),
_ => None,
};
match elems {
Some(elems) if elems.iter().any(|v| is_container(v)) => {
elems.iter().map(|v| channel_numerics(v)).collect()
}
Some(elems) => vec![elems.iter().filter_map(|v| steel_num(v)).collect()],
None => vec![steel_num(val).into_iter().collect()],
}
}
pub(crate) fn is_container(v: &SteelVal) -> bool {
matches!(v, SteelVal::ListV(_) | SteelVal::VectorV(_))
}
fn channel_numerics(val: &SteelVal) -> Vec<f64> {
match val {
SteelVal::ListV(list) => list.iter().filter_map(steel_num).collect(),
SteelVal::VectorV(vec) => vec.iter().filter_map(steel_num).collect(),
other => steel_num(other).into_iter().collect(),
}
}
pub(crate) fn steel_num(val: &SteelVal) -> Option<f64> {
match val {
SteelVal::NumV(f) => Some(*f),
SteelVal::IntV(i) => Some(*i as f64),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_channels_by_shape() {
let num = |n: f64| SteelVal::NumV(n);
let list = |xs: Vec<SteelVal>| SteelVal::ListV(xs.into_iter().collect());
let vector = |xs: Vec<SteelVal>| SteelVal::VectorV(xs.into_iter().collect());
assert_eq!(
split_channels(&list(vec![num(1.0), num(2.0), num(3.0)])),
vec![vec![1.0, 2.0, 3.0]],
);
assert_eq!(
split_channels(&vector(vec![num(1.0), num(2.0), num(3.0)])),
vec![vec![1.0, 2.0, 3.0]],
);
assert_eq!(split_channels(&num(7.0)), vec![vec![7.0]]);
let expected = vec![vec![1.0, 3.0], vec![2.0, 4.0]];
assert_eq!(
split_channels(&list(vec![
list(vec![num(1.0), num(3.0)]),
list(vec![num(2.0), num(4.0)]),
])),
expected,
);
assert_eq!(
split_channels(&vector(vec![
vector(vec![num(1.0), num(3.0)]),
vector(vec![num(2.0), num(4.0)]),
])),
expected,
);
assert_eq!(
split_channels(&list(vec![
vector(vec![num(1.0), num(3.0)]),
vector(vec![num(2.0), num(4.0)]),
])),
expected,
);
}
#[test]
fn value_bounds_by_style() {
use gantz_ui::PlotStyle::{Bars, Line};
let ([xlo, ylo], [xhi, yhi]) = value_bounds(&[1.0, 2.0, 3.0], Bars, None, None);
assert_eq!((xlo, xhi), (-0.5, 2.5));
assert_eq!((ylo, yhi), (0.0, 3.0));
let ([xlo, ylo], [xhi, yhi]) = value_bounds(&[1.0, 2.0, 3.0], Line, None, None);
assert_eq!((xlo, xhi), (0.0, 2.0));
assert_eq!((ylo, yhi), (1.0, 3.0));
let ([_, ylo], [_, yhi]) = value_bounds(&[2.0, 2.0], Line, None, None);
assert_eq!((ylo, yhi), (1.0, 3.0));
let ([_, ylo], [_, yhi]) = value_bounds(&[1.0, 2.0], Line, Some(-1.0), Some(1.0));
assert_eq!((ylo, yhi), (-1.0, 1.0));
let ([xlo, ylo], [xhi, yhi]) = value_bounds(&[], Bars, None, None);
assert_eq!((xlo, xhi), (-0.5, 0.5));
assert_eq!((ylo, yhi), (0.0, 1.0));
}
}