Files
xserial/crates/xserial-gui/src/panels/plot_view.rs
FallenSigh a2c7c0fa71 Fix all clippy warnings across workspace
collapsible_if, let-chains, derivable_impls, clone_on_copy, useless_vec, approx_constant, io_other_error, single_match, collapsible_match, unnecessary_map_or, manual_is_multiple_of, result_large_err, too_many_arguments, upper_case_acronyms

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-12 21:09:47 +08:00

220 lines
7.0 KiB
Rust

use crate::buffers::{PlotBuffer, PlotSeriesKind};
use crate::perf::PlotRenderStats;
use egui::{Button, Ui, vec2};
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PlotDisplayMode {
Embedded,
Detached,
}
#[derive(Default)]
pub struct PlotViewState {
pub lock_x: bool,
pub lock_y: bool,
pub box_zoom: bool,
pub follow_latest: bool,
pub last_bounds: Option<PlotBounds>,
pub detached: bool,
}
pub struct PlotRenderOutput {
pub stats: PlotRenderStats,
pub toggle_detached: bool,
}
pub fn render(
ui: &mut Ui,
buf: &mut PlotBuffer,
session_id: u64,
state: &mut PlotViewState,
) -> PlotRenderOutput {
let mode = if state.detached {
PlotDisplayMode::Detached
} else {
PlotDisplayMode::Embedded
};
let mut toggle_detached = false;
if buf.is_empty() {
ui.horizontal_wrapped(|ui| {
let toggle_label = match mode {
PlotDisplayMode::Embedded => "Pop Out",
PlotDisplayMode::Detached => "Dock Back",
};
if ui.button(toggle_label).clicked() {
toggle_detached = true;
}
ui.label("no plot data");
});
return PlotRenderOutput {
stats: PlotRenderStats {
series_count: 0,
stored_points: 0,
rendered_points: 0,
},
toggle_detached,
};
}
let mut auto_fit = false;
let mut reset_view = false;
let mut zoom_x = 1.0_f32;
let mut zoom_y = 1.0_f32;
ui.horizontal_wrapped(|ui| {
if ui.button("Auto Fit").clicked() {
auto_fit = true;
}
if ui.button("Reset View").clicked() {
reset_view = true;
}
let toggle_label = match mode {
PlotDisplayMode::Embedded => "Pop Out",
PlotDisplayMode::Detached => "Dock Back",
};
if ui.button(toggle_label).clicked() {
toggle_detached = true;
}
ui.separator();
ui.checkbox(&mut state.lock_x, "Lock X");
ui.checkbox(&mut state.lock_y, "Lock Y");
ui.checkbox(&mut state.box_zoom, "Box Zoom");
if matches!(buf.kind(), PlotSeriesKind::TimeSeries) {
ui.checkbox(&mut state.follow_latest, "Follow Latest");
} else {
state.follow_latest = false;
}
ui.separator();
if ui
.add_enabled(!state.lock_x, Button::new("X-"))
.on_hover_text("Zoom out X axis")
.clicked()
{
zoom_x = 0.8;
}
if ui
.add_enabled(!state.lock_x, Button::new("X+"))
.on_hover_text("Zoom in X axis")
.clicked()
{
zoom_x = 1.25;
}
if ui
.add_enabled(!state.lock_y, Button::new("Y-"))
.on_hover_text("Zoom out Y axis")
.clicked()
{
zoom_y = 0.8;
}
if ui
.add_enabled(!state.lock_y, Button::new("Y+"))
.on_hover_text("Zoom in Y axis")
.clicked()
{
zoom_y = 1.25;
}
});
if let Some(bounds) = state.last_bounds {
ui.horizontal_wrapped(|ui| {
ui.monospace(format!(
"X [{:.3}, {:.3}] Y [{:.3}, {:.3}]",
bounds.min()[0],
bounds.max()[0],
bounds.min()[1],
bounds.max()[1],
));
});
}
let data_bounds = buf.plot_bounds();
let mut plot = Plot::new(format!("plot_view_{session_id}"))
.legend(Legend::default())
.allow_boxed_zoom(state.box_zoom)
.allow_zoom([!state.lock_x, !state.lock_y])
.allow_drag([!state.lock_x, !state.lock_y])
.allow_axis_zoom_drag([!state.lock_x, !state.lock_y])
.allow_scroll([
!state.lock_x && matches!(buf.kind(), PlotSeriesKind::TimeSeries),
!state.lock_y && matches!(buf.kind(), PlotSeriesKind::TimeSeries),
]);
if reset_view {
plot = plot.reset();
}
let mut rendered_points = 0usize;
plot.show(ui, |plot_ui| {
let current_bounds = plot_ui.plot_bounds();
state.last_bounds = current_bounds.is_finite().then_some(current_bounds);
let pixel_width = plot_ui.response().rect.width().max(64.0);
let max_render_points = (pixel_width as usize).saturating_mul(2);
if auto_fit {
if let Some(bounds) = data_bounds {
plot_ui.set_auto_bounds(false);
plot_ui.set_plot_bounds(bounds);
state.last_bounds = Some(bounds);
}
} else if state.follow_latest && matches!(buf.kind(), PlotSeriesKind::TimeSeries) {
if let Some(bounds) = data_bounds {
let current_width = current_bounds.width();
let data_width = bounds.width();
let x_range = if current_width.is_finite()
&& current_width > 0.0
&& data_width > current_width
{
(bounds.max()[0] - current_width)..=bounds.max()[0]
} else {
bounds.min()[0]..=bounds.max()[0]
};
plot_ui.set_auto_bounds([false, plot_ui.auto_bounds().y]);
plot_ui.set_plot_bounds_x(x_range.clone());
let mut followed = current_bounds;
followed.set_x(&PlotBounds::from_min_max(
[*x_range.start(), current_bounds.min()[1]],
[*x_range.end(), current_bounds.max()[1]],
));
state.last_bounds = Some(followed);
}
} else if zoom_x != 1.0 || zoom_y != 1.0 {
let center = plot_ui.plot_bounds().center();
plot_ui.zoom_bounds(vec2(zoom_x, zoom_y), center);
}
if let Some(bounds) = data_bounds {
if !bounds.is_valid_x() {
plot_ui.set_auto_bounds([true, plot_ui.auto_bounds().y]);
}
if !bounds.is_valid_y() {
plot_ui.set_auto_bounds([plot_ui.auto_bounds().x, true]);
}
}
for series in buf.iter() {
let sampled_points = match buf.kind() {
PlotSeriesKind::TimeSeries => series.render_points_time_series(
current_bounds.min()[0],
current_bounds.max()[0],
max_render_points,
),
PlotSeriesKind::XY => series.render_points_xy(max_render_points),
};
rendered_points += sampled_points.len();
let points = PlotPoints::from_iter(sampled_points);
plot_ui.line(Line::new(series.name.clone(), points));
}
});
PlotRenderOutput {
stats: PlotRenderStats {
series_count: buf.series_len(),
stored_points: buf.total_points(),
rendered_points,
},
toggle_detached,
}
}