summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorClement Tsang <34804052+ClementTsang@users.noreply.github.com>2024-02-03 19:59:12 -0500
committerGitHub <noreply@github.com>2024-02-03 19:59:12 -0500
commitb6660610d02a88aeb23fbcda8378be67a85259e7 (patch)
tree630345cf215dc0e894a8a6f70a60eedea71c5d0f /src
parent8d84b688b004b9c6f383e446a4048cadf1000a31 (diff)
deps: bump ratatui to 0.26 (#1406)
* deps: bump ratatui to 0.26 * adjust process width * a few nonzero optimizations * add a todo * update comments to be less confusing about time chart
Diffstat (limited to 'src')
-rw-r--r--src/app.rs10
-rw-r--r--src/canvas.rs5
-rw-r--r--src/canvas/components/data_table.rs8
-rw-r--r--src/canvas/components/data_table/column.rs96
-rw-r--r--src/canvas/components/data_table/data_type.rs4
-rw-r--r--src/canvas/components/data_table/draw.rs13
-rw-r--r--src/canvas/components/data_table/sortable.rs35
-rw-r--r--src/canvas/components/data_table/state.rs6
-rw-r--r--src/canvas/components/time_graph.rs4
-rw-r--r--src/canvas/components/tui_widget/time_chart.rs1280
-rw-r--r--src/canvas/components/tui_widget/time_chart/canvas.rs142
-rw-r--r--src/canvas/components/tui_widget/time_chart/points.rs215
-rw-r--r--src/canvas/widgets/network_graph.rs4
-rw-r--r--src/utils/general.rs7
-rw-r--r--src/widgets/cpu_graph.rs6
-rw-r--r--src/widgets/disk_table.rs9
-rw-r--r--src/widgets/process_table.rs2
-rw-r--r--src/widgets/process_table/proc_widget_data.rs7
-rw-r--r--src/widgets/process_table/sort_table.rs18
-rw-r--r--src/widgets/temperature_table.rs12
20 files changed, 1421 insertions, 462 deletions
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
+/// <https://github.com/ClementTsang/bottom/issues/896>.
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<Rect> {
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<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H
self.data.get(self.state.current_index)
}
- /// Returns tui-rs' internal selection.
- pub fn tui_selected(&self) -> Option<usize> {
+ /// Returns ratatui's internal selection.
+ pub fn ratatui_selected(&self) -> Option<usize> {
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<tui::text::Text<'_>> {
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<H> {
///
/// * `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<u16>;
+ fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16>;
}
impl<H, C> CalculateColumnWidths<H> for [C]
@@ -159,19 +160,25 @@ where
H: ColumnHeader,
C: DataTableColumn<H>,
{
- fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<u16> {
+ fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16> {
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<Text<'_>>;
+ fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Text<'_>>;
/// 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::<Vec<_>>()),
+ 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<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_>
+ fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>
where
H: ColumnHeader,
C: DataTableColumn<H>,
{
- 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<H, C>(&self, columns: &[C], widths: &[u16]) -> Row<'_>
+ fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>
where
H: ColumnHeader,
C: DataTableColumn<H>,
@@ -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<ColumnType> for TestType {
fn to_cell(
- &self, _column: &ColumnType, _calculated_width: u16,
+ &self, _column: &ColumnType, _calculated_width: NonZeroU16,
) -> Option<tui::text::Text<'_>> {
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<u16>,
+ pub calculated_widths: Vec<NonZeroU16>,
/// 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 @@
+//! 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`.
+
mod canvas;
+mod points;
-use std::{borrow::Cow, cmp::max};
+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<Line<'a>>,
+ pub(crate) title: Option<Line<'a>>,
/// 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<Vec<Span<'a>>>,
- /// The style used to draw the axis itself - NOT The labels.
- pub style: Style,
-}
-
-impl<'a> Default for Axis<'a> {
- fn default() -> Axis<'a> {
- Axis {
- title: None,
- bounds: [0.0, 0.0],
- labels: None,
- style: Default::default(),
- }
- }
+ pub(crate) labels: Option<Vec<Span<'a>>>,
+ /// 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,
}
-#[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<T>(mut self, title: T) -> Axis<'a>
where
T: Into<Line<'a>>,
@@ -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<Span<'a>>) -> 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<S: Into<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<Rect> {
+ 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<Line<'a>>,
/// 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<S>(mut self, name: S) -> Dataset<'a>
where
- S: Into<Cow<'a, str>>,
+ S: Into<Line<'a>>,
{
- 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