mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 12:43:16 +00:00
feat!: add support for empty bar style to Sparkline
(#1326)
- distingiush between empty bars and bars with a value of 0 - provide custom styling for empty bars - provide custom styling for individual bars - inverts the rendering algorithm to be item first Closes: #1325 BREAKING CHANGE: `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
parent
a52ee82fc7
commit
60cc15bbb0
5 changed files with 465 additions and 52 deletions
|
@ -11,6 +11,7 @@ GitHub with a [breaking change] label.
|
||||||
This is a quick summary of the sections below:
|
This is a quick summary of the sections below:
|
||||||
|
|
||||||
- [v0.29.0](#unreleased)
|
- [v0.29.0](#unreleased)
|
||||||
|
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||||
- Removed public fields from `Rect` iterators
|
- Removed public fields from `Rect` iterators
|
||||||
- `Line` now implements `From<Cow<str>`
|
- `Line` now implements `From<Cow<str>`
|
||||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||||
|
@ -73,6 +74,35 @@ This is a quick summary of the sections below:
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
|
||||||
|
|
||||||
|
[#1326]: https://github.com/ratatui/ratatui/pull/1326
|
||||||
|
|
||||||
|
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
|
||||||
|
instead of `&[u64]`.
|
||||||
|
|
||||||
|
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
|
||||||
|
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
|
||||||
|
data points differently, depending on whether they are present or absent.
|
||||||
|
|
||||||
|
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
|
||||||
|
addition to any other styling applied to the `Sparkline`.
|
||||||
|
|
||||||
|
Several `From` implementations have been added to `SparklineBar` to support existing callers who
|
||||||
|
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
|
||||||
|
|
||||||
|
If you encounter any type inference issues, you may need to provide an explicit type for the data
|
||||||
|
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
|
||||||
|
`into()` to convert it to form that can be used as a `SparklineBar`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let value = 1u8;
|
||||||
|
- Sparkline::default().data(&[value.into()]);
|
||||||
|
+ Sparkline::default().data(&[u64::from(value)]);
|
||||||
|
```
|
||||||
|
|
||||||
|
As a consequence of this change, the `data` method is no longer a `const fn`.
|
||||||
|
|
||||||
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
||||||
|
|
||||||
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
||||||
|
|
|
@ -99,7 +99,7 @@ pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.border_type(BorderType::Thick),
|
.border_type(BorderType::Thick),
|
||||||
)
|
)
|
||||||
.data(&data)
|
.data(data)
|
||||||
.style(THEME.traceroute.ping)
|
.style(THEME.traceroute.ping)
|
||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,6 +203,14 @@ pub mod scrollbar {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod shade {
|
||||||
|
pub const EMPTY: &str = " ";
|
||||||
|
pub const LIGHT: &str = "░";
|
||||||
|
pub const MEDIUM: &str = "▒";
|
||||||
|
pub const DARK: &str = "▓";
|
||||||
|
pub const FULL: &str = "█";
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use strum::ParseError;
|
use strum::ParseError;
|
||||||
|
|
|
@ -50,7 +50,7 @@ pub use self::{
|
||||||
logo::{RatatuiLogo, Size as RatatuiLogoSize},
|
logo::{RatatuiLogo, Size as RatatuiLogoSize},
|
||||||
paragraph::{Paragraph, Wrap},
|
paragraph::{Paragraph, Wrap},
|
||||||
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||||
sparkline::{RenderDirection, Sparkline},
|
sparkline::{RenderDirection, Sparkline, SparklineBar},
|
||||||
table::{Cell, HighlightSpacing, Row, Table, TableState},
|
table::{Cell, HighlightSpacing, Row, Table, TableState},
|
||||||
tabs::Tabs,
|
tabs::Tabs,
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,10 +12,29 @@ use crate::{
|
||||||
|
|
||||||
/// Widget to render a sparkline over one or more lines.
|
/// Widget to render a sparkline over one or more lines.
|
||||||
///
|
///
|
||||||
|
/// Each bar in a `Sparkline` represents a value from the provided dataset. The height of the bar
|
||||||
|
/// is determined by the value in the dataset.
|
||||||
|
///
|
||||||
/// You can create a `Sparkline` using [`Sparkline::default`].
|
/// You can create a `Sparkline` using [`Sparkline::default`].
|
||||||
///
|
///
|
||||||
|
/// The data is set using [`Sparkline::data`]. The data can be a slice of `u64`, `Option<u64>`, or a
|
||||||
|
/// [`SparklineBar`]. For the `Option<u64>` and [`SparklineBar`] cases, a data point with a value
|
||||||
|
/// of `None` is interpreted an as the _absence_ of a value.
|
||||||
|
///
|
||||||
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
|
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
|
||||||
/// provided by the [`Stylize`](crate::style::Stylize) trait.
|
/// provided by the [`Stylize`](crate::style::Stylize) trait. The style may be set for the entire
|
||||||
|
/// widget or for individual bars by setting individual [`SparklineBar::style`].
|
||||||
|
///
|
||||||
|
/// The bars are rendered using a set of symbols. The default set is [`symbols::bar::NINE_LEVELS`].
|
||||||
|
/// You can change the set using [`Sparkline::bar_set`].
|
||||||
|
///
|
||||||
|
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
|
||||||
|
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
|
||||||
|
/// styled with the style of the sparkline combined with the style provided in the [`SparklineBar`]
|
||||||
|
/// if it is set, otherwise the sparkline style will be used.
|
||||||
|
///
|
||||||
|
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`] and
|
||||||
|
/// the symbol set by [`Sparkline::absent_value_symbol`].
|
||||||
///
|
///
|
||||||
/// # Setter methods
|
/// # Setter methods
|
||||||
///
|
///
|
||||||
|
@ -37,7 +56,9 @@ use crate::{
|
||||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||||
/// .max(5)
|
/// .max(5)
|
||||||
/// .direction(RenderDirection::RightToLeft)
|
/// .direction(RenderDirection::RightToLeft)
|
||||||
/// .style(Style::default().red().on_white());
|
/// .style(Style::default().red().on_white())
|
||||||
|
/// .absent_value_style(Style::default().fg(Color::Red))
|
||||||
|
/// .absent_value_symbol(symbols::shade::FULL);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||||
pub struct Sparkline<'a> {
|
pub struct Sparkline<'a> {
|
||||||
|
@ -45,14 +66,18 @@ pub struct Sparkline<'a> {
|
||||||
block: Option<Block<'a>>,
|
block: Option<Block<'a>>,
|
||||||
/// Widget style
|
/// Widget style
|
||||||
style: Style,
|
style: Style,
|
||||||
|
/// Style of absent values
|
||||||
|
absent_value_style: Style,
|
||||||
|
/// The symbol to use for absent values
|
||||||
|
absent_value_symbol: AbsentValueSymbol,
|
||||||
/// A slice of the data to display
|
/// A slice of the data to display
|
||||||
data: &'a [u64],
|
data: Vec<SparklineBar>,
|
||||||
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
||||||
/// widget uses the max of the dataset)
|
/// widget uses the max of the dataset)
|
||||||
max: Option<u64>,
|
max: Option<u64>,
|
||||||
/// A set of bar symbols used to represent the give data
|
/// A set of bar symbols used to represent the give data
|
||||||
bar_set: symbols::bar::Set,
|
bar_set: symbols::bar::Set,
|
||||||
// The direction to render the sparkine, either from left to right, or from right to left
|
/// The direction to render the sparkline, either from left to right, or from right to left
|
||||||
direction: RenderDirection,
|
direction: RenderDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,9 +115,51 @@ impl<'a> Sparkline<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the style to use for absent values.
|
||||||
|
///
|
||||||
|
/// Absent values are values in the dataset that are `None`.
|
||||||
|
///
|
||||||
|
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||||
|
/// your own type that implements [`Into<Style>`]).
|
||||||
|
///
|
||||||
|
/// The foreground corresponds to the bars while the background is everything else.
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||||
|
self.absent_value_style = style.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the symbol to use for absent values.
|
||||||
|
///
|
||||||
|
/// Absent values are values in the dataset that are `None`.
|
||||||
|
///
|
||||||
|
/// The default is [`symbols::shade::EMPTY`].
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
|
||||||
|
self.absent_value_symbol = AbsentValueSymbol(symbol.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the dataset for the sparkline.
|
/// Sets the dataset for the sparkline.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// Each item in the dataset is a bar in the sparkline. The height of the bar is determined by
|
||||||
|
/// the value in the dataset.
|
||||||
|
///
|
||||||
|
/// The data can be a slice of `u64`, `Option<u64>`, or a [`SparklineBar`]. For the
|
||||||
|
/// `Option<u64>` and [`SparklineBar`] cases, a data point with a value of `None` is
|
||||||
|
/// interpreted an as the _absence_ of a value.
|
||||||
|
///
|
||||||
|
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
|
||||||
|
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
|
||||||
|
/// styled with the style of the sparkline combined with the style provided in the
|
||||||
|
/// [`SparklineBar`] if it is set, otherwise the sparkline style will be used.
|
||||||
|
///
|
||||||
|
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`]
|
||||||
|
/// and the symbol set by [`Sparkline::absent_value_symbol`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Create a `Sparkline` from a slice of `u64`:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use ratatui::{layout::Rect, widgets::Sparkline, Frame};
|
/// use ratatui::{layout::Rect, widgets::Sparkline, Frame};
|
||||||
|
@ -103,9 +170,41 @@ impl<'a> Sparkline<'a> {
|
||||||
/// frame.render_widget(sparkline, area);
|
/// frame.render_widget(sparkline, area);
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// Create a `Sparkline` from a slice of `Option<u64>` such that some bars are absent:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use ratatui::{prelude::*, widgets::*};
|
||||||
|
/// # fn ui(frame: &mut Frame) {
|
||||||
|
/// # let area = Rect::default();
|
||||||
|
/// let data = vec![Some(1), None, Some(3)];
|
||||||
|
/// let sparkline = Sparkline::default().data(data);
|
||||||
|
/// frame.render_widget(sparkline, area);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Create a [`Sparkline`] from a a Vec of [`SparklineBar`] such that some bars are styled:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use ratatui::{prelude::*, widgets::*};
|
||||||
|
/// # fn ui(frame: &mut Frame) {
|
||||||
|
/// # let area = Rect::default();
|
||||||
|
/// let data = vec![
|
||||||
|
/// SparklineBar::from(1).style(Some(Style::default().fg(Color::Red))),
|
||||||
|
/// SparklineBar::from(2),
|
||||||
|
/// SparklineBar::from(3).style(Some(Style::default().fg(Color::Blue))),
|
||||||
|
/// ];
|
||||||
|
/// let sparkline = Sparkline::default().data(data);
|
||||||
|
/// frame.render_widget(sparkline, area);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub const fn data(mut self, data: &'a [u64]) -> Self {
|
pub fn data<T>(mut self, data: T) -> Self
|
||||||
self.data = data;
|
where
|
||||||
|
T: IntoIterator,
|
||||||
|
T::Item: Into<SparklineBar>,
|
||||||
|
{
|
||||||
|
self.data = data.into_iter().map(Into::into).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +238,73 @@ impl<'a> Sparkline<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An bar in a `Sparkline`.
|
||||||
|
///
|
||||||
|
/// The height of the bar is determined by the value and a value of `None` is interpreted as the
|
||||||
|
/// _absence_ of a value, as distinct from a value of `Some(0)`.
|
||||||
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct SparklineBar {
|
||||||
|
/// The value of the bar.
|
||||||
|
///
|
||||||
|
/// If `None`, the bar is absent.
|
||||||
|
value: Option<u64>,
|
||||||
|
/// The style of the bar.
|
||||||
|
///
|
||||||
|
/// If `None`, the bar will use the style of the sparkline.
|
||||||
|
style: Option<Style>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SparklineBar {
|
||||||
|
/// Sets the style of the bar.
|
||||||
|
///
|
||||||
|
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||||
|
/// your own type that implements [`Into<Style>`]).
|
||||||
|
///
|
||||||
|
/// If not set, the default style of the sparkline will be used.
|
||||||
|
///
|
||||||
|
/// As well as the style of the sparkline, each [`SparklineBar`] may optionally set its own
|
||||||
|
/// style. If set, the style of the bar will be the style of the sparkline combined with
|
||||||
|
/// the style of the bar.
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
|
||||||
|
self.style = style.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<u64>> for SparklineBar {
|
||||||
|
fn from(value: Option<u64>) -> Self {
|
||||||
|
Self { value, style: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for SparklineBar {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(value),
|
||||||
|
style: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&u64> for SparklineBar {
|
||||||
|
fn from(value: &u64) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(*value),
|
||||||
|
style: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Option<u64>> for SparklineBar {
|
||||||
|
fn from(value: &Option<u64>) -> Self {
|
||||||
|
Self {
|
||||||
|
value: *value,
|
||||||
|
style: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Styled for Sparkline<'a> {
|
impl<'a> Styled for Sparkline<'a> {
|
||||||
type Item = Self;
|
type Item = Self;
|
||||||
|
|
||||||
|
@ -165,31 +331,89 @@ impl WidgetRef for Sparkline<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A newtype wrapper for the symbol to use for absent values.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
struct AbsentValueSymbol(String);
|
||||||
|
|
||||||
|
impl Default for AbsentValueSymbol {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(symbols::shade::EMPTY.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Sparkline<'_> {
|
impl Sparkline<'_> {
|
||||||
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
|
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
|
||||||
if spark_area.is_empty() {
|
if spark_area.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// determine the maximum height across all bars
|
||||||
let max = self
|
let max_height = self
|
||||||
.max
|
.max
|
||||||
.unwrap_or_else(|| *self.data.iter().max().unwrap_or(&1));
|
.unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
|
||||||
|
|
||||||
|
// determine the maximum index to render
|
||||||
let max_index = min(spark_area.width as usize, self.data.len());
|
let max_index = min(spark_area.width as usize, self.data.len());
|
||||||
let mut data = self
|
|
||||||
.data
|
// render each item in the data
|
||||||
.iter()
|
for (i, item) in self.data.iter().take(max_index).enumerate() {
|
||||||
.take(max_index)
|
let x = match self.direction {
|
||||||
.map(|e| {
|
RenderDirection::LeftToRight => spark_area.left() + i as u16,
|
||||||
if max == 0 {
|
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// determine the height, symbol and style to use for the item
|
||||||
|
//
|
||||||
|
// if the item is not absent:
|
||||||
|
// - the height is the value of the item scaled to the height of the spark area
|
||||||
|
// - the symbol is determined by the scaled height
|
||||||
|
// - the style is the style of the item, if one is set
|
||||||
|
//
|
||||||
|
// otherwise:
|
||||||
|
// - the height is the total height of the spark area
|
||||||
|
// - the symbol is the absent value symbol
|
||||||
|
// - the style is the absent value style
|
||||||
|
let (mut height, symbol, style) = match item {
|
||||||
|
SparklineBar {
|
||||||
|
value: Some(value),
|
||||||
|
style,
|
||||||
|
} => {
|
||||||
|
let height = if max_height == 0 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
e * u64::from(spark_area.height) * 8 / max
|
*value * u64::from(spark_area.height) * 8 / max_height
|
||||||
|
};
|
||||||
|
(height, None, *style)
|
||||||
}
|
}
|
||||||
})
|
_ => (
|
||||||
.collect::<Vec<u64>>();
|
u64::from(spark_area.height) * 8,
|
||||||
|
Some(self.absent_value_symbol.0.as_str()),
|
||||||
|
Some(self.absent_value_style),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// render the item from top to bottom
|
||||||
|
//
|
||||||
|
// if the symbol is set it will be used for the entire height of the bar, otherwise the
|
||||||
|
// symbol will be determined by the _remaining_ height.
|
||||||
|
//
|
||||||
|
// if the style is set it will be used for the entire height of the bar, otherwise the
|
||||||
|
// sparkline style will be used.
|
||||||
for j in (0..spark_area.height).rev() {
|
for j in (0..spark_area.height).rev() {
|
||||||
for (i, d) in data.iter_mut().enumerate() {
|
let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
|
||||||
let symbol = match *d {
|
if height > 8 {
|
||||||
|
height -= 8;
|
||||||
|
} else {
|
||||||
|
height = 0;
|
||||||
|
}
|
||||||
|
buf[(x, spark_area.top() + j)]
|
||||||
|
.set_symbol(symbol)
|
||||||
|
.set_style(self.style.patch(style.unwrap_or_default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn symbol_for_height(&self, height: u64) -> &str {
|
||||||
|
match height {
|
||||||
0 => self.bar_set.empty,
|
0 => self.bar_set.empty,
|
||||||
1 => self.bar_set.one_eighth,
|
1 => self.bar_set.one_eighth,
|
||||||
2 => self.bar_set.one_quarter,
|
2 => self.bar_set.one_quarter,
|
||||||
|
@ -199,21 +423,6 @@ impl Sparkline<'_> {
|
||||||
6 => self.bar_set.three_quarters,
|
6 => self.bar_set.three_quarters,
|
||||||
7 => self.bar_set.seven_eighths,
|
7 => self.bar_set.seven_eighths,
|
||||||
_ => self.bar_set.full,
|
_ => self.bar_set.full,
|
||||||
};
|
|
||||||
let x = match self.direction {
|
|
||||||
RenderDirection::LeftToRight => spark_area.left() + i as u16,
|
|
||||||
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
|
|
||||||
};
|
|
||||||
buf[(x, spark_area.top() + j)]
|
|
||||||
.set_symbol(symbol)
|
|
||||||
.set_style(self.style);
|
|
||||||
|
|
||||||
if *d > 8 {
|
|
||||||
*d -= 8;
|
|
||||||
} else {
|
|
||||||
*d = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +459,78 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_vec_of_u64() {
|
||||||
|
let data = vec![1_u64, 2, 3];
|
||||||
|
let spark_data = Sparkline::default().data(data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(2),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_vec_of_option_u64() {
|
||||||
|
let data = vec![Some(1_u64), None, Some(3)];
|
||||||
|
let spark_data = Sparkline::default().data(data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(None),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_array_of_u64() {
|
||||||
|
let data = [1_u64, 2, 3];
|
||||||
|
let spark_data = Sparkline::default().data(data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(2),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_array_of_option_u64() {
|
||||||
|
let data = [Some(1_u64), None, Some(3)];
|
||||||
|
let spark_data = Sparkline::default().data(data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(None),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_slice_of_u64() {
|
||||||
|
let data = vec![1_u64, 2, 3];
|
||||||
|
let spark_data = Sparkline::default().data(&data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(2),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_be_created_from_slice_of_option_u64() {
|
||||||
|
let data = vec![Some(1_u64), None, Some(3)];
|
||||||
|
let spark_data = Sparkline::default().data(&data).data;
|
||||||
|
let expected = vec![
|
||||||
|
SparklineBar::from(1),
|
||||||
|
SparklineBar::from(None),
|
||||||
|
SparklineBar::from(3),
|
||||||
|
];
|
||||||
|
assert_eq!(spark_data, expected);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to render a sparkline to a buffer with a given width
|
// Helper function to render a sparkline to a buffer with a given width
|
||||||
// filled with x symbols to make it easier to assert on the result
|
// filled with x symbols to make it easier to assert on the result
|
||||||
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
|
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
|
||||||
|
@ -261,7 +542,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_does_not_panic_if_max_is_zero() {
|
fn it_does_not_panic_if_max_is_zero() {
|
||||||
let widget = Sparkline::default().data(&[0, 0, 0]);
|
let widget = Sparkline::default().data([0, 0, 0]);
|
||||||
let buffer = render(widget, 6);
|
let buffer = render(widget, 6);
|
||||||
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
||||||
}
|
}
|
||||||
|
@ -270,22 +551,31 @@ mod tests {
|
||||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||||
// see https://github.com/rust-lang/rust-clippy/issues/13191
|
// see https://github.com/rust-lang/rust-clippy/issues/13191
|
||||||
#[allow(clippy::unnecessary_min_or_max)]
|
#[allow(clippy::unnecessary_min_or_max)]
|
||||||
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
|
let widget = Sparkline::default().data([0, 1, 2]).max(0);
|
||||||
let buffer = render(widget, 6);
|
let buffer = render(widget, 6);
|
||||||
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_draws() {
|
fn it_draws() {
|
||||||
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||||
let buffer = render(widget, 12);
|
let buffer = render(widget, 12);
|
||||||
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_draws_double_height() {
|
||||||
|
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||||
|
let area = Rect::new(0, 0, 12, 2);
|
||||||
|
let mut buffer = Buffer::filled(area, Cell::new("x"));
|
||||||
|
widget.render(area, &mut buffer);
|
||||||
|
assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_renders_left_to_right() {
|
fn it_renders_left_to_right() {
|
||||||
let widget = Sparkline::default()
|
let widget = Sparkline::default()
|
||||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||||
.direction(RenderDirection::LeftToRight);
|
.direction(RenderDirection::LeftToRight);
|
||||||
let buffer = render(widget, 12);
|
let buffer = render(widget, 12);
|
||||||
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
||||||
|
@ -294,12 +584,97 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn it_renders_right_to_left() {
|
fn it_renders_right_to_left() {
|
||||||
let widget = Sparkline::default()
|
let widget = Sparkline::default()
|
||||||
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
|
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||||
.direction(RenderDirection::RightToLeft);
|
.direction(RenderDirection::RightToLeft);
|
||||||
let buffer = render(widget, 12);
|
let buffer = render(widget, 12);
|
||||||
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
|
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_renders_with_absent_value_style() {
|
||||||
|
let widget = Sparkline::default()
|
||||||
|
.absent_value_style(Style::default().fg(Color::Red))
|
||||||
|
.absent_value_symbol(symbols::shade::FULL)
|
||||||
|
.data([
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
Some(2),
|
||||||
|
Some(3),
|
||||||
|
Some(4),
|
||||||
|
Some(5),
|
||||||
|
Some(6),
|
||||||
|
Some(7),
|
||||||
|
Some(8),
|
||||||
|
]);
|
||||||
|
let buffer = render(widget, 12);
|
||||||
|
let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
|
||||||
|
assert_eq!(buffer, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_renders_with_absent_value_style_double_height() {
|
||||||
|
let widget = Sparkline::default()
|
||||||
|
.absent_value_style(Style::default().fg(Color::Red))
|
||||||
|
.absent_value_symbol(symbols::shade::FULL)
|
||||||
|
.data([
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
Some(2),
|
||||||
|
Some(3),
|
||||||
|
Some(4),
|
||||||
|
Some(5),
|
||||||
|
Some(6),
|
||||||
|
Some(7),
|
||||||
|
Some(8),
|
||||||
|
]);
|
||||||
|
let area = Rect::new(0, 0, 12, 2);
|
||||||
|
let mut buffer = Buffer::filled(area, Cell::new("x"));
|
||||||
|
widget.render(area, &mut buffer);
|
||||||
|
let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
|
||||||
|
assert_eq!(buffer, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_renders_with_custom_absent_value_style() {
|
||||||
|
let widget = Sparkline::default().absent_value_symbol('*').data([
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
Some(2),
|
||||||
|
Some(3),
|
||||||
|
Some(4),
|
||||||
|
Some(5),
|
||||||
|
Some(6),
|
||||||
|
Some(7),
|
||||||
|
Some(8),
|
||||||
|
]);
|
||||||
|
let buffer = render(widget, 12);
|
||||||
|
let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
|
||||||
|
assert_eq!(buffer, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_renders_with_custom_bar_styles() {
|
||||||
|
let widget = Sparkline::default().data(vec![
|
||||||
|
SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
|
||||||
|
SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
|
||||||
|
SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
|
||||||
|
SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
|
||||||
|
SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
|
||||||
|
SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
|
||||||
|
SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
|
||||||
|
SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
|
||||||
|
SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
|
||||||
|
]);
|
||||||
|
let buffer = render(widget, 12);
|
||||||
|
let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
|
||||||
|
expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
|
||||||
|
expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
|
||||||
|
assert_eq!(buffer, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_be_stylized() {
|
fn can_be_stylized() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
Loading…
Reference in a new issue