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>
220 lines
7.0 KiB
Rust
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,
|
|
}
|
|
}
|