Skip to main content

embedded_tiles/
embedded_tiles.rs

1//! Example: Embedding multiple LivePlot instances in a single egui window
2//!
3//! What it demonstrates
4//! - Arranging four independent embedded `LivePlotPanel` instances in a 2 x 2 layout inside one `eframe` window.
5//! - Feeding each plot with its own waveform from the surrounding application via `PlotSink`/`Trace` handles.
6//! - Starting the host `eframe` window maximized to showcase a dashboard-like layout.
7//!
8//! How to run
9//! ```bash
10//! cargo run --example embedded_tiles
11//! ```
12
13use std::time::Duration;
14
15use eframe::{egui, NativeOptions};
16use liveplot::{channel_plot, LivePlotPanel, PlotPoint, PlotSink, Trace, TracesController};
17
18#[derive(Clone, Copy)]
19enum PanelWave {
20    Sine,
21    Cosine,
22    Sum { secondary_freq_hz: f64 },
23    AmMod { modulation_hz: f64 },
24}
25
26struct PlotPanel {
27    wave: PanelWave,
28    freq_hz: f64,
29    phase_cycles: f64,
30    sink: PlotSink,
31    trace: Trace,
32}
33
34impl PlotPanel {
35    fn new(
36        label: &'static str,
37        wave: PanelWave,
38        freq_hz: f64,
39        phase_cycles: f64,
40        color_rgb: Option<[u8; 3]>,
41    ) -> (Self, LivePlotPanel) {
42        let (sink, rx) = channel_plot();
43        let trace = sink.create_trace(label, None);
44        let mut plot = LivePlotPanel::new(rx);
45        for scope in plot.liveplot_panel.get_data_mut() {
46            scope.time_window = 8.0;
47        }
48        plot.traces_data.max_points = 5_000;
49        plot.liveplot_panel.update_data(&plot.traces_data);
50        // Attach a per-plot traces controller so we can request a custom color
51        let ctrl = TracesController::new();
52        plot.set_controllers(None, None, Some(ctrl.clone()), None, None, None, None);
53
54        // If a color hint is supplied, request it via the traces controller (applied during tick)
55        if let Some(rgb) = color_rgb {
56            ctrl.request_set_color(label, rgb);
57        }
58
59        let panel = Self {
60            wave,
61            freq_hz,
62            phase_cycles,
63            sink,
64            trace,
65        };
66        (panel, plot)
67    }
68
69    fn feed(&self, t: f64) {
70        const TAU: f64 = std::f64::consts::PI * 2.0;
71        let base_phase = (t * self.freq_hz + self.phase_cycles) * TAU;
72        let value = match self.wave {
73            PanelWave::Sine => base_phase.sin(),
74            PanelWave::Cosine => base_phase.cos(),
75            PanelWave::Sum { secondary_freq_hz } => {
76                base_phase.sin() + (t * secondary_freq_hz * TAU).cos()
77            }
78            PanelWave::AmMod { modulation_hz } => {
79                let envelope = 1.0 + 0.5 * (t * modulation_hz * TAU).sin();
80                base_phase.sin() * envelope
81            }
82        };
83        let _ = self
84            .sink
85            .send_point(&self.trace, PlotPoint { x: t, y: value });
86    }
87}
88
89struct DashboardApp {
90    panels: Vec<(PlotPanel, LivePlotPanel)>,
91}
92
93impl DashboardApp {
94    fn new() -> Self {
95        let configs = [
96            ("Sine 1 Hz", PanelWave::Sine, 1.0, 0.0),
97            ("Cosine 0.5 Hz", PanelWave::Cosine, 0.5, 0.25),
98            (
99                "Sine + Cosine",
100                PanelWave::Sum {
101                    secondary_freq_hz: 2.0,
102                },
103                1.5,
104                0.0,
105            ),
106            (
107                "AM Sine",
108                PanelWave::AmMod { modulation_hz: 0.2 },
109                0.75,
110                0.5,
111            ),
112        ];
113        let mut panels = Vec::new();
114        for (i, (label, wave, freq, phase)) in configs.into_iter().enumerate() {
115            // Per-panel color hints (u8 RGB)
116            let color = match i {
117                0 => Some([102, 204, 255]), // light blue
118                1 => Some([255, 120, 120]), // light red
119                2 => Some([200, 255, 170]), // light green
120                3 => Some([255, 210, 120]), // gold / orange
121                _ => None,
122            };
123            let (panel, plot) = PlotPanel::new(label, wave, freq, phase, color);
124            panels.push((panel, plot));
125        }
126
127        // Fix the Y range for the "Sine + Cosine" panel (index 2) to [-2, 2]
128        if let Some((_panel, plot)) = panels.get_mut(2) {
129            if let Some(scope) = plot.liveplot_panel.get_data_mut().first_mut() {
130                scope.y_axis.bounds = (-2.0, 2.0);
131                scope.y_axis.auto_fit = false;
132            }
133        }
134
135        Self { panels }
136    }
137
138    fn render_dashboard(&mut self, ui: &mut egui::Ui) {
139        let cols = 2;
140        let rows = (self.panels.len() + cols - 1) / cols;
141        let avail = ui.available_size();
142        let cell_w = avail.x / cols as f32;
143        let cell_h = avail.y / rows as f32;
144
145        egui::Grid::new("embedded_dashboard_grid")
146            .num_columns(cols)
147            .spacing([0.0, 0.0])
148            .show(ui, |ui| {
149                for (idx, (_panel, plot)) in self.panels.iter_mut().enumerate() {
150                    let (_, rect) = ui.allocate_space(egui::vec2(cell_w, cell_h));
151                    let mut child_ui = ui.new_child(egui::UiBuilder::new().max_rect(rect));
152                    plot.update_embedded(&mut child_ui);
153
154                    if idx % cols == cols - 1 {
155                        ui.end_row();
156                    }
157                }
158            });
159    }
160}
161
162impl eframe::App for DashboardApp {
163    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
164        let now_us = chrono::Utc::now().timestamp_micros();
165        let t = (now_us as f64) * 1e-6;
166        for (panel, _) in &self.panels {
167            panel.feed(t);
168        }
169
170        egui::CentralPanel::default().show(ctx, |ui| {
171            ui.heading("Embedded LivePlot dashboard");
172            ui.label("Four independent LivePlot instances embedded in a resizable 2 x 2 grid.");
173            ui.add_space(8.0);
174            self.render_dashboard(ui);
175        });
176
177        ctx.request_repaint_after(Duration::from_millis(16));
178    }
179}
180
181fn main() -> eframe::Result<()> {
182    let app = DashboardApp::new();
183    eframe::run_native(
184        "LivePlot embedded dashboard demo",
185        NativeOptions {
186            viewport: egui::ViewportBuilder::default().with_maximized(true),
187            ..NativeOptions::default()
188        },
189        Box::new(|_cc| Ok(Box::new(app))),
190    )
191}