use std::{borrow::Cow, time::Instant};
use concat_string::concat_string;
use tui::{
Frame,
layout::{Constraint, Rect},
style::Style,
symbols::Marker,
text::{Line, Span},
widgets::{BorderType, GraphType},
};
use crate::canvas::components::time_graph::*;
use crate::{app::data::Values, canvas::drawing_utils::widget_block};
#[derive(Default)]
pub(crate) struct GraphData<'a> {
time: &'a [Instant],
values: Option<&'a Values>,
style: Style,
name: Option<Cow<'a, str>>,
}
impl<'a> GraphData<'a> {
pub fn time(mut self, time: &'a [Instant]) -> Self {
self.time = time;
self
}
pub fn values(mut self, values: &'a Values) -> Self {
self.values = Some(values);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn name(mut self, name: Cow<'a, str>) -> Self {
self.name = Some(name);
self
}
}
pub struct TimeGraph<'a> {
pub x_min: f64,
pub hide_x_labels: bool,
pub y_bounds: AxisBound,
pub y_labels: &'a [Cow<'a, str>],
pub graph_style: Style,
pub border_style: Style,
pub border_type: BorderType,
pub title: Cow<'a, str>,
pub is_selected: bool,
pub is_expanded: bool,
pub title_style: Style,
pub legend_position: Option<LegendPosition>,
pub legend_constraints: Option<(Constraint, Constraint)>,
pub marker: Marker,
pub scaling: ChartScaling,
}
impl TimeGraph<'_> {
fn generate_x_axis(&self) -> Axis<'_> {
let adjusted_x_bounds = AxisBound::Min(self.x_min);
if self.hide_x_labels {
Axis::default().bounds(adjusted_x_bounds)
} else {
let x_bound_left = ((-self.x_min) as u64 / 1000).to_string();
let x_bound_right = "0s";
let x_labels = vec![
Span::styled(concat_string!(x_bound_left, "s"), self.graph_style),
Span::styled(x_bound_right, self.graph_style),
];
Axis::default()
.bounds(adjusted_x_bounds)
.labels(x_labels)
.style(self.graph_style)
}
}
fn generate_y_axis(&self) -> Axis<'_> {
Axis::default()
.bounds(self.y_bounds)
.style(self.graph_style)
.labels(
self.y_labels
.iter()
.map(|label| Span::styled(label.clone(), self.graph_style))
.collect(),
)
}
pub fn draw(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec<GraphData<'_>>) {
let x_axis = self.generate_x_axis();
let y_axis = self.generate_y_axis();
let data = graph_data.into_iter().map(create_dataset).collect();
let block = {
let mut b = widget_block(false, self.is_selected, self.border_type)
.border_style(self.border_style)
.title_top(Line::styled(self.title.as_ref(), self.title_style));
if self.is_expanded {
b = b.title_top(Line::styled(" Esc to go back ", self.title_style).right_aligned())
}
b
};
f.render_widget(
TimeChart::new(data)
.block(block)
.x_axis(x_axis)
.y_axis(y_axis)
.marker(self.marker)
.legend_style(self.graph_style)
.legend_position(self.legend_position)
.hidden_legend_constraints(
self.legend_constraints
.unwrap_or(DEFAULT_LEGEND_CONSTRAINTS),
)
.scaling(self.scaling),
draw_loc,
)
}
}
fn create_dataset(data: GraphData<'_>) -> Dataset<'_> {
let GraphData {
time,
values,
style,
name,
} = data;
let Some(values) = values else {
return Dataset::default();
};
let dataset = Dataset::default()
.style(style)
.data(time, values)
.graph_type(GraphType::Line);
if let Some(name) = name {
dataset.name(name)
} else {
dataset
}
}
#[cfg(test)]
mod test {
use std::borrow::Cow;
use tui::{
style::{Color, Style},
symbols::Marker,
text::Span,
widgets::BorderType,
};
use super::{AxisBound, ChartScaling, TimeGraph};
use crate::canvas::components::time_graph::Axis;
const Y_LABELS: [Cow<'static, str>; 3] = [
Cow::Borrowed("0%"),
Cow::Borrowed("50%"),
Cow::Borrowed("100%"),
];
fn create_time_graph() -> TimeGraph<'static> {
TimeGraph {
title: " Network ".into(),
x_min: -15000.0,
hide_x_labels: false,
y_bounds: AxisBound::Max(100.5),
y_labels: &Y_LABELS,
graph_style: Style::default().fg(Color::Red),
border_style: Style::default().fg(Color::Blue),
border_type: BorderType::Plain,
is_selected: false,
is_expanded: false,
title_style: Style::default().fg(Color::Cyan),
legend_position: None,
legend_constraints: None,
marker: Marker::Braille,
scaling: ChartScaling::Linear,
}
}
#[test]
fn time_graph_gen_x_axis() {
let tg = create_time_graph();
let style = Style::default().fg(Color::Red);
let x_axis = tg.generate_x_axis();
let actual = Axis::default()
.bounds(AxisBound::Min(-15000.0))
.labels(vec![Span::styled("15s", style), Span::styled("0s", style)])
.style(style);
assert_eq!(x_axis.bounds, actual.bounds);
assert_eq!(x_axis.labels, actual.labels);
assert_eq!(x_axis.style, actual.style);
}
#[test]
fn time_graph_gen_y_axis() {
let tg = create_time_graph();
let style = Style::default().fg(Color::Red);
let y_axis = tg.generate_y_axis();
let actual = Axis::default()
.bounds(AxisBound::Max(100.5))
.labels(vec![
Span::styled("0%", style),
Span::styled("50%", style),
Span::styled("100%", style),
])
.style(style);
assert_eq!(y_axis.bounds, actual.bounds);
assert_eq!(y_axis.labels, actual.labels);
assert_eq!(y_axis.style, actual.style);
}
}