use eframe::{egui, NativeOptions};
use liveplot::{channel_plot, LivePlotPanel, PlotPoint, PlotSink, Trace, TracesController};
use std::time::Duration;
const COLS: usize = 20;
const ROWS: usize = 15;
const TOTAL: usize = COLS * ROWS;
const UPDATE_MS: u64 = 16;
const DEFAULT_SAMPLES_PER_SEC: f64 = 60.0;
const DEFAULT_SINE_HZ: f64 = 1.5;
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> [u8; 3] {
let h6 = (h.fract() * 6.0).max(0.0);
let i = h6.floor() as i32;
let f = h6 - (i as f64);
let p = v * (1.0 - s);
let q = v * (1.0 - f * s);
let t = v * (1.0 - (1.0 - f) * s);
let (r, g, b) = match i.rem_euclid(6) {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
5 => (v, p, q),
_ => (v, p, q),
};
[
(r.clamp(0.0, 1.0) * 255.0) as u8,
(g.clamp(0.0, 1.0) * 255.0) as u8,
(b.clamp(0.0, 1.0) * 255.0) as u8,
]
}
struct TinyPlot {
sink: PlotSink,
trace: Trace,
phase_cycles: f64,
}
impl TinyPlot {
fn new(label: &str, phase_cycles: f64, color_hint: [u8; 3]) -> (Self, LivePlotPanel) {
let (sink, rx) = channel_plot();
let trace = sink.create_trace(label, None);
let mut panel = LivePlotPanel::new(rx);
panel.traces_data.max_points = 2_000;
panel.compact = true;
panel.top_bar_buttons = Some(vec![]);
panel.sidebar_buttons = Some(vec![]);
panel.min_height_for_top_bar = 0.0;
panel.min_width_for_sidebar = 0.0;
panel.min_height_for_sidebar = 0.0;
for s in panel.liveplot_panel.get_data_mut() {
s.time_window = 4.0;
s.force_hide_legend = true;
}
let ctrl = TracesController::new();
panel.set_controllers(None, None, Some(ctrl.clone()), None, None, None, None);
ctrl.request_set_color(label, color_hint);
(
Self {
sink,
trace,
phase_cycles,
},
panel,
)
}
fn feed(&self, t: f64, freq_hz: f64) {
const TAU: f64 = std::f64::consts::TAU;
let y = ((t * freq_hz + self.phase_cycles) * TAU).sin();
let _ = self.sink.send_point(&self.trace, PlotPoint { x: t, y });
}
}
struct LotsOfTinyPlotsApp {
plots: Vec<(TinyPlot, LivePlotPanel)>,
last_window_size: egui::Vec2,
samples_per_second: f64,
sine_hz: f64,
last_sample_time: f64,
}
impl LotsOfTinyPlotsApp {
fn new(samples_per_second: f64, sine_hz: f64) -> Self {
let now_us = chrono::Utc::now().timestamp_micros();
let start_t = (now_us as f64) * 1e-6;
let mut plots = Vec::with_capacity(TOTAL);
for i in 0..TOTAL {
let phase = (i as f64) / (TOTAL as f64); let hue = (i as f64) / (TOTAL as f64);
let col = hsv_to_rgb(hue, 0.85, 0.9);
let label = format!("sine_{:03}", i);
let (p, mp) = TinyPlot::new(&label, phase, col);
plots.push((p, mp));
}
Self {
plots,
last_window_size: egui::Vec2::ZERO,
samples_per_second,
sine_hz,
last_sample_time: start_t,
}
}
fn render_grid(&mut self, ui: &mut egui::Ui) {
let avail = ui.available_size();
let (grid_rect, _) = ui.allocate_exact_size(avail, egui::Sense::hover());
let cell_w = (grid_rect.width() / COLS as f32).floor().max(1.0);
let cell_h = (grid_rect.height() / ROWS as f32).floor().max(1.0);
for row in 0..ROWS {
for col in 0..COLS {
let idx = row * COLS + col;
let x = (grid_rect.left() + col as f32 * cell_w).round();
let y = (grid_rect.top() + row as f32 * cell_h).round();
let cell_rect =
egui::Rect::from_min_size(egui::pos2(x, y), egui::vec2(cell_w, cell_h));
let (_p, panel) = &mut self.plots[idx];
let mut child_ui =
ui.new_child(egui::UiBuilder::new().id_salt(idx).max_rect(cell_rect));
panel.update_embedded(&mut child_ui);
}
}
}
}
impl eframe::App for LotsOfTinyPlotsApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let now_us = chrono::Utc::now().timestamp_micros();
let t = (now_us as f64) * 1e-6;
let sample_interval = 1.0 / self.samples_per_second;
let mut next_time = self.last_sample_time + sample_interval;
while next_time <= t {
for (p, _) in &self.plots {
p.feed(next_time, self.sine_hz);
}
self.last_sample_time = next_time;
next_time += sample_interval;
}
let current_size = ctx.input(|i| i.viewport_rect().size());
if self.last_window_size != egui::Vec2::ZERO && self.last_window_size != current_size {
for (_p, panel) in &mut self.plots {
panel.fit_all_bounds();
}
}
self.last_window_size = current_size;
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Lots of tiny sine plots — 20 × 15");
ui.label(format!(
"Each plot shows the same sine wave shifted by phase; every trace has its own color. — samples: {:.1} Hz, sine: {:.3} Hz",
self.samples_per_second,
self.sine_hz
));
ui.add_space(6.0);
self.render_grid(ui);
});
ctx.request_repaint_after(Duration::from_millis(UPDATE_MS));
}
}
fn main() -> eframe::Result<()> {
let mut samples_per_second = DEFAULT_SAMPLES_PER_SEC;
let mut sine_hz = DEFAULT_SINE_HZ;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "-s" || arg == "--samples-per-second" {
if let Some(val) = args.next() {
match val.parse::<f64>() {
Ok(v) => samples_per_second = v,
Err(_) => eprintln!("invalid value for {}: {}", arg, val),
}
}
} else if arg == "-h" || arg == "--hz" {
if let Some(val) = args.next() {
match val.parse::<f64>() {
Ok(v) => sine_hz = v,
Err(_) => eprintln!("invalid value for {}: {}", arg, val),
}
}
} else if let Some(rest) = arg.strip_prefix("--samples-per-second=") {
match rest.parse::<f64>() {
Ok(v) => samples_per_second = v,
Err(_) => eprintln!("invalid value for --samples-per-second: {}", rest),
}
} else if let Some(rest) = arg.strip_prefix("--hz=") {
match rest.parse::<f64>() {
Ok(v) => sine_hz = v,
Err(_) => eprintln!("invalid value for --hz: {}", rest),
}
} else {
}
}
let app = LotsOfTinyPlotsApp::new(samples_per_second, sine_hz);
eframe::run_native(
"Lots of tiny plots",
NativeOptions::default(),
Box::new(|_cc| Ok(Box::new(app))),
)
}