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}