ratatui_widgets/
sparkline.rs

1//! The [`Sparkline`] widget is used to display a sparkline over one or more lines.
2
3use alloc::string::{String, ToString};
4use alloc::vec::Vec;
5use core::cmp::min;
6
7use ratatui_core::buffer::Buffer;
8use ratatui_core::layout::Rect;
9use ratatui_core::style::{Style, Styled};
10use ratatui_core::symbols;
11use ratatui_core::widgets::Widget;
12use strum::{Display, EnumString};
13
14use crate::block::{Block, BlockExt};
15
16/// Widget to render a sparkline over one or more lines.
17///
18/// Each bar in a `Sparkline` represents a value from the provided dataset. The height of the bar
19/// is determined by the value in the dataset.
20///
21/// You can create a `Sparkline` using [`Sparkline::default`].
22///
23/// The data is set using [`Sparkline::data`]. The data can be a slice of `u64`, `Option<u64>`, or a
24/// [`SparklineBar`].  For the `Option<u64>` and [`SparklineBar`] cases, a data point with a value
25/// of `None` is interpreted an as the _absence_ of a value.
26///
27/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
28/// provided by the [`Stylize`](ratatui_core::style::Stylize) trait.  The style may be set for the
29/// entire widget or for individual bars by setting individual [`SparklineBar::style`].
30///
31/// The bars are rendered using a set of symbols. The default set is [`symbols::bar::NINE_LEVELS`].
32/// You can change the set using [`Sparkline::bar_set`].
33///
34/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
35/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
36/// styled with the style of the sparkline combined with the style provided in the [`SparklineBar`]
37/// if it is set, otherwise the sparkline style will be used.
38///
39/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`] and
40/// the symbol set by [`Sparkline::absent_value_symbol`].
41///
42/// # Setter methods
43///
44/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
45/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
46/// - [`Sparkline::max`] sets the maximum value of bars
47/// - [`Sparkline::direction`] sets the render direction
48///
49/// # Examples
50///
51/// ```
52/// use ratatui::style::{Color, Style, Stylize};
53/// use ratatui::symbols;
54/// use ratatui::widgets::{Block, RenderDirection, Sparkline};
55///
56/// Sparkline::default()
57///     .block(Block::bordered().title("Sparkline"))
58///     .data(&[0, 2, 3, 4, 1, 4, 10])
59///     .max(5)
60///     .direction(RenderDirection::RightToLeft)
61///     .style(Style::default().red().on_white())
62///     .absent_value_style(Style::default().fg(Color::Red))
63///     .absent_value_symbol(symbols::shade::FULL);
64/// ```
65#[derive(Debug, Default, Clone, Eq, PartialEq)]
66pub struct Sparkline<'a> {
67    /// A block to wrap the widget in
68    block: Option<Block<'a>>,
69    /// Widget style
70    style: Style,
71    /// Style of absent values
72    absent_value_style: Style,
73    /// The symbol to use for absent values
74    absent_value_symbol: AbsentValueSymbol,
75    /// A slice of the data to display
76    data: Vec<SparklineBar>,
77    /// The maximum value to take to compute the maximum bar height (if nothing is specified, the
78    /// widget uses the max of the dataset)
79    max: Option<u64>,
80    /// A set of bar symbols used to represent the give data
81    bar_set: symbols::bar::Set<'a>,
82    /// The direction to render the sparkline, either from left to right, or from right to left
83    direction: RenderDirection,
84}
85
86/// Defines the direction in which sparkline will be rendered.
87///
88/// See [`Sparkline::direction`].
89#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub enum RenderDirection {
92    /// The first value is on the left, going to the right
93    #[default]
94    LeftToRight,
95    /// The first value is on the right, going to the left
96    RightToLeft,
97}
98
99impl<'a> Sparkline<'a> {
100    /// Wraps the sparkline with the given `block`.
101    #[must_use = "method moves the value of self and returns the modified value"]
102    pub fn block(mut self, block: Block<'a>) -> Self {
103        self.block = Some(block);
104        self
105    }
106
107    /// Sets the style of the entire widget.
108    ///
109    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
110    /// your own type that implements [`Into<Style>`]).
111    ///
112    /// The foreground corresponds to the bars while the background is everything else.
113    ///
114    /// [`Color`]: ratatui_core::style::Color
115    #[must_use = "method moves the value of self and returns the modified value"]
116    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
117        self.style = style.into();
118        self
119    }
120
121    /// Sets the style to use for absent values.
122    ///
123    /// Absent values are values in the dataset that are `None`.
124    ///
125    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
126    /// your own type that implements [`Into<Style>`]).
127    ///
128    /// The foreground corresponds to the bars while the background is everything else.
129    ///
130    /// [`Color`]: ratatui_core::style::Color
131    #[must_use = "method moves the value of self and returns the modified value"]
132    pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
133        self.absent_value_style = style.into();
134        self
135    }
136
137    /// Sets the symbol to use for absent values.
138    ///
139    /// Absent values are values in the dataset that are `None`.
140    ///
141    /// The default is [`symbols::shade::EMPTY`].
142    #[must_use = "method moves the value of self and returns the modified value"]
143    pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
144        self.absent_value_symbol = AbsentValueSymbol(symbol.into());
145        self
146    }
147
148    /// Sets the dataset for the sparkline.
149    ///
150    /// Each item in the dataset is a bar in the sparkline. The height of the bar is determined by
151    /// the value in the dataset.
152    ///
153    /// The data can be a slice of `u64`, `Option<u64>`, or a [`SparklineBar`].  For the
154    /// `Option<u64>` and [`SparklineBar`] cases, a data point with a value of `None` is
155    /// interpreted an as the _absence_ of a value.
156    ///
157    /// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
158    /// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
159    /// styled with the style of the sparkline combined with the style provided in the
160    /// [`SparklineBar`] if it is set, otherwise the sparkline style will be used.
161    ///
162    /// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`]
163    /// and the symbol set by [`Sparkline::absent_value_symbol`].
164    ///
165    /// # Examples
166    ///
167    /// Create a `Sparkline` from a slice of `u64`:
168    ///
169    /// ```
170    /// use ratatui::Frame;
171    /// use ratatui::layout::Rect;
172    /// use ratatui::widgets::Sparkline;
173    ///
174    /// # fn ui(frame: &mut Frame) {
175    /// # let area = Rect::default();
176    /// let sparkline = Sparkline::default().data(&[1, 2, 3]);
177    /// frame.render_widget(sparkline, area);
178    /// # }
179    /// ```
180    ///
181    /// Create a `Sparkline` from a slice of `Option<u64>` such that some bars are absent:
182    ///
183    /// ```
184    /// # use ratatui::{prelude::*, widgets::*};
185    /// # fn ui(frame: &mut Frame) {
186    /// # let area = Rect::default();
187    /// let data = vec![Some(1), None, Some(3)];
188    /// let sparkline = Sparkline::default().data(data);
189    /// frame.render_widget(sparkline, area);
190    /// # }
191    /// ```
192    ///
193    /// Create a [`Sparkline`] from a a Vec of [`SparklineBar`] such that some bars are styled:
194    ///
195    /// ```
196    /// # use ratatui::{prelude::*, widgets::*};
197    /// # fn ui(frame: &mut Frame) {
198    /// # let area = Rect::default();
199    /// let data = vec![
200    ///     SparklineBar::from(1).style(Some(Style::default().fg(Color::Red))),
201    ///     SparklineBar::from(2),
202    ///     SparklineBar::from(3).style(Some(Style::default().fg(Color::Blue))),
203    /// ];
204    /// let sparkline = Sparkline::default().data(data);
205    /// frame.render_widget(sparkline, area);
206    /// # }
207    /// ```
208    #[must_use = "method moves the value of self and returns the modified value"]
209    pub fn data<T>(mut self, data: T) -> Self
210    where
211        T: IntoIterator,
212        T::Item: Into<SparklineBar>,
213    {
214        self.data = data.into_iter().map(Into::into).collect();
215        self
216    }
217
218    /// Sets the maximum value of bars.
219    ///
220    /// Every bar will be scaled accordingly. If no max is given, this will be the max in the
221    /// dataset.
222    #[must_use = "method moves the value of self and returns the modified value"]
223    pub const fn max(mut self, max: u64) -> Self {
224        self.max = Some(max);
225        self
226    }
227
228    /// Sets the characters used to display the bars.
229    ///
230    /// Can be [`symbols::bar::THREE_LEVELS`], [`symbols::bar::NINE_LEVELS`] (default) or a custom
231    /// [`Set`](symbols::bar::Set).
232    #[must_use = "method moves the value of self and returns the modified value"]
233    pub const fn bar_set(mut self, bar_set: symbols::bar::Set<'a>) -> Self {
234        self.bar_set = bar_set;
235        self
236    }
237
238    /// Sets the direction of the sparkline.
239    ///
240    /// [`RenderDirection::LeftToRight`] by default.
241    #[must_use = "method moves the value of self and returns the modified value"]
242    pub const fn direction(mut self, direction: RenderDirection) -> Self {
243        self.direction = direction;
244        self
245    }
246}
247
248/// An bar in a `Sparkline`.
249///
250/// The height of the bar is determined by the value and a value of `None` is interpreted as the
251/// _absence_ of a value, as distinct from a value of `Some(0)`.
252#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
253pub struct SparklineBar {
254    /// The value of the bar.
255    ///
256    /// If `None`, the bar is absent.
257    value: Option<u64>,
258    /// The style of the bar.
259    ///
260    /// If `None`, the bar will use the style of the sparkline.
261    style: Option<Style>,
262}
263
264impl SparklineBar {
265    /// Sets the style of the bar.
266    ///
267    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
268    /// your own type that implements [`Into<Style>`]).
269    ///
270    /// If not set, the default style of the sparkline will be used.
271    ///
272    /// As well as the style of the sparkline, each [`SparklineBar`] may optionally set its own
273    /// style.  If set, the style of the bar will be the style of the sparkline combined with
274    /// the style of the bar.
275    ///
276    /// [`Color`]: ratatui_core::style::Color
277    #[must_use = "method moves the value of self and returns the modified value"]
278    pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
279        self.style = style.into();
280        self
281    }
282}
283
284impl From<Option<u64>> for SparklineBar {
285    fn from(value: Option<u64>) -> Self {
286        Self { value, style: None }
287    }
288}
289
290impl From<u64> for SparklineBar {
291    fn from(value: u64) -> Self {
292        Self {
293            value: Some(value),
294            style: None,
295        }
296    }
297}
298
299impl From<&u64> for SparklineBar {
300    fn from(value: &u64) -> Self {
301        Self {
302            value: Some(*value),
303            style: None,
304        }
305    }
306}
307
308impl From<&Option<u64>> for SparklineBar {
309    fn from(value: &Option<u64>) -> Self {
310        Self {
311            value: *value,
312            style: None,
313        }
314    }
315}
316
317impl Styled for Sparkline<'_> {
318    type Item = Self;
319
320    fn style(&self) -> Style {
321        self.style
322    }
323
324    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
325        self.style(style)
326    }
327}
328
329impl Widget for Sparkline<'_> {
330    fn render(self, area: Rect, buf: &mut Buffer) {
331        Widget::render(&self, area, buf);
332    }
333}
334
335impl Widget for &Sparkline<'_> {
336    fn render(self, area: Rect, buf: &mut Buffer) {
337        self.block.as_ref().render(area, buf);
338        let inner = self.block.inner_if_some(area);
339        self.render_sparkline(inner, buf);
340    }
341}
342
343/// A newtype wrapper for the symbol to use for absent values.
344#[derive(Debug, Clone, Eq, PartialEq)]
345struct AbsentValueSymbol(String);
346
347impl Default for AbsentValueSymbol {
348    fn default() -> Self {
349        Self(symbols::shade::EMPTY.to_string())
350    }
351}
352
353impl Sparkline<'_> {
354    fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
355        if spark_area.is_empty() {
356            return;
357        }
358        // determine the maximum height across all bars
359        let max_height = self
360            .max
361            .unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
362
363        // determine the maximum index to render
364        let max_index = min(spark_area.width as usize, self.data.len());
365
366        // render each item in the data
367        for (i, item) in self.data.iter().take(max_index).enumerate() {
368            let x = match self.direction {
369                RenderDirection::LeftToRight => spark_area.left() + i as u16,
370                RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
371            };
372
373            // determine the height, symbol and style to use for the item
374            //
375            // if the item is not absent:
376            // - the height is the value of the item scaled to the height of the spark area
377            // - the symbol is determined by the scaled height
378            // - the style is the style of the item, if one is set
379            //
380            // otherwise:
381            // - the height is the total height of the spark area
382            // - the symbol is the absent value symbol
383            // - the style is the absent value style
384            let (mut height, symbol, style) = match item {
385                SparklineBar {
386                    value: Some(value),
387                    style,
388                } => {
389                    let height = if max_height == 0 {
390                        0
391                    } else {
392                        *value * u64::from(spark_area.height) * 8 / max_height
393                    };
394                    (height, None, *style)
395                }
396                _ => (
397                    u64::from(spark_area.height) * 8,
398                    Some(self.absent_value_symbol.0.as_str()),
399                    Some(self.absent_value_style),
400                ),
401            };
402
403            // render the item from top to bottom
404            //
405            // if the symbol is set it will be used for the entire height of the bar, otherwise the
406            // symbol will be determined by the _remaining_ height.
407            //
408            // if the style is set it will be used for the entire height of the bar, otherwise the
409            // sparkline style will be used.
410            for j in (0..spark_area.height).rev() {
411                let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
412                if height > 8 {
413                    height -= 8;
414                } else {
415                    height = 0;
416                }
417                buf[(x, spark_area.top() + j)]
418                    .set_symbol(symbol)
419                    .set_style(self.style.patch(style.unwrap_or_default()));
420            }
421        }
422    }
423
424    const fn symbol_for_height(&self, height: u64) -> &str {
425        match height {
426            0 => self.bar_set.empty,
427            1 => self.bar_set.one_eighth,
428            2 => self.bar_set.one_quarter,
429            3 => self.bar_set.three_eighths,
430            4 => self.bar_set.half,
431            5 => self.bar_set.five_eighths,
432            6 => self.bar_set.three_quarters,
433            7 => self.bar_set.seven_eighths,
434            _ => self.bar_set.full,
435        }
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use alloc::vec;
442
443    use ratatui_core::buffer::Cell;
444    use ratatui_core::style::{Color, Modifier, Stylize};
445    use strum::ParseError;
446
447    use super::*;
448
449    #[test]
450    fn render_direction_to_string() {
451        assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
452        assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
453    }
454
455    #[test]
456    fn render_direction_from_str() {
457        assert_eq!(
458            "LeftToRight".parse::<RenderDirection>(),
459            Ok(RenderDirection::LeftToRight)
460        );
461        assert_eq!(
462            "RightToLeft".parse::<RenderDirection>(),
463            Ok(RenderDirection::RightToLeft)
464        );
465        assert_eq!(
466            "".parse::<RenderDirection>(),
467            Err(ParseError::VariantNotFound)
468        );
469    }
470
471    #[test]
472    fn it_can_be_created_from_vec_of_u64() {
473        let data = vec![1_u64, 2, 3];
474        let spark_data = Sparkline::default().data(data).data;
475        let expected = vec![
476            SparklineBar::from(1),
477            SparklineBar::from(2),
478            SparklineBar::from(3),
479        ];
480        assert_eq!(spark_data, expected);
481    }
482
483    #[test]
484    fn it_can_be_created_from_vec_of_option_u64() {
485        let data = vec![Some(1_u64), None, Some(3)];
486        let spark_data = Sparkline::default().data(data).data;
487        let expected = vec![
488            SparklineBar::from(1),
489            SparklineBar::from(None),
490            SparklineBar::from(3),
491        ];
492        assert_eq!(spark_data, expected);
493    }
494
495    #[test]
496    fn it_can_be_created_from_array_of_u64() {
497        let data = [1_u64, 2, 3];
498        let spark_data = Sparkline::default().data(data).data;
499        let expected = vec![
500            SparklineBar::from(1),
501            SparklineBar::from(2),
502            SparklineBar::from(3),
503        ];
504        assert_eq!(spark_data, expected);
505    }
506
507    #[test]
508    fn it_can_be_created_from_array_of_option_u64() {
509        let data = [Some(1_u64), None, Some(3)];
510        let spark_data = Sparkline::default().data(data).data;
511        let expected = vec![
512            SparklineBar::from(1),
513            SparklineBar::from(None),
514            SparklineBar::from(3),
515        ];
516        assert_eq!(spark_data, expected);
517    }
518
519    #[test]
520    fn it_can_be_created_from_slice_of_u64() {
521        let data = vec![1_u64, 2, 3];
522        let spark_data = Sparkline::default().data(&data).data;
523        let expected = vec![
524            SparklineBar::from(1),
525            SparklineBar::from(2),
526            SparklineBar::from(3),
527        ];
528        assert_eq!(spark_data, expected);
529    }
530
531    #[test]
532    fn it_can_be_created_from_slice_of_option_u64() {
533        let data = vec![Some(1_u64), None, Some(3)];
534        let spark_data = Sparkline::default().data(&data).data;
535        let expected = vec![
536            SparklineBar::from(1),
537            SparklineBar::from(None),
538            SparklineBar::from(3),
539        ];
540        assert_eq!(spark_data, expected);
541    }
542
543    // Helper function to render a sparkline to a buffer with a given width
544    // filled with x symbols to make it easier to assert on the result
545    fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
546        let area = Rect::new(0, 0, width, 1);
547        let mut buffer = Buffer::filled(area, Cell::new("x"));
548        widget.render(area, &mut buffer);
549        buffer
550    }
551
552    #[test]
553    fn it_does_not_panic_if_max_is_zero() {
554        let widget = Sparkline::default().data([0, 0, 0]);
555        let buffer = render(widget, 6);
556        assert_eq!(buffer, Buffer::with_lines(["   xxx"]));
557    }
558
559    #[test]
560    fn it_does_not_panic_if_max_is_set_to_zero() {
561        // see https://github.com/rust-lang/rust-clippy/issues/13191
562        let widget = Sparkline::default().data([0, 1, 2]).max(0);
563        let buffer = render(widget, 6);
564        assert_eq!(buffer, Buffer::with_lines(["   xxx"]));
565    }
566
567    #[test]
568    fn it_draws() {
569        let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
570        let buffer = render(widget, 12);
571        assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
572    }
573
574    #[test]
575    fn it_draws_double_height() {
576        let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
577        let area = Rect::new(0, 0, 12, 2);
578        let mut buffer = Buffer::filled(area, Cell::new("x"));
579        widget.render(area, &mut buffer);
580        assert_eq!(buffer, Buffer::with_lines(["     ▂▄▆█xxx", " ▂▄▆█████xxx"]));
581    }
582
583    #[test]
584    fn it_renders_left_to_right() {
585        let widget = Sparkline::default()
586            .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
587            .direction(RenderDirection::LeftToRight);
588        let buffer = render(widget, 12);
589        assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
590    }
591
592    #[test]
593    fn it_renders_right_to_left() {
594        let widget = Sparkline::default()
595            .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
596            .direction(RenderDirection::RightToLeft);
597        let buffer = render(widget, 12);
598        assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
599    }
600
601    #[test]
602    fn it_renders_with_absent_value_style() {
603        let widget = Sparkline::default()
604            .absent_value_style(Style::default().fg(Color::Red))
605            .absent_value_symbol(symbols::shade::FULL)
606            .data([
607                None,
608                Some(1),
609                Some(2),
610                Some(3),
611                Some(4),
612                Some(5),
613                Some(6),
614                Some(7),
615                Some(8),
616            ]);
617        let buffer = render(widget, 12);
618        let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
619        expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
620        assert_eq!(buffer, expected);
621    }
622
623    #[test]
624    fn it_renders_with_absent_value_style_double_height() {
625        let widget = Sparkline::default()
626            .absent_value_style(Style::default().fg(Color::Red))
627            .absent_value_symbol(symbols::shade::FULL)
628            .data([
629                None,
630                Some(1),
631                Some(2),
632                Some(3),
633                Some(4),
634                Some(5),
635                Some(6),
636                Some(7),
637                Some(8),
638            ]);
639        let area = Rect::new(0, 0, 12, 2);
640        let mut buffer = Buffer::filled(area, Cell::new("x"));
641        widget.render(area, &mut buffer);
642        let mut expected = Buffer::with_lines(["█    ▂▄▆█xxx", "█▂▄▆█████xxx"]);
643        expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
644        assert_eq!(buffer, expected);
645    }
646
647    #[test]
648    fn it_renders_with_custom_absent_value_style() {
649        let widget = Sparkline::default().absent_value_symbol('*').data([
650            None,
651            Some(1),
652            Some(2),
653            Some(3),
654            Some(4),
655            Some(5),
656            Some(6),
657            Some(7),
658            Some(8),
659        ]);
660        let buffer = render(widget, 12);
661        let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
662        assert_eq!(buffer, expected);
663    }
664
665    #[test]
666    fn it_renders_with_custom_bar_styles() {
667        let widget = Sparkline::default().data(vec![
668            SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
669            SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
670            SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
671            SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
672            SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
673            SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
674            SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
675            SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
676            SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
677        ]);
678        let buffer = render(widget, 12);
679        let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
680        expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
681        expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
682        expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
683        assert_eq!(buffer, expected);
684    }
685
686    #[test]
687    fn can_be_stylized() {
688        assert_eq!(
689            Sparkline::default()
690                .black()
691                .on_white()
692                .bold()
693                .not_dim()
694                .style,
695            Style::default()
696                .fg(Color::Black)
697                .bg(Color::White)
698                .add_modifier(Modifier::BOLD)
699                .remove_modifier(Modifier::DIM)
700        );
701    }
702
703    #[test]
704    fn render_in_minimal_buffer() {
705        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
706        let sparkline = Sparkline::default()
707            .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
708            .max(10);
709        // This should not panic, even if the buffer is too small to render the sparkline.
710        sparkline.render(buffer.area, &mut buffer);
711        assert_eq!(buffer, Buffer::with_lines([" "]));
712    }
713
714    #[test]
715    fn render_in_zero_size_buffer() {
716        let mut buffer = Buffer::empty(Rect::ZERO);
717        let sparkline = Sparkline::default()
718            .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
719            .max(10);
720        // This should not panic, even if the buffer has zero size.
721        sparkline.render(buffer.area, &mut buffer);
722    }
723}