use std::cmp::max; use unicode_width::UnicodeWidthStr; use widgets::{Block, Borders, Widget}; use widgets::canvas::{Canvas, Points}; use buffer::Buffer; use layout::Rect; use style::Style; use symbols; /// An X or Y axis for the chart widget pub struct Axis<'a, L> where L: AsRef + 'a, { /// Title displayed next to axis end title: Option<&'a str>, /// Style of the title title_style: Style, /// Bounds for the axis (all data points outside these limits will not be represented) bounds: [f64; 2], /// A list of labels to put to the left or below the axis labels: Option<&'a [L]>, /// The labels' style labels_style: Style, /// The style used to draw the axis itself style: Style, } impl<'a, L> Default for Axis<'a, L> where L: AsRef, { fn default() -> Axis<'a, L> { Axis { title: None, title_style: Default::default(), bounds: [0.0, 0.0], labels: None, labels_style: Default::default(), style: Default::default(), } } } impl<'a, L> Axis<'a, L> where L: AsRef, { pub fn title(mut self, title: &'a str) -> Axis<'a, L> { self.title = Some(title); self } pub fn title_style(mut self, style: Style) -> Axis<'a, L> { self.title_style = style; self } pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> { self.bounds = bounds; self } pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> { self.labels = Some(labels); self } pub fn labels_style(mut self, style: Style) -> Axis<'a, L> { self.labels_style = style; self } pub fn style(mut self, style: Style) -> Axis<'a, L> { self.style = style; self } } /// Marker to use when plotting data points pub enum Marker { /// One point per cell Dot, /// Up to 8 points per cell Braille, } /// A group of data points pub struct Dataset<'a> { /// Name of the dataset (used in the legend if shown) name: &'a str, /// A reference to the actual data data: &'a [(f64, f64)], /// Symbol used for each points of this dataset marker: Marker, /// Style used to plot this dataset style: Style, } impl<'a> Default for Dataset<'a> { fn default() -> Dataset<'a> { Dataset { name: "", data: &[], marker: Marker::Dot, style: Style::default(), } } } impl<'a> Dataset<'a> { pub fn name(mut self, name: &'a str) -> Dataset<'a> { self.name = name; self } pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> { self.data = data; self } pub fn marker(mut self, marker: Marker) -> Dataset<'a> { self.marker = marker; self } pub fn style(mut self, style: Style) -> Dataset<'a> { self.style = style; self } } /// A container that holds all the infos about where to display each elements of the chart (axis, /// labels, legend, ...). #[derive(Debug)] struct ChartLayout { title_x: Option<(u16, u16)>, title_y: Option<(u16, u16)>, label_x: Option, label_y: Option, axis_x: Option, axis_y: Option, legend_area: Option, graph_area: Rect, } impl Default for ChartLayout { fn default() -> ChartLayout { ChartLayout { title_x: None, title_y: None, label_x: None, label_y: None, axis_x: None, axis_y: None, legend_area: None, graph_area: Rect::default(), } } } /// A widget to plot one or more dataset in a cartesian coordinate system /// /// # Examples /// /// ``` /// # extern crate tui; /// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker}; /// # use tui::style::{Style, Color}; /// # fn main() { /// Chart::default() /// .block(Block::default().title("Chart")) /// .x_axis(Axis::default() /// .title("X Axis") /// .title_style(Style::default().fg(Color::Red)) /// .style(Style::default().fg(Color::Gray)) /// .bounds([0.0, 10.0]) /// .labels(&["0.0", "5.0", "10.0"])) /// .y_axis(Axis::default() /// .title("Y Axis") /// .title_style(Style::default().fg(Color::Red)) /// .style(Style::default().fg(Color::Gray)) /// .bounds([0.0, 10.0]) /// .labels(&["0.0", "5.0", "10.0"])) /// .datasets(&[Dataset::default() /// .name("data1") /// .marker(Marker::Dot) /// .style(Style::default().fg(Color::Cyan)) /// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]), /// Dataset::default() /// .name("data2") /// .marker(Marker::Braille) /// .style(Style::default().fg(Color::Magenta)) /// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]); /// # } pub struct Chart<'a, LX, LY> where LX: AsRef + 'a, LY: AsRef + 'a, { /// A block to display around the widget eventually block: Option>, /// The horizontal axis x_axis: Axis<'a, LX>, /// The vertical axis y_axis: Axis<'a, LY>, /// A reference to the datasets datasets: &'a [Dataset<'a>], /// The widget base style style: Style, } impl<'a, LX, LY> Default for Chart<'a, LX, LY> where LX: AsRef, LY: AsRef, { fn default() -> Chart<'a, LX, LY> { Chart { block: None, x_axis: Axis::default(), y_axis: Axis::default(), style: Default::default(), datasets: &[], } } } impl<'a, LX, LY> Chart<'a, LX, LY> where LX: AsRef, LY: AsRef, { pub fn block(&'a mut self, block: Block<'a>) -> &mut Chart<'a, LX, LY> { self.block = Some(block); self } pub fn style(&mut self, style: Style) -> &mut Chart<'a, LX, LY> { self.style = style; self } pub fn x_axis(&mut self, axis: Axis<'a, LX>) -> &mut Chart<'a, LX, LY> { self.x_axis = axis; self } pub fn y_axis(&mut self, axis: Axis<'a, LY>) -> &mut Chart<'a, LX, LY> { self.y_axis = axis; self } pub fn datasets(&mut self, datasets: &'a [Dataset<'a>]) -> &mut Chart<'a, LX, LY> { self.datasets = datasets; self } /// Compute the internal layout of the chart given the area. If the area is too small some /// elements may be automatically hidden fn layout(&self, area: &Rect) -> ChartLayout { let mut layout = ChartLayout::default(); if area.height == 0 || area.width == 0 { return layout; } let mut x = area.left(); let mut y = area.bottom() - 1; if self.x_axis.labels.is_some() && y > area.top() { layout.label_x = Some(y); y -= 1; } if let Some(y_labels) = self.y_axis.labels { let mut max_width = y_labels .iter() .fold(0, |acc, l| max(l.as_ref().width(), acc)) as u16; if let Some(x_labels) = self.x_axis.labels { if x_labels.len() > 0 { max_width = max(max_width, x_labels[0].as_ref().width() as u16); } } if x + max_width < area.right() { layout.label_y = Some(x); x += max_width; } } if self.x_axis.labels.is_some() && y > area.top() { layout.axis_x = Some(y); y -= 1; } if self.y_axis.labels.is_some() && x + 1 < area.right() { layout.axis_y = Some(x); x += 1; } if x < area.right() && y > 1 { layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1); } if let Some(title) = self.x_axis.title { let w = title.width() as u16; if w < layout.graph_area.width && layout.graph_area.height > 2 { layout.title_x = Some((x + layout.graph_area.width - w, y)); } } if let Some(title) = self.y_axis.title { let w = title.width() as u16; if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 { layout.title_y = Some((x + 1, area.top())); } } if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() { let legend_width = inner_width + 2; let legend_height = self.datasets.len() as u16 + 2; if legend_width < layout.graph_area.width / 3 && legend_height < layout.graph_area.height / 3 { layout.legend_area = Some(Rect::new( layout.graph_area.right() - legend_width, layout.graph_area.top(), legend_width, legend_height, )); } } layout } } impl<'a, LX, LY> Widget for Chart<'a, LX, LY> where LX: AsRef, LY: AsRef, { fn draw(&mut self, area: &Rect, buf: &mut Buffer) { let chart_area = match self.block { Some(ref mut b) => { b.draw(area, buf); b.inner(area) } None => *area, }; let layout = self.layout(&chart_area); let graph_area = layout.graph_area; if graph_area.width < 1 || graph_area.height < 1 { return; } self.background(&chart_area, buf, self.style.bg); if let Some((x, y)) = layout.title_x { let title = self.x_axis.title.unwrap(); buf.set_string(x, y, title, &self.x_axis.style); } if let Some((x, y)) = layout.title_y { let title = self.y_axis.title.unwrap(); buf.set_string(x, y, title, &self.y_axis.style); } if let Some(y) = layout.label_x { let labels = self.x_axis.labels.unwrap(); let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16; let labels_len = labels.len() as u16; if total_width < graph_area.width && labels_len > 1 { for (i, label) in labels.iter().enumerate() { buf.set_string( graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1) - label.as_ref().width() as u16, y, label.as_ref(), &self.x_axis.labels_style, ); } } } if let Some(x) = layout.label_y { let labels = self.y_axis.labels.unwrap(); 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() { buf.set_string( x, graph_area.bottom() - 1 - dy, label.as_ref(), &self.y_axis.labels_style, ); } } } if let Some(y) = layout.axis_x { for x in graph_area.left()..graph_area.right() { buf.get_mut(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.get_mut(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.get_mut(x, y) .set_symbol(symbols::line::BOTTOM_LEFT) .set_style(self.x_axis.style); } } for dataset in self.datasets { match dataset.marker { Marker::Dot => for &(x, y) in dataset.data.iter().filter(|&&(x, y)| { !(x < self.x_axis.bounds[0] || x > self.x_axis.bounds[1] || y < self.y_axis.bounds[0] || y > self.y_axis.bounds[1]) }) { let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1) / (self.y_axis.bounds[1] - self.y_axis.bounds[0])) as u16; let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1) / (self.x_axis.bounds[1] - self.x_axis.bounds[0])) as u16; buf.get_mut(graph_area.left() + dx, graph_area.top() + dy) .set_symbol(symbols::DOT) .set_fg(dataset.style.fg) .set_bg(dataset.style.bg); }, Marker::Braille => { Canvas::default() .background_color(self.style.bg) .x_bounds(self.x_axis.bounds) .y_bounds(self.y_axis.bounds) .paint(|ctx| { ctx.draw(&Points { coords: dataset.data, color: dataset.style.fg, }); }) .draw(&graph_area, buf); } } } if let Some(legend_area) = layout.legend_area { Block::default() .borders(Borders::ALL) .draw(&legend_area, buf); for (i, dataset) in self.datasets.iter().enumerate() { buf.set_string( legend_area.x + 1, legend_area.y + 1 + i as u16, dataset.name, &dataset.style, ); } } } }