Skip to main content

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 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 = Self::scale_height(*value, max_height, spark_area.height);
390                    (height, None, *style)
391                }
392                _ => (
393                    u64::from(spark_area.height) * 8,
394                    Some(self.absent_value_symbol.0.as_str()),
395                    Some(self.absent_value_style),
396                ),
397            };
398
399            // render the item from top to bottom
400            //
401            // if the symbol is set it will be used for the entire height of the bar, otherwise the
402            // symbol will be determined by the _remaining_ height.
403            //
404            // if the style is set it will be used for the entire height of the bar, otherwise the
405            // sparkline style will be used.
406            for j in (0..spark_area.height).rev() {
407                let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
408                if height > 8 {
409                    height -= 8;
410                } else {
411                    height = 0;
412                }
413                buf[(x, spark_area.top() + j)]
414                    .set_symbol(symbol)
415                    .set_style(self.style.patch(style.unwrap_or_default()));
416            }
417        }
418    }
419
420    const fn symbol_for_height(&self, height: u64) -> &str {
421        match height {
422            0 => self.bar_set.empty,
423            1 => self.bar_set.one_eighth,
424            2 => self.bar_set.one_quarter,
425            3 => self.bar_set.three_eighths,
426            4 => self.bar_set.half,
427            5 => self.bar_set.five_eighths,
428            6 => self.bar_set.three_quarters,
429            7 => self.bar_set.seven_eighths,
430            _ => self.bar_set.full,
431        }
432    }
433
434    fn scale_height(value: u64, max: u64, max_height: u16) -> u64 {
435        if max == 0 {
436            return 0;
437        }
438
439        let max_ticks = u128::from(max_height) * 8;
440        let ticks = u128::from(value) * max_ticks / u128::from(max);
441        ticks.min(max_ticks) as u64
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use alloc::vec;
448
449    use ratatui_core::buffer::Cell;
450    use ratatui_core::style::{Color, Modifier, Stylize};
451    use strum::ParseError;
452
453    use super::*;
454
455    #[test]
456    fn render_direction_to_string() {
457        assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
458        assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
459    }
460
461    #[test]
462    fn render_direction_from_str() {
463        assert_eq!(
464            "LeftToRight".parse::<RenderDirection>(),
465            Ok(RenderDirection::LeftToRight)
466        );
467        assert_eq!(
468            "RightToLeft".parse::<RenderDirection>(),
469            Ok(RenderDirection::RightToLeft)
470        );
471        assert_eq!(
472            "".parse::<RenderDirection>(),
473            Err(ParseError::VariantNotFound)
474        );
475    }
476
477    #[test]
478    fn it_can_be_created_from_vec_of_u64() {
479        let data = vec![1_u64, 2, 3];
480        let spark_data = Sparkline::default().data(data).data;
481        let expected = vec![
482            SparklineBar::from(1),
483            SparklineBar::from(2),
484            SparklineBar::from(3),
485        ];
486        assert_eq!(spark_data, expected);
487    }
488
489    #[test]
490    fn it_can_be_created_from_vec_of_option_u64() {
491        let data = vec![Some(1_u64), None, Some(3)];
492        let spark_data = Sparkline::default().data(data).data;
493        let expected = vec![
494            SparklineBar::from(1),
495            SparklineBar::from(None),
496            SparklineBar::from(3),
497        ];
498        assert_eq!(spark_data, expected);
499    }
500
501    #[test]
502    fn it_can_be_created_from_array_of_u64() {
503        let data = [1_u64, 2, 3];
504        let spark_data = Sparkline::default().data(data).data;
505        let expected = vec![
506            SparklineBar::from(1),
507            SparklineBar::from(2),
508            SparklineBar::from(3),
509        ];
510        assert_eq!(spark_data, expected);
511    }
512
513    #[test]
514    fn it_can_be_created_from_array_of_option_u64() {
515        let data = [Some(1_u64), None, Some(3)];
516        let spark_data = Sparkline::default().data(data).data;
517        let expected = vec![
518            SparklineBar::from(1),
519            SparklineBar::from(None),
520            SparklineBar::from(3),
521        ];
522        assert_eq!(spark_data, expected);
523    }
524
525    #[test]
526    fn it_can_be_created_from_slice_of_u64() {
527        let data = vec![1_u64, 2, 3];
528        let spark_data = Sparkline::default().data(&data).data;
529        let expected = vec![
530            SparklineBar::from(1),
531            SparklineBar::from(2),
532            SparklineBar::from(3),
533        ];
534        assert_eq!(spark_data, expected);
535    }
536
537    #[test]
538    fn it_can_be_created_from_slice_of_option_u64() {
539        let data = vec![Some(1_u64), None, Some(3)];
540        let spark_data = Sparkline::default().data(&data).data;
541        let expected = vec![
542            SparklineBar::from(1),
543            SparklineBar::from(None),
544            SparklineBar::from(3),
545        ];
546        assert_eq!(spark_data, expected);
547    }
548
549    // Helper function to render a sparkline to a buffer with a given width
550    // filled with x symbols to make it easier to assert on the result
551    fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
552        let area = Rect::new(0, 0, width, 1);
553        let mut buffer = Buffer::filled(area, Cell::new("x"));
554        widget.render(area, &mut buffer);
555        buffer
556    }
557
558    #[test]
559    fn it_does_not_panic_if_max_is_zero() {
560        let widget = Sparkline::default().data([0, 0, 0]);
561        let buffer = render(widget, 6);
562        assert_eq!(buffer, Buffer::with_lines(["   xxx"]));
563    }
564
565    #[test]
566    fn it_does_not_panic_if_max_is_set_to_zero() {
567        // see https://github.com/rust-lang/rust-clippy/issues/13191
568        let widget = Sparkline::default().data([0, 1, 2]).max(0);
569        let buffer = render(widget, 6);
570        assert_eq!(buffer, Buffer::with_lines(["   xxx"]));
571    }
572
573    #[test]
574    fn it_draws() {
575        let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
576        let buffer = render(widget, 12);
577        assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
578    }
579
580    #[test]
581    fn it_draws_double_height() {
582        let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
583        let area = Rect::new(0, 0, 12, 2);
584        let mut buffer = Buffer::filled(area, Cell::new("x"));
585        widget.render(area, &mut buffer);
586        assert_eq!(buffer, Buffer::with_lines(["     ▂▄▆█xxx", " ▂▄▆█████xxx"]));
587    }
588
589    #[test]
590    fn render_handles_u64_max_value() {
591        let widget = Sparkline::default().data([u64::MAX]).max(u64::MAX);
592        let area = Rect::new(0, 0, 1, 3);
593        let mut buffer = Buffer::empty(area);
594
595        widget.render(area, &mut buffer);
596
597        assert_eq!(buffer, Buffer::with_lines(["█"; 3]));
598    }
599
600    #[test]
601    fn render_keeps_integer_precision_for_large_values() {
602        let widget = Sparkline::default().data([u64::MAX - 1]).max(u64::MAX);
603        let area = Rect::new(0, 0, 1, 1);
604        let mut buffer = Buffer::empty(area);
605
606        widget.render(area, &mut buffer);
607
608        assert_eq!(buffer, Buffer::with_lines(["▇"]));
609    }
610
611    #[test]
612    fn it_renders_left_to_right() {
613        let widget = Sparkline::default()
614            .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
615            .direction(RenderDirection::LeftToRight);
616        let buffer = render(widget, 12);
617        assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
618    }
619
620    #[test]
621    fn it_renders_right_to_left() {
622        let widget = Sparkline::default()
623            .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
624            .direction(RenderDirection::RightToLeft);
625        let buffer = render(widget, 12);
626        assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
627    }
628
629    #[test]
630    fn it_renders_with_absent_value_style() {
631        let widget = Sparkline::default()
632            .absent_value_style(Style::default().fg(Color::Red))
633            .absent_value_symbol(symbols::shade::FULL)
634            .data([
635                None,
636                Some(1),
637                Some(2),
638                Some(3),
639                Some(4),
640                Some(5),
641                Some(6),
642                Some(7),
643                Some(8),
644            ]);
645        let buffer = render(widget, 12);
646        let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
647        expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
648        assert_eq!(buffer, expected);
649    }
650
651    #[test]
652    fn it_renders_with_absent_value_style_double_height() {
653        let widget = Sparkline::default()
654            .absent_value_style(Style::default().fg(Color::Red))
655            .absent_value_symbol(symbols::shade::FULL)
656            .data([
657                None,
658                Some(1),
659                Some(2),
660                Some(3),
661                Some(4),
662                Some(5),
663                Some(6),
664                Some(7),
665                Some(8),
666            ]);
667        let area = Rect::new(0, 0, 12, 2);
668        let mut buffer = Buffer::filled(area, Cell::new("x"));
669        widget.render(area, &mut buffer);
670        let mut expected = Buffer::with_lines(["█    ▂▄▆█xxx", "█▂▄▆█████xxx"]);
671        expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
672        assert_eq!(buffer, expected);
673    }
674
675    #[test]
676    fn it_renders_with_custom_absent_value_style() {
677        let widget = Sparkline::default().absent_value_symbol('*').data([
678            None,
679            Some(1),
680            Some(2),
681            Some(3),
682            Some(4),
683            Some(5),
684            Some(6),
685            Some(7),
686            Some(8),
687        ]);
688        let buffer = render(widget, 12);
689        let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
690        assert_eq!(buffer, expected);
691    }
692
693    #[test]
694    fn it_renders_with_custom_bar_styles() {
695        let widget = Sparkline::default().data(vec![
696            SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
697            SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
698            SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
699            SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
700            SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
701            SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
702            SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
703            SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
704            SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
705        ]);
706        let buffer = render(widget, 12);
707        let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
708        expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
709        expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
710        expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
711        assert_eq!(buffer, expected);
712    }
713
714    #[test]
715    fn can_be_stylized() {
716        assert_eq!(
717            Sparkline::default()
718                .black()
719                .on_white()
720                .bold()
721                .not_dim()
722                .style,
723            Style::default()
724                .fg(Color::Black)
725                .bg(Color::White)
726                .add_modifier(Modifier::BOLD)
727                .remove_modifier(Modifier::DIM)
728        );
729    }
730
731    #[test]
732    fn render_in_minimal_buffer() {
733        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
734        let sparkline = Sparkline::default()
735            .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
736            .max(10);
737        // This should not panic, even if the buffer is too small to render the sparkline.
738        sparkline.render(buffer.area, &mut buffer);
739        assert_eq!(buffer, Buffer::with_lines([" "]));
740    }
741
742    #[test]
743    fn render_in_zero_size_buffer() {
744        let mut buffer = Buffer::empty(Rect::ZERO);
745        let sparkline = Sparkline::default()
746            .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
747            .max(10);
748        // This should not panic, even if the buffer has zero size.
749        sparkline.render(buffer.area, &mut buffer);
750    }
751}