use egui::{
plot::{Legend, Line, Plot, Points},
Color32,
};
use re_arrow_store::TimeType;
use crate::{
misc::format_time::next_grid_tick_magnitude_ns, ui::view_time_series::scene::PlotSeriesKind,
ViewerContext,
};
use super::SceneTimeSeries;
pub(crate) const HELP_TEXT: &str = "Pan by dragging, or scroll (+ shift = horizontal).\n\
Box zooming: Right click to zoom in and zoom out using a selection.\n\
Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture.\n\
Reset view with double-click.\n\
Right click to move the time cursor to the current position.";
#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct ViewTimeSeriesState;
pub(crate) fn view_time_series(
ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
_state: &mut ViewTimeSeriesState,
scene: &SceneTimeSeries,
) -> egui::Response {
crate::profile_function!();
let time_ctrl = &ctx.rec_cfg.time_ctrl;
let current_time = time_ctrl.time_i64();
let time_type = time_ctrl.time_type();
let timeline = time_ctrl.timeline();
let timeline_name = timeline.name().to_string();
let min_time = scene
.lines
.iter()
.flat_map(|line| line.points.iter().map(|p| p.0))
.min()
.unwrap_or(0);
let time_offset = if timeline.typ() == TimeType::Time {
round_ns_to_start_of_day(min_time)
} else {
min_time
};
let plot_id_src = ("plot", &timeline_name);
let mut plot = Plot::new(plot_id_src)
.legend(Legend {
position: egui::plot::Corner::RightBottom,
..Default::default()
})
.x_axis_formatter(move |time, _| format_time(time_type, time as i64 + time_offset))
.label_formatter(move |name, value| {
let name = if name.is_empty() { "y" } else { name };
let is_integer = value.y.round() == value.y;
let decimals = if is_integer { 0 } else { 5 };
format!(
"{timeline_name}: {}\n{name}: {:.*}",
time_type.format((value.x as i64 + time_offset).into()),
decimals,
value.y,
)
});
if timeline.typ() == TimeType::Time {
let canvas_size = ui.available_size();
plot = plot.x_grid_spacer(move |spacer| ns_grid_spacer(canvas_size, &spacer));
}
let egui::InnerResponse {
inner: time_x,
response,
} = plot.show(ui, |plot_ui| {
if plot_ui.plot_secondary_clicked() {
let timeline = ctx.rec_cfg.time_ctrl.timeline();
ctx.rec_cfg.time_ctrl.set_timeline_and_time(
*timeline,
plot_ui.pointer_coordinate().unwrap().x as i64 + time_offset,
);
ctx.rec_cfg.time_ctrl.pause();
}
for line in &scene.lines {
let points = line
.points
.iter()
.map(|p| [(p.0 - time_offset) as _, p.1])
.collect::<Vec<_>>();
let c = line.color;
let color = Color32::from_rgba_premultiplied(c[0], c[1], c[2], c[3]);
match line.kind {
PlotSeriesKind::Continuous => plot_ui.line(
Line::new(points)
.name(&line.label)
.color(color)
.width(line.width),
),
PlotSeriesKind::Scatter => plot_ui.points(
Points::new(points)
.name(&line.label)
.color(color)
.radius(line.width),
),
}
}
current_time.map(|current_time| {
let time_x = (current_time - time_offset) as f64;
plot_ui.screen_from_plot([time_x, 0.0].into()).x
})
});
if let Some(time_x) = time_x {
let stroke = ui.visuals().widgets.inactive.fg_stroke;
crate::ui::time_panel::paint_time_cursor(
ui.painter(),
time_x,
response.rect.y_range(),
stroke,
);
}
response
}
fn format_time(time_type: TimeType, time_int: i64) -> String {
if time_type == TimeType::Time {
let time = re_log_types::Time::from_ns_since_epoch(time_int);
crate::misc::format_time::format_time_compact(time)
} else {
time_type.format(re_log_types::TimeInt::from(time_int))
}
}
fn ns_grid_spacer(
canvas_size: egui::Vec2,
input: &egui::plot::GridInput,
) -> Vec<egui::plot::GridMark> {
let minimum_medium_line_spacing = 150.0; let max_medium_lines = canvas_size.x as f64 / minimum_medium_line_spacing;
let (min_ns, max_ns) = input.bounds;
let width_ns = max_ns - min_ns;
let mut small_spacing_ns = 1;
while width_ns / (next_grid_tick_magnitude_ns(small_spacing_ns) as f64) > max_medium_lines {
small_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns);
}
let medium_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns);
let big_spacing_ns = next_grid_tick_magnitude_ns(medium_spacing_ns);
let mut current_ns = (min_ns.floor() as i64) / small_spacing_ns * small_spacing_ns;
let mut marks = vec![];
while current_ns <= max_ns.ceil() as i64 {
let is_big_line = current_ns % big_spacing_ns == 0;
let is_medium_line = current_ns % medium_spacing_ns == 0;
let step_size = if is_big_line {
big_spacing_ns
} else if is_medium_line {
medium_spacing_ns
} else {
small_spacing_ns
};
marks.push(egui::plot::GridMark {
value: current_ns as f64,
step_size: step_size as f64,
});
current_ns += small_spacing_ns;
}
marks
}
fn round_ns_to_start_of_day(ns: i64) -> i64 {
let ns_per_day = 24 * 60 * 60 * 1_000_000_000;
(ns + ns_per_day / 2) / ns_per_day * ns_per_day
}