summaryrefslogtreecommitdiffstats
path: root/src/canvas/components/time_graph.rs
diff options
context:
space:
mode:
authorClementTsang <cjhtsang@uwaterloo.ca>2022-04-27 02:13:48 -0400
committerClementTsang <cjhtsang@uwaterloo.ca>2022-04-28 23:36:53 -0400
commit2401e583fb3a6441c5d4d7483d5ce654b2f75b07 (patch)
treec22bbfe2ac448f2ec3c28e2307b759e507584929 /src/canvas/components/time_graph.rs
parent1f731358baa8e4802d8bc1f9f8171fe85132f3f1 (diff)
refactor: consolidate time graph components
This consolidates all the time graph drawing to one main location, as well as some small improvements. This is helpful in that I don't have to reimplement the same thing across three locations if I have to make one change that in theory should affect them all. In particular, the CPU graph, memory graph, and network graph are all now using the same, generic implementation for drawing, which we call (for now) a component. Note this only affects drawing - it accepts some parameters affecting style and labels, as well as data points, and draw similarly to how it used to before. Widget-specific actions, or things affecting widget state, should all be handled by the widget-specific code instead. For example, our current implementation of x-axis autohide is still controlled by the widget, not the component, even if some of the code is shared. Components are, again, only responsible for drawing (at least for now). For that matter, the graph component does not have mutable access to any form of state outside of tui-rs' `Frame`. Note this *might* change in the future, where we might give the component state. Note that while functionally, the graph behaviour for now is basically the same, a few changes were made internally other than the move to components. The big change is that rather than using tui-rs' `Chart` for the underlying drawing, we now use a tweaked custom `TimeChart` tui-rs widget, which also handles all interpolation steps and some extra customization. Personally, I don't like having to deviate from the library's implementation, but this gives us more flexibility and allows greater control. For example, this allows me to move away from the old hacks required to do interpolation (where I had to mutate the existing list to avoid having to reallocate an extra vector just to insert one extra interpolated point). I can also finally allow customizable legends (which will be added in the future).
Diffstat (limited to 'src/canvas/components/time_graph.rs')
-rw-r--r--src/canvas/components/time_graph.rs273
1 files changed, 273 insertions, 0 deletions
diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph.rs
new file mode 100644
index 00000000..ab5cec2e
--- /dev/null
+++ b/src/canvas/components/time_graph.rs
@@ -0,0 +1,273 @@
+use std::borrow::Cow;
+
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Rect},
+ style::Style,
+ symbols::Marker,
+ text::{Span, Spans},
+ widgets::{Block, Borders, GraphType},
+ Frame,
+};
+
+use concat_string::concat_string;
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::{Axis, Dataset, TimeChart};
+
+/// A single graph point.
+pub type Point = (f64, f64);
+
+/// Represents the data required by the [`TimeGraph`].
+pub struct GraphData<'a> {
+ pub points: &'a [Point],
+ pub style: Style,
+ pub name: Option<Cow<'a, str>>,
+}
+
+#[derive(Default)]
+pub struct TimeGraph<'a> {
+ /// Whether to use a dot marker over the default braille markers.
+ pub use_dot: bool,
+
+ /// The min and max x boundaries. Expects a f64 representing the time range in milliseconds.
+ pub x_bounds: [u64; 2],
+
+ /// Whether to hide the time/x-labels.
+ pub hide_x_labels: bool,
+
+ /// The min and max y boundaries.
+ pub y_bounds: [f64; 2],
+
+ /// Any y-labels.
+ pub y_labels: &'a [Cow<'a, str>],
+
+ /// The graph style.
+ pub graph_style: Style,
+
+ /// The border style.
+ pub border_style: Style,
+
+ /// The graph title.
+ pub title: Cow<'a, str>,
+
+ /// Whether this graph is expanded.
+ pub is_expanded: bool,
+
+ /// The title style.
+ pub title_style: Style,
+
+ /// Any legend constraints.
+ pub legend_constraints: Option<(Constraint, Constraint)>,
+}
+
+impl<'a> TimeGraph<'a> {
+ /// Generates the [`Axis`] for the x-axis.
+ fn generate_x_axis(&self) -> Axis<'_> {
+ // Due to how we display things, we need to adjust the time bound values.
+ let time_start = -(self.x_bounds[1] as f64);
+ let adjusted_x_bounds = [time_start, 0.0];
+
+ if self.hide_x_labels {
+ Axis::default().bounds(adjusted_x_bounds)
+ } else {
+ let x_labels = vec![
+ Span::raw(concat_string!((self.x_bounds[1] / 1000).to_string(), "s")),
+ Span::raw(concat_string!((self.x_bounds[0] / 1000).to_string(), "s")),
+ ];
+
+ Axis::default()
+ .bounds(adjusted_x_bounds)
+ .labels(x_labels)
+ .style(self.graph_style)
+ }
+ }
+
+ /// Generates the [`Axis`] for the y-axis.
+ fn generate_y_axis(&self) -> Axis<'_> {
+ Axis::default()
+ .bounds(self.y_bounds)
+ .style(self.graph_style)
+ .labels(
+ self.y_labels
+ .iter()
+ .map(|label| Span::raw(label.clone()))
+ .collect(),
+ )
+ }
+
+ /// Generates a title for the [`TimeGraph`] widget, given the available space.
+ fn generate_title(&self, draw_loc: Rect) -> Spans<'_> {
+ if self.is_expanded {
+ let title_base = concat_string!(self.title, "── Esc to go back ");
+ Spans::from(vec![
+ Span::styled(self.title.as_ref(), self.title_style),
+ Span::styled(
+ concat_string!(
+ "─",
+ "─".repeat(usize::from(draw_loc.width).saturating_sub(
+ UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
+ )),
+ "─ Esc to go back "
+ ),
+ self.border_style,
+ ),
+ ])
+ } else {
+ Spans::from(Span::styled(self.title.as_ref(), self.title_style))
+ }
+ }
+
+ /// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time graph is used to display data points
+ /// throughout time in the x-axis.
+ ///
+ /// This time graph:
+ /// - Draws with the higher time value on the left, and lower on the right.
+ /// - Expects a [`TimeGraph`] to be passed in, which details how to draw the graph.
+ /// - Expects `graph_data`, which represents *what* data to draw, and various details like style and optional legends.
+ pub fn draw_time_graph<B: Backend>(
+ &self, f: &mut Frame<'_, B>, draw_loc: Rect, graph_data: &[GraphData<'_>],
+ ) {
+ let x_axis = self.generate_x_axis();
+ let y_axis = self.generate_y_axis();
+
+ // This is some ugly manual loop unswitching. Maybe unnecessary.
+ let data = if self.use_dot {
+ graph_data
+ .iter()
+ .map(|data| create_dataset(data, Marker::Dot))
+ .collect()
+ } else {
+ graph_data
+ .iter()
+ .map(|data| create_dataset(data, Marker::Braille))
+ .collect()
+ };
+
+ f.render_widget(
+ TimeChart::new(data)
+ .block(
+ Block::default()
+ .title(self.generate_title(draw_loc))
+ .borders(Borders::ALL)
+ .border_style(self.border_style),
+ )
+ .x_axis(x_axis)
+ .y_axis(y_axis)
+ .hidden_legend_constraints(
+ self.legend_constraints
+ .unwrap_or(super::DEFAULT_LEGEND_CONSTRAINTS),
+ ),
+ draw_loc,
+ )
+ }
+}
+
+/// Creates a new [`Dataset`].
+fn create_dataset<'a>(data: &'a GraphData<'a>, marker: Marker) -> Dataset<'a> {
+ let GraphData {
+ points,
+ style,
+ name,
+ } = data;
+
+ let dataset = Dataset::default()
+ .style(*style)
+ .data(points)
+ .graph_type(GraphType::Line)
+ .marker(marker);
+
+ if let Some(name) = name {
+ dataset.name(name.as_ref())
+ } else {
+ dataset
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::borrow::Cow;
+
+ use tui::{
+ layout::Rect,
+ style::{Color, Style},
+ text::{Span, Spans},
+ };
+
+ use crate::canvas::components::Axis;
+
+ use super::TimeGraph;
+
+ 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(),
+ use_dot: true,
+ x_bounds: [0, 15000],
+ hide_x_labels: false,
+ y_bounds: [0.0, 100.5],
+ y_labels: &Y_LABELS,
+ graph_style: Style::default().fg(Color::Red),
+ border_style: Style::default().fg(Color::Blue),
+ is_expanded: false,
+ title_style: Style::default().fg(Color::Cyan),
+ legend_constraints: None,
+ }
+ }
+
+ #[test]
+ fn time_graph_gen_x_axis() {
+ let tg = create_time_graph();
+
+ let x_axis = tg.generate_x_axis();
+ let actual = Axis::default()
+ .bounds([-15000.0, 0.0])
+ .labels(vec![Span::raw("15s"), Span::raw("0s")])
+ .style(Style::default().fg(Color::Red));
+ 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 y_axis = tg.generate_y_axis();
+ let actual = Axis::default()
+ .bounds([0.0, 100.5])
+ .labels(vec![Span::raw("0%"), Span::raw("50%"), Span::raw("100%")])
+ .style(Style::default().fg(Color::Red));
+
+ assert_eq!(y_axis.bounds, actual.bounds);
+ assert_eq!(y_axis.labels, actual.labels);
+ assert_eq!(y_axis.style, actual.style);
+ }
+
+ #[test]
+ fn time_graph_gen_title() {
+ let mut tg = create_time_graph();
+ let draw_loc = Rect::new(0, 0, 32, 100);
+
+ let title = tg.generate_title(draw_loc);
+ assert_eq!(
+ title,
+ Spans::from(Span::styled(" Network ", Style::default().fg(Color::Cyan)))
+ );
+
+ tg.is_expanded = true;
+ let title = tg.generate_title(draw_loc);
+ assert_eq!(
+ title,
+ Spans::from(vec![
+ Span::styled(" Network ", Style::default().fg(Color::Cyan)),
+ Span::styled("───── Esc to go back ", Style::default().fg(Color::Blue))
+ ])
+ );
+ }
+}