From eb6a737d3430920061cd5e54bf4dc40da21f1fc5 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sun, 4 Apr 2021 05:38:57 -0400 Subject: feature: Rework network y-axis, linear interpolation for off-screen data (#437) Rewrite of the y-axis labeling and scaling for the network widget, along with more customization. This still has one step to be optimized (cache results so we don't have to recalculate the legend each time), but will be done in another PR for sake of this one being too large already. Furthermore, this change adds linear interpolation at the 0 point in the case a data point shoots too far back - this seems to have lead to ugly gaps to the left of graphs in some cases, because the left hand limit was not big enough for the data point. We address this by grabbing values just outside the time range and linearly interpolating at the leftmost limit. This affects all graph widgets (CPU, mem, network). This can be optimized, and will hopefully be prior to release in a separate change. --- src/canvas/dialogs/dd_dialog.rs | 8 +- src/canvas/drawing_utils.rs | 17 ++ src/canvas/widgets/cpu_graph.rs | 81 ++++- src/canvas/widgets/mem_graph.rs | 106 ++++++- src/canvas/widgets/network_graph.rs | 577 +++++++++++++++++++++++++++++------- 5 files changed, 655 insertions(+), 134 deletions(-) (limited to 'src/canvas') diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index cd9029c8..0415aa2d 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -72,11 +72,11 @@ impl KillDialog for Painter { ) { if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill { let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal { - KillSignal::KILL(_) => ( + KillSignal::Kill(_) => ( Span::styled("Yes", self.colours.currently_selected_text_style), Span::raw("No"), ), - KillSignal::CANCEL => ( + KillSignal::Cancel => ( Span::raw("Yes"), Span::styled("No", self.colours.currently_selected_text_style), ), @@ -249,8 +249,8 @@ impl KillDialog for Painter { .split(*button_draw_loc)[1]; let mut selected = match app_state.delete_dialog_state.selected_signal { - KillSignal::CANCEL => 0, - KillSignal::KILL(signal) => signal, + KillSignal::Cancel => 0, + KillSignal::Kill(signal) => signal, }; // 32+33 are skipped if selected > 31 { diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index 222ca852..6aee1aa7 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -117,6 +117,12 @@ pub fn get_column_widths( filtered_column_widths } +/// FIXME: [command move] This is a greedy method of determining column widths. This is reserved for columns where we are okay with +/// shoving information as far right as required. +// pub fn greedy_get_column_widths() -> Vec { +// vec![] +// } + pub fn get_search_start_position( num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize, current_cursor_position: usize, is_force_redraw: bool, @@ -205,3 +211,14 @@ pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) num_bars_available, ) } + +/// Interpolates between two points. Mainly used to help fill in tui-rs blanks in certain situations. +/// It is expected point_one is "further left" compared to point_two. +/// A point is two floats, in (x, y) form. x is time, y is value. +pub fn interpolate_points(point_one: &(f64, f64), point_two: &(f64, f64), time: f64) -> f64 { + let delta_x = point_two.0 - point_one.0; + let delta_y = point_two.1 - point_one.1; + let slope = delta_y / delta_x; + + (point_one.1 + (time - point_one.0) * slope).max(0.0) +} diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 1b79482f..ff6838f2 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ app::{layout_manager::WidgetDirection, App}, canvas::{ - drawing_utils::{get_column_widths, get_start_position}, + drawing_utils::{get_column_widths, get_start_position, interpolate_points}, Painter, }, constants::*, @@ -146,32 +146,34 @@ impl CpuGraphWidget for Painter { ]; let y_axis_labels = vec![ - Span::styled("0%", self.colours.graph_style), + Span::styled(" 0%", self.colours.graph_style), Span::styled("100%", self.colours.graph_style), ]; + let time_start = -(cpu_widget_state.current_display_time as f64); + let x_axis = if app_state.app_config_fields.hide_time || (app_state.app_config_fields.autohide_time && cpu_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = cpu_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { cpu_widget_state.autohide_timer = None; - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; @@ -184,6 +186,59 @@ impl CpuGraphWidget for Painter { let use_dot = app_state.app_config_fields.use_dot; let show_avg_cpu = app_state.app_config_fields.show_average_cpu; let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; + + let interpolated_cpu_points = cpu_data + .iter_mut() + .enumerate() + .map(|(itx, cpu)| { + let to_show = if current_scroll_position == ALL_POSITION { + true + } else { + itx == current_scroll_position + }; + + if to_show { + if let Some(end_pos) = cpu + .cpu_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = cpu.cpu_data.get(start_pos); + let inside_point = cpu.cpu_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = + (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + } + } else { + None + } + }) + .collect::>(); + let dataset_vector: Vec> = if current_scroll_position == ALL_POSITION { cpu_data .iter() @@ -311,6 +366,18 @@ impl CpuGraphWidget for Painter { .y_axis(y_axis), draw_loc, ); + + // Reset interpolated points + cpu_data + .iter_mut() + .zip(interpolated_cpu_points) + .for_each(|(cpu, interpolation)| { + if let Some((index, old_value)) = interpolation { + if let Some(to_replace) = cpu.cpu_data.get_mut(index) { + *to_replace = old_value; + } + } + }); } } diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index 9136b1ba..6b5bc5ab 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -1,4 +1,8 @@ -use crate::{app::App, canvas::Painter, constants::*}; +use crate::{ + app::App, + canvas::{drawing_utils::interpolate_points, Painter}, + constants::*, +}; use tui::{ backend::Backend, @@ -22,8 +26,10 @@ impl MemGraphWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) { - let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; - let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; + let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data; + let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data; + + let time_start = -(mem_widget_state.current_display_time as f64); let display_time_labels = vec![ Span::styled( @@ -33,7 +39,7 @@ impl MemGraphWidget for Painter { Span::styled("0s".to_string(), self.colours.graph_style), ]; let y_axis_label = vec![ - Span::styled("0%", self.colours.graph_style), + Span::styled(" 0%", self.colours.graph_style), Span::styled("100%", self.colours.graph_style), ]; @@ -41,24 +47,24 @@ impl MemGraphWidget for Painter { || (app_state.app_config_fields.autohide_time && mem_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = mem_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { mem_widget_state.autohide_timer = None; - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; @@ -68,6 +74,75 @@ impl MemGraphWidget for Painter { .bounds([0.0, 100.5]) .labels(y_axis_label); + // Interpolate values to avoid ugly gaps + let interpolated_mem_point = if let Some(end_pos) = mem_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = mem_data.get(start_pos); + let inside_point = mem_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = mem_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + let interpolated_swap_point = if let Some(end_pos) = swap_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = swap_data.get(start_pos); + let inside_point = swap_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = swap_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + let mut mem_canvas_vec: Vec> = vec![]; if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { @@ -147,6 +222,19 @@ impl MemGraphWidget for Painter { .hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), draw_loc, ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_mem_point { + if let Some(to_replace) = mem_data.get_mut(index) { + *to_replace = old_value; + } + } + + if let Some((index, old_value)) = interpolated_swap_point { + if let Some(to_replace) = swap_data.get_mut(index) { + *to_replace = old_value; + } + } } if app_state.should_get_widget_bounds() { diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index b6ee080b..74509224 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -3,9 +3,13 @@ use std::cmp::max; use unicode_segmentation::UnicodeSegmentation; use crate::{ - app::App, - canvas::{drawing_utils::get_column_widths, Painter}, + app::{App, AxisScaling}, + canvas::{ + drawing_utils::{get_column_widths, interpolate_points}, + Painter, + }, constants::*, + units::data_units::DataUnit, utils::gen_util::*, }; @@ -82,103 +86,344 @@ impl NetworkGraphWidget for Painter { /// Point is of time, data type Point = (f64, f64); - /// Returns the required max data point and labels. - fn adjust_network_data_point( - rx: &[Point], tx: &[Point], time_start: f64, time_end: f64, - ) -> (f64, Vec) { - // First, filter and find the maximal rx or tx so we know how to scale - let mut max_val_bytes = 0.0; - let filtered_rx = rx - .iter() - .cloned() - .filter(|(time, _data)| *time >= time_start && *time <= time_end); - - let filtered_tx = tx - .iter() - .cloned() - .filter(|(time, _data)| *time >= time_start && *time <= time_end); - - for (_time, data) in filtered_rx.clone().chain(filtered_tx.clone()) { - if data > max_val_bytes { - max_val_bytes = data; + /// Returns the max data point and time given a time. + fn get_max_entry( + rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling, + network_use_binary_prefix: bool, + ) -> (f64, f64) { + /// Determines a "fake" max value in circumstances where we couldn't find one from the data. + fn calculate_missing_max( + network_scale_type: &AxisScaling, network_use_binary_prefix: bool, + ) -> f64 { + match network_scale_type { + AxisScaling::Log => { + if network_use_binary_prefix { + LOG_KIBI_LIMIT + } else { + LOG_KILO_LIMIT + } + } + AxisScaling::Linear => { + if network_use_binary_prefix { + KIBI_LIMIT_F64 + } else { + KILO_LIMIT_F64 + } + } } } - // FIXME [NETWORKING]: Granularity. Just scale up the values. - // FIXME [NETWORKING]: Ability to set fixed scale in config. - // Currently we do 32 -> 33... which skips some gigabit values - let true_max_val: f64; - let mut labels = vec![]; - if max_val_bytes < LOG_KIBI_LIMIT { - true_max_val = LOG_KIBI_LIMIT; - labels = vec!["0B".to_string(), "1KiB".to_string()]; - } else if max_val_bytes < LOG_MEBI_LIMIT { - true_max_val = LOG_MEBI_LIMIT; - labels = vec!["0B".to_string(), "1KiB".to_string(), "1MiB".to_string()]; - } else if max_val_bytes < LOG_GIBI_LIMIT { - true_max_val = LOG_GIBI_LIMIT; - labels = vec![ - "0B".to_string(), - "1KiB".to_string(), - "1MiB".to_string(), - "1GiB".to_string(), - ]; - } else if max_val_bytes < LOG_TEBI_LIMIT { - true_max_val = max_val_bytes.ceil() + 1.0; - let cap_u32 = true_max_val as u32; - - for i in 0..=cap_u32 { - match i { - 0 => labels.push("0B".to_string()), - LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()), - LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()), - LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()), - _ if i == cap_u32 => { - labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_GIBI_LIMIT_U32))) + // First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays + // are sorted, so we can short-circuit our search to filter out only the relevant data points... + let filtered_rx = if let (Some(rx_start), Some(rx_end)) = ( + rx.iter().position(|(time, _data)| *time >= time_start), + rx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&rx[rx_start..=rx_end]) + } else { + None + }; + + let filtered_tx = if let (Some(tx_start), Some(tx_end)) = ( + tx.iter().position(|(time, _data)| *time >= time_start), + tx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&tx[tx_start..=tx_end]) + } else { + None + }; + + // Then, find the maximal rx/tx so we know how to scale, and return it. + match (filtered_rx, filtered_tx) { + (None, None) => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + (None, Some(filtered_tx)) => { + match filtered_tx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } } - _ if i == (LOG_GIBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!( - "{}GiB", - 2_u64.pow(cap_u32 - ((LOG_GIBI_LIMIT_U32 + cap_u32) / 2)) - )), // ~Halfway point - _ => labels.push(String::default()), + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), } } - } else { - true_max_val = max_val_bytes.ceil() + 1.0; - let cap_u32 = true_max_val as u32; - - for i in 0..=cap_u32 { - match i { - 0 => labels.push("0B".to_string()), - LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()), - LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()), - LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()), - LOG_TEBI_LIMIT_U32 => labels.push("1TiB".to_string()), - _ if i == cap_u32 => { - labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_TEBI_LIMIT_U32))) + (Some(filtered_rx), None) => { + match filtered_rx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } } - _ if i == (LOG_TEBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!( - "{}TiB", - 2_u64.pow(cap_u32 - ((LOG_TEBI_LIMIT_U32 + cap_u32) / 2)) - )), // ~Halfway point - _ => labels.push(String::default()), + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + (Some(filtered_rx), Some(filtered_tx)) => { + match filtered_rx + .iter() + .chain(filtered_tx) + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + *best_time, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), } } } + } + + /// Returns the required max data point and labels. + fn adjust_network_data_point( + max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, + network_use_binary_prefix: bool, + ) -> (f64, Vec) { + // So, we're going with an approach like this for linear data: + // - Main goal is to maximize the amount of information displayed given a specific height. + // We don't want to drown out some data if the ranges are too far though! Nor do we want to filter + // out too much data... + // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load. + // + // The idea is we take the top value, build our scale such that each "point" is a scaled version of that. + // So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and + // probably something like 438.75? + // + // So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max + // value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will + // properly space them all out... we just work with that and space it out properly. + // + // Dynamic chart idea based off of FreeNAS's chart design. + // + // === + // + // For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple. + + // Now just check the largest unit we correspond to... then proceed to build some entries from there! + + let unit_char = match network_unit_type { + DataUnit::Byte => "B", + DataUnit::Bit => "b", + }; + + match network_scale_type { + AxisScaling::Linear => { + let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix { + ( + KIBI_LIMIT_F64, + MEBI_LIMIT_F64, + GIBI_LIMIT_F64, + TEBI_LIMIT_F64, + ) + } else { + ( + KILO_LIMIT_F64, + MEGA_LIMIT_F64, + GIGA_LIMIT_F64, + TERA_LIMIT_F64, + ) + }; + + let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type. + let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = + if bumped_max_entry < k_limit { + (max_entry, "", unit_char) + } else if bumped_max_entry < m_limit { + ( + max_entry / k_limit, + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char, + ) + } else if bumped_max_entry < g_limit { + ( + max_entry / m_limit, + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char, + ) + } else if bumped_max_entry < t_limit { + ( + max_entry / g_limit, + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char, + ) + } else { + ( + max_entry / t_limit, + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char, + ) + }; + + // Finally, build an acceptable range starting from there, using the given height! + // Note we try to put more of a weight on the bottom section vs. the top, since the top has less data. + + let base_unit = max_value_scaled; + let labels: Vec = vec![ + format!("0{}{}", unit_prefix, unit_type), + format!("{:.1}", base_unit * 0.5), + format!("{:.1}", base_unit), + format!("{:.1}", base_unit * 1.5), + ] + .into_iter() + .map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second) + .collect(); + + (bumped_max_entry, labels) + } + AxisScaling::Log => { + let (m_limit, g_limit, t_limit) = if network_use_binary_prefix { + (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT) + } else { + (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT) + }; + + fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "{}0{}", + if network_use_binary_prefix { " " } else { " " }, + unit_char + ) + } + + fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char + ) + } + + fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char + ) + } + + fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char + ) + } + + fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char + ) + } - (true_max_val, labels) + fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Pi" } else { "P" }, + unit_char + ) + } + + if max_entry < m_limit { + ( + m_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < g_limit { + ( + g_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < t_limit { + ( + t_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + ], + ) + } else { + // I really doubt anyone's transferring beyond petabyte speeds... + ( + if network_use_binary_prefix { + LOG_PEBI_LIMIT + } else { + LOG_PETA_LIMIT + }, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + get_p(network_use_binary_prefix, unit_char), + ], + ) + } + } + } } if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { - let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; - let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; + let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx; + let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx; + + let time_start = -(network_widget_state.current_display_time as f64); - let (max_range, labels) = adjust_network_data_point( - network_data_rx, - network_data_tx, - -(network_widget_state.current_display_time as f64), - 0.0, - ); let display_time_labels = vec![ Span::styled( format!("{}s", network_widget_state.current_display_time / 1000), @@ -190,29 +435,138 @@ impl NetworkGraphWidget for Painter { || (app_state.app_config_fields.autohide_time && network_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = network_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { network_widget_state.autohide_timer = None; - Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; + // Interpolate a point for rx and tx between the last value outside of the left bounds and the first value + // inside it. + // Because we assume it is all in order for... basically all our code, we can't just append it, + // and insertion in the middle seems. So instead, we swap *out* the value that is outside with our + // interpolated point, draw and do whatever calculations, then swap back in the old value! + // + // Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from + // get_max_entry... + let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if rx_end_pos > 1 { + let rx_start_pos = rx_end_pos - 1; + let outside_rx_point = network_data_rx.get(rx_start_pos); + let inside_rx_point = network_data_rx.get(rx_end_pos); + + if let (Some(outside_rx_point), Some(inside_rx_point)) = + (outside_rx_point, inside_rx_point) + { + let old = *outside_rx_point; + + let new_point = ( + time_start, + interpolate_points(outside_rx_point, inside_rx_point, time_start), + ); + + // debug!( + // "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}", + // outside_rx_point, inside_rx_point, time_start, new_point + // ); + + if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) { + *to_replace = new_point; + Some((rx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_rx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if tx_end_pos > 1 { + let tx_start_pos = tx_end_pos - 1; + let outside_tx_point = network_data_tx.get(tx_start_pos); + let inside_tx_point = network_data_tx.get(tx_end_pos); + + if let (Some(outside_tx_point), Some(inside_tx_point)) = + (outside_tx_point, inside_tx_point) + { + let old = *outside_tx_point; + + let new_point = ( + time_start, + interpolate_points(outside_tx_point, inside_tx_point, time_start), + ); + + if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) { + *to_replace = new_point; + Some((tx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_tx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + // TODO: Cache network results: Only update if: + // - Force update (includes time interval change) + // - Old max time is off screen + // - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!) + + // Find the maximal rx/tx so we know how to scale, and return it. + + let (_best_time, max_entry) = get_max_entry( + network_data_rx, + network_data_tx, + time_start, + &app_state.app_config_fields.network_scale_type, + app_state.app_config_fields.network_use_binary_prefix, + ); + + let (max_range, labels) = adjust_network_data_point( + max_entry, + &app_state.app_config_fields.network_scale_type, + &app_state.app_config_fields.network_unit_type, + app_state.app_config_fields.network_use_binary_prefix, + ); + + // Cache results. + // network_widget_state.draw_max_range_cache = max_range; + // network_widget_state.draw_time_start_cache = best_time; + // network_widget_state.draw_labels_cache = labels; + let y_axis_labels = labels .iter() .map(|label| Span::styled(label, self.colours.graph_style)) @@ -250,12 +604,12 @@ impl NetworkGraphWidget for Painter { let legend_constraints = if hide_legend { (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)) } else { - (Constraint::Ratio(3, 4), Constraint::Ratio(3, 4)) + (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)) }; + // TODO: Add support for clicking on legend to only show that value on chart. let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend { - let mut ret_val = vec![]; - ret_val.push( + vec![ Dataset::default() .name(format!("RX: {:7}", app_state.canvas_data.rx_display)) .marker(if app_state.app_config_fields.use_dot { @@ -266,9 +620,6 @@ impl NetworkGraphWidget for Painter { .style(self.colours.rx_style) .data(&network_data_rx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val.push( Dataset::default() .name(format!("TX: {:7}", app_state.canvas_data.tx_display)) .marker(if app_state.app_config_fields.use_dot { @@ -279,30 +630,21 @@ impl NetworkGraphWidget for Painter { .style(self.colours.tx_style) .data(&network_data_tx) .graph_type(tui::widgets::GraphType::Line), - ); - ret_val.push( Dataset::default() .name(format!( "Total RX: {:7}", app_state.canvas_data.total_rx_display )) .style(self.colours.total_rx_style), - ); - - ret_val.push( Dataset::default() .name(format!( "Total TX: {:7}", app_state.canvas_data.total_tx_display )) .style(self.colours.total_tx_style), - ); - - ret_val + ] } else { - let mut ret_val = vec![]; - - ret_val.push( + vec![ Dataset::default() .name(&app_state.canvas_data.rx_display) .marker(if app_state.app_config_fields.use_dot { @@ -313,9 +655,6 @@ impl NetworkGraphWidget for Painter { .style(self.colours.rx_style) .data(&network_data_rx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val.push( Dataset::default() .name(&app_state.canvas_data.tx_display) .marker(if app_state.app_config_fields.use_dot { @@ -326,9 +665,7 @@ impl NetworkGraphWidget for Painter { .style(self.colours.tx_style) .data(&network_data_tx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val + ] }; f.render_widget( @@ -348,10 +685,22 @@ impl NetworkGraphWidget for Painter { .hidden_legend_constraints(legend_constraints), draw_loc, ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_rx_point { + if let Some(to_replace) = network_data_rx.get_mut(index) { + *to_replace = old_value; + } + } + + if let Some((index, old_value)) = interpolated_tx_point { + if let Some(to_replace) = network_data_tx.get_mut(index) { + *to_replace = old_value; + } + } } } - // TODO: [DEPRECATED] Get rid of this in, like, 0.6...? fn draw_network_labels( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { -- cgit v1.2.3