ratatui/widgets/
chart.rs

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