diff options
Diffstat (limited to 'src/canvas/widgets/network_graph.rs')
-rw-r--r-- | src/canvas/widgets/network_graph.rs | 577 |
1 files changed, 463 insertions, 114 deletions
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<String>) { - // 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<String>) { + // 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<String> = 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<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { |