2021-12-30 22:40:00 -05:00

720 lines
24 KiB
Rust

use std::borrow::Cow;
use crossterm::event::{KeyEvent, MouseEvent};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
use crate::{
app::{
data_farmer::DataCollection, event::ComponentEventResult, text_table::SimpleColumn,
time_graph::TimeGraphData, widgets::tui_stuff::BlockBuilder, AppConfig, AxisScaling,
Component, TextTable, TimeGraph, Widget,
},
canvas::Painter,
data_conversion::convert_network_data_points,
options::layout_options::WidgetLayoutRule,
units::data_units::DataUnit,
utils::gen_util::*,
};
/// Returns the max data point and time given a time.
fn get_max_entry(
rx: &[(f64, f64)], tx: &[(f64, f64)], 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
}
}
}
}
// 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)
}
}
None => (
time_start,
calculate_missing_max(network_scale_type, network_use_binary_prefix),
),
}
}
(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)
}
}
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
)
}
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),
],
)
}
}
}
}
/// A struct containing useful cached information for a [`NetGraph`].
#[derive(Clone)]
pub struct NetGraphCache {
max_value: f64,
cached_upper_bound: f64,
labels: Vec<Cow<'static, str>>,
}
enum NetGraphCacheState {
Uncached,
Cached(NetGraphCache),
}
/// A widget denoting network usage via a graph. This version is self-contained within a single [`TimeGraph`];
/// if you need the older one that splits into two sections, use [`OldNetGraph`], which is built on a [`NetGraph`].
///
/// As of now, this is essentially just a wrapper around a [`TimeGraph`].
pub struct NetGraph {
/// The graph itself. Just a [`TimeGraph`].
graph: TimeGraph,
// Cached details for drawing purposes; probably want to move at some point...
draw_cache: NetGraphCacheState,
pub rx_display: String,
pub tx_display: String,
pub total_rx_display: String,
pub total_tx_display: String,
pub network_data_rx: Vec<(f64, f64)>,
pub network_data_tx: Vec<(f64, f64)>,
pub scale_type: AxisScaling,
pub unit_type: DataUnit,
pub use_binary_prefix: bool,
hide_legend: bool,
bounds: Rect,
width: WidgetLayoutRule,
height: WidgetLayoutRule,
}
impl NetGraph {
/// Creates a new [`NetGraph`] given a [`AppConfigFields`].
pub fn from_config(app_config_fields: &AppConfig) -> Self {
let graph = TimeGraph::from_config(app_config_fields);
Self {
graph,
draw_cache: NetGraphCacheState::Uncached,
rx_display: Default::default(),
tx_display: Default::default(),
total_rx_display: Default::default(),
total_tx_display: Default::default(),
network_data_rx: Default::default(),
network_data_tx: Default::default(),
scale_type: app_config_fields.network_scale_type.clone(),
unit_type: app_config_fields.network_unit_type.clone(),
use_binary_prefix: app_config_fields.network_use_binary_prefix,
hide_legend: false,
bounds: Rect::default(),
width: WidgetLayoutRule::default(),
height: WidgetLayoutRule::default(),
}
}
/// Hides the legend. Only really useful for [`OldNetGraph`].
pub fn hide_legend(mut self) -> Self {
self.hide_legend = true;
self
}
/// Sets the width.
pub fn width(mut self, width: WidgetLayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: WidgetLayoutRule) -> Self {
self.height = height;
self
}
/// Sets the draw cache for a [`NetGraph`].
pub fn set_draw_cache(&mut self) {
let current_time = -(self.graph.get_current_display_time() as f64);
let (_current_max_time, current_max_value) = get_max_entry(
&self.network_data_rx,
&self.network_data_tx,
current_time,
&self.scale_type,
self.use_binary_prefix,
);
match &mut self.draw_cache {
NetGraphCacheState::Uncached => {
let (cached_upper_bound, labels) = adjust_network_data_point(
current_max_value,
&self.scale_type,
&self.unit_type,
self.use_binary_prefix,
);
let labels: Vec<Cow<'static, str>> = labels.into_iter().map(Into::into).collect();
self.draw_cache = NetGraphCacheState::Cached(NetGraphCache {
max_value: current_max_value,
cached_upper_bound,
labels: labels.clone(),
});
}
NetGraphCacheState::Cached(cache) => {
if (current_max_value - cache.max_value).abs() > f64::EPSILON {
// Invalidated.
let (upper_bound, labels) = adjust_network_data_point(
current_max_value,
&self.scale_type,
&self.unit_type,
self.use_binary_prefix,
);
*cache = NetGraphCache {
max_value: current_max_value,
cached_upper_bound: upper_bound,
labels: labels.into_iter().map(Into::into).collect(),
};
}
}
}
}
}
impl Component for NetGraph {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.graph.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.graph.handle_mouse_event(event)
}
}
impl Widget for NetGraph {
fn get_pretty_name(&self) -> &'static str {
"Network"
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
expanded: bool,
) {
let block = self
.block()
.selected(selected)
.show_esc(expanded)
.build(painter, area);
self.set_draw_cache();
let chart_data = vec![
TimeGraphData {
data: &self.network_data_rx,
label: if self.hide_legend {
None
} else {
Some(self.rx_display.clone().into())
},
style: painter.colours.rx_style,
},
TimeGraphData {
data: &self.network_data_tx,
label: if self.hide_legend {
None
} else {
Some(self.tx_display.clone().into())
},
style: painter.colours.tx_style,
},
];
let (y_bounds, y_bound_labels) = match &self.draw_cache {
NetGraphCacheState::Cached(cache) => ([0.0, cache.cached_upper_bound], &cache.labels),
NetGraphCacheState::Uncached => unreachable!(),
};
self.graph.draw_tui_chart(
painter,
f,
&chart_data,
y_bound_labels,
y_bounds,
false,
block,
area,
);
}
fn update_data(&mut self, data_collection: &DataCollection) {
let network_data = convert_network_data_points(
data_collection,
false,
&self.scale_type,
&self.unit_type,
self.use_binary_prefix,
);
self.network_data_rx = network_data.rx;
self.network_data_tx = network_data.tx;
self.rx_display = network_data.rx_display;
self.tx_display = network_data.tx_display;
if let Some(total_rx_display) = network_data.total_rx_display {
self.total_rx_display = total_rx_display;
}
if let Some(total_tx_display) = network_data.total_tx_display {
self.total_tx_display = total_tx_display;
}
}
fn width(&self) -> WidgetLayoutRule {
self.width
}
fn height(&self) -> WidgetLayoutRule {
self.height
}
}
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
/// and the main difference is that it also contains a bounding box for the graph + text.
pub struct OldNetGraph {
net_graph: NetGraph,
table: TextTable,
bounds: Rect,
width: WidgetLayoutRule,
height: WidgetLayoutRule,
}
impl OldNetGraph {
/// Creates a new [`OldNetGraph`] from a [`AppConfigFields`].
pub fn from_config(config: &AppConfig) -> Self {
Self {
net_graph: NetGraph::from_config(config).hide_legend(),
table: TextTable::new(vec![
SimpleColumn::new_flex("RX".into(), 0.25),
SimpleColumn::new_flex("TX".into(), 0.25),
SimpleColumn::new_flex("Total RX".into(), 0.25),
SimpleColumn::new_flex("Total TX".into(), 0.25),
])
.try_show_gap(config.table_gap)
.unselectable(),
bounds: Rect::default(),
width: WidgetLayoutRule::default(),
height: WidgetLayoutRule::default(),
}
}
/// Sets the width.
pub fn width(mut self, width: WidgetLayoutRule) -> Self {
self.width = width;
self
}
/// Sets the height.
pub fn height(mut self, height: WidgetLayoutRule) -> Self {
self.height = height;
self
}
}
impl Component for OldNetGraph {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.net_graph.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.net_graph.handle_mouse_event(event)
}
}
impl Widget for OldNetGraph {
fn get_pretty_name(&self) -> &'static str {
"Network"
}
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
expanded: bool,
) {
let constraints = [
Constraint::Min(0),
Constraint::Length(if self.table.show_gap { 5 } else { 4 }),
];
let split_area = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let graph_area = split_area[0];
let table_area = split_area[1];
self.net_graph
.draw(painter, f, graph_area, selected, expanded);
let table_block = BlockBuilder::new("").hide_title(true);
self.table.draw_tui_table(
painter,
f,
&[vec![
(
self.net_graph.rx_display.clone().into(),
None,
Some(painter.colours.rx_style),
),
(
self.net_graph.tx_display.clone().into(),
None,
Some(painter.colours.tx_style),
),
(self.net_graph.total_rx_display.clone().into(), None, None),
(self.net_graph.total_tx_display.clone().into(), None, None),
]],
table_block,
table_area,
selected,
false,
);
}
fn update_data(&mut self, data_collection: &DataCollection) {
let network_data = convert_network_data_points(
data_collection,
true,
&self.net_graph.scale_type,
&self.net_graph.unit_type,
self.net_graph.use_binary_prefix,
);
self.net_graph.network_data_rx = network_data.rx;
self.net_graph.network_data_tx = network_data.tx;
self.net_graph.rx_display = network_data.rx_display;
self.net_graph.tx_display = network_data.tx_display;
if let Some(total_rx_display) = network_data.total_rx_display {
self.net_graph.total_rx_display = total_rx_display;
}
if let Some(total_tx_display) = network_data.total_tx_display {
self.net_graph.total_tx_display = total_tx_display;
}
}
fn width(&self) -> WidgetLayoutRule {
self.width
}
fn height(&self) -> WidgetLayoutRule {
self.height
}
}