Skip to main content

lots_of_tiny_plots/
lots_of_tiny_plots.rs

1//! Example: Lots of tiny plots (20×15 grid)
2//!
3//! What it demonstrates
4//! - Embedding many `LivePlotPanel` instances in a grid
5//! - Each plot shows the same sine waveform but shifted in phase
6//! - Each trace receives a unique color (HSV wheel)
7//!
8//! How to run
9//! ```bash
10//! # default
11//! cargo run --example lots_of_tiny_plots --
12//! # set samples-per-second (Hz) and sine frequency (Hz)
13//! cargo run --example lots_of_tiny_plots -- -s 10 -h 2.5
14//! cargo run --example lots_of_tiny_plots -- --samples-per-second 10.0 --hz 2.5
15//! ```
16
17use eframe::{egui, NativeOptions};
18use liveplot::{channel_plot, LivePlotPanel, PlotPoint, PlotSink, Trace, TracesController};
19use std::time::Duration;
20
21const COLS: usize = 20;
22const ROWS: usize = 15;
23const TOTAL: usize = COLS * ROWS;
24
25/// Repaint interval in milliseconds (controls UI repaint frequency; independent of sampling)
26const UPDATE_MS: u64 = 16;
27/// Default samples-per-second (Hz) used when feeding points into the plots.
28/// A value near the UI refresh rate keeps things smooth.
29const DEFAULT_SAMPLES_PER_SEC: f64 = 60.0;
30/// Default frequency of the sine wave itself (cycles per second).
31const DEFAULT_SINE_HZ: f64 = 1.5;
32
33fn hsv_to_rgb(h: f64, s: f64, v: f64) -> [u8; 3] {
34    // h in [0,1), s,v in [0,1]
35    let h6 = (h.fract() * 6.0).max(0.0);
36    let i = h6.floor() as i32;
37    let f = h6 - (i as f64);
38    let p = v * (1.0 - s);
39    let q = v * (1.0 - f * s);
40    let t = v * (1.0 - (1.0 - f) * s);
41    let (r, g, b) = match i.rem_euclid(6) {
42        0 => (v, t, p),
43        1 => (q, v, p),
44        2 => (p, v, t),
45        3 => (p, q, v),
46        4 => (t, p, v),
47        5 => (v, p, q),
48        _ => (v, p, q),
49    };
50    [
51        (r.clamp(0.0, 1.0) * 255.0) as u8,
52        (g.clamp(0.0, 1.0) * 255.0) as u8,
53        (b.clamp(0.0, 1.0) * 255.0) as u8,
54    ]
55}
56
57struct TinyPlot {
58    sink: PlotSink,
59    trace: Trace,
60    phase_cycles: f64,
61}
62
63impl TinyPlot {
64    fn new(label: &str, phase_cycles: f64, color_hint: [u8; 3]) -> (Self, LivePlotPanel) {
65        let (sink, rx) = channel_plot();
66        let trace = sink.create_trace(label, None);
67
68        let mut panel = LivePlotPanel::new(rx);
69        // keep buffers small for many plots
70        panel.traces_data.max_points = 2_000;
71        // strip all borders/margins so each cell is pure plot
72        panel.compact = true;
73        // suppress top-bar buttons so nothing competes for the tiny space
74        panel.top_bar_buttons = Some(vec![]);
75        panel.sidebar_buttons = Some(vec![]);
76        panel.min_height_for_top_bar = 0.0;
77        panel.min_width_for_sidebar = 0.0;
78        panel.min_height_for_sidebar = 0.0;
79        for s in panel.liveplot_panel.get_data_mut() {
80            s.time_window = 4.0;
81            // Force-hide the legend overlay to avoid wasting space in tiny cells
82            s.force_hide_legend = true;
83        }
84
85        // Attach a traces controller so we can request a color for this trace
86        let ctrl = TracesController::new();
87        panel.set_controllers(None, None, Some(ctrl.clone()), None, None, None, None);
88        ctrl.request_set_color(label, color_hint);
89
90        (
91            Self {
92                sink,
93                trace,
94                phase_cycles,
95            },
96            panel,
97        )
98    }
99
100    fn feed(&self, t: f64, freq_hz: f64) {
101        const TAU: f64 = std::f64::consts::TAU;
102        let y = ((t * freq_hz + self.phase_cycles) * TAU).sin();
103        let _ = self.sink.send_point(&self.trace, PlotPoint { x: t, y });
104    }
105}
106
107struct LotsOfTinyPlotsApp {
108    plots: Vec<(TinyPlot, LivePlotPanel)>,
109    /// Last known window size; used to detect resizes and trigger auto-fit.
110    last_window_size: egui::Vec2,
111    /// Sample rate in Hz (samples per second) for the sine waveform fed to every plot.
112    samples_per_second: f64,
113    /// Frequency of the sine wave itself (cycles per second).
114    sine_hz: f64,
115    /// Timestamp (seconds) of the last sample we generated; used to step the
116    /// sampler forward at `samples_per_second` even if frame rate varies.
117    last_sample_time: f64,
118}
119
120impl LotsOfTinyPlotsApp {
121    fn new(samples_per_second: f64, sine_hz: f64) -> Self {
122        let now_us = chrono::Utc::now().timestamp_micros();
123        let start_t = (now_us as f64) * 1e-6;
124
125        let mut plots = Vec::with_capacity(TOTAL);
126        for i in 0..TOTAL {
127            let phase = (i as f64) / (TOTAL as f64); // cycles [0..1)
128            let hue = (i as f64) / (TOTAL as f64);
129            let col = hsv_to_rgb(hue, 0.85, 0.9);
130            let label = format!("sine_{:03}", i);
131            let (p, mp) = TinyPlot::new(&label, phase, col);
132            plots.push((p, mp));
133        }
134        Self {
135            plots,
136            last_window_size: egui::Vec2::ZERO,
137            samples_per_second,
138            sine_hz,
139            last_sample_time: start_t,
140        }
141    }
142
143    fn render_grid(&mut self, ui: &mut egui::Ui) {
144        // Claim the entire remaining area so the grid fills and resizes with the window.
145        let avail = ui.available_size();
146        let (grid_rect, _) = ui.allocate_exact_size(avail, egui::Sense::hover());
147
148        // Floor to whole pixels; use the grid_rect origin for pixel-aligned placement.
149        let cell_w = (grid_rect.width() / COLS as f32).floor().max(1.0);
150        let cell_h = (grid_rect.height() / ROWS as f32).floor().max(1.0);
151
152        for row in 0..ROWS {
153            for col in 0..COLS {
154                let idx = row * COLS + col;
155                let x = (grid_rect.left() + col as f32 * cell_w).round();
156                let y = (grid_rect.top() + row as f32 * cell_h).round();
157                let cell_rect =
158                    egui::Rect::from_min_size(egui::pos2(x, y), egui::vec2(cell_w, cell_h));
159                let (_p, panel) = &mut self.plots[idx];
160                let mut child_ui =
161                    ui.new_child(egui::UiBuilder::new().id_salt(idx).max_rect(cell_rect));
162                panel.update_embedded(&mut child_ui);
163            }
164        }
165    }
166}
167
168impl eframe::App for LotsOfTinyPlotsApp {
169    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
170        // use wall-clock time as sample x
171        let now_us = chrono::Utc::now().timestamp_micros();
172        let t = (now_us as f64) * 1e-6;
173        // generate samples at configured rate; each sample uses the sine frequency
174        let sample_interval = 1.0 / self.samples_per_second;
175        let mut next_time = self.last_sample_time + sample_interval;
176        while next_time <= t {
177            for (p, _) in &self.plots {
178                p.feed(next_time, self.sine_hz);
179            }
180            // advance last_sample_time by one interval per sample generated
181            self.last_sample_time = next_time;
182            next_time += sample_interval;
183        }
184
185        // Detect window resizes and auto-fit all plots when the size changes.
186        let current_size = ctx.input(|i| i.viewport_rect().size());
187        if self.last_window_size != egui::Vec2::ZERO && self.last_window_size != current_size {
188            for (_p, panel) in &mut self.plots {
189                panel.fit_all_bounds();
190            }
191        }
192        self.last_window_size = current_size;
193
194        egui::CentralPanel::default().show(ctx, |ui| {
195            ui.heading("Lots of tiny sine plots — 20 × 15");
196            ui.label(format!(
197                "Each plot shows the same sine wave shifted by phase; every trace has its own color. — samples: {:.1} Hz, sine: {:.3} Hz",
198                self.samples_per_second,
199                self.sine_hz
200            ));
201            ui.add_space(6.0);
202            self.render_grid(ui);
203        });
204
205        ctx.request_repaint_after(Duration::from_millis(UPDATE_MS));
206    }
207}
208
209fn main() -> eframe::Result<()> {
210    // Parse simple CLI: -s / --samples-per-second <Hz> and -h / --hz <Hz>
211    let mut samples_per_second = DEFAULT_SAMPLES_PER_SEC;
212    let mut sine_hz = DEFAULT_SINE_HZ;
213    let mut args = std::env::args().skip(1);
214    while let Some(arg) = args.next() {
215        if arg == "-s" || arg == "--samples-per-second" {
216            if let Some(val) = args.next() {
217                match val.parse::<f64>() {
218                    Ok(v) => samples_per_second = v,
219                    Err(_) => eprintln!("invalid value for {}: {}", arg, val),
220                }
221            }
222        } else if arg == "-h" || arg == "--hz" {
223            if let Some(val) = args.next() {
224                match val.parse::<f64>() {
225                    Ok(v) => sine_hz = v,
226                    Err(_) => eprintln!("invalid value for {}: {}", arg, val),
227                }
228            }
229        } else if let Some(rest) = arg.strip_prefix("--samples-per-second=") {
230            match rest.parse::<f64>() {
231                Ok(v) => samples_per_second = v,
232                Err(_) => eprintln!("invalid value for --samples-per-second: {}", rest),
233            }
234        } else if let Some(rest) = arg.strip_prefix("--hz=") {
235            match rest.parse::<f64>() {
236                Ok(v) => sine_hz = v,
237                Err(_) => eprintln!("invalid value for --hz: {}", rest),
238            }
239        } else {
240            // ignore unknown args
241        }
242    }
243
244    let app = LotsOfTinyPlotsApp::new(samples_per_second, sine_hz);
245    eframe::run_native(
246        "Lots of tiny plots",
247        NativeOptions::default(),
248        Box::new(|_cc| Ok(Box::new(app))),
249    )
250}