use std::{cmp::max, ops::Not};
use strum::{Display, EnumString};
use crate::{
layout::Flex,
prelude::*,
style::Styled,
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block,
},
};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Axis<'a> {
title: Option<Line<'a>>,
bounds: [f64; 2],
labels: Vec<Line<'a>>,
style: Style,
labels_alignment: Alignment,
}
impl<'a> Axis<'a> {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title<T>(mut self, title: T) -> Self
where
T: Into<Line<'a>>,
{
self.title = Some(title.into());
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn bounds(mut self, bounds: [f64; 2]) -> Self {
self.bounds = bounds;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn labels<Labels>(mut self, labels: Labels) -> Self
where
Labels: IntoIterator,
Labels::Item: Into<Line<'a>>,
{
self.labels = labels.into_iter().map(Into::into).collect();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn labels_alignment(mut self, alignment: Alignment) -> Self {
self.labels_alignment = alignment;
self
}
}
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GraphType {
#[default]
Scatter,
Line,
Bar,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum LegendPosition {
Top,
#[default]
TopRight,
TopLeft,
Left,
Right,
Bottom,
BottomRight,
BottomLeft,
}
impl LegendPosition {
fn layout(
self,
area: Rect,
legend_width: u16,
legend_height: u16,
x_title_width: u16,
y_title_width: u16,
) -> Option<Rect> {
let mut height_margin = i32::from(area.height - legend_height);
if x_title_width != 0 {
height_margin -= 1;
}
if y_title_width != 0 {
height_margin -= 1;
}
if height_margin < 0 {
return None;
};
let (x, y) = match self {
Self::TopRight => {
if legend_width + y_title_width > area.width {
(area.right() - legend_width, area.top() + 1)
} else {
(area.right() - legend_width, area.top())
}
}
Self::TopLeft => {
if y_title_width != 0 {
(area.left(), area.top() + 1)
} else {
(area.left(), area.top())
}
}
Self::Top => {
let x = (area.width - legend_width) / 2;
if area.left() + y_title_width > x {
(area.left() + x, area.top() + 1)
} else {
(area.left() + x, area.top())
}
}
Self::Left => {
let mut y = (area.height - legend_height) / 2;
if y_title_width != 0 {
y += 1;
}
if x_title_width != 0 {
y = y.saturating_sub(1);
}
(area.left(), area.top() + y)
}
Self::Right => {
let mut y = (area.height - legend_height) / 2;
if y_title_width != 0 {
y += 1;
}
if x_title_width != 0 {
y = y.saturating_sub(1);
}
(area.right() - legend_width, area.top() + y)
}
Self::BottomLeft => {
if x_title_width + legend_width > area.width {
(area.left(), area.bottom() - legend_height - 1)
} else {
(area.left(), area.bottom() - legend_height)
}
}
Self::BottomRight => {
if x_title_width != 0 {
(
area.right() - legend_width,
area.bottom() - legend_height - 1,
)
} else {
(area.right() - legend_width, area.bottom() - legend_height)
}
}
Self::Bottom => {
let x = area.left() + (area.width - legend_width) / 2;
if x + legend_width > area.right() - x_title_width {
(x, area.bottom() - legend_height - 1)
} else {
(x, area.bottom() - legend_height)
}
}
};
Some(Rect::new(x, y, legend_width, legend_height))
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Dataset<'a> {
name: Option<Line<'a>>,
data: &'a [(f64, f64)],
marker: symbols::Marker,
graph_type: GraphType,
style: Style,
}
impl<'a> Dataset<'a> {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn name<S>(mut self, name: S) -> Self
where
S: Into<Line<'a>>,
{
self.name = Some(name.into());
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn data(mut self, data: &'a [(f64, f64)]) -> Self {
self.data = data;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn marker(mut self, marker: symbols::Marker) -> Self {
self.marker = marker;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn graph_type(mut self, graph_type: GraphType) -> Self {
self.graph_type = graph_type;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
}
struct ChartLayout {
title_x: Option<Position>,
title_y: Option<Position>,
label_x: Option<u16>,
label_y: Option<u16>,
axis_x: Option<u16>,
axis_y: Option<u16>,
legend_area: Option<Rect>,
graph_area: Rect,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Chart<'a> {
block: Option<Block<'a>>,
x_axis: Axis<'a>,
y_axis: Axis<'a>,
datasets: Vec<Dataset<'a>>,
style: Style,
hidden_legend_constraints: (Constraint, Constraint),
legend_position: Option<LegendPosition>,
}
impl<'a> Chart<'a> {
pub fn new(datasets: Vec<Dataset<'a>>) -> Self {
Self {
block: None,
x_axis: Axis::default(),
y_axis: Axis::default(),
style: Style::default(),
datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
legend_position: Some(LegendPosition::default()),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn x_axis(mut self, axis: Axis<'a>) -> Self {
self.x_axis = axis;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn y_axis(mut self, axis: Axis<'a>) -> Self {
self.y_axis = axis;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn hidden_legend_constraints(
mut self,
constraints: (Constraint, Constraint),
) -> Self {
self.hidden_legend_constraints = constraints;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn legend_position(mut self, position: Option<LegendPosition>) -> Self {
self.legend_position = position;
self
}
fn layout(&self, area: Rect) -> Option<ChartLayout> {
if area.height == 0 || area.width == 0 {
return None;
}
let mut x = area.left();
let mut y = area.bottom() - 1;
let mut label_x = None;
if !self.x_axis.labels.is_empty() && y > area.top() {
label_x = Some(y);
y -= 1;
}
let label_y = self.y_axis.labels.is_empty().not().then_some(x);
x += self.max_width_of_labels_left_of_y_axis(area, !self.y_axis.labels.is_empty());
let mut axis_x = None;
if !self.x_axis.labels.is_empty() && y > area.top() {
axis_x = Some(y);
y -= 1;
}
let mut axis_y = None;
if !self.y_axis.labels.is_empty() && x + 1 < area.right() {
axis_y = Some(x);
x += 1;
}
let graph_width = area.right().saturating_sub(x);
let graph_height = y.saturating_sub(area.top()).saturating_add(1);
debug_assert_ne!(
graph_width, 0,
"Axis and labels should have been hidden due to the small area"
);
debug_assert_ne!(
graph_height, 0,
"Axis and labels should have been hidden due to the small area"
);
let graph_area = Rect::new(x, area.top(), graph_width, graph_height);
let mut title_x = None;
if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16;
if w < graph_area.width && graph_area.height > 2 {
title_x = Some(Position::new(x + graph_area.width - w, y));
}
}
let mut title_y = None;
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < graph_area.width && graph_area.height > 2 {
title_y = Some(Position::new(x, area.top()));
}
}
let mut legend_area = None;
if let Some(legend_position) = self.legend_position {
let legends = self
.datasets
.iter()
.filter_map(|d| Some(d.name.as_ref()?.width() as u16));
if let Some(inner_width) = legends.clone().max() {
let legend_width = inner_width + 2;
let legend_height = legends.count() as u16 + 2;
let [max_legend_width] = Layout::horizontal([self.hidden_legend_constraints.0])
.flex(Flex::Start)
.areas(graph_area);
let [max_legend_height] = Layout::vertical([self.hidden_legend_constraints.1])
.flex(Flex::Start)
.areas(graph_area);
if inner_width > 0
&& legend_width <= max_legend_width.width
&& legend_height <= max_legend_height.height
{
legend_area = legend_position.layout(
graph_area,
legend_width,
legend_height,
title_x
.and(self.x_axis.title.as_ref())
.map(|t| t.width() as u16)
.unwrap_or_default(),
title_y
.and(self.y_axis.title.as_ref())
.map(|t| t.width() as u16)
.unwrap_or_default(),
);
}
}
}
Some(ChartLayout {
title_x,
title_y,
label_x,
label_y,
axis_x,
axis_y,
legend_area,
graph_area,
})
}
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
let mut max_width = self
.y_axis
.labels
.iter()
.map(Line::width)
.max()
.unwrap_or_default() as u16;
if let Some(first_x_label) = self.x_axis.labels.first() {
let first_label_width = first_x_label.width() as u16;
let width_left_of_y_axis = match self.x_axis.labels_alignment {
Alignment::Left => {
let y_axis_offset = u16::from(has_y_axis);
first_label_width.saturating_sub(y_axis_offset)
}
Alignment::Center => first_label_width / 2,
Alignment::Right => 0,
};
max_width = max(max_width, width_left_of_y_axis);
}
max_width.min(area.width / 3)
}
fn render_x_labels(
&self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let Some(y) = layout.label_x else { return };
let labels = &self.x_axis.labels;
let labels_len = labels.len() as u16;
if labels_len < 2 {
return;
}
let width_between_ticks = graph_area.width / labels_len;
let label_area = self.first_x_label_area(
y,
labels.first().unwrap().width() as u16,
width_between_ticks,
chart_area,
graph_area,
);
let label_alignment = match self.x_axis.labels_alignment {
Alignment::Left => Alignment::Right,
Alignment::Center => Alignment::Center,
Alignment::Right => Alignment::Left,
};
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
Self::render_label(buf, label, label_area, Alignment::Center);
}
let x = graph_area.right() - width_between_ticks;
let label_area = Rect::new(x, y, width_between_ticks, 1);
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
}
fn first_x_label_area(
&self,
y: u16,
label_width: u16,
max_width_after_y_axis: u16,
chart_area: Rect,
graph_area: Rect,
) -> Rect {
let (min_x, max_x) = match self.x_axis.labels_alignment {
Alignment::Left => (chart_area.left(), graph_area.left()),
Alignment::Center => (
chart_area.left(),
graph_area.left() + max_width_after_y_axis.min(label_width),
),
Alignment::Right => (
graph_area.left().saturating_sub(1),
graph_area.left() + max_width_after_y_axis,
),
};
Rect::new(min_x, y, max_x - min_x, 1)
}
fn render_label(buf: &mut Buffer, label: &Line, label_area: Rect, alignment: Alignment) {
let label = match alignment {
Alignment::Left => label.clone().left_aligned(),
Alignment::Center => label.clone().centered(),
Alignment::Right => label.clone().right_aligned(),
};
label.render(label_area, buf);
}
fn render_y_labels(
&self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let Some(x) = layout.label_y else { return };
let labels = &self.y_axis.labels;
let labels_len = labels.len() as u16;
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
let label_area = Rect::new(
x,
graph_area.bottom().saturating_sub(1) - dy,
(graph_area.left() - chart_area.left()).saturating_sub(1),
1,
);
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
}
}
}
}
impl Widget for Chart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Chart<'_> {
#[allow(clippy::too_many_lines)]
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
let chart_area = self.block.inner_if_some(area);
let Some(layout) = self.layout(chart_area) else {
return;
};
let graph_area = layout.graph_area;
let original_style = buf[(area.left(), area.top())].style();
self.render_x_labels(buf, &layout, chart_area, graph_area);
self.render_y_labels(buf, &layout, chart_area, graph_area);
if let Some(y) = layout.axis_x {
for x in graph_area.left()..graph_area.right() {
buf[(x, y)]
.set_symbol(symbols::line::HORIZONTAL)
.set_style(self.x_axis.style);
}
}
if let Some(x) = layout.axis_y {
for y in graph_area.top()..graph_area.bottom() {
buf[(x, y)]
.set_symbol(symbols::line::VERTICAL)
.set_style(self.y_axis.style);
}
}
if let Some(y) = layout.axis_x {
if let Some(x) = layout.axis_y {
buf[(x, y)]
.set_symbol(symbols::line::BOTTOM_LEFT)
.set_style(self.x_axis.style);
}
}
for dataset in &self.datasets {
Canvas::default()
.background_color(self.style.bg.unwrap_or(Color::Reset))
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.marker(dataset.marker)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
match dataset.graph_type {
GraphType::Line => {
for data in dataset.data.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Bar => {
for (x, y) in dataset.data {
ctx.draw(&CanvasLine {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Scatter => {}
}
})
.render(graph_area, buf);
}
if let Some(Position { x, y }) = layout.title_x {
let title = self.x_axis.title.as_ref().unwrap();
let width = graph_area
.right()
.saturating_sub(x)
.min(title.width() as u16);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_line(x, y, title, width);
}
if let Some(Position { x, y }) = layout.title_y {
let title = self.y_axis.title.as_ref().unwrap();
let width = graph_area
.right()
.saturating_sub(x)
.min(title.width() as u16);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_line(x, y, title, width);
}
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::bordered().render(legend_area, buf);
for (i, (dataset_name, dataset_style)) in self
.datasets
.iter()
.filter_map(|ds| Some((ds.name.as_ref()?, ds.style())))
.enumerate()
{
let name = dataset_name.clone().patch_style(dataset_style);
name.render(
Rect {
x: legend_area.x + 1,
y: legend_area.y + 1 + i as u16,
width: legend_area.width - 2,
height: 1,
},
buf,
);
}
}
}
}
impl<'a> Styled for Axis<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for Dataset<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a> Styled for Chart<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::ParseError;
use super::*;
struct LegendTestCase {
chart_area: Rect,
hidden_legend_constraints: (Constraint, Constraint),
legend_area: Option<Rect>,
}
#[test]
fn it_should_hide_the_legend() {
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
let cases = [
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
legend_area: Some(Rect::new(88, 0, 12, 12)),
},
LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
legend_area: None,
},
];
for case in &cases {
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{i}");
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let chart = Chart::new(datasets)
.x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints);
let layout = chart.layout(case.chart_area).unwrap();
assert_eq!(layout.legend_area, case.legend_area);
}
}
#[test]
fn axis_can_be_stylized() {
assert_eq!(
Axis::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn dataset_can_be_stylized() {
assert_eq!(
Dataset::default().black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn chart_can_be_stylized() {
assert_eq!(
Chart::new(vec![]).black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn graph_type_to_string() {
assert_eq!(GraphType::Scatter.to_string(), "Scatter");
assert_eq!(GraphType::Line.to_string(), "Line");
assert_eq!(GraphType::Bar.to_string(), "Bar");
}
#[test]
fn graph_type_from_str() {
assert_eq!("Scatter".parse::<GraphType>(), Ok(GraphType::Scatter));
assert_eq!("Line".parse::<GraphType>(), Ok(GraphType::Line));
assert_eq!("Bar".parse::<GraphType>(), Ok(GraphType::Bar));
assert_eq!("".parse::<GraphType>(), Err(ParseError::VariantNotFound));
}
#[test]
fn it_does_not_panic_if_title_is_wider_than_buffer() {
let widget = Chart::default()
.y_axis(Axis::default().title("xxxxxxxxxxxxxxxx"))
.x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
widget.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]));
}
#[test]
fn datasets_without_name_dont_contribute_to_legend_height() {
let data_named_1 = Dataset::default().name("data1"); let data_named_2 = Dataset::default().name(""); let data_unnamed = Dataset::default(); let widget = Chart::new(vec![data_named_1, data_unnamed, data_named_2]);
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
let layout = widget.layout(buffer.area).unwrap();
assert!(layout.legend_area.is_some());
assert_eq!(layout.legend_area.unwrap().height, 4); }
#[test]
fn no_legend_if_no_named_datasets() {
let dataset = Dataset::default();
let widget = Chart::new(vec![dataset; 3]);
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
let layout = widget.layout(buffer.area).unwrap();
assert!(layout.legend_area.is_none());
}
#[test]
fn dataset_legend_style_is_patched() {
let long_dataset_name = Dataset::default().name("Very long name");
let short_dataset =
Dataset::default().name(Line::from("Short name").alignment(Alignment::Right));
let widget = Chart::new(vec![long_dataset_name, short_dataset])
.hidden_legend_constraints((100.into(), 100.into()));
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
widget.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ┌──────────────┐",
" │Very long name│",
" │ Short name│",
" └──────────────┘",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn test_chart_have_a_topleft_legend() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.legend_position(Some(LegendPosition::TopLeft));
let area = Rect::new(0, 0, 30, 20);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
"┌───┐ ",
"│Ds1│ ",
"└───┘ ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn test_chart_have_a_long_y_axis_title_overlapping_legend() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.y_axis(Axis::default().title("The title overlap a legend."));
let area = Rect::new(0, 0, 30, 20);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
"The title overlap a legend. ",
" ┌───┐",
" │Ds1│",
" └───┘",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn test_chart_have_overflowed_y_axis() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.y_axis(Axis::default().title("The title overlap a legend."));
let area = Rect::new(0, 0, 10, 10);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn test_legend_area_can_fit_same_chart_area() {
let name = "Data";
let chart = Chart::new(vec![Dataset::default().name(name)])
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
let area = Rect::new(0, 0, name.len() as u16 + 2, 3);
let mut buffer = Buffer::empty(area);
for position in [
LegendPosition::TopLeft,
LegendPosition::Top,
LegendPosition::TopRight,
LegendPosition::Left,
LegendPosition::Right,
LegendPosition::Bottom,
LegendPosition::BottomLeft,
LegendPosition::BottomRight,
] {
let chart = chart.clone().legend_position(Some(position));
buffer.reset();
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌────┐",
"│Data│",
"└────┘",
]);
assert_eq!(buffer, expected);
}
}
#[rstest]
#[case(Some(LegendPosition::TopLeft), [
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::Top), [
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::TopRight), [
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::Left), [
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
])]
#[case(Some(LegendPosition::Right), [
" ",
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
])]
#[case(Some(LegendPosition::BottomLeft), [
" ",
" ",
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
])]
#[case(Some(LegendPosition::Bottom), [
" ",
" ",
" ",
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
])]
#[case(Some(LegendPosition::BottomRight), [
" ",
" ",
" ",
" ┌────┐",
" │Data│",
" └────┘",
])]
#[case(None, [
" ",
" ",
" ",
" ",
" ",
" ",
])]
fn test_legend_of_chart_have_odd_margin_size<'line, Lines>(
#[case] legend_position: Option<LegendPosition>,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let name = "Data";
let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);
let mut buffer = Buffer::empty(area);
let chart = Chart::new(vec![Dataset::default().name(name)])
.legend_position(legend_position)
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn bar_chart() {
let data = [
(0.0, 0.0),
(2.0, 1.0),
(4.0, 4.0),
(6.0, 8.0),
(8.0, 9.0),
(10.0, 10.0),
];
let chart = Chart::new(vec![Dataset::default()
.data(&data)
.marker(symbols::Marker::Dot)
.graph_type(GraphType::Bar)])
.x_axis(Axis::default().bounds([0.0, 10.0]))
.y_axis(Axis::default().bounds([0.0, 10.0]));
let area = Rect::new(0, 0, 11, 11);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" •",
" • •",
" • • •",
" • • •",
" • • •",
" • • •",
" • • • •",
" • • • •",
" • • • •",
" • • • • •",
"• • • • • •",
]);
assert_eq!(buffer, expected);
}
}