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