feat(widgets/chart): add more control on the visibility of the legend

pull/210/head
Florian Dehau 4 years ago
parent ea43413507
commit 28017f97ea

@ -30,6 +30,21 @@ pub enum Constraint {
Min(u16),
}
impl Constraint {
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => length * p / 100,
Constraint::Ratio(num, den) => {
let r = num * u32::from(length) / den;
r as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
vertical: u16,
@ -443,46 +458,51 @@ impl Rect {
}
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
}

@ -1,9 +1,9 @@
use std::cmp::max;
use std::{borrow::Cow, cmp::max};
use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::layout::{Constraint, Rect};
use crate::style::Style;
use crate::symbols;
use crate::widgets::canvas::{Canvas, Line, Points};
@ -98,7 +98,7 @@ pub enum GraphType {
/// A group of data points
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: &'a str,
name: Cow<'a, str>,
/// A reference to the actual data
data: &'a [(f64, f64)],
/// Symbol used for each points of this dataset
@ -112,7 +112,7 @@ pub struct Dataset<'a> {
impl<'a> Default for Dataset<'a> {
fn default() -> Dataset<'a> {
Dataset {
name: "",
name: Cow::from(""),
data: &[],
marker: Marker::Dot,
graph_type: GraphType::Scatter,
@ -122,8 +122,11 @@ impl<'a> Default for Dataset<'a> {
}
impl<'a> Dataset<'a> {
pub fn name(mut self, name: &'a str) -> Dataset<'a> {
self.name = name;
pub fn name<S>(mut self, name: S) -> Dataset<'a>
where
S: Into<Cow<'a, str>>,
{
self.name = name.into();
self
}
@ -150,15 +153,23 @@ impl<'a> Dataset<'a> {
/// A container that holds all the infos about where to display each elements of the chart (axis,
/// labels, legend, ...).
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
/// Location of the title of the y axis
title_y: Option<(u16, u16)>,
/// Location of the first label of the x axis
label_x: Option<u16>,
/// Location of the first label of the y axis
label_y: Option<u16>,
/// Y coordinate of the horizontal axis
axis_x: Option<u16>,
/// X coordinate of the vertical axis
axis_y: Option<u16>,
/// Area of the legend
legend_area: Option<Rect>,
/// Area of the graph
graph_area: Rect,
}
@ -226,6 +237,9 @@ where
datasets: &'a [Dataset<'a>],
/// The widget base style
style: Style,
/// Constraints used to determine whether the legend should be shown or
/// not
hidden_legend_constraints: (Constraint, Constraint),
}
impl<'a, LX, LY> Default for Chart<'a, LX, LY>
@ -240,6 +254,7 @@ where
y_axis: Axis::default(),
style: Default::default(),
datasets: &[],
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
}
}
}
@ -274,6 +289,32 @@ where
self
}
/// Set the constraints used to determine whether the legend should be shown or
/// not.
///
/// # Examples
///
/// ```
/// # use tui::widgets::Chart;
/// # use tui::layout::Constraint;
/// # fn main() {
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)
/// );
/// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart<String, String> = Chart::default()
/// .hidden_legend_constraints(constraints);
/// # }
pub fn hidden_legend_constraints(
mut self,
constraints: (Constraint, Constraint),
) -> Chart<'a, LX, LY> {
self.hidden_legend_constraints = constraints;
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 {
@ -336,9 +377,17 @@ where
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
&& inner_width > 0
let max_legend_width = self
.hidden_legend_constraints
.0
.apply(layout.graph_area.width);
let max_legend_height = self
.hidden_legend_constraints
.1
.apply(layout.graph_area.height);
if inner_width > 0
&& legend_width < max_legend_width
&& legend_height < max_legend_height
{
layout.legend_area = Some(Rect::new(
layout.graph_area.right() - legend_width,
@ -498,10 +547,53 @@ where
buf.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
dataset.name,
&dataset.name,
dataset.style,
);
}
}
}
}
#[cfg(test)]
mod tests {
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 datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
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 chart: Chart<String, String> = Chart::default()
.x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints)
.datasets(datasets.as_slice());
let layout = chart.layout(case.chart_area);
assert_eq!(layout.legend_area, case.legend_area);
}
}
}

Loading…
Cancel
Save