Skip to main content

ratatui_widgets/
chart.rs

1//! The [`Chart`] widget is used to plot one or more [`Dataset`] in a cartesian coordinate system.
2use alloc::vec::Vec;
3use core::cmp::max;
4use core::ops::Not;
5
6use ratatui_core::buffer::Buffer;
7use ratatui_core::layout::{Alignment, Constraint, Flex, Layout, Position, Rect};
8use ratatui_core::style::{Color, Style, Styled};
9use ratatui_core::symbols;
10use ratatui_core::text::Line;
11use ratatui_core::widgets::Widget;
12use strum::{Display, EnumString};
13
14use crate::block::{Block, BlockExt};
15use crate::canvas::{Canvas, FilledLine, Line as CanvasLine, Points};
16
17/// An X or Y axis for the [`Chart`] widget
18///
19/// An axis can have a [title](Axis::title) which will be displayed at the end of the axis. For an
20/// X axis this is the right, for a Y axis, this is the top.
21///
22/// You can also set the bounds and labels on this axis using respectively [`Axis::bounds`] and
23/// [`Axis::labels`].
24///
25/// See [`Chart::x_axis`] and [`Chart::y_axis`] to set an axis on a chart.
26///
27/// # Example
28///
29/// ```rust
30/// use ratatui::style::{Style, Stylize};
31/// use ratatui::widgets::Axis;
32///
33/// let axis = Axis::default()
34///     .title("X Axis")
35///     .style(Style::default().gray())
36///     .bounds([0.0, 50.0])
37///     .labels(["0".bold(), "25".into(), "50".bold()]);
38/// ```
39#[derive(Debug, Default, Clone, PartialEq)]
40pub struct Axis<'a> {
41    /// Title displayed next to axis end
42    title: Option<Line<'a>>,
43    /// Bounds for the axis (all data points outside these limits will not be represented)
44    bounds: [f64; 2],
45    /// A list of labels to put to the left or below the axis
46    labels: Vec<Line<'a>>,
47    /// The style used to draw the axis itself
48    style: Style,
49    /// The alignment of the labels of the Axis
50    labels_alignment: Alignment,
51}
52
53impl<'a> Axis<'a> {
54    /// Sets the axis title
55    ///
56    /// It will be displayed at the end of the axis. For an X axis this is the right, for a Y axis,
57    /// this is the top.
58    ///
59    /// This is a fluent setter method which must be chained or used as it consumes self
60    #[must_use = "method moves the value of self and returns the modified value"]
61    pub fn title<T>(mut self, title: T) -> Self
62    where
63        T: Into<Line<'a>>,
64    {
65        self.title = Some(title.into());
66        self
67    }
68
69    /// Sets the bounds of this axis
70    ///
71    /// In other words, sets the min and max value on this axis.
72    ///
73    /// This is a fluent setter method which must be chained or used as it consumes self
74    #[must_use = "method moves the value of self and returns the modified value"]
75    pub const fn bounds(mut self, bounds: [f64; 2]) -> Self {
76        self.bounds = bounds;
77        self
78    }
79
80    /// Sets the axis labels
81    ///
82    /// - For the X axis, the labels are displayed left to right.
83    /// - For the Y axis, the labels are displayed bottom to top.
84    ///
85    /// Currently, you need to give at least two labels for them to be rendered. Also, giving more
86    /// than 3 labels is currently broken and the middle labels won't be in the correct position,
87    /// see [issue 334].
88    ///
89    /// [issue 334]: https://github.com/ratatui/ratatui/issues/334
90    ///
91    /// `labels` is a vector of any type that can be converted into a [`Line`] (e.g. `&str`,
92    /// `String`, `&Line`, `Span`, ...). This allows you to style the labels using the methods
93    /// provided by [`Line`]. Any alignment set on the labels will be ignored as the alignment is
94    /// determined by the axis.
95    ///
96    /// This is a fluent setter method which must be chained or used as it consumes self
97    ///
98    /// # Examples
99    ///
100    /// ```rust
101    /// use ratatui::style::Stylize;
102    /// use ratatui::widgets::Axis;
103    ///
104    /// let axis = Axis::default()
105    ///     .bounds([0.0, 50.0])
106    ///     .labels(["0".bold(), "25".into(), "50".bold()]);
107    /// ```
108    #[must_use = "method moves the value of self and returns the modified value"]
109    pub fn labels<Labels>(mut self, labels: Labels) -> Self
110    where
111        Labels: IntoIterator,
112        Labels::Item: Into<Line<'a>>,
113    {
114        self.labels = labels.into_iter().map(Into::into).collect();
115        self
116    }
117
118    /// Sets the axis style
119    ///
120    /// This is a fluent setter method which must be chained or used as it consumes self
121    ///
122    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
123    /// your own type that implements [`Into<Style>`]).
124    ///
125    /// # Example
126    ///
127    /// [`Axis`] also implements [`Stylize`](ratatui_core::style::Stylize) which mean you can style
128    /// it like so
129    ///
130    /// ```rust
131    /// use ratatui::style::Stylize;
132    /// use ratatui::widgets::Axis;
133    ///
134    /// let axis = Axis::default().red();
135    /// ```
136    #[must_use = "method moves the value of self and returns the modified value"]
137    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
138        self.style = style.into();
139        self
140    }
141
142    /// Sets the labels alignment of the axis
143    ///
144    /// The alignment behaves differently based on the axis:
145    /// - Y axis: The labels are aligned within the area on the left of the axis
146    /// - X axis: The first X-axis label is aligned relative to the Y-axis
147    ///
148    /// On the X axis, this parameter only affects the first label.
149    #[must_use = "method moves the value of self and returns the modified value"]
150    pub const fn labels_alignment(mut self, alignment: Alignment) -> Self {
151        self.labels_alignment = alignment;
152        self
153    }
154}
155
156/// Used to determine which style of graphing to use
157#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
158pub enum GraphType {
159    /// Draw each point. This is the default.
160    #[default]
161    Scatter,
162
163    /// Draw a line between each following point.
164    ///
165    /// The order of the lines will be the same as the order of the points in the dataset, which
166    /// allows this widget to draw lines both left-to-right and right-to-left
167    Line,
168
169    /// Draw a bar chart. This will draw a bar for each point in the dataset.
170    Bar,
171
172    /// Draw a line chart with the area filled. Like [`Line`](GraphType::Line), this draws a line
173    /// between each following point, but also fills the area between the line and the y-coordinate
174    /// specified by [`Dataset::fill_to_y`].
175    Area,
176}
177
178/// Allow users to specify the position of a legend in a [`Chart`]
179///
180/// See [`Chart::legend_position`]
181#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
182pub enum LegendPosition {
183    /// Legend is centered on top
184    Top,
185    /// Legend is in the top-right corner. This is the **default**.
186    #[default]
187    TopRight,
188    /// Legend is in the top-left corner
189    TopLeft,
190    /// Legend is centered on the left
191    Left,
192    /// Legend is centered on the right
193    Right,
194    /// Legend is centered on the bottom
195    Bottom,
196    /// Legend is in the bottom-right corner
197    BottomRight,
198    /// Legend is in the bottom-left corner
199    BottomLeft,
200}
201
202impl LegendPosition {
203    fn layout(
204        self,
205        area: Rect,
206        legend_width: u16,
207        legend_height: u16,
208        x_title_width: u16,
209        y_title_width: u16,
210    ) -> Option<Rect> {
211        let mut height_margin = i32::from(area.height - legend_height);
212        if x_title_width != 0 {
213            height_margin -= 1;
214        }
215        if y_title_width != 0 {
216            height_margin -= 1;
217        }
218        if height_margin < 0 {
219            return None;
220        }
221
222        let (x, y) = match self {
223            Self::TopRight => {
224                if legend_width + y_title_width > area.width {
225                    (area.right() - legend_width, area.top() + 1)
226                } else {
227                    (area.right() - legend_width, area.top())
228                }
229            }
230            Self::TopLeft => {
231                if y_title_width != 0 {
232                    (area.left(), area.top() + 1)
233                } else {
234                    (area.left(), area.top())
235                }
236            }
237            Self::Top => {
238                let x = (area.width - legend_width) / 2;
239                if area.left() + y_title_width > x {
240                    (area.left() + x, area.top() + 1)
241                } else {
242                    (area.left() + x, area.top())
243                }
244            }
245            Self::Left => {
246                let mut y = (area.height - legend_height) / 2;
247                if y_title_width != 0 {
248                    y += 1;
249                }
250                if x_title_width != 0 {
251                    y = y.saturating_sub(1);
252                }
253                (area.left(), area.top() + y)
254            }
255            Self::Right => {
256                let mut y = (area.height - legend_height) / 2;
257                if y_title_width != 0 {
258                    y += 1;
259                }
260                if x_title_width != 0 {
261                    y = y.saturating_sub(1);
262                }
263                (area.right() - legend_width, area.top() + y)
264            }
265            Self::BottomLeft => {
266                if x_title_width + legend_width > area.width {
267                    (area.left(), area.bottom() - legend_height - 1)
268                } else {
269                    (area.left(), area.bottom() - legend_height)
270                }
271            }
272            Self::BottomRight => {
273                if x_title_width != 0 {
274                    (
275                        area.right() - legend_width,
276                        area.bottom() - legend_height - 1,
277                    )
278                } else {
279                    (area.right() - legend_width, area.bottom() - legend_height)
280                }
281            }
282            Self::Bottom => {
283                let x = area.left() + (area.width - legend_width) / 2;
284                if x + legend_width > area.right() - x_title_width {
285                    (x, area.bottom() - legend_height - 1)
286                } else {
287                    (x, area.bottom() - legend_height)
288                }
289            }
290        };
291
292        Some(Rect::new(x, y, legend_width, legend_height))
293    }
294}
295
296/// A group of data points
297///
298/// This is the main element composing a [`Chart`].
299///
300/// A dataset can be [named](Dataset::name). Only named datasets will be rendered in the legend.
301///
302/// After that, you can pass it data with [`Dataset::data`]. Data is an array of `f64` tuples
303/// (`(f64, f64)`), the first element being X and the second Y. It's also worth noting that, unlike
304/// the [`Rect`], here the Y axis is bottom to top, as in math.
305///
306/// You can also customize the rendering by using [`Dataset::marker`] and [`Dataset::graph_type`].
307///
308/// # Example
309///
310/// This example draws a red line between two points.
311///
312/// ```rust
313/// use ratatui::style::Stylize;
314/// use ratatui::symbols::Marker;
315/// use ratatui::widgets::{Dataset, GraphType};
316///
317/// let dataset = Dataset::default()
318///     .name("dataset 1")
319///     .data(&[(1., 1.), (5., 5.)])
320///     .marker(Marker::Braille)
321///     .graph_type(GraphType::Line)
322///     .red();
323/// ```
324#[derive(Debug, Default, Clone, PartialEq)]
325pub struct Dataset<'a> {
326    /// Name of the dataset (used in the legend if shown)
327    name: Option<Line<'a>>,
328    /// A reference to the actual data
329    data: &'a [(f64, f64)],
330    /// Symbol used for each points of this dataset
331    marker: symbols::Marker,
332    /// Determines graph type used for drawing points
333    graph_type: GraphType,
334    /// Style used to plot this dataset
335    style: Style,
336    /// The y-coordinate to fill area to when using [`GraphType::Area`]
337    fill_to_y: f64,
338}
339
340impl<'a> Dataset<'a> {
341    /// Sets the name of the dataset
342    ///
343    /// The dataset's name is used when displaying the chart legend. Datasets don't require a name
344    /// and can be created without specifying one. Once assigned, a name can't be removed, only
345    /// changed
346    ///
347    /// The name can be styled (see [`Line`] for that), but the dataset's style will always have
348    /// precedence.
349    ///
350    /// This is a fluent setter method which must be chained or used as it consumes self
351    #[must_use = "method moves the value of self and returns the modified value"]
352    pub fn name<S>(mut self, name: S) -> Self
353    where
354        S: Into<Line<'a>>,
355    {
356        self.name = Some(name.into());
357        self
358    }
359
360    /// Sets the data points of this dataset
361    ///
362    /// Points will then either be rendered as scattered points or with lines between them
363    /// depending on [`Dataset::graph_type`].
364    ///
365    /// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the
366    /// second Y. It's also worth noting that, unlike the [`Rect`], here the Y axis is bottom to
367    /// top, as in math.
368    ///
369    /// This is a fluent setter method which must be chained or used as it consumes self
370    #[must_use = "method moves the value of self and returns the modified value"]
371    pub const fn data(mut self, data: &'a [(f64, f64)]) -> Self {
372        self.data = data;
373        self
374    }
375
376    /// Sets the kind of character to use to display this dataset
377    ///
378    /// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`), half-blocks
379    /// (`█`, `▄`, and `▀`) or if you need custom chars use
380    /// [`Marker::Custom`](symbols::Marker::Custom). See [`symbols::Marker`] for more details.
381    ///
382    /// Note [`Marker::Braille`](symbols::Marker::Braille) requires a font that supports Unicode
383    /// Braille Patterns.
384    ///
385    /// This is a fluent setter method which must be chained or used as it consumes self
386    #[must_use = "method moves the value of self and returns the modified value"]
387    pub const fn marker(mut self, marker: symbols::Marker) -> Self {
388        self.marker = marker;
389        self
390    }
391
392    /// Sets how the dataset should be drawn
393    ///
394    /// [`Chart`] can draw [scatter](GraphType::Scatter), [line](GraphType::Line) or
395    /// [bar](GraphType::Bar) charts. A scatter chart draws only the points in the dataset, a line
396    /// char draws a line between each point, and a bar chart draws a line from the x axis to the
397    /// point.  See [`GraphType`] for more details
398    ///
399    /// This is a fluent setter method which must be chained or used as it consumes self
400    #[must_use = "method moves the value of self and returns the modified value"]
401    pub const fn graph_type(mut self, graph_type: GraphType) -> Self {
402        self.graph_type = graph_type;
403        self
404    }
405
406    /// Sets the style of this dataset
407    ///
408    /// The given style will be used to draw the legend and the data points. Currently the legend
409    /// will use the entire style whereas the data points will only use the foreground.
410    ///
411    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
412    /// your own type that implements [`Into<Style>`]).
413    ///
414    /// This is a fluent setter method which must be chained or used as it consumes self
415    ///
416    /// # Example
417    ///
418    /// [`Dataset`] also implements [`Stylize`](ratatui_core::style::Stylize) which mean you can
419    /// style it like so
420    ///
421    /// ```rust
422    /// use ratatui::style::Stylize;
423    /// use ratatui::widgets::Dataset;
424    ///
425    /// let dataset = Dataset::default().red();
426    /// ```
427    #[must_use = "method moves the value of self and returns the modified value"]
428    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
429        self.style = style.into();
430        self
431    }
432
433    /// Sets the y-coordinate to fill the area to when using [`GraphType::Area`]
434    ///
435    /// When the graph type is set to [`GraphType::Area`], the area between the data points and the
436    /// specified y-coordinate will be filled with the dataset's style. The default is `0.0`.
437    ///
438    /// This is a fluent setter method which must be chained or used as it consumes self
439    #[must_use = "method moves the value of self and returns the modified value"]
440    pub const fn fill_to_y(mut self, fill_to_y: f64) -> Self {
441        self.fill_to_y = fill_to_y;
442        self
443    }
444}
445
446/// A container that holds all the infos about where to display each elements of the chart (axis,
447/// labels, legend, ...).
448struct ChartLayout {
449    /// Location of the title of the x axis
450    title_x: Option<Position>,
451    /// Location of the title of the y axis
452    title_y: Option<Position>,
453    /// Location of the first label of the x axis
454    label_x: Option<u16>,
455    /// Location of the first label of the y axis
456    label_y: Option<u16>,
457    /// Y coordinate of the horizontal axis
458    axis_x: Option<u16>,
459    /// X coordinate of the vertical axis
460    axis_y: Option<u16>,
461    /// Area of the legend
462    legend_area: Option<Rect>,
463    /// Area of the graph
464    graph_area: Rect,
465}
466
467/// A widget to plot one or more [`Dataset`] in a cartesian coordinate system
468///
469/// To use this widget, start by creating one or more [`Dataset`]. With it, you can set the
470/// [data points](Dataset::data), the [name](Dataset::name) or the
471/// [chart type](Dataset::graph_type). See [`Dataset`] for a complete documentation of what is
472/// possible.
473///
474/// Then, you'll usually want to configure the [`Axis`]. Axis [titles](Axis::title),
475/// [bounds](Axis::bounds) and [labels](Axis::labels) can be configured on both axis. See [`Axis`]
476/// for a complete documentation of what is possible.
477///
478/// Finally, you can pass all of that to the `Chart` via [`Chart::new`], [`Chart::x_axis`] and
479/// [`Chart::y_axis`].
480///
481/// Additionally, `Chart` allows configuring the legend [position](Chart::legend_position) and
482/// [hiding constraints](Chart::hidden_legend_constraints).
483///
484/// # Examples
485///
486/// ```
487/// use ratatui::style::{Style, Stylize};
488/// use ratatui::symbols;
489/// use ratatui::widgets::{Axis, Block, Chart, Dataset, GraphType};
490///
491/// // Create the datasets to fill the chart with
492/// let datasets = vec![
493///     // Scatter chart
494///     Dataset::default()
495///         .name("data1")
496///         .marker(symbols::Marker::Dot)
497///         .graph_type(GraphType::Scatter)
498///         .style(Style::default().cyan())
499///         .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
500///     // Line chart
501///     Dataset::default()
502///         .name("data2")
503///         .marker(symbols::Marker::Braille)
504///         .graph_type(GraphType::Line)
505///         .style(Style::default().magenta())
506///         .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
507/// ];
508///
509/// // Create the X axis and define its properties
510/// let x_axis = Axis::default()
511///     .title("X Axis".red())
512///     .style(Style::default().white())
513///     .bounds([0.0, 10.0])
514///     .labels(["0.0", "5.0", "10.0"]);
515///
516/// // Create the Y axis and define its properties
517/// let y_axis = Axis::default()
518///     .title("Y Axis".red())
519///     .style(Style::default().white())
520///     .bounds([0.0, 10.0])
521///     .labels(["0.0", "5.0", "10.0"]);
522///
523/// // Create the chart and link all the parts together
524/// let chart = Chart::new(datasets)
525///     .block(Block::new().title("Chart"))
526///     .x_axis(x_axis)
527///     .y_axis(y_axis);
528/// ```
529#[derive(Debug, Default, Clone, PartialEq)]
530pub struct Chart<'a> {
531    /// A block to display around the widget eventually
532    block: Option<Block<'a>>,
533    /// The horizontal axis
534    x_axis: Axis<'a>,
535    /// The vertical axis
536    y_axis: Axis<'a>,
537    /// A reference to the datasets
538    datasets: Vec<Dataset<'a>>,
539    /// The widget base style
540    style: Style,
541    /// Constraints used to determine whether the legend should be shown or not
542    hidden_legend_constraints: (Constraint, Constraint),
543    /// The position determine where the length is shown or hide regardless of
544    /// `hidden_legend_constraints`
545    legend_position: Option<LegendPosition>,
546}
547
548impl<'a> Chart<'a> {
549    /// Creates a chart with the given [datasets](Dataset)
550    ///
551    /// A chart can render multiple datasets.
552    ///
553    /// # Example
554    ///
555    /// This creates a simple chart with one [`Dataset`]
556    ///
557    /// ```rust
558    /// use ratatui::widgets::{Chart, Dataset};
559    ///
560    /// let data_points = vec![];
561    /// let chart = Chart::new(vec![Dataset::default().data(&data_points)]);
562    /// ```
563    ///
564    /// This creates a chart with multiple [`Dataset`]s
565    ///
566    /// ```rust
567    /// use ratatui::widgets::{Chart, Dataset};
568    ///
569    /// let data_points = vec![];
570    /// let data_points2 = vec![];
571    /// let chart = Chart::new(vec![
572    ///     Dataset::default().data(&data_points),
573    ///     Dataset::default().data(&data_points2),
574    /// ]);
575    /// ```
576    pub fn new(datasets: Vec<Dataset<'a>>) -> Self {
577        Self {
578            block: None,
579            x_axis: Axis::default(),
580            y_axis: Axis::default(),
581            style: Style::default(),
582            datasets,
583            hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
584            legend_position: Some(LegendPosition::default()),
585        }
586    }
587
588    /// Wraps the chart with the given [`Block`]
589    ///
590    /// This is a fluent setter method which must be chained or used as it consumes self
591    #[must_use = "method moves the value of self and returns the modified value"]
592    pub fn block(mut self, block: Block<'a>) -> Self {
593        self.block = Some(block);
594        self
595    }
596
597    /// Sets the style of the entire chart
598    ///
599    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
600    /// your own type that implements [`Into<Style>`]).
601    ///
602    /// Styles of [`Axis`] and [`Dataset`] will have priority over this style.
603    ///
604    /// This is a fluent setter method which must be chained or used as it consumes self
605    #[must_use = "method moves the value of self and returns the modified value"]
606    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
607        self.style = style.into();
608        self
609    }
610
611    /// Sets the X [`Axis`]
612    ///
613    /// The default is an empty [`Axis`], i.e. only a line.
614    ///
615    /// This is a fluent setter method which must be chained or used as it consumes self
616    ///
617    /// # Example
618    ///
619    /// ```rust
620    /// use ratatui::widgets::{Axis, Chart};
621    ///
622    /// let chart = Chart::new(vec![]).x_axis(
623    ///     Axis::default()
624    ///         .title("X Axis")
625    ///         .bounds([0.0, 20.0])
626    ///         .labels(["0", "20"]),
627    /// );
628    /// ```
629    #[must_use = "method moves the value of self and returns the modified value"]
630    pub fn x_axis(mut self, axis: Axis<'a>) -> Self {
631        self.x_axis = axis;
632        self
633    }
634
635    /// Sets the Y [`Axis`]
636    ///
637    /// The default is an empty [`Axis`], i.e. only a line.
638    ///
639    /// This is a fluent setter method which must be chained or used as it consumes self
640    ///
641    /// # Example
642    ///
643    /// ```rust
644    /// use ratatui::widgets::{Axis, Chart};
645    ///
646    /// let chart = Chart::new(vec![]).y_axis(
647    ///     Axis::default()
648    ///         .title("Y Axis")
649    ///         .bounds([0.0, 20.0])
650    ///         .labels(["0", "20"]),
651    /// );
652    /// ```
653    #[must_use = "method moves the value of self and returns the modified value"]
654    pub fn y_axis(mut self, axis: Axis<'a>) -> Self {
655        self.y_axis = axis;
656        self
657    }
658
659    /// Sets the constraints used to determine whether the legend should be shown or not.
660    ///
661    /// The tuple's first constraint is used for the width and the second for the height. If the
662    /// legend takes more space than what is allowed by any constraint, the legend is hidden.
663    /// [`Constraint::Min`] is an exception and will always show the legend.
664    ///
665    /// If this is not set, the default behavior is to hide the legend if it is greater than 25% of
666    /// the chart, either horizontally or vertically.
667    ///
668    /// This is a fluent setter method which must be chained or used as it consumes self
669    ///
670    /// # Examples
671    ///
672    /// Hide the legend when either its width is greater than 33% of the total widget width or if
673    /// its height is greater than 25% of the total widget height.
674    ///
675    /// ```
676    /// use ratatui::layout::Constraint;
677    /// use ratatui::widgets::Chart;
678    ///
679    /// let constraints = (Constraint::Ratio(1, 3), Constraint::Ratio(1, 4));
680    /// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
681    /// ```
682    ///
683    /// Always show the legend, note the second constraint doesn't matter in this case since the
684    /// first one is always true.
685    ///
686    /// ```
687    /// use ratatui::layout::Constraint;
688    /// use ratatui::widgets::Chart;
689    ///
690    /// let constraints = (Constraint::Min(0), Constraint::Ratio(1, 4));
691    /// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
692    /// ```
693    ///
694    /// Always hide the legend. Note this can be accomplished more explicitly by passing `None` to
695    /// [`Chart::legend_position`].
696    ///
697    /// ```
698    /// use ratatui::layout::Constraint;
699    /// use ratatui::widgets::Chart;
700    ///
701    /// let constraints = (Constraint::Length(0), Constraint::Ratio(1, 4));
702    /// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
703    /// ```
704    #[must_use = "method moves the value of self and returns the modified value"]
705    pub const fn hidden_legend_constraints(
706        mut self,
707        constraints: (Constraint, Constraint),
708    ) -> Self {
709        self.hidden_legend_constraints = constraints;
710        self
711    }
712
713    /// Sets the position of a legend or hide it
714    ///
715    /// The default is [`LegendPosition::TopRight`].
716    ///
717    /// If [`None`] is given, hide the legend even if [`hidden_legend_constraints`] determines it
718    /// should be shown. In contrast, if `Some(...)` is given, [`hidden_legend_constraints`] might
719    /// still decide whether to show the legend or not.
720    ///
721    /// See [`LegendPosition`] for all available positions.
722    ///
723    /// [`hidden_legend_constraints`]: Self::hidden_legend_constraints
724    ///
725    /// This is a fluent setter method which must be chained or used as it consumes self
726    ///
727    /// # Examples
728    ///
729    /// Show the legend on the top left corner.
730    ///
731    /// ```
732    /// use ratatui::widgets::{Chart, LegendPosition};
733    ///
734    /// let chart: Chart = Chart::new(vec![]).legend_position(Some(LegendPosition::TopLeft));
735    /// ```
736    ///
737    /// Hide the legend altogether
738    ///
739    /// ```
740    /// use ratatui::widgets::{Chart, LegendPosition};
741    ///
742    /// let chart = Chart::new(vec![]).legend_position(None);
743    /// ```
744    #[must_use = "method moves the value of self and returns the modified value"]
745    pub const fn legend_position(mut self, position: Option<LegendPosition>) -> Self {
746        self.legend_position = position;
747        self
748    }
749
750    /// Compute the internal layout of the chart given the area. If the area is too small some
751    /// elements may be automatically hidden
752    fn layout(&self, area: Rect) -> Option<ChartLayout> {
753        if area.height == 0 || area.width == 0 {
754            return None;
755        }
756        let mut x = area.left();
757        let mut y = area.bottom() - 1;
758
759        let mut label_x = None;
760        if !self.x_axis.labels.is_empty() && y > area.top() {
761            label_x = Some(y);
762            y -= 1;
763        }
764
765        let label_y = self.y_axis.labels.is_empty().not().then_some(x);
766        x += self.max_width_of_labels_left_of_y_axis(area, !self.y_axis.labels.is_empty());
767
768        let mut axis_x = None;
769        if !self.x_axis.labels.is_empty() && y > area.top() {
770            axis_x = Some(y);
771            y -= 1;
772        }
773
774        let mut axis_y = None;
775        if !self.y_axis.labels.is_empty() && x + 1 < area.right() {
776            axis_y = Some(x);
777            x += 1;
778        }
779
780        let graph_width = area.right().saturating_sub(x);
781        let graph_height = y.saturating_sub(area.top()).saturating_add(1);
782        debug_assert_ne!(
783            graph_width, 0,
784            "Axis and labels should have been hidden due to the small area"
785        );
786        debug_assert_ne!(
787            graph_height, 0,
788            "Axis and labels should have been hidden due to the small area"
789        );
790        let graph_area = Rect::new(x, area.top(), graph_width, graph_height);
791
792        let mut title_x = None;
793        if let Some(ref title) = self.x_axis.title {
794            let w = title.width() as u16;
795            if w < graph_area.width && graph_area.height > 2 {
796                title_x = Some(Position::new(x + graph_area.width - w, y));
797            }
798        }
799
800        let mut title_y = None;
801        if let Some(ref title) = self.y_axis.title {
802            let w = title.width() as u16;
803            if w + 1 < graph_area.width && graph_area.height > 2 {
804                title_y = Some(Position::new(x, area.top()));
805            }
806        }
807
808        let mut legend_area = None;
809        if let Some(legend_position) = self.legend_position {
810            let legends = self
811                .datasets
812                .iter()
813                .filter_map(|d| Some(d.name.as_ref()?.width() as u16));
814
815            if let Some(inner_width) = legends.clone().max() {
816                let legend_width = inner_width + 2;
817                let legend_height = legends.count() as u16 + 2;
818
819                let [max_legend_width] = Layout::horizontal([self.hidden_legend_constraints.0])
820                    .flex(Flex::Start)
821                    .areas(graph_area);
822
823                let [max_legend_height] = Layout::vertical([self.hidden_legend_constraints.1])
824                    .flex(Flex::Start)
825                    .areas(graph_area);
826
827                if inner_width > 0
828                    && legend_width <= max_legend_width.width
829                    && legend_height <= max_legend_height.height
830                {
831                    legend_area = legend_position.layout(
832                        graph_area,
833                        legend_width,
834                        legend_height,
835                        title_x
836                            .and(self.x_axis.title.as_ref())
837                            .map(|t| t.width() as u16)
838                            .unwrap_or_default(),
839                        title_y
840                            .and(self.y_axis.title.as_ref())
841                            .map(|t| t.width() as u16)
842                            .unwrap_or_default(),
843                    );
844                }
845            }
846        }
847        Some(ChartLayout {
848            title_x,
849            title_y,
850            label_x,
851            label_y,
852            axis_x,
853            axis_y,
854            legend_area,
855            graph_area,
856        })
857    }
858
859    fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
860        let mut max_width = self
861            .y_axis
862            .labels
863            .iter()
864            .map(Line::width)
865            .max()
866            .unwrap_or_default() as u16;
867
868        if let Some(first_x_label) = self.x_axis.labels.first() {
869            let first_label_width = first_x_label.width() as u16;
870            let width_left_of_y_axis = match self.x_axis.labels_alignment {
871                Alignment::Left => {
872                    // The last character of the label should be below the Y-Axis when it exists,
873                    // not on its left
874                    let y_axis_offset = u16::from(has_y_axis);
875                    first_label_width.saturating_sub(y_axis_offset)
876                }
877                Alignment::Center => first_label_width / 2,
878                Alignment::Right => 0,
879            };
880            max_width = max(max_width, width_left_of_y_axis);
881        }
882        // labels of y axis and first label of x axis can take at most 1/3rd of the total width
883        max_width.min(area.width / 3)
884    }
885
886    fn render_x_labels(
887        &self,
888        buf: &mut Buffer,
889        layout: &ChartLayout,
890        chart_area: Rect,
891        graph_area: Rect,
892    ) {
893        let Some(y) = layout.label_x else { return };
894        let labels = &self.x_axis.labels;
895        let labels_len = labels.len() as u16;
896        if labels_len < 2 {
897            return;
898        }
899
900        let width_between_ticks = graph_area.width / labels_len;
901
902        let label_area = self.first_x_label_area(
903            y,
904            labels.first().unwrap().width() as u16,
905            width_between_ticks,
906            chart_area,
907            graph_area,
908        );
909
910        let label_alignment = match self.x_axis.labels_alignment {
911            Alignment::Left => Alignment::Right,
912            Alignment::Center => Alignment::Center,
913            Alignment::Right => Alignment::Left,
914        };
915
916        Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
917
918        for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
919            // We add 1 to x (and width-1 below) to leave at least one space before each
920            // intermediate labels
921            let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
922            let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
923
924            Self::render_label(buf, label, label_area, Alignment::Center);
925        }
926
927        let x = graph_area.right() - width_between_ticks;
928        let label_area = Rect::new(x, y, width_between_ticks, 1);
929        // The last label should be aligned Right to be at the edge of the graph area
930        Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
931    }
932
933    fn first_x_label_area(
934        &self,
935        y: u16,
936        label_width: u16,
937        max_width_after_y_axis: u16,
938        chart_area: Rect,
939        graph_area: Rect,
940    ) -> Rect {
941        let (min_x, max_x) = match self.x_axis.labels_alignment {
942            Alignment::Left => (chart_area.left(), graph_area.left()),
943            Alignment::Center => (
944                chart_area.left(),
945                graph_area.left() + max_width_after_y_axis.min(label_width),
946            ),
947            Alignment::Right => (
948                graph_area.left().saturating_sub(1),
949                graph_area.left() + max_width_after_y_axis,
950            ),
951        };
952
953        Rect::new(min_x, y, max_x - min_x, 1)
954    }
955
956    fn render_label(buf: &mut Buffer, label: &Line, label_area: Rect, alignment: Alignment) {
957        let label = match alignment {
958            Alignment::Left => label.clone().left_aligned(),
959            Alignment::Center => label.clone().centered(),
960            Alignment::Right => label.clone().right_aligned(),
961        };
962        label.render(label_area, buf);
963    }
964
965    fn render_y_labels(
966        &self,
967        buf: &mut Buffer,
968        layout: &ChartLayout,
969        chart_area: Rect,
970        graph_area: Rect,
971    ) {
972        let Some(x) = layout.label_y else { return };
973        let labels = &self.y_axis.labels;
974        let labels_len = labels.len() as u16;
975        if labels_len < 2 {
976            return;
977        }
978
979        for (i, label) in labels.iter().enumerate() {
980            let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
981            if dy < graph_area.bottom() {
982                let label_area = Rect::new(
983                    x,
984                    graph_area.bottom().saturating_sub(1) - dy,
985                    (graph_area.left() - chart_area.left()).saturating_sub(1),
986                    1,
987                );
988                Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
989            }
990        }
991    }
992}
993
994impl Widget for Chart<'_> {
995    fn render(self, area: Rect, buf: &mut Buffer) {
996        Widget::render(&self, area, buf);
997    }
998}
999
1000impl Widget for &Chart<'_> {
1001    #[expect(clippy::too_many_lines)]
1002    fn render(self, area: Rect, buf: &mut Buffer) {
1003        buf.set_style(area, self.style);
1004
1005        self.block.as_ref().render(area, buf);
1006        let chart_area = self.block.inner_if_some(area);
1007        let Some(layout) = self.layout(chart_area) else {
1008            return;
1009        };
1010        let graph_area = layout.graph_area;
1011
1012        // Sample the style of the entire widget. This sample will be used to reset the style of
1013        // the cells that are part of the components put on top of the grah area (i.e legend and
1014        // axis names).
1015        let original_style = buf[(area.left(), area.top())].style();
1016
1017        self.render_x_labels(buf, &layout, chart_area, graph_area);
1018        self.render_y_labels(buf, &layout, chart_area, graph_area);
1019
1020        if let Some(y) = layout.axis_x {
1021            for x in graph_area.left()..graph_area.right() {
1022                buf[(x, y)]
1023                    .set_symbol(symbols::line::HORIZONTAL)
1024                    .set_style(self.x_axis.style);
1025            }
1026        }
1027
1028        if let Some(x) = layout.axis_y {
1029            for y in graph_area.top()..graph_area.bottom() {
1030                buf[(x, y)]
1031                    .set_symbol(symbols::line::VERTICAL)
1032                    .set_style(self.y_axis.style);
1033            }
1034        }
1035
1036        if let Some(y) = layout.axis_x
1037            && let Some(x) = layout.axis_y
1038        {
1039            buf[(x, y)]
1040                .set_symbol(symbols::line::BOTTOM_LEFT)
1041                .set_style(self.x_axis.style);
1042        }
1043
1044        Canvas::default()
1045            .background_color(self.style.bg.unwrap_or(Color::Reset))
1046            .x_bounds(self.x_axis.bounds)
1047            .y_bounds(self.y_axis.bounds)
1048            .paint(|ctx| {
1049                for dataset in &self.datasets {
1050                    ctx.marker(dataset.marker);
1051
1052                    let color = dataset.style.fg.unwrap_or(Color::Reset);
1053                    ctx.draw(&Points {
1054                        coords: dataset.data,
1055                        color,
1056                    });
1057                    match dataset.graph_type {
1058                        GraphType::Line => {
1059                            for data in dataset.data.windows(2) {
1060                                ctx.draw(&CanvasLine {
1061                                    x1: data[0].0,
1062                                    y1: data[0].1,
1063                                    x2: data[1].0,
1064                                    y2: data[1].1,
1065                                    color,
1066                                });
1067                            }
1068                        }
1069                        GraphType::Bar => {
1070                            for (x, y) in dataset.data {
1071                                ctx.draw(&CanvasLine {
1072                                    x1: *x,
1073                                    y1: 0.0,
1074                                    x2: *x,
1075                                    y2: *y,
1076                                    color,
1077                                });
1078                            }
1079                        }
1080                        GraphType::Area => {
1081                            for data in dataset.data.windows(2) {
1082                                ctx.draw(&FilledLine {
1083                                    x1: data[0].0,
1084                                    y1: data[0].1,
1085                                    x2: data[1].0,
1086                                    y2: data[1].1,
1087                                    fill_to_y: dataset.fill_to_y,
1088                                    color,
1089                                });
1090                            }
1091                        }
1092
1093                        GraphType::Scatter => {}
1094                    }
1095                }
1096            })
1097            .render(graph_area, buf);
1098
1099        if let Some(Position { x, y }) = layout.title_x {
1100            let title = self.x_axis.title.as_ref().unwrap();
1101            let width = graph_area
1102                .right()
1103                .saturating_sub(x)
1104                .min(title.width() as u16);
1105            buf.set_style(
1106                Rect {
1107                    x,
1108                    y,
1109                    width,
1110                    height: 1,
1111                },
1112                original_style,
1113            );
1114            buf.set_line(x, y, title, width);
1115        }
1116
1117        if let Some(Position { x, y }) = layout.title_y {
1118            let title = self.y_axis.title.as_ref().unwrap();
1119            let width = graph_area
1120                .right()
1121                .saturating_sub(x)
1122                .min(title.width() as u16);
1123            buf.set_style(
1124                Rect {
1125                    x,
1126                    y,
1127                    width,
1128                    height: 1,
1129                },
1130                original_style,
1131            );
1132            buf.set_line(x, y, title, width);
1133        }
1134
1135        if let Some(legend_area) = layout.legend_area {
1136            buf.set_style(legend_area, original_style);
1137            Block::bordered().render(legend_area, buf);
1138
1139            for (i, (dataset_name, dataset_style)) in self
1140                .datasets
1141                .iter()
1142                .filter_map(|ds| Some((ds.name.as_ref()?, ds.style())))
1143                .enumerate()
1144            {
1145                let name = dataset_name.clone().patch_style(dataset_style);
1146                name.render(
1147                    Rect {
1148                        x: legend_area.x + 1,
1149                        y: legend_area.y + 1 + i as u16,
1150                        width: legend_area.width - 2,
1151                        height: 1,
1152                    },
1153                    buf,
1154                );
1155            }
1156        }
1157    }
1158}
1159
1160impl Styled for Axis<'_> {
1161    type Item = Self;
1162
1163    fn style(&self) -> Style {
1164        self.style
1165    }
1166
1167    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1168        self.style(style)
1169    }
1170}
1171
1172impl Styled for Dataset<'_> {
1173    type Item = Self;
1174
1175    fn style(&self) -> Style {
1176        self.style
1177    }
1178
1179    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1180        self.style(style)
1181    }
1182}
1183
1184impl Styled for Chart<'_> {
1185    type Item = Self;
1186
1187    fn style(&self) -> Style {
1188        self.style
1189    }
1190
1191    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1192        self.style(style)
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use alloc::string::ToString;
1199    use alloc::{format, vec};
1200
1201    use ratatui_core::style::{Modifier, Stylize};
1202    use rstest::rstest;
1203    use strum::ParseError;
1204
1205    use super::*;
1206
1207    struct LegendTestCase {
1208        chart_area: Rect,
1209        hidden_legend_constraints: (Constraint, Constraint),
1210        legend_area: Option<Rect>,
1211    }
1212
1213    #[test]
1214    fn it_should_hide_the_legend() {
1215        let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
1216        let cases = [
1217            LegendTestCase {
1218                chart_area: Rect::new(0, 0, 100, 100),
1219                hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
1220                legend_area: Some(Rect::new(88, 0, 12, 12)),
1221            },
1222            LegendTestCase {
1223                chart_area: Rect::new(0, 0, 100, 100),
1224                hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
1225                legend_area: None,
1226            },
1227        ];
1228        for case in &cases {
1229            let datasets = (0..10)
1230                .map(|i| {
1231                    let name = format!("Dataset #{i}");
1232                    Dataset::default().name(name).data(&data)
1233                })
1234                .collect::<Vec<_>>();
1235            let chart = Chart::new(datasets)
1236                .x_axis(Axis::default().title("X axis"))
1237                .y_axis(Axis::default().title("Y axis"))
1238                .hidden_legend_constraints(case.hidden_legend_constraints);
1239            let layout = chart.layout(case.chart_area).unwrap();
1240            assert_eq!(layout.legend_area, case.legend_area);
1241        }
1242    }
1243
1244    #[test]
1245    fn axis_can_be_stylized() {
1246        assert_eq!(
1247            Axis::default().black().on_white().bold().not_dim().style,
1248            Style::default()
1249                .fg(Color::Black)
1250                .bg(Color::White)
1251                .add_modifier(Modifier::BOLD)
1252                .remove_modifier(Modifier::DIM)
1253        );
1254    }
1255
1256    #[test]
1257    fn dataset_can_be_stylized() {
1258        assert_eq!(
1259            Dataset::default().black().on_white().bold().not_dim().style,
1260            Style::default()
1261                .fg(Color::Black)
1262                .bg(Color::White)
1263                .add_modifier(Modifier::BOLD)
1264                .remove_modifier(Modifier::DIM)
1265        );
1266    }
1267
1268    #[test]
1269    fn chart_can_be_stylized() {
1270        assert_eq!(
1271            Chart::new(vec![]).black().on_white().bold().not_dim().style,
1272            Style::default()
1273                .fg(Color::Black)
1274                .bg(Color::White)
1275                .add_modifier(Modifier::BOLD)
1276                .remove_modifier(Modifier::DIM)
1277        );
1278    }
1279
1280    #[test]
1281    fn graph_type_to_string() {
1282        assert_eq!(GraphType::Scatter.to_string(), "Scatter");
1283        assert_eq!(GraphType::Line.to_string(), "Line");
1284        assert_eq!(GraphType::Bar.to_string(), "Bar");
1285    }
1286
1287    #[test]
1288    fn graph_type_from_str() {
1289        assert_eq!("Scatter".parse::<GraphType>(), Ok(GraphType::Scatter));
1290        assert_eq!("Line".parse::<GraphType>(), Ok(GraphType::Line));
1291        assert_eq!("Bar".parse::<GraphType>(), Ok(GraphType::Bar));
1292        assert_eq!("".parse::<GraphType>(), Err(ParseError::VariantNotFound));
1293    }
1294
1295    #[test]
1296    fn it_does_not_panic_if_title_is_wider_than_buffer() {
1297        let widget = Chart::default()
1298            .y_axis(Axis::default().title("xxxxxxxxxxxxxxxx"))
1299            .x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
1300        let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
1301        widget.render(buffer.area, &mut buffer);
1302        assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]));
1303    }
1304
1305    #[test]
1306    fn it_does_not_panic_if_y_axis_has_one_label() {
1307        let widget = Chart::new(vec![]).y_axis(Axis::default().bounds([0.0, 1.0]).labels(["only"]));
1308        let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
1309
1310        widget.render(buffer.area, &mut buffer);
1311    }
1312
1313    #[test]
1314    fn datasets_without_name_dont_contribute_to_legend_height() {
1315        let data_named_1 = Dataset::default().name("data1"); // must occupy a row in legend
1316        let data_named_2 = Dataset::default().name(""); // must occupy a row in legend, even if name is empty
1317        let data_unnamed = Dataset::default(); // must not occupy a row in legend
1318        let widget = Chart::new(vec![data_named_1, data_unnamed, data_named_2]);
1319        let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
1320        let layout = widget.layout(buffer.area).unwrap();
1321
1322        assert!(layout.legend_area.is_some());
1323        assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2 for rows
1324    }
1325
1326    #[test]
1327    fn no_legend_if_no_named_datasets() {
1328        let dataset = Dataset::default();
1329        let widget = Chart::new(vec![dataset; 3]);
1330        let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
1331        let layout = widget.layout(buffer.area).unwrap();
1332
1333        assert!(layout.legend_area.is_none());
1334    }
1335
1336    #[test]
1337    fn dataset_legend_style_is_patched() {
1338        let long_dataset_name = Dataset::default().name("Very long name");
1339        let short_dataset =
1340            Dataset::default().name(Line::from("Short name").alignment(Alignment::Right));
1341        let widget = Chart::new(vec![long_dataset_name, short_dataset])
1342            .hidden_legend_constraints((100.into(), 100.into()));
1343        let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
1344        widget.render(buffer.area, &mut buffer);
1345        let expected = Buffer::with_lines([
1346            "    ┌──────────────┐",
1347            "    │Very long name│",
1348            "    │    Short name│",
1349            "    └──────────────┘",
1350            "                    ",
1351        ]);
1352        assert_eq!(buffer, expected);
1353    }
1354
1355    #[test]
1356    fn test_chart_have_a_topleft_legend() {
1357        let chart = Chart::new(vec![Dataset::default().name("Ds1")])
1358            .legend_position(Some(LegendPosition::TopLeft));
1359        let area = Rect::new(0, 0, 30, 20);
1360        let mut buffer = Buffer::empty(area);
1361        chart.render(buffer.area, &mut buffer);
1362        let expected = Buffer::with_lines([
1363            "┌───┐                         ",
1364            "│Ds1│                         ",
1365            "└───┘                         ",
1366            "                              ",
1367            "                              ",
1368            "                              ",
1369            "                              ",
1370            "                              ",
1371            "                              ",
1372            "                              ",
1373            "                              ",
1374            "                              ",
1375            "                              ",
1376            "                              ",
1377            "                              ",
1378            "                              ",
1379            "                              ",
1380            "                              ",
1381            "                              ",
1382            "                              ",
1383        ]);
1384        assert_eq!(buffer, expected);
1385    }
1386
1387    #[test]
1388    fn test_chart_have_a_long_y_axis_title_overlapping_legend() {
1389        let chart = Chart::new(vec![Dataset::default().name("Ds1")])
1390            .y_axis(Axis::default().title("The title overlap a legend."));
1391        let area = Rect::new(0, 0, 30, 20);
1392        let mut buffer = Buffer::empty(area);
1393        chart.render(buffer.area, &mut buffer);
1394        let expected = Buffer::with_lines([
1395            "The title overlap a legend.   ",
1396            "                         ┌───┐",
1397            "                         │Ds1│",
1398            "                         └───┘",
1399            "                              ",
1400            "                              ",
1401            "                              ",
1402            "                              ",
1403            "                              ",
1404            "                              ",
1405            "                              ",
1406            "                              ",
1407            "                              ",
1408            "                              ",
1409            "                              ",
1410            "                              ",
1411            "                              ",
1412            "                              ",
1413            "                              ",
1414            "                              ",
1415        ]);
1416        assert_eq!(buffer, expected);
1417    }
1418
1419    #[test]
1420    fn test_chart_have_overflowed_y_axis() {
1421        let chart = Chart::new(vec![Dataset::default().name("Ds1")])
1422            .y_axis(Axis::default().title("The title overlap a legend."));
1423        let area = Rect::new(0, 0, 10, 10);
1424        let mut buffer = Buffer::empty(area);
1425        chart.render(buffer.area, &mut buffer);
1426        let expected = Buffer::with_lines([
1427            "          ",
1428            "          ",
1429            "          ",
1430            "          ",
1431            "          ",
1432            "          ",
1433            "          ",
1434            "          ",
1435            "          ",
1436            "          ",
1437        ]);
1438        assert_eq!(buffer, expected);
1439    }
1440
1441    #[test]
1442    fn test_legend_area_can_fit_same_chart_area() {
1443        let name = "Data";
1444        let chart = Chart::new(vec![Dataset::default().name(name)])
1445            .hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
1446        let area = Rect::new(0, 0, name.len() as u16 + 2, 3);
1447        let mut buffer = Buffer::empty(area);
1448        for position in [
1449            LegendPosition::TopLeft,
1450            LegendPosition::Top,
1451            LegendPosition::TopRight,
1452            LegendPosition::Left,
1453            LegendPosition::Right,
1454            LegendPosition::Bottom,
1455            LegendPosition::BottomLeft,
1456            LegendPosition::BottomRight,
1457        ] {
1458            let chart = chart.clone().legend_position(Some(position));
1459            buffer.reset();
1460            chart.render(buffer.area, &mut buffer);
1461            #[rustfmt::skip]
1462            let expected = Buffer::with_lines([
1463                "┌────┐",
1464                "│Data│",
1465                "└────┘",
1466            ]);
1467            assert_eq!(buffer, expected);
1468        }
1469    }
1470
1471    #[rstest]
1472    #[case(Some(LegendPosition::TopLeft), [
1473        "┌────┐   ",
1474        "│Data│   ",
1475        "└────┘   ",
1476        "         ",
1477        "         ",
1478        "         ",
1479    ])]
1480    #[case(Some(LegendPosition::Top), [
1481        " ┌────┐  ",
1482        " │Data│  ",
1483        " └────┘  ",
1484        "         ",
1485        "         ",
1486        "         ",
1487    ])]
1488    #[case(Some(LegendPosition::TopRight), [
1489        "   ┌────┐",
1490        "   │Data│",
1491        "   └────┘",
1492        "         ",
1493        "         ",
1494        "         ",
1495    ])]
1496    #[case(Some(LegendPosition::Left), [
1497        "         ",
1498        "┌────┐   ",
1499        "│Data│   ",
1500        "└────┘   ",
1501        "         ",
1502        "         ",
1503    ])]
1504    #[case(Some(LegendPosition::Right), [
1505        "         ",
1506        "   ┌────┐",
1507        "   │Data│",
1508        "   └────┘",
1509        "         ",
1510        "         ",
1511    ])]
1512    #[case(Some(LegendPosition::BottomLeft), [
1513        "         ",
1514        "         ",
1515        "         ",
1516        "┌────┐   ",
1517        "│Data│   ",
1518        "└────┘   ",
1519    ])]
1520    #[case(Some(LegendPosition::Bottom), [
1521        "         ",
1522        "         ",
1523        "         ",
1524        " ┌────┐  ",
1525        " │Data│  ",
1526        " └────┘  ",
1527    ])]
1528    #[case(Some(LegendPosition::BottomRight), [
1529        "         ",
1530        "         ",
1531        "         ",
1532        "   ┌────┐",
1533        "   │Data│",
1534        "   └────┘",
1535    ])]
1536    #[case(None, [
1537        "         ",
1538        "         ",
1539        "         ",
1540        "         ",
1541        "         ",
1542        "         ",
1543    ])]
1544    fn test_legend_of_chart_have_odd_margin_size<'line, Lines>(
1545        #[case] legend_position: Option<LegendPosition>,
1546        #[case] expected: Lines,
1547    ) where
1548        Lines: IntoIterator,
1549        Lines::Item: Into<Line<'line>>,
1550    {
1551        let name = "Data";
1552        let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);
1553        let mut buffer = Buffer::empty(area);
1554        let chart = Chart::new(vec![Dataset::default().name(name)])
1555            .legend_position(legend_position)
1556            .hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
1557        chart.render(buffer.area, &mut buffer);
1558        assert_eq!(buffer, Buffer::with_lines(expected));
1559    }
1560
1561    #[test]
1562    fn bar_chart() {
1563        let data = [
1564            (0.0, 0.0),
1565            (2.0, 1.0),
1566            (4.0, 4.0),
1567            (6.0, 8.0),
1568            (8.0, 9.0),
1569            (10.0, 10.0),
1570        ];
1571        let chart = Chart::new(vec![
1572            Dataset::default()
1573                .data(&data)
1574                .marker(symbols::Marker::Dot)
1575                .graph_type(GraphType::Bar),
1576        ])
1577        .x_axis(Axis::default().bounds([0.0, 10.0]))
1578        .y_axis(Axis::default().bounds([0.0, 10.0]));
1579        let area = Rect::new(0, 0, 11, 11);
1580        let mut buffer = Buffer::empty(area);
1581        chart.render(buffer.area, &mut buffer);
1582        let expected = Buffer::with_lines([
1583            "          •",
1584            "        • •",
1585            "      • • •",
1586            "      • • •",
1587            "      • • •",
1588            "      • • •",
1589            "    • • • •",
1590            "    • • • •",
1591            "    • • • •",
1592            "  • • • • •",
1593            "• • • • • •",
1594        ]);
1595        assert_eq!(buffer, expected);
1596    }
1597
1598    #[rstest]
1599    #[case::dot(symbols::Marker::Dot, '•')]
1600    #[case::dot(symbols::Marker::Braille, '⢣')]
1601    fn overlapping_lines(#[case] marker: symbols::Marker, #[case] symbol: char) {
1602        let data_diagonal_up = [(0.0, 0.0), (5.0, 5.0)];
1603        let data_diagonal_down = [(0.0, 5.0), (5.0, 0.0)];
1604        let lines = vec![
1605            Dataset::default()
1606                .data(&data_diagonal_up)
1607                .marker(symbols::Marker::Block)
1608                .graph_type(GraphType::Line)
1609                .blue(),
1610            Dataset::default()
1611                .data(&data_diagonal_down)
1612                .marker(marker)
1613                .graph_type(GraphType::Line)
1614                .red(),
1615        ];
1616        let chart = Chart::new(lines)
1617            .x_axis(Axis::default().bounds([0.0, 5.0]))
1618            .y_axis(Axis::default().bounds([0.0, 5.0]));
1619        let area = Rect::new(0, 0, 5, 5);
1620        let mut buffer = Buffer::empty(area);
1621        chart.render(buffer.area, &mut buffer);
1622        #[rustfmt::skip]
1623        let mut expected = Buffer::with_lines([
1624            format!("{symbol}   █"),
1625            format!(" {symbol} █ "),
1626            format!("  {symbol}  "),
1627            format!(" █ {symbol} "),
1628            format!("█   {symbol}"),
1629        ]);
1630        for i in 0..5 {
1631            // The Marker::Dot and Marker::Braille tiles have the
1632            // foreground set to Red.
1633            expected.set_style(Rect::new(i, i, 1, 1), Style::new().fg(Color::Red));
1634            // The Marker::Block tiles have both the foreground and
1635            // background set to Blue.
1636            expected.set_style(
1637                Rect::new(i, 4 - i, 1, 1),
1638                Style::new().fg(Color::Blue).bg(Color::Blue),
1639            );
1640        }
1641        // Where the Marker::Dot/Braille overlaps with Marker::Block,
1642        // the background is set to blue from the Block, but the
1643        // foreground is set to red from the Dot/Braille.  This allows
1644        // two line plots to overlap, so long as one of them is a
1645        // Block.
1646        expected.set_style(
1647            Rect::new(2, 2, 1, 1),
1648            Style::new().fg(Color::Red).bg(Color::Blue),
1649        );
1650
1651        assert_eq!(buffer, expected);
1652    }
1653
1654    #[test]
1655    fn filled_line() {
1656        let data = [(0.0, 0.0), (5.0, 5.0), (10.0, 5.0)];
1657        let chart = Chart::new(vec![
1658            Dataset::default()
1659                .data(&data)
1660                .marker(symbols::Marker::Dot)
1661                .fill_to_y(0.0)
1662                .graph_type(GraphType::Area),
1663        ])
1664        .x_axis(Axis::default().bounds([0.0, 10.0]))
1665        .y_axis(Axis::default().bounds([0.0, 10.0]));
1666        let area = Rect::new(0, 0, 11, 11);
1667        let mut buffer = Buffer::empty(area);
1668        chart.render(buffer.area, &mut buffer);
1669        let expected = Buffer::with_lines([
1670            "           ",
1671            "           ",
1672            "           ",
1673            "           ",
1674            "           ",
1675            "     ••••••",
1676            "    •••••••",
1677            "   ••••••••",
1678            "  •••••••••",
1679            " ••••••••••",
1680            "•••••••••••",
1681        ]);
1682        assert_eq!(buffer, expected);
1683    }
1684
1685    #[test]
1686    fn render_in_minimal_buffer() {
1687        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
1688        let chart = Chart::new(vec![Dataset::default().data(&[(0.0, 0.0), (1.0, 1.0)])])
1689            .x_axis(Axis::default().bounds([0.0, 1.0]))
1690            .y_axis(Axis::default().bounds([0.0, 1.0]));
1691        // This should not panic, even if the buffer is too small to render the chart.
1692        chart.render(buffer.area, &mut buffer);
1693        assert_eq!(buffer, Buffer::with_lines(["•"]));
1694    }
1695
1696    #[test]
1697    fn render_in_zero_size_buffer() {
1698        let mut buffer = Buffer::empty(Rect::ZERO);
1699        let chart = Chart::new(vec![Dataset::default().data(&[(0.0, 0.0), (1.0, 1.0)])])
1700            .x_axis(Axis::default().bounds([0.0, 1.0]))
1701            .y_axis(Axis::default().bounds([0.0, 1.0]));
1702        // This should not panic, even if the buffer has zero size.
1703        chart.render(buffer.area, &mut buffer);
1704    }
1705}