| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | use crate::app::AppState;
|
| | use crate::gmp_engine::{self, IntervalProfile};
|
| | use crate::timeframe;
|
| | use egui;
|
| | use egui_plot::{self, Plot, PlotPoints, Line, PlotPoint, Text};
|
| |
|
| |
|
| | pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
|
| |
|
| | ui.horizontal(|ui| {
|
| | ui.label("Interval:");
|
| | let resp = ui.add(
|
| | egui::TextEdit::singleline(&mut state.time_input_str)
|
| | .desired_width(80.0)
|
| | .hint_text("e.g. 1:30, 4h30m"),
|
| | );
|
| | if resp.lost_focus() || resp.changed() {
|
| | match timeframe::parse_timeframe(&state.time_input_str) {
|
| | Ok(secs) => {
|
| | state.time_interval_secs = secs;
|
| | state.profiles_dirty = true;
|
| | state.time_parse_err = None;
|
| | }
|
| | Err(e) => {
|
| | state.time_parse_err = Some(e);
|
| | }
|
| | }
|
| | }
|
| | if state.time_interval_secs > 0 {
|
| | ui.label(format!(
|
| | "= {}",
|
| | timeframe::format_seconds(state.time_interval_secs)
|
| | ));
|
| | }
|
| | if let Some(ref e) = state.time_parse_err {
|
| | ui.colored_label(egui::Color32::from_rgb(255, 100, 100), e);
|
| | }
|
| | });
|
| |
|
| | ui.separator();
|
| |
|
| |
|
| | if state.profiles_dirty || state.time_profiles.is_empty() {
|
| | state.time_profiles = gmp_engine::aggregate_by_time(
|
| | &state.raw_ticks,
|
| | state.time_interval_secs,
|
| | state.bin_size,
|
| | );
|
| | state.profiles_dirty = false;
|
| | }
|
| |
|
| |
|
| | ui.horizontal(|ui| {
|
| | ui.label("View:");
|
| | ui.selectable_value(&mut state.show_footprint, false, "GMP");
|
| | ui.selectable_value(&mut state.show_footprint, true, "Footprint");
|
| | ui.label(format!(
|
| | "| {} intervals | {} ticks",
|
| | state.time_profiles.len(),
|
| | state.raw_ticks.len()
|
| | ));
|
| | });
|
| |
|
| | ui.separator();
|
| |
|
| |
|
| | render_profile_chart(ui, &state.time_profiles, state.show_footprint, state.bin_size, "time_chart");
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | pub fn render_profile_chart(
|
| | ui: &mut egui::Ui,
|
| | profiles: &[IntervalProfile],
|
| | show_footprint: bool,
|
| | beta: f64,
|
| | plot_id: &str,
|
| | ) {
|
| | if profiles.is_empty() {
|
| | ui.label("No data to display");
|
| | return;
|
| | }
|
| |
|
| | let plot = Plot::new(plot_id)
|
| | .legend(egui_plot::Legend::default())
|
| | .allow_boxed_zoom(true)
|
| | .allow_drag(true)
|
| | .allow_scroll(true)
|
| | .allow_zoom(true)
|
| | .y_axis_formatter(move |y, _range, _width| {
|
| |
|
| | let bin = (y.value / beta).round() as i64;
|
| | format!("{:.2}", bin as f64 * beta)
|
| | });
|
| |
|
| | plot.show(ui, |plot_ui| {
|
| |
|
| | let (global_min, global_max) = profiles.iter().fold(
|
| | (i64::MAX, i64::MIN),
|
| | |(gmin, gmax), p| {
|
| | let pmin = if show_footprint { p.footprint.min_bin } else { p.gmp.min_bin };
|
| | let pmax = if show_footprint { p.footprint.max_bin } else { p.gmp.max_bin };
|
| | (gmin.min(pmin), gmax.max(pmax))
|
| | },
|
| | );
|
| |
|
| |
|
| | let max_count = profiles.iter().flat_map(|p| {
|
| | if show_footprint {
|
| | p.footprint.bins.values().map(|b| b.count).collect::<Vec<_>>()
|
| | } else {
|
| | p.gmp.bins.values().map(|b| b.count).collect::<Vec<_>>()
|
| | }
|
| | }).max().unwrap_or(1).max(1) as f64;
|
| |
|
| |
|
| | let col_width = 0.8;
|
| | let bar_scale = col_width / max_count;
|
| |
|
| | for (col, profile) in profiles.iter().enumerate() {
|
| | let x_center = col as f64;
|
| |
|
| | if show_footprint {
|
| |
|
| | for (&bin_idx, bin) in &profile.footprint.bins {
|
| | let y = bin_idx as f64 * beta + beta * 0.5;
|
| |
|
| | if bin.up > 0 {
|
| | let w = bin.up as f64 * bar_scale;
|
| | let points = PlotPoints::new(vec![
|
| | [x_center, y],
|
| | [x_center + w, y],
|
| | ]);
|
| | plot_ui.line(
|
| | Line::new(points)
|
| | .color(egui::Color32::from_rgb(50, 200, 100))
|
| | .width(2.0),
|
| | );
|
| | }
|
| | if bin.down > 0 {
|
| | let w = bin.down as f64 * bar_scale;
|
| | let points = PlotPoints::new(vec![
|
| | [x_center, y],
|
| | [x_center - w, y],
|
| | ]);
|
| | plot_ui.line(
|
| | Line::new(points)
|
| | .color(egui::Color32::from_rgb(255, 80, 80))
|
| | .width(2.0),
|
| | );
|
| | }
|
| |
|
| |
|
| | let delta = bin.delta();
|
| | if delta != 0 {
|
| | let label = if delta > 0 {
|
| | format!("+{}", delta)
|
| | } else {
|
| | format!("{}", delta)
|
| | };
|
| | plot_ui.text(
|
| | Text::new(PlotPoint::new(x_center + col_width * 0.5, y), label)
|
| | .color(if delta > 0 {
|
| | egui::Color32::from_rgb(50, 200, 100)
|
| | } else {
|
| | egui::Color32::from_rgb(255, 80, 80)
|
| | }),
|
| | );
|
| | }
|
| | }
|
| | } else {
|
| |
|
| | for (&bin_idx, bin) in &profile.gmp.bins {
|
| | let y = bin_idx as f64 * beta + beta * 0.5;
|
| | let w = bin.count as f64 * bar_scale;
|
| |
|
| | let points = PlotPoints::new(vec![
|
| | [x_center - w * 0.5, y],
|
| | [x_center + w * 0.5, y],
|
| | ]);
|
| | plot_ui.line(
|
| | Line::new(points)
|
| | .color(egui::Color32::from_rgb(100, 150, 255))
|
| | .width(2.5),
|
| | );
|
| | }
|
| | }
|
| |
|
| |
|
| | plot_ui.vline(
|
| | egui_plot::VLine::new(x_center - 0.5)
|
| | .color(egui::Color32::from_rgba_premultiplied(100, 100, 100, 40))
|
| | .width(0.5),
|
| | );
|
| | }
|
| | });
|
| | }
|
| |
|