diff --git a/Cargo.lock b/Cargo.lock index c92a9d68..d101392b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.83" @@ -312,6 +321,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "concat-string" version = "1.0.1" @@ -990,12 +1012,13 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" +checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" dependencies = [ "bitflags 2.4.1", "cassowary", + "compact_str", "crossterm", "indoc", "itertools 0.12.0", @@ -1309,18 +1332,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index b0007203..f1c2c8e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ sysinfo = "=0.30.5" thiserror = "1.0.56" time = { version = "0.3.30", features = ["formatting", "macros"] } toml_edit = { version = "0.21.0", features = ["serde"] } -tui = { version = "0.25.0", package = "ratatui" } +tui = { version = "0.26.0", package = "ratatui" } unicode-segmentation = "1.10.1" unicode-width = "0.1.11" diff --git a/src/app.rs b/src/app.rs index f9e7de16..a149869f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2586,7 +2586,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - proc_widget_state.table.tui_selected() + proc_widget_state.table.ratatui_selected() { let is_tree_mode = matches!( proc_widget_state.mode, @@ -2614,7 +2614,7 @@ impl App { .get_widget_state(self.current_widget.widget_id - 2) { if let Some(visual_index) = - proc_widget_state.sort_table.tui_selected() + proc_widget_state.sort_table.ratatui_selected() { self.change_process_sort_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2629,7 +2629,7 @@ impl App { .get_widget_state(self.current_widget.widget_id - 1) { if let Some(visual_index) = - cpu_widget_state.table.tui_selected() + cpu_widget_state.table.ratatui_selected() { self.change_cpu_legend_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2644,7 +2644,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - temp_widget_state.table.tui_selected() + temp_widget_state.table.ratatui_selected() { self.change_temp_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2659,7 +2659,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - disk_widget_state.table.tui_selected() + disk_widget_state.table.ratatui_selected() { self.change_disk_position( offset_clicked_entry as i64 - visual_index as i64, diff --git a/src/canvas.rs b/src/canvas.rs index 0f3e7202..ca144c15 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -72,7 +72,8 @@ pub struct Painter { /// The constraints of a widget relative to its parent. /// -/// This is used over ratatui's internal representation due to https://github.com/ClementTsang/bottom/issues/896. +/// This is used over ratatui's internal representation due to +/// . pub enum LayoutConstraint { CanvasHandled, Grow, @@ -498,6 +499,8 @@ impl Painter { } if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw { + // TODO: Can I remove this? Does ratatui's layout constraints work properly for fixing + // https://github.com/ClementTsang/bottom/issues/896 now? fn get_constraints( direction: Direction, constraints: &[LayoutConstraint], area: Rect, ) -> Vec { diff --git a/src/canvas/components/data_table.rs b/src/canvas/components/data_table.rs index 5fb4a591..41ebffb0 100644 --- a/src/canvas/components/data_table.rs +++ b/src/canvas/components/data_table.rs @@ -144,14 +144,16 @@ impl, H: ColumnHeader, S: SortType, C: DataTableColumn Option { + /// Returns ratatui's internal selection. + pub fn ratatui_selected(&self) -> Option { self.state.table_state.selected() } } #[cfg(test)] mod test { + use std::num::NonZeroU16; + use super::*; #[derive(Clone, PartialEq, Eq, Debug)] @@ -161,7 +163,7 @@ mod test { impl DataToCell<&'static str> for TestType { fn to_cell( - &self, _column: &&'static str, _calculated_width: u16, + &self, _column: &&'static str, _calculated_width: NonZeroU16, ) -> Option> { None } diff --git a/src/canvas/components/data_table/column.rs b/src/canvas/components/data_table/column.rs index e7b82b29..7bbfcbfa 100644 --- a/src/canvas/components/data_table/column.rs +++ b/src/canvas/components/data_table/column.rs @@ -1,12 +1,13 @@ use std::{ borrow::Cow, cmp::{max, min}, + num::NonZeroU16, }; /// A bound on the width of a column. #[derive(Clone, Copy, Debug)] pub enum ColumnWidthBounds { - /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. + /// A width of this type is as long as `desired`, but can otherwise shrink and grow up to a point. Soft { /// The desired, calculated width. Take this if possible as the base starting width. desired: u16, @@ -151,7 +152,7 @@ pub trait CalculateColumnWidths { /// /// * `total_width` is the total width on the canvas that the columns can try and work with. /// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`). - fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec; + fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec; } impl CalculateColumnWidths for [C] @@ -159,19 +160,25 @@ where H: ColumnHeader, C: DataTableColumn, { - fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec { + fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec { use itertools::Either; + const COLUMN_SPACING: u16 = 1; + + #[inline] + fn stop_allocating_space(desired: u16, available: u16) -> bool { + desired > available || desired == 0 + } + let mut total_width_left = total_width; - let mut calculated_widths = vec![0; self.len()]; + let mut calculated_widths = vec![]; let columns = if left_to_right { - Either::Left(self.iter().zip(calculated_widths.iter_mut())) + Either::Left(self.iter()) } else { - Either::Right(self.iter().zip(calculated_widths.iter_mut()).rev()) + Either::Right(self.iter().rev()) }; - let mut num_columns = 0; - for (column, calculated_width) in columns { + for column in columns { if column.is_hidden() { continue; } @@ -196,41 +203,60 @@ where ); let space_taken = min(min(soft_limit, *desired), total_width_left); - if min_width > space_taken || min_width == 0 { + if stop_allocating_space(space_taken, total_width_left) { break; - } else if space_taken > 0 { - total_width_left = total_width_left.saturating_sub(space_taken + 1); - *calculated_width = space_taken; - num_columns += 1; + } else { + total_width_left = + total_width_left.saturating_sub(space_taken + COLUMN_SPACING); + + // SAFETY: This is safe as we call `stop_allocating_space` which checks that + // the value pushed is greater than zero. + unsafe { + calculated_widths.push(NonZeroU16::new_unchecked(space_taken)); + } } } ColumnWidthBounds::Hard(width) => { let min_width = *width; - if min_width > total_width_left || min_width == 0 { + if stop_allocating_space(min_width, total_width_left) { break; - } else if min_width > 0 { - total_width_left = total_width_left.saturating_sub(min_width + 1); - *calculated_width = min_width; - num_columns += 1; + } else { + total_width_left = + total_width_left.saturating_sub(min_width + COLUMN_SPACING); + + // SAFETY: This is safe as we call `stop_allocating_space` which checks that + // the value pushed is greater than zero. + unsafe { + calculated_widths.push(NonZeroU16::new_unchecked(min_width)); + } } } ColumnWidthBounds::FollowHeader => { let min_width = column.header_len() as u16; - if min_width > total_width_left || min_width == 0 { + if stop_allocating_space(min_width, total_width_left) { break; - } else if min_width > 0 { - total_width_left = total_width_left.saturating_sub(min_width + 1); - *calculated_width = min_width; - num_columns += 1; + } else { + total_width_left = + total_width_left.saturating_sub(min_width + COLUMN_SPACING); + + // SAFETY: This is safe as we call `stop_allocating_space` which checks that + // the value pushed is greater than zero. + unsafe { + calculated_widths.push(NonZeroU16::new_unchecked(min_width)); + } } } } } - if num_columns > 0 { - // Redistribute remaining. - let mut num_dist = num_columns; - let amount_per_slot = total_width_left / num_dist; + if !calculated_widths.is_empty() { + if !left_to_right { + calculated_widths.reverse(); + } + + // Redistribute remaining space. + let mut num_dist = calculated_widths.len() as u16; + let amount_per_slot = total_width_left / num_dist; // Safe from DBZ by above empty check. total_width_left %= num_dist; for width in calculated_widths.iter_mut() { @@ -238,16 +264,14 @@ where break; } - if *width > 0 { - if total_width_left > 0 { - *width += amount_per_slot + 1; - total_width_left -= 1; - } else { - *width += amount_per_slot; - } - - num_dist -= 1; + if total_width_left > 0 { + *width = width.saturating_add(amount_per_slot + 1); + total_width_left -= 1; + } else { + *width = width.saturating_add(amount_per_slot); } + + num_dist -= 1; } } diff --git a/src/canvas/components/data_table/data_type.rs b/src/canvas/components/data_table/data_type.rs index bbfceb8c..0be5646f 100644 --- a/src/canvas/components/data_table/data_type.rs +++ b/src/canvas/components/data_table/data_type.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU16; + use tui::{text::Text, widgets::Row}; use super::{ColumnHeader, DataTableColumn}; @@ -8,7 +10,7 @@ where H: ColumnHeader, { /// Given data, a column, and its corresponding width, return what should be displayed in the [`DataTable`](super::DataTable). - fn to_cell(&self, column: &H, calculated_width: u16) -> Option>; + fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option>; /// Apply styling to the generated [`Row`] of cells. /// diff --git a/src/canvas/components/data_table/draw.rs b/src/canvas/components/data_table/draw.rs index 15faae22..486a430e 100644 --- a/src/canvas/components/data_table/draw.rs +++ b/src/canvas/components/data_table/draw.rs @@ -249,18 +249,7 @@ where }; let mut table = Table::new( rows, - &(self - .state - .calculated_widths - .iter() - .filter_map(|&width| { - if width == 0 { - None - } else { - Some(Constraint::Length(width)) - } - }) - .collect::>()), + self.state.calculated_widths.iter().map(|nzu| nzu.get()), ) .block(block) .highlight_style(highlight_style) diff --git a/src/canvas/components/data_table/sortable.rs b/src/canvas/components/data_table/sortable.rs index f6c3b502..7b548309 100644 --- a/src/canvas/components/data_table/sortable.rs +++ b/src/canvas/components/data_table/sortable.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, marker::PhantomData}; +use std::{borrow::Cow, marker::PhantomData, num::NonZeroU16}; use concat_string::concat_string; use itertools::Itertools; @@ -52,18 +52,17 @@ pub struct Sortable { /// and therefore only [`Unsortable`] and [`Sortable`] can implement it. pub trait SortType: private::Sealed { /// Constructs the table header. - fn build_header(&self, columns: &[C], widths: &[u16]) -> Row<'_> + fn build_header(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_> where H: ColumnHeader, C: DataTableColumn, { - Row::new(columns.iter().zip(widths).filter_map(|(c, &width)| { - if width == 0 { - None - } else { - Some(truncate_to_text(&c.header(), width)) - } - })) + Row::new( + columns + .iter() + .zip(widths) + .map(|(c, &width)| truncate_to_text(&c.header(), width.get())), + ) } } @@ -79,7 +78,7 @@ mod private { impl SortType for Unsortable {} impl SortType for Sortable { - fn build_header(&self, columns: &[C], widths: &[u16]) -> Row<'_> + fn build_header(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_> where H: ColumnHeader, C: DataTableColumn, @@ -92,17 +91,17 @@ impl SortType for Sortable { .iter() .zip(widths) .enumerate() - .filter_map(|(index, (c, &width))| { - if width == 0 { - None - } else if index == self.sort_index { + .map(|(index, (c, &width))| { + if index == self.sort_index { let arrow = match self.order { SortOrder::Ascending => UP_ARROW, SortOrder::Descending => DOWN_ARROW, }; - Some(truncate_to_text(&concat_string!(c.header(), arrow), width)) + // TODO: I think I can get away with removing the truncate_to_text call since + // I almost always bind to at least the header size... + truncate_to_text(&concat_string!(c.header(), arrow), width.get()) } else { - Some(truncate_to_text(&c.header(), width)) + truncate_to_text(&c.header(), width.get()) } }), ) @@ -331,7 +330,7 @@ where .iter() .map(|width| { let entry_start = start; - start += width + 1; // +1 for the gap b/w cols. + start += width.get() + 1; // +1 for the gap b/w cols. entry_start }) @@ -361,7 +360,7 @@ mod test { impl DataToCell for TestType { fn to_cell( - &self, _column: &ColumnType, _calculated_width: u16, + &self, _column: &ColumnType, _calculated_width: NonZeroU16, ) -> Option> { None } diff --git a/src/canvas/components/data_table/state.rs b/src/canvas/components/data_table/state.rs index 0e2ed450..911d3b47 100644 --- a/src/canvas/components/data_table/state.rs +++ b/src/canvas/components/data_table/state.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU16; + use tui::{layout::Rect, widgets::TableState}; #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] @@ -21,11 +23,11 @@ pub struct DataTableState { /// The direction of the last attempted scroll. pub scroll_direction: ScrollDirection, - /// tui-rs' internal table state. + /// ratatui's internal table state. pub table_state: TableState, /// The calculated widths. - pub calculated_widths: Vec, + pub calculated_widths: Vec, /// The current inner [`Rect`]. pub inner_rect: Rect, diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph.rs index bd19be74..3f438349 100644 --- a/src/canvas/components/time_graph.rs +++ b/src/canvas/components/time_graph.rs @@ -51,8 +51,8 @@ pub struct TimeGraph<'a> { /// Any legend constraints. pub legend_constraints: Option<(Constraint, Constraint)>, - /// The marker type. Unlike tui-rs' native charts, we assume - /// only a single type of market. + /// The marker type. Unlike ratatui's native charts, we assume + /// only a single type of marker. pub marker: Marker, } diff --git a/src/canvas/components/tui_widget/time_chart.rs b/src/canvas/components/tui_widget/time_chart.rs index 747c2ae3..60c797bc 100644 --- a/src/canvas/components/tui_widget/time_chart.rs +++ b/src/canvas/components/tui_widget/time_chart.rs @@ -1,52 +1,54 @@ -mod canvas; +//! A [`tui::widgets::Chart`] but slightly more specialized to show right-aligned timeseries +//! data. +//! +//! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs); +//! the specializations are factored out to `time_chart/points.rs`. -use std::{borrow::Cow, cmp::max}; +mod canvas; +mod points; + +use std::cmp::max; use canvas::*; use tui::{ buffer::Buffer, - layout::{Constraint, Rect}, - style::{Color, Style}, + layout::{Alignment, Constraint, Flex, Layout, Rect}, + style::{Color, Style, Styled}, symbols::{self, Marker}, text::{Line, Span}, - widgets::{ - canvas::{Line as CanvasLine, Points}, - Block, Borders, GraphType, Widget, - }, + widgets::{block::BlockExt, Block, Borders, GraphType, Widget}, }; use unicode_width::UnicodeWidthStr; -use crate::utils::general::partial_ordering; +pub const DEFAULT_LEGEND_CONSTRAINTS: (Constraint, Constraint) = + (Constraint::Ratio(1, 4), Constraint::Length(4)); /// A single graph point. pub type Point = (f64, f64); -/// An X or Y axis for the chart widget -#[derive(Debug, Clone)] +/// An X or Y axis for the [`TimeChart`] widget +#[derive(Debug, Default, Clone, PartialEq)] pub struct Axis<'a> { /// Title displayed next to axis end - pub title: Option>, + pub(crate) title: Option>, /// Bounds for the axis (all data points outside these limits will not be represented) - pub bounds: [f64; 2], + pub(crate) bounds: [f64; 2], /// A list of labels to put to the left or below the axis - pub labels: Option>>, - /// The style used to draw the axis itself - NOT The labels. - pub style: Style, + pub(crate) labels: Option>>, + /// The style used to draw the axis itself + pub(crate) style: Style, + /// The alignment of the labels of the Axis + pub(crate) labels_alignment: Alignment, } -impl<'a> Default for Axis<'a> { - fn default() -> Axis<'a> { - Axis { - title: None, - bounds: [0.0, 0.0], - labels: None, - style: Default::default(), - } - } -} - -#[allow(dead_code)] impl<'a> Axis<'a> { + /// Sets the axis title + /// + /// It will be displayed at the end of the axis. For an X axis this is the right, for a Y axis, + /// this is the top. + /// + /// This is a fluent setter method which must be chained or used as it consumes self + #[must_use = "method moves the value of self and returns the modified value"] pub fn title(mut self, title: T) -> Axis<'a> where T: Into>, @@ -55,75 +57,253 @@ impl<'a> Axis<'a> { self } + /// Sets the bounds of this axis + /// + /// In other words, sets the min and max value on this axis. + /// + /// This is a fluent setter method which must be chained or used as it consumes self + #[must_use = "method moves the value of self and returns the modified value"] pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> { self.bounds = bounds; self } + /// Sets the axis labels + /// + /// - For the X axis, the labels are displayed left to right. + /// - For the Y axis, the labels are displayed bottom to top. + #[must_use = "method moves the value of self and returns the modified value"] pub fn labels(mut self, labels: Vec>) -> Axis<'a> { self.labels = Some(labels); self } - pub fn style(mut self, style: Style) -> Axis<'a> { - self.style = style; + /// Sets the axis style. + #[must_use = "method moves the value of self and returns the modified value"] + pub fn style>(mut self, style: S) -> Axis<'a> { + self.style = style.into(); + self + } + + /// Sets the labels alignment of the axis + /// + /// The alignment behaves differently based on the axis: + /// - Y axis: The labels are aligned within the area on the left of the axis + /// - X axis: The first X-axis label is aligned relative to the Y-axis + /// + /// On the X axis, this parameter only affects the first label. + #[must_use = "method moves the value of self and returns the modified value"] + pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> { + self.labels_alignment = alignment; self } } +/// Allow users to specify the position of a legend in a [`TimeChart`] +/// +/// See [`TimeChart::legend_position`] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +pub enum LegendPosition { + /// Legend is centered on top + Top, + /// Legend is in the top-right corner. This is the **default**. + #[default] + TopRight, + /// Legend is in the top-left corner + TopLeft, + /// Legend is centered on the left + Left, + /// Legend is centered on the right + Right, + /// Legend is centered on the bottom + Bottom, + /// Legend is in the bottom-right corner + BottomRight, + /// Legend is in the bottom-left corner + BottomLeft, +} + +impl LegendPosition { + fn layout( + &self, area: Rect, legend_width: u16, legend_height: u16, x_title_width: u16, + y_title_width: u16, + ) -> Option { + let mut height_margin = (area.height - legend_height) as i32; + if x_title_width != 0 { + height_margin -= 1; + } + if y_title_width != 0 { + height_margin -= 1; + } + if height_margin < 0 { + return None; + }; + + let (x, y) = match self { + Self::TopRight => { + if legend_width + y_title_width > area.width { + (area.right() - legend_width, area.top() + 1) + } else { + (area.right() - legend_width, area.top()) + } + } + Self::TopLeft => { + if y_title_width != 0 { + (area.left(), area.top() + 1) + } else { + (area.left(), area.top()) + } + } + Self::Top => { + let x = (area.width - legend_width) / 2; + if area.left() + y_title_width > x { + (area.left() + x, area.top() + 1) + } else { + (area.left() + x, area.top()) + } + } + Self::Left => { + let mut y = (area.height - legend_height) / 2; + if y_title_width != 0 { + y += 1; + } + if x_title_width != 0 { + y = y.saturating_sub(1); + } + (area.left(), area.top() + y) + } + Self::Right => { + let mut y = (area.height - legend_height) / 2; + if y_title_width != 0 { + y += 1; + } + if x_title_width != 0 { + y = y.saturating_sub(1); + } + (area.right() - legend_width, area.top() + y) + } + Self::BottomLeft => { + if x_title_width + legend_width > area.width { + (area.left(), area.bottom() - legend_height - 1) + } else { + (area.left(), area.bottom() - legend_height) + } + } + Self::BottomRight => { + if x_title_width != 0 { + ( + area.right() - legend_width, + area.bottom() - legend_height - 1, + ) + } else { + (area.right() - legend_width, area.bottom() - legend_height) + } + } + Self::Bottom => { + let x = area.left() + (area.width - legend_width) / 2; + if x + legend_width > area.right() - x_title_width { + (x, area.bottom() - legend_height - 1) + } else { + (x, area.bottom() - legend_height) + } + } + }; + + Some(Rect::new(x, y, legend_width, legend_height)) + } +} + /// A group of data points -#[derive(Debug, Clone)] +/// +/// This is the main element composing a [`TimeChart`]. +/// +/// A dataset can be [named](Dataset::name). Only named datasets will be rendered in the legend. +/// +/// After that, you can pass it data with [`Dataset::data`]. Data is an array of `f64` tuples +/// (`(f64, f64)`), the first element being X and the second Y. It's also worth noting that, unlike +/// the [`Rect`], here the Y axis is bottom to top, as in math. +#[derive(Debug, Default, Clone, PartialEq)] pub struct Dataset<'a> { /// Name of the dataset (used in the legend if shown) - name: Cow<'a, str>, + name: Option>, /// A reference to the actual data - data: &'a [Point], + data: &'a [(f64, f64)], + /// Symbol used for each points of this dataset + marker: symbols::Marker, /// Determines graph type used for drawing points graph_type: GraphType, /// Style used to plot this dataset style: Style, } -impl<'a> Default for Dataset<'a> { - fn default() -> Dataset<'a> { - Dataset { - name: Cow::from(""), - data: &[], - graph_type: GraphType::Scatter, - style: Style::default(), - } - } -} - -#[allow(dead_code)] impl<'a> Dataset<'a> { + /// Sets the name of the dataset. + #[must_use = "method moves the value of self and returns the modified value"] pub fn name(mut self, name: S) -> Dataset<'a> where - S: Into>, + S: Into>, { - self.name = name.into(); + self.name = Some(name.into()); self } - pub fn data(mut self, data: &'a [Point]) -> Dataset<'a> { + /// Sets the data points of this dataset + /// + /// Points will then either be rendered as scrattered points or with lines between them + /// depending on [`Dataset::graph_type`]. + /// + /// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the + /// second Y. It's also worth noting that, unlike the [`Rect`], here the Y axis is bottom to + /// top, as in math. + #[must_use = "method moves the value of self and returns the modified value"] + pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> { self.data = data; self } + /// Sets the kind of character to use to display this dataset + /// + /// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`) or half-blocks + /// (`█`, `▄`, and `▀`). See [symbols::Marker] for more details. + /// + /// Note [`Marker::Braille`](symbols::Marker::Braille) requires a font that supports Unicode + /// Braille Patterns. + /// + /// This is a fluent setter method which must be chained or used as it consumes self + #[must_use = "method moves the value of self and returns the modified value"] + pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> { + self.marker = marker; + self + } + + /// Sets how the dataset should be drawn + /// + /// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or [line](GraphType::Line) charts. + /// A scatter will draw only the points in the dataset while a line will also draw a line + /// between them. See [`GraphType`] for more details + #[must_use = "method moves the value of self and returns the modified value"] pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> { self.graph_type = graph_type; self } - pub fn style(mut self, style: Style) -> Dataset<'a> { - self.style = style; + /// Sets the style of this dataset + /// + /// The given style will be used to draw the legend and the data points. Currently the legend + /// will use the entire style whereas the data points will only use the foreground. + /// + /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or + /// your own type that implements [`Into