use std::collections::VecDeque;
use egui::{Color32, Pos2, Sense, Shape, Stroke, Vec2, pos2};
use crate::viz::color::hash_color;
use crate::viz::layout::PlotLayout;
pub struct Metric<R> {
pub name: &'static str,
pub unit: &'static str,
pub get: fn(&R) -> f64,
pub integer: bool,
pub description: &'static str,
}
impl<R> Copy for Metric<R> {}
impl<R> Clone for Metric<R> {
fn clone(&self) -> Self {
*self
}
}
impl<R> Metric<R> {
pub const fn new(name: &'static str, get: fn(&R) -> f64, unit: &'static str) -> Self {
Self {
name,
unit,
get,
integer: false,
description: "",
}
}
pub const fn as_integer(mut self) -> Self {
self.integer = true;
self
}
pub const fn describe(mut self, description: &'static str) -> Self {
self.description = description;
self
}
}
pub(super) const ROW_H: f32 = 32.0;
pub(super) const LABEL_COL_W: f32 = 96.0;
pub(super) const STAT_COL_W: f32 = 96.0;
#[derive(Clone, Copy)]
pub(super) struct RowView {
pub pinned: Option<usize>,
pub selection: Option<(usize, usize)>,
pub plot_w: f32,
pub history_capacity: usize,
}
pub(super) struct RowSpec<'a> {
pub name: &'a str,
pub unit: &'a str,
pub integer: bool,
pub description: &'a str,
}
pub(super) fn metric_row<R>(
ui: &mut egui::Ui,
m: &Metric<R>,
history: &VecDeque<R>,
view: RowView,
contribution: Option<f64>,
) {
let samples: Vec<f64> = history.iter().map(m.get).collect();
let spec = RowSpec {
name: m.name,
unit: m.unit,
integer: m.integer,
description: m.description,
};
plot_series(ui, &spec, &samples, view, contribution);
}
pub(super) fn plot_series(
ui: &mut egui::Ui,
spec: &RowSpec,
samples: &[f64],
view: RowView,
contribution: Option<f64>,
) {
let RowSpec {
name,
unit,
integer,
description,
} = *spec;
let RowView {
pinned,
selection,
plot_w,
history_capacity,
} = view;
ui.horizontal(|ui| {
let label = ui.add_sized(
Vec2::new(LABEL_COL_W, ROW_H),
egui::Label::new(name).sense(Sense::hover()),
);
if !description.is_empty() {
label.on_hover_text(description);
}
let (rect, _) = ui.allocate_exact_size(Vec2::new(plot_w, ROW_H), Sense::hover());
let painter = ui.painter_at(rect);
painter.rect_filled(rect, 1.0, Color32::from_gray(22));
if samples.is_empty() {
ui.add_sized(
Vec2::new(STAT_COL_W, ROW_H),
egui::Label::new(format!("— {unit}")),
);
return;
}
let layout = PlotLayout::new(rect, samples.len(), selection, history_capacity);
draw_line_plot(&painter, rect, samples, &layout, hash_color(name));
draw_cursor(&painter, rect, &layout, pinned);
let mut label = stat_label(unit, integer, samples, pinned, selection);
if let Some(c) = contribution {
label.push_str(&format!(" (+{c:.2})"));
}
ui.add_sized(Vec2::new(STAT_COL_W, ROW_H), egui::Label::new(label));
});
}
fn draw_line_plot(
painter: &egui::Painter,
rect: egui::Rect,
samples: &[f64],
layout: &PlotLayout,
color: Color32,
) {
let max = samples[layout.range.clone()]
.iter()
.copied()
.fold(0.0_f64, f64::max)
.max(1e-9) as f32;
let usable_h = rect.height() - 4.0;
let pts: Vec<Pos2> = layout
.range
.clone()
.map(|i| {
let x = layout.base_x
+ (i - layout.range.start) as f32 * layout.slot_w
+ layout.slot_w * 0.5;
let y = rect.bottom() - 2.0 - (samples[i] as f32 / max) * usable_h;
pos2(x, y)
})
.collect();
painter.add(Shape::line(pts, Stroke::new(1.4, color)));
}
fn draw_cursor(
painter: &egui::Painter,
rect: egui::Rect,
layout: &PlotLayout,
pinned: Option<usize>,
) {
let Some(idx) = pinned else {
return;
};
let Some(x) = layout.cursor_x(idx) else {
return;
};
painter.line_segment(
[pos2(x, rect.top()), pos2(x, rect.bottom())],
Stroke::new(1.0, Color32::WHITE),
);
}
fn stat_label(
unit: &str,
integer: bool,
samples: &[f64],
pinned: Option<usize>,
selection: Option<(usize, usize)>,
) -> String {
if let Some((lo, hi)) = selection {
let hi = hi.min(samples.len().saturating_sub(1));
if lo > hi {
return format!("— {unit}");
}
let n = (hi - lo + 1).max(1);
let mean: f64 = samples[lo..=hi].iter().sum::<f64>() / n as f64;
let peak: f64 = samples[lo..=hi].iter().copied().fold(f64::MIN, f64::max);
return if integer {
format!("μ {} · max {} {unit}", mean as u64, peak as u64)
} else {
format!("μ {mean:.2} · max {peak:.2} {unit}")
};
}
let v = match pinned.and_then(|i| samples.get(i)) {
Some(v) => *v,
None => *samples.last().unwrap(),
};
if integer {
format!("{} {unit}", v as u64)
} else {
format!("{v:.2} {unit}")
}
}