ratatui/widgets/
sparkline.rs

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