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