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