From 9aa3e24b9aa3435ee24796c3424717e84bf5125f Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Mon, 4 May 2026 02:54:07 -0400 Subject: [PATCH] refactor: unify some time series graph logic, part 1 (#2050) Part 1 of a series of PRs to unify and clean up time series graph logic. This PR mostly just unifies the zoom logic and common states. --- src/app.rs | 323 +++++++----------- src/app/data/store.rs | 8 +- src/canvas/components/mod.rs | 2 +- .../time_graph/variants/auto_y_axis.rs | 2 - .../{time_graph.rs => time_series.rs} | 0 .../{time_graph => time_series}/base.rs | 14 +- .../{time_graph => time_series}/variants.rs | 0 .../time_series/variants/auto_y_axis.rs | 2 + .../variants/percent.rs | 2 +- .../{time_graph => time_series}/vendored.rs | 6 +- .../vendored/canvas.rs | 0 .../vendored/grid.rs | 0 .../vendored/points.rs | 2 +- src/canvas/widgets/cpu_graph.rs | 10 +- src/canvas/widgets/mem_graph.rs | 20 +- src/canvas/widgets/network_graph.rs | 18 +- src/canvas/widgets/temperature_graph.rs | 12 +- src/options.rs | 4 +- src/widgets/common/mod.rs | 5 + src/widgets/common/time_series.rs | 309 +++++++++++++++++ src/widgets/cpu_graph.rs | 10 +- src/widgets/mem_graph.rs | 11 +- src/widgets/mod.rs | 174 +--------- src/widgets/network_graph.rs | 11 +- src/widgets/temperature_graph.rs | 15 +- 25 files changed, 514 insertions(+), 446 deletions(-) delete mode 100644 src/canvas/components/time_graph/variants/auto_y_axis.rs rename src/canvas/components/{time_graph.rs => time_series.rs} (100%) rename src/canvas/components/{time_graph => time_series}/base.rs (96%) rename src/canvas/components/{time_graph => time_series}/variants.rs (100%) create mode 100644 src/canvas/components/time_series/variants/auto_y_axis.rs rename src/canvas/components/{time_graph => time_series}/variants/percent.rs (98%) rename src/canvas/components/{time_graph => time_series}/vendored.rs (99%) rename src/canvas/components/{time_graph => time_series}/vendored/canvas.rs (100%) rename src/canvas/components/{time_graph => time_series}/vendored/grid.rs (100%) rename src/canvas/components/{time_graph => time_series}/vendored/points.rs (99%) create mode 100644 src/widgets/common/mod.rs create mode 100644 src/widgets/common/time_series.rs diff --git a/src/app.rs b/src/app.rs index fc1c564d..e0d6860a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,7 +13,7 @@ pub use states::*; use crate::{ canvas::{ - components::time_graph::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog, + components::time_series::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog, }, constants, options::config::flags::TableGap, @@ -23,8 +23,6 @@ use crate::{ }, }; -const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds - #[derive(Debug, Clone, Eq, PartialEq, Default, Copy)] pub enum AxisScaling { #[default] @@ -194,10 +192,35 @@ impl App { self.data_store.reset(); - // Reset zoom - self.reset_cpu_zoom(); - self.reset_mem_zoom(); - self.reset_net_zoom(); + // Reset zoom. + // TODO: Make this suck less... should just make it so that calling reset fixes this all (including above too). + for widget_state in self.states.cpu_state.widget_states.values_mut() { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + + for widget_state in self.states.mem_state.widget_states.values_mut() { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + + for widget_state in self.states.net_state.widget_states.values_mut() { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + + for widget_state in self.states.temp_graph_state.widget_states.values_mut() { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } } pub fn should_get_widget_bounds(&self) -> bool { @@ -1966,81 +1989,45 @@ impl App { fn zoom_out(&mut self) { match self.current_widget.widget_type { BottomWidgetType::Cpu => { - if let Some(cpu_widget_state) = self + if let Some(widget_state) = self .states .cpu_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = cpu_widget_state - .current_display_time - .saturating_add(self.app_config_fields.time_interval); - - if new_time <= self.app_config_fields.retention_ms { - cpu_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - cpu_widget_state.autohide_timer = Some(Instant::now()); - } - } else if cpu_widget_state.current_display_time - != self.app_config_fields.retention_ms - { - cpu_widget_state.current_display_time = self.app_config_fields.retention_ms; - if self.app_config_fields.autohide_time { - cpu_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_out( + self.app_config_fields.time_interval, + self.app_config_fields.retention_ms, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::Mem => { - if let Some(mem_widget_state) = self + if let Some(widget_state) = self .states .mem_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = mem_widget_state - .current_display_time - .saturating_add(self.app_config_fields.time_interval); - - if new_time <= self.app_config_fields.retention_ms { - mem_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - mem_widget_state.autohide_timer = Some(Instant::now()); - } - } else if mem_widget_state.current_display_time - != self.app_config_fields.retention_ms - { - mem_widget_state.current_display_time = self.app_config_fields.retention_ms; - if self.app_config_fields.autohide_time { - mem_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_out( + self.app_config_fields.time_interval, + self.app_config_fields.retention_ms, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::Net => { - if let Some(net_widget_state) = self + if let Some(widget_state) = self .states .net_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = net_widget_state - .current_display_time - .saturating_add(self.app_config_fields.time_interval); - - if new_time <= self.app_config_fields.retention_ms { - net_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - net_widget_state.autohide_timer = Some(Instant::now()); - } - } else if net_widget_state.current_display_time - != self.app_config_fields.retention_ms - { - net_widget_state.current_display_time = self.app_config_fields.retention_ms; - if self.app_config_fields.autohide_time { - net_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_out( + self.app_config_fields.time_interval, + self.app_config_fields.retention_ms, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::TempGraph => { @@ -2049,23 +2036,11 @@ impl App { .temp_graph_state .get_mut_widget_state(self.current_widget.widget_id) { - let new_time = widget_state - .current_display_time - .saturating_add(self.app_config_fields.time_interval); - - if new_time <= self.app_config_fields.retention_ms { - widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - widget_state.autohide_timer = Some(Instant::now()); - } - } else if widget_state.current_display_time - != self.app_config_fields.retention_ms - { - widget_state.current_display_time = self.app_config_fields.retention_ms; - if self.app_config_fields.autohide_time { - widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_out( + self.app_config_fields.time_interval, + self.app_config_fields.retention_ms, + self.app_config_fields.autohide_time, + ); } } _ => {} @@ -2075,75 +2050,42 @@ impl App { fn zoom_in(&mut self) { match self.current_widget.widget_type { BottomWidgetType::Cpu => { - if let Some(cpu_widget_state) = self + if let Some(widget_state) = self .states .cpu_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = cpu_widget_state - .current_display_time - .saturating_sub(self.app_config_fields.time_interval); - - if new_time >= STALE_MIN_MILLISECONDS { - cpu_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - cpu_widget_state.autohide_timer = Some(Instant::now()); - } - } else if cpu_widget_state.current_display_time != STALE_MIN_MILLISECONDS { - cpu_widget_state.current_display_time = STALE_MIN_MILLISECONDS; - if self.app_config_fields.autohide_time { - cpu_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_in( + self.app_config_fields.time_interval, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::Mem => { - if let Some(mem_widget_state) = self + if let Some(widget_state) = self .states .mem_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = mem_widget_state - .current_display_time - .saturating_sub(self.app_config_fields.time_interval); - - if new_time >= STALE_MIN_MILLISECONDS { - mem_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - mem_widget_state.autohide_timer = Some(Instant::now()); - } - } else if mem_widget_state.current_display_time != STALE_MIN_MILLISECONDS { - mem_widget_state.current_display_time = STALE_MIN_MILLISECONDS; - if self.app_config_fields.autohide_time { - mem_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_in( + self.app_config_fields.time_interval, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::Net => { - if let Some(net_widget_state) = self + if let Some(widget_state) = self .states .net_state .widget_states .get_mut(&self.current_widget.widget_id) { - let new_time = net_widget_state - .current_display_time - .saturating_sub(self.app_config_fields.time_interval); - - if new_time >= STALE_MIN_MILLISECONDS { - net_widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - net_widget_state.autohide_timer = Some(Instant::now()); - } - } else if net_widget_state.current_display_time != STALE_MIN_MILLISECONDS { - net_widget_state.current_display_time = STALE_MIN_MILLISECONDS; - if self.app_config_fields.autohide_time { - net_widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_in( + self.app_config_fields.time_interval, + self.app_config_fields.autohide_time, + ); } } BottomWidgetType::TempGraph => { @@ -2152,88 +2094,69 @@ impl App { .temp_graph_state .get_mut_widget_state(self.current_widget.widget_id) { - let new_time = widget_state - .current_display_time - .saturating_sub(self.app_config_fields.time_interval); - - if new_time >= STALE_MIN_MILLISECONDS { - widget_state.current_display_time = new_time; - if self.app_config_fields.autohide_time { - widget_state.autohide_timer = Some(Instant::now()); - } - } else if widget_state.current_display_time != STALE_MIN_MILLISECONDS { - widget_state.current_display_time = STALE_MIN_MILLISECONDS; - if self.app_config_fields.autohide_time { - widget_state.autohide_timer = Some(Instant::now()); - } - } + widget_state.time_series_state.zoom_in( + self.app_config_fields.time_interval, + self.app_config_fields.autohide_time, + ); } } _ => {} } } - fn reset_cpu_zoom(&mut self) { - if let Some(cpu_widget_state) = self - .states - .cpu_state - .widget_states - .get_mut(&self.current_widget.widget_id) - { - cpu_widget_state.current_display_time = self.app_config_fields.default_time_value; - if self.app_config_fields.autohide_time { - cpu_widget_state.autohide_timer = Some(Instant::now()); - } - } - } - - fn reset_mem_zoom(&mut self) { - if let Some(mem_widget_state) = self - .states - .mem_state - .widget_states - .get_mut(&self.current_widget.widget_id) - { - mem_widget_state.current_display_time = self.app_config_fields.default_time_value; - if self.app_config_fields.autohide_time { - mem_widget_state.autohide_timer = Some(Instant::now()); - } - } - } - - fn reset_net_zoom(&mut self) { - if let Some(net_widget_state) = self - .states - .net_state - .widget_states - .get_mut(&self.current_widget.widget_id) - { - net_widget_state.current_display_time = self.app_config_fields.default_time_value; - if self.app_config_fields.autohide_time { - net_widget_state.autohide_timer = Some(Instant::now()); - } - } - } - - fn reset_temp_graph_zoom(&mut self) { - if let Some(widget_state) = self - .states - .temp_graph_state - .get_mut_widget_state(self.current_widget.widget_id) - { - widget_state.current_display_time = self.app_config_fields.default_time_value; - if self.app_config_fields.autohide_time { - widget_state.autohide_timer = Some(Instant::now()); - } - } - } - fn reset_zoom(&mut self) { match self.current_widget.widget_type { - BottomWidgetType::Cpu => self.reset_cpu_zoom(), - BottomWidgetType::Mem => self.reset_mem_zoom(), - BottomWidgetType::Net => self.reset_net_zoom(), - BottomWidgetType::TempGraph => self.reset_temp_graph_zoom(), + BottomWidgetType::Cpu => { + if let Some(widget_state) = self + .states + .cpu_state + .widget_states + .get_mut(&self.current_widget.widget_id) + { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + } + BottomWidgetType::Mem => { + if let Some(widget_state) = self + .states + .mem_state + .widget_states + .get_mut(&self.current_widget.widget_id) + { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + } + BottomWidgetType::Net => { + if let Some(widget_state) = self + .states + .net_state + .widget_states + .get_mut(&self.current_widget.widget_id) + { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + } + BottomWidgetType::TempGraph => { + if let Some(widget_state) = self + .states + .temp_graph_state + .get_mut_widget_state(self.current_widget.widget_id) + { + widget_state.time_series_state.reset_zoom( + self.app_config_fields.default_time_value, + self.app_config_fields.autohide_time, + ); + } + } _ => {} } } diff --git a/src/app/data/store.rs b/src/app/data/store.rs index 6ea69ec8..0aadc5ee 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -27,7 +27,7 @@ use crate::{ pub struct StoredData { // FIXME: (points_rework_v1) we could be able to remove this with some more refactoring. pub last_update_time: Instant, - pub timeseries_data: TimeSeriesData, + pub time_series_data: TimeSeriesData, pub network_harvest: NetworkHarvest, pub ram_harvest: Option, pub swap_harvest: Option, @@ -53,7 +53,7 @@ impl Default for StoredData { fn default() -> Self { StoredData { last_update_time: Instant::now(), - timeseries_data: TimeSeriesData::default(), + time_series_data: TimeSeriesData::default(), network_harvest: NetworkHarvest::default(), ram_harvest: None, #[cfg(not(target_os = "windows"))] @@ -100,7 +100,7 @@ impl StoredData { } if !settings.use_basic_mode { - self.timeseries_data + self.time_series_data .add(&data, used_widgets, settings, filters); } @@ -345,7 +345,7 @@ impl DataStore { /// Clean data. pub fn clean_data(&mut self, max_duration: Duration) { - self.main.timeseries_data.prune(max_duration); + self.main.time_series_data.prune(max_duration); } /// Reset data state. diff --git a/src/canvas/components/mod.rs b/src/canvas/components/mod.rs index 19b945e9..2c9d57c7 100644 --- a/src/canvas/components/mod.rs +++ b/src/canvas/components/mod.rs @@ -3,5 +3,5 @@ pub mod data_table; pub mod pipe_gauge; pub mod scroll_bar; -pub mod time_graph; +pub mod time_series; pub mod widget_carousel; diff --git a/src/canvas/components/time_graph/variants/auto_y_axis.rs b/src/canvas/components/time_graph/variants/auto_y_axis.rs deleted file mode 100644 index 432f6b24..00000000 --- a/src/canvas/components/time_graph/variants/auto_y_axis.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! A variant of a [`crate::canvas::components::time_graph::TimeGraph`] that -//! automatically adjusts the y-axis based on the data provided. diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_series.rs similarity index 100% rename from src/canvas/components/time_graph.rs rename to src/canvas/components/time_series.rs diff --git a/src/canvas/components/time_graph/base.rs b/src/canvas/components/time_series/base.rs similarity index 96% rename from src/canvas/components/time_graph/base.rs rename to src/canvas/components/time_series/base.rs index 6c458a74..1f0a8bcc 100644 --- a/src/canvas/components/time_graph/base.rs +++ b/src/canvas/components/time_series/base.rs @@ -11,7 +11,7 @@ use tui::{ widgets::{BorderType, GraphType}, }; -use crate::canvas::{components::time_graph::*, drawing_utils::widget_block}; +use crate::canvas::{components::time_series::*, drawing_utils::widget_block}; /// Represents the data required by the [`TimeGraph`]. /// @@ -238,7 +238,7 @@ mod test { }; use super::{AxisBound, ChartScaling, TimeGraph}; - use crate::canvas::components::time_graph::Axis; + use crate::canvas::components::time_series::Axis; const Y_LABELS: [Cow<'static, str>; 3] = [ Cow::Borrowed("0%"), @@ -246,7 +246,7 @@ mod test { Cow::Borrowed("100%"), ]; - fn create_time_graph() -> TimeGraph<'static> { + fn create_time_series() -> TimeGraph<'static> { TimeGraph { title: " Network ".into(), x_min: -15000.0, @@ -268,8 +268,8 @@ mod test { } #[test] - fn time_graph_gen_x_axis() { - let tg = create_time_graph(); + fn time_series_gen_x_axis() { + let tg = create_time_series(); let style = Style::default().fg(Color::Red); let x_axis = tg.generate_x_axis(); @@ -283,8 +283,8 @@ mod test { } #[test] - fn time_graph_gen_y_axis() { - let tg = create_time_graph(); + fn time_series_gen_y_axis() { + let tg = create_time_series(); let style = Style::default().fg(Color::Red); let y_axis = tg.generate_y_axis(); diff --git a/src/canvas/components/time_graph/variants.rs b/src/canvas/components/time_series/variants.rs similarity index 100% rename from src/canvas/components/time_graph/variants.rs rename to src/canvas/components/time_series/variants.rs diff --git a/src/canvas/components/time_series/variants/auto_y_axis.rs b/src/canvas/components/time_series/variants/auto_y_axis.rs new file mode 100644 index 00000000..808ae661 --- /dev/null +++ b/src/canvas/components/time_series/variants/auto_y_axis.rs @@ -0,0 +1,2 @@ +//! A variant of a [`crate::canvas::components::time_series::TimeGraph`] that +//! automatically adjusts the y-axis based on the data provided. diff --git a/src/canvas/components/time_graph/variants/percent.rs b/src/canvas/components/time_series/variants/percent.rs similarity index 98% rename from src/canvas/components/time_graph/variants/percent.rs rename to src/canvas/components/time_series/variants/percent.rs index 68cb4489..6ef8663f 100644 --- a/src/canvas/components/time_graph/variants/percent.rs +++ b/src/canvas/components/time_series/variants/percent.rs @@ -7,7 +7,7 @@ use tui::symbols::Marker; use crate::{ app::AppConfigFields, - canvas::components::time_graph::{ + canvas::components::time_series::{ AxisBound, ChartScaling, LegendConstraints, LegendPosition, TimeGraph, variants::get_border_style, }, diff --git a/src/canvas/components/time_graph/vendored.rs b/src/canvas/components/time_series/vendored.rs similarity index 99% rename from src/canvas/components/time_graph/vendored.rs rename to src/canvas/components/time_series/vendored.rs index cdde4295..272418ec 100644 --- a/src/canvas/components/time_graph/vendored.rs +++ b/src/canvas/components/time_series/vendored.rs @@ -1,8 +1,8 @@ //! A [`tui::widgets::Chart`] but slightly more specialized to show -//! right-aligned timeseries data. +//! right-aligned time_series data. //! //! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs); -//! the specializations are factored out to `time_graph/points.rs`. +//! the specializations are factored out to `time_series/points.rs`. mod canvas; mod grid; @@ -27,7 +27,7 @@ use tui::{ use unicode_width::UnicodeWidthStr; use crate::{ - canvas::components::time_graph::LegendConstraints, + canvas::components::time_series::LegendConstraints, utils::general::{saturating_log2, saturating_log10}, }; diff --git a/src/canvas/components/time_graph/vendored/canvas.rs b/src/canvas/components/time_series/vendored/canvas.rs similarity index 100% rename from src/canvas/components/time_graph/vendored/canvas.rs rename to src/canvas/components/time_series/vendored/canvas.rs diff --git a/src/canvas/components/time_graph/vendored/grid.rs b/src/canvas/components/time_series/vendored/grid.rs similarity index 100% rename from src/canvas/components/time_graph/vendored/grid.rs rename to src/canvas/components/time_series/vendored/grid.rs diff --git a/src/canvas/components/time_graph/vendored/points.rs b/src/canvas/components/time_series/vendored/points.rs similarity index 99% rename from src/canvas/components/time_graph/vendored/points.rs rename to src/canvas/components/time_series/vendored/points.rs index b31344fc..56b14310 100644 --- a/src/canvas/components/time_graph/vendored/points.rs +++ b/src/canvas/components/time_series/vendored/points.rs @@ -115,7 +115,7 @@ mod test { use super::*; #[test] - fn time_graph_test_interpolation() { + fn time_series_test_interpolation() { let data = [(-3.0, 8.0), (-1.0, 6.0), (0.0, 5.0)]; assert_eq!(interpolate_point(&data[1], &data[2], 0.0), 5.0); diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 63f56a0d..d9bd6778 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -9,7 +9,7 @@ use crate::{ Painter, components::{ data_table::{DrawInfo, SelectionState}, - time_graph::{GraphData, PercentTimeGraph}, + time_series::{GraphData, PercentTimeGraph}, }, drawing_utils::should_hide_x_label, }, @@ -122,8 +122,8 @@ impl Painter { let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; let current_scroll_position = cpu_widget_state.table.state.current_index; let cpu_entries = &data.cpu_harvest; - let cpu_points = &data.timeseries_data.cpu; - let time = &data.timeseries_data.time; + let cpu_points = &data.time_series_data.cpu; + let time = &data.time_series_data.time; if current_scroll_position == ALL_POSITION { // This case ensures the other cases cannot have the position be equal to 0. @@ -177,7 +177,7 @@ impl Painter { let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, - &mut cpu_widget_state.autohide_timer, + cpu_widget_state.time_series_state.autohide_timer_mut(), draw_loc, ); @@ -206,7 +206,7 @@ impl Painter { }; PercentTimeGraph { - display_range: cpu_widget_state.current_display_time, + display_range: cpu_widget_state.time_series_state.current_display_time(), hide_x_labels, app_config_fields: &app_state.app_config_fields, current_widget: app_state.current_widget.widget_id, diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index c0a7a7c0..3ffa1ddb 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -10,7 +10,7 @@ use crate::{ app::{App, data::Values}, canvas::{ Painter, - components::time_graph::{GraphData, LegendConstraints, PercentTimeGraph}, + components::time_series::{GraphData, LegendConstraints, PercentTimeGraph}, drawing_utils::should_hide_x_label, }, collection::memory::MemData, @@ -60,7 +60,7 @@ impl Painter { let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, - &mut mem_state.autohide_timer, + mem_state.time_series_state.autohide_timer_mut(), draw_loc, ); let graph_data = { @@ -85,8 +85,8 @@ impl Painter { } let mut points = Vec::with_capacity(size); - let timeseries = &data.timeseries_data; - let time = ×eries.time; + let time_series = &data.time_series_data; + let time = &time_series.time; // TODO: Add a "no data" option here/to time graph if there is no entries graph_data( @@ -94,7 +94,7 @@ impl Painter { "RAM", data.ram_harvest.as_ref(), time, - ×eries.ram, + &time_series.ram, self.styles.ram_style, ); @@ -103,7 +103,7 @@ impl Painter { "SWP", data.swap_harvest.as_ref(), time, - ×eries.swap, + &time_series.swap, self.styles.swap_style, ); @@ -114,7 +114,7 @@ impl Painter { "CACHE", // TODO: Figure out how to line this up better data.cache_harvest.as_ref(), time, - ×eries.cache_mem, + &time_series.cache_mem, self.styles.cache_style, ); } @@ -126,7 +126,7 @@ impl Painter { "ARC", data.arc_harvest.as_ref(), time, - ×eries.arc_mem, + &time_series.arc_mem, self.styles.arc_style, ); } @@ -137,7 +137,7 @@ impl Painter { let gpu_styles = &self.styles.gpu_colours; for (name, harvest) in &data.gpu_harvest { - if let Some(gpu_data) = data.timeseries_data.gpu_mem.get(name) { + if let Some(gpu_data) = data.time_series_data.gpu_mem.get(name) { let style = { if gpu_styles.is_empty() { Style::default() @@ -165,7 +165,7 @@ impl Painter { }; PercentTimeGraph { - display_range: mem_state.current_display_time, + display_range: mem_state.time_series_state.current_display_time(), hide_x_labels, app_config_fields: &app_state.app_config_fields, current_widget: app_state.current_widget.widget_id, diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index c8ac9cbf..050d5f4a 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -10,7 +10,7 @@ use crate::{ app::{App, AppConfigFields, AxisScaling}, canvas::{ Painter, - components::time_graph::{ + components::time_series::{ AxisBound, ChartScaling, GraphData, LegendConstraints, TimeGraph, }, drawing_utils::{should_hide_x_label, widget_block}, @@ -63,16 +63,18 @@ impl Painter { { let shared_data = app_state.data_store.get_data(); let network_latest_data = &(shared_data.network_harvest); - let rx_points = &(shared_data.timeseries_data.rx); - let tx_points = &(shared_data.timeseries_data.tx); - let times = &(shared_data.timeseries_data.time); - let time_start = -(network_widget_state.current_display_time as f64); + let rx_points = &(shared_data.time_series_data.rx); + let tx_points = &(shared_data.time_series_data.tx); + let times = &(shared_data.time_series_data.time); + let time_start = -(network_widget_state + .time_series_state + .current_display_time() as f64); let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, - &mut network_widget_state.autohide_timer, + network_widget_state.time_series_state.autohide_timer_mut(), draw_loc, ); @@ -81,7 +83,9 @@ impl Painter { let cache = &mut network_widget_state.height_cache; cache.get_or_update( last_time, - network_widget_state.current_display_time, + network_widget_state + .time_series_state + .current_display_time(), [rx_points, tx_points].into_iter(), times, ) diff --git a/src/canvas/widgets/temperature_graph.rs b/src/canvas/widgets/temperature_graph.rs index 0160f6e9..5126d101 100644 --- a/src/canvas/widgets/temperature_graph.rs +++ b/src/canvas/widgets/temperature_graph.rs @@ -8,7 +8,7 @@ use crate::{ app::{App, AppConfigFields}, canvas::{ Painter, - components::time_graph::{ + components::time_series::{ AxisBound, ChartScaling, GraphData, LegendConstraints, TimeGraph, }, drawing_utils::should_hide_x_label, @@ -25,15 +25,15 @@ impl Painter { .get_mut_widget_state(widget_id) { let shared_data = app_state.data_store.get_data(); - let points = &(shared_data.timeseries_data.temperature); - let times = &(shared_data.timeseries_data.time); - let time_start = -(widget_state.current_display_time as f64); + let points = &(shared_data.time_series_data.temperature); + let times = &(shared_data.time_series_data.time); + let time_start = -(widget_state.time_series_state.current_display_time() as f64); let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, - &mut widget_state.autohide_timer, + widget_state.time_series_state.autohide_timer_mut(), draw_loc, ); @@ -42,7 +42,7 @@ impl Painter { let cache = &mut widget_state.height_cache; cache.get_or_update( last_time, - widget_state.current_display_time, + widget_state.time_series_state.current_display_time(), points.values(), times, ) diff --git a/src/options.rs b/src/options.rs index 14fc2426..1169ef4b 100644 --- a/src/options.rs +++ b/src/options.rs @@ -32,7 +32,7 @@ use self::{ }; use crate::{ app::{filter::Filter, layout_manager::*, *}, - canvas::components::time_graph::LegendPosition, + canvas::components::time_series::LegendPosition, constants::*, utils::data_units::DataUnit, widgets::*, @@ -1112,7 +1112,7 @@ mod test { use crate::{ app::App, args::BottomArgs, - canvas::components::time_graph::LegendPosition, + canvas::components::time_series::LegendPosition, options::{ OptionError, config::flags::GeneralConfig, get_default_time_value, get_retention, get_update_rate, parse_legend_position, try_parse_ms, diff --git a/src/widgets/common/mod.rs b/src/widgets/common/mod.rs new file mode 100644 index 00000000..559829d1 --- /dev/null +++ b/src/widgets/common/mod.rs @@ -0,0 +1,5 @@ +//! Common widget code. + +mod time_series; + +pub use time_series::*; diff --git a/src/widgets/common/time_series.rs b/src/widgets/common/time_series.rs new file mode 100644 index 00000000..2321fb00 --- /dev/null +++ b/src/widgets/common/time_series.rs @@ -0,0 +1,309 @@ +use std::{ + cmp::{max, min}, + time::{Duration, Instant}, +}; + +use timeless::data::ChunkedData; + +const STALE_MIN_MILLISECONDS: u64 = Duration::from_secs(30).as_millis() as u64; + +/// A time_series graph widget displays data over a period of time. +pub struct TimeseriesState { + current_display_time: u64, + autohide_timer: Option, +} + +impl TimeseriesState { + /// Create a new [`TimeseriesState`] that displays starting from `starting_time`. + pub fn new(starting_time: u64) -> Self { + Self { + current_display_time: starting_time, + autohide_timer: None, + } + } + + /// Set the autohide timer. + pub fn with_autohide_timer(mut self, autohide_timer: Option) -> Self { + self.autohide_timer = autohide_timer; + self + } + + /// Get a mutable reference to the autohide timer. + pub fn autohide_timer_mut(&mut self) -> &mut Option { + &mut self.autohide_timer + } + + /// Get the current display time. + pub fn current_display_time(&self) -> u64 { + self.current_display_time + } + + /// Zoom in on the x-axis (reducing the time range shown). + pub fn zoom_in(&mut self, time_interval: u64, autohide_time: bool) { + let new_time = self.current_display_time.saturating_sub(time_interval); + + self.current_display_time = max(new_time, STALE_MIN_MILLISECONDS); + self.maybe_start_autohide(autohide_time); + } + + /// Zoom out on the x-axis (increasing the time range shown). + pub fn zoom_out(&mut self, time_interval: u64, retention_ms: u64, autohide_time: bool) { + let new_time = self.current_display_time.saturating_add(time_interval); + + self.current_display_time = min(new_time, retention_ms); + self.maybe_start_autohide(autohide_time); + } + + /// Reset the zoom level to the default. + pub fn reset_zoom(&mut self, default_time_value: u64, autohide_time: bool) { + self.current_display_time = default_time_value; + self.maybe_start_autohide(autohide_time); + } + + /// Set the autohide timer if needed. + fn maybe_start_autohide(&mut self, autohide_time: bool) { + if autohide_time { + self.autohide_timer = Some(Instant::now()); + } + } +} + +struct GraphHeightCacheInner { + best_point: (Instant, f64), + right_edge: Instant, + period: u64, +} + +#[derive(Default)] +pub struct GraphHeightCache { + inner: Option, +} + +impl GraphHeightCache { + /// Get the cached height if it exists, or set it otherwise. + pub(crate) fn get_or_update< + 'a, + F: Into + Clone + Copy + 'a, + S: Iterator>, + >( + &mut self, last_time: &Instant, current_display_time: u64, sources: S, times: &[Instant], + ) -> f64 { + let visible_duration = Duration::from_millis(current_display_time); + + let (mut biggest, mut biggest_time, oldest_to_check) = if let Some(GraphHeightCacheInner { + best_point, + right_edge, + period, + }) = self.inner.as_ref() + && *period == current_display_time + && last_time.duration_since(best_point.0) < visible_duration + { + (best_point.1, best_point.0, *right_edge) + } else { + let visible_duration = Duration::from_millis(current_display_time); + + let visible_left_bound = match last_time.checked_sub(visible_duration) { + Some(v) => v, + None => { + // On some systems (like Windows) it can be possible that the + // current display time + // causes subtraction to fail if, for example, the uptime of the + // system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825. + // + // As such, we instead take the oldest visible time. This is a + // bit inefficient, but + // since it should only happen rarely, it should be fine. + times + .iter() + .take_while(|t| last_time.duration_since(**t) < visible_duration) + .last() + .cloned() + .unwrap_or(*last_time) + } + }; + + (0.0, visible_left_bound, visible_left_bound) + }; + + for source in sources { + for (&time, &v) in source + .iter_along_base(times) + .rev() + .take_while(|&(&time, _)| time >= oldest_to_check) + { + let v = v.into(); + if v > biggest { + biggest = v; + biggest_time = time; + } + } + } + + self.inner = Some(GraphHeightCacheInner { + best_point: (biggest_time, biggest), + right_edge: *last_time, + period: current_display_time, + }); + + biggest + } +} + +#[cfg(test)] +mod time_series_tests { + use super::*; + + #[test] + fn zoom_in_decreases_display_time() { + let mut state = TimeseriesState { + current_display_time: 60_000, + autohide_timer: None, + }; + + state.zoom_in(15_000, false); + assert_eq!(state.current_display_time, 45_000); + } + + #[test] + fn zoom_in_clamps_at_minimum() { + let mut state = TimeseriesState { + current_display_time: 35_000, + autohide_timer: None, + }; + + state.zoom_in(15_000, false); + assert_eq!(state.current_display_time, STALE_MIN_MILLISECONDS); // 30_000 + } + + #[test] + fn zoom_out_increases_display_time() { + let mut state = TimeseriesState { + current_display_time: 60_000, + autohide_timer: None, + }; + + state.zoom_out(15_000, 300_000, false); + assert_eq!(state.current_display_time, 75_000); + } + + #[test] + fn zoom_out_clamps_at_retention() { + let mut state = TimeseriesState { + current_display_time: 290_000, + autohide_timer: None, + }; + + state.zoom_out(15_000, 300_000, false); + assert_eq!(state.current_display_time, 300_000); + } + + #[test] + fn reset_zoom_restores_default() { + let mut state = TimeseriesState { + current_display_time: 120_000, + autohide_timer: None, + }; + + state.reset_zoom(60_000, false); + assert_eq!(state.current_display_time, 60_000); + } + + #[test] + fn autohide_armed_on_change() { + let mut state = TimeseriesState { + current_display_time: 60_000, + autohide_timer: None, + }; + + state.zoom_in(15_000, true); + assert!(state.autohide_timer.is_some()); + } +} + +#[cfg(test)] +mod graph_height_tests { + use super::*; + + fn build(times: &[Instant], values: &[f64]) -> ChunkedData { + assert_eq!(times.len(), values.len()); + let mut data = ChunkedData::default(); + for &v in values { + data.push(v); + } + data + } + + #[test] + fn empty_sources_returns_zero() { + let mut cache = GraphHeightCache::default(); + let last_time = Instant::now(); + let times: Vec = vec![]; + let sources: Vec> = vec![]; + + let result = cache.get_or_update(&last_time, 1_000, sources.iter(), ×); + assert_eq!(result, 0.0); + assert!(cache.inner.is_some()); + } + + #[test] + fn picks_max_across_sources() { + let mut cache = GraphHeightCache::default(); + let now = Instant::now(); + let times = vec![ + now - Duration::from_millis(300), + now - Duration::from_millis(200), + now - Duration::from_millis(100), + now, + ]; + let a = build(×, &[1.0, 2.0, 3.0, 4.0]); + let b = build(×, &[10.0, 5.0, 0.5, 0.25]); + + let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), ×); + assert_eq!(result, 10.0); + } + + #[test] + fn cache_hit_skips_older_points() { + // On a cache hit, only points after `right_edge` are rescanned. So if + // we pass a fresh source whose only large values are *older* than the + // previous `last_time`, the cached max should still win. + let mut cache = GraphHeightCache::default(); + let now = Instant::now(); + let times = vec![ + now - Duration::from_millis(200), + now - Duration::from_millis(100), + now, + ]; + let first = build(×, &[3.0, 5.0, 7.0]); + + let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), ×); + assert_eq!(first_result, 7.0); + + // Older points carry huge values; newest is small. A full rescan would + // return 1000.0 — a true cache hit returns the cached 7.0. + let second = build(×, &[1000.0, 999.0, 4.0]); + let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), ×); + assert_eq!(second_result, 7.0); + } + + #[test] + fn cache_invalidates_on_period_change() { + // First call uses a small window that excludes the older high value. + // Second call uses a larger window that should include it; a stale + // cache would miss it and return the previous max. + let mut cache = GraphHeightCache::default(); + let now = Instant::now(); + let times = vec![ + now - Duration::from_millis(500), + now - Duration::from_millis(50), + now, + ]; + let data = build(×, &[100.0, 5.0, 7.0]); + + let first = cache.get_or_update(&now, 100, [&data].into_iter(), ×); + assert_eq!(first, 7.0); + + let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), ×); + assert_eq!(second, 100.0); + } +} diff --git a/src/widgets/cpu_graph.rs b/src/widgets/cpu_graph.rs index ac442718..08c59e2c 100644 --- a/src/widgets/cpu_graph.rs +++ b/src/widgets/cpu_graph.rs @@ -14,6 +14,7 @@ use crate::{ }, collection::cpu::{CpuData, CpuDataType}, options::config::{cpu::CpuDefault, style::Styles}, + widgets::TimeseriesState, }; pub enum CpuWidgetColumn { @@ -125,16 +126,15 @@ impl DataToCell for CpuWidgetTableData { } pub struct CpuWidgetState { - pub current_display_time: u64, + pub time_series_state: TimeseriesState, pub is_legend_hidden: bool, - pub autohide_timer: Option, pub table: DataTable, pub force_update_data: bool, } impl CpuWidgetState { pub(crate) fn new( - config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64, + config: &AppConfigFields, default_selection: CpuDefault, starting_time: u64, autohide_timer: Option, colours: &Styles, ) -> Self { let columns = [ @@ -168,9 +168,9 @@ impl CpuWidgetState { } CpuWidgetState { - current_display_time, + time_series_state: TimeseriesState::new(starting_time) + .with_autohide_timer(autohide_timer), is_legend_hidden: false, - autohide_timer, table, force_update_data: false, } diff --git a/src/widgets/mem_graph.rs b/src/widgets/mem_graph.rs index a86bd02a..24f99985 100644 --- a/src/widgets/mem_graph.rs +++ b/src/widgets/mem_graph.rs @@ -1,15 +1,16 @@ use std::time::Instant; +use crate::widgets::TimeseriesState; + pub struct MemWidgetState { - pub current_display_time: u64, - pub autohide_timer: Option, + pub time_series_state: TimeseriesState, } impl MemWidgetState { - pub fn init(current_display_time: u64, autohide_timer: Option) -> Self { + pub fn init(starting_time: u64, autohide_timer: Option) -> Self { MemWidgetState { - current_display_time, - autohide_timer, + time_series_state: TimeseriesState::new(starting_time) + .with_autohide_timer(autohide_timer), } } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 6d44f868..55a99900 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,4 +1,5 @@ pub mod battery_info; +mod common; pub mod cpu_graph; pub mod disk_table; pub mod mem_graph; @@ -7,9 +8,8 @@ pub mod process_table; pub mod temperature_graph; pub mod temperature_table; -use std::time::{Duration, Instant}; - pub use battery_info::*; +pub(crate) use common::*; pub use cpu_graph::*; pub use disk_table::*; pub use mem_graph::*; @@ -17,173 +17,3 @@ pub use network_graph::*; pub use process_table::*; pub use temperature_graph::*; pub use temperature_table::*; -use timeless::data::ChunkedData; - -struct GraphHeightCacheInner { - best_point: (Instant, f64), - right_edge: Instant, - period: u64, -} - -#[derive(Default)] -pub struct GraphHeightCache { - inner: Option, -} - -impl GraphHeightCache { - /// Get the cached height if it exists, or set it otherwise. - pub(crate) fn get_or_update< - 'a, - F: Into + Clone + Copy + 'a, - S: Iterator>, - >( - &mut self, last_time: &Instant, current_display_time: u64, sources: S, times: &[Instant], - ) -> f64 { - let visible_duration = Duration::from_millis(current_display_time); - - let (mut biggest, mut biggest_time, oldest_to_check) = if let Some(GraphHeightCacheInner { - best_point, - right_edge, - period, - }) = self.inner.as_ref() - && *period == current_display_time - && last_time.duration_since(best_point.0) < visible_duration - { - (best_point.1, best_point.0, *right_edge) - } else { - let visible_duration = Duration::from_millis(current_display_time); - - let visible_left_bound = match last_time.checked_sub(visible_duration) { - Some(v) => v, - None => { - // On some systems (like Windows) it can be possible that the - // current display time - // causes subtraction to fail if, for example, the uptime of the - // system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825. - // - // As such, we instead take the oldest visible time. This is a - // bit inefficient, but - // since it should only happen rarely, it should be fine. - times - .iter() - .take_while(|t| last_time.duration_since(**t) < visible_duration) - .last() - .cloned() - .unwrap_or(*last_time) - } - }; - - (0.0, visible_left_bound, visible_left_bound) - }; - - for source in sources { - for (&time, &v) in source - .iter_along_base(times) - .rev() - .take_while(|&(&time, _)| time >= oldest_to_check) - { - let v = v.into(); - if v > biggest { - biggest = v; - biggest_time = time; - } - } - } - - self.inner = Some(GraphHeightCacheInner { - best_point: (biggest_time, biggest), - right_edge: *last_time, - period: current_display_time, - }); - - biggest - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn build(times: &[Instant], values: &[f64]) -> ChunkedData { - assert_eq!(times.len(), values.len()); - let mut data = ChunkedData::default(); - for &v in values { - data.push(v); - } - data - } - - #[test] - fn empty_sources_returns_zero() { - let mut cache = GraphHeightCache::default(); - let last_time = Instant::now(); - let times: Vec = vec![]; - let sources: Vec> = vec![]; - - let result = cache.get_or_update(&last_time, 1_000, sources.iter(), ×); - assert_eq!(result, 0.0); - assert!(cache.inner.is_some()); - } - - #[test] - fn picks_max_across_sources() { - let mut cache = GraphHeightCache::default(); - let now = Instant::now(); - let times = vec![ - now - Duration::from_millis(300), - now - Duration::from_millis(200), - now - Duration::from_millis(100), - now, - ]; - let a = build(×, &[1.0, 2.0, 3.0, 4.0]); - let b = build(×, &[10.0, 5.0, 0.5, 0.25]); - - let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), ×); - assert_eq!(result, 10.0); - } - - #[test] - fn cache_hit_skips_older_points() { - // On a cache hit, only points after `right_edge` are rescanned. So if - // we pass a fresh source whose only large values are *older* than the - // previous `last_time`, the cached max should still win. - let mut cache = GraphHeightCache::default(); - let now = Instant::now(); - let times = vec![ - now - Duration::from_millis(200), - now - Duration::from_millis(100), - now, - ]; - let first = build(×, &[3.0, 5.0, 7.0]); - - let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), ×); - assert_eq!(first_result, 7.0); - - // Older points carry huge values; newest is small. A full rescan would - // return 1000.0 — a true cache hit returns the cached 7.0. - let second = build(×, &[1000.0, 999.0, 4.0]); - let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), ×); - assert_eq!(second_result, 7.0); - } - - #[test] - fn cache_invalidates_on_period_change() { - // First call uses a small window that excludes the older high value. - // Second call uses a larger window that should include it; a stale - // cache would miss it and return the previous max. - let mut cache = GraphHeightCache::default(); - let now = Instant::now(); - let times = vec![ - now - Duration::from_millis(500), - now - Duration::from_millis(50), - now, - ]; - let data = build(×, &[100.0, 5.0, 7.0]); - - let first = cache.get_or_update(&now, 100, [&data].into_iter(), ×); - assert_eq!(first, 7.0); - - let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), ×); - assert_eq!(second, 100.0); - } -} diff --git a/src/widgets/network_graph.rs b/src/widgets/network_graph.rs index 943899aa..3ee02cf6 100644 --- a/src/widgets/network_graph.rs +++ b/src/widgets/network_graph.rs @@ -1,18 +1,17 @@ use std::time::Instant; -use crate::widgets::GraphHeightCache; +use crate::widgets::{GraphHeightCache, TimeseriesState}; pub struct NetWidgetState { - pub current_display_time: u64, - pub autohide_timer: Option, + pub time_series_state: TimeseriesState, pub height_cache: GraphHeightCache, } impl NetWidgetState { - pub fn init(current_display_time: u64, autohide_timer: Option) -> Self { + pub fn init(starting_time: u64, autohide_timer: Option) -> Self { NetWidgetState { - current_display_time, - autohide_timer, + time_series_state: TimeseriesState::new(starting_time) + .with_autohide_timer(autohide_timer), height_cache: GraphHeightCache::default(), } } diff --git a/src/widgets/temperature_graph.rs b/src/widgets/temperature_graph.rs index bde96b4e..02325aa8 100644 --- a/src/widgets/temperature_graph.rs +++ b/src/widgets/temperature_graph.rs @@ -2,23 +2,20 @@ use std::time::Instant; -use crate::widgets::GraphHeightCache; +use crate::widgets::{GraphHeightCache, TimeseriesState}; -/// A timeseries graph widget displaying temperature usage over time. +/// A time_series graph widget displaying temperature usage over time. pub struct TempGraphWidgetState { - pub current_display_time: u64, - pub autohide_timer: Option, + pub time_series_state: TimeseriesState, pub height_cache: GraphHeightCache, pub max_temp: Option, } impl TempGraphWidgetState { - pub fn new( - current_display_time: u64, autohide_timer: Option, max_temp: Option, - ) -> Self { + pub fn new(starting_time: u64, autohide_timer: Option, max_temp: Option) -> Self { TempGraphWidgetState { - current_display_time, - autohide_timer, + time_series_state: TimeseriesState::new(starting_time) + .with_autohide_timer(autohide_timer), height_cache: GraphHeightCache::default(), max_temp, }