Skip to content
This repository has been archived by the owner on Aug 6, 2023. It is now read-only.

fix(widgets/chart): remove panics with long axis labels #512

Merged
merged 2 commits into from Aug 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 80 additions & 39 deletions src/widgets/chart.rs
Expand Up @@ -296,18 +296,8 @@ impl<'a> Chart<'a> {
y -= 1;
}

if let Some(ref y_labels) = self.y_axis.labels {
let mut max_width = y_labels.iter().map(Span::width).max().unwrap_or_default() as u16;
if let Some(ref x_labels) = self.x_axis.labels {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].content.width() as u16);
}
}
if x + max_width < area.right() {
layout.label_y = Some(x);
x += max_width;
}
}
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
x += self.max_width_of_labels_left_of_y_axis(area);

if self.x_axis.labels.is_some() && y > area.top() {
layout.axis_x = Some(y);
Expand Down Expand Up @@ -362,6 +352,82 @@ impl<'a> Chart<'a> {
}
layout
}

fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
let mut max_width = self
.y_axis
.labels
.as_ref()
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
.unwrap_or_default();
if let Some(ref x_labels) = self.x_axis.labels {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].content.width() as u16);
}
}
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
max_width.min(area.width / 3)
}

fn render_x_labels(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let y = match layout.label_x {
Some(y) => y,
None => return,
};
let labels = self.x_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
if labels_len < 2 {
return;
}
let width_between_ticks = graph_area.width / (labels_len - 1);
for (i, label) in labels.iter().enumerate() {
let label_width = label.width() as u16;
let label_width = if i == 0 {
// the first label is put between the left border of the chart and the y axis.
graph_area
.left()
.saturating_sub(chart_area.left())
.min(label_width)
} else {
// other labels are put on the left of each tick on the x axis
width_between_ticks.min(label_width)
};
buf.set_span(
graph_area.left() + i as u16 * width_between_ticks - label_width,
y,
label,
label_width,
);
}
}

fn render_y_labels(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let x = match layout.label_y {
Some(x) => x,
None => return,
};
let labels = self.y_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
let label_width = graph_area.left().saturating_sub(chart_area.left());
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_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
}
}
}
}

impl<'a> Widget for Chart<'a> {
Expand Down Expand Up @@ -390,33 +456,8 @@ impl<'a> Widget for Chart<'a> {
return;
}

if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().map(Span::width).sum::<usize>() 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_span(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.content.width() as u16,
y,
label,
label.width() as u16,
);
}
}
}

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_span(x, graph_area.bottom() - 1 - dy, label, label.width() as u16);
}
}
}
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() {
Expand Down
5 changes: 1 addition & 4 deletions src/widgets/list.rs
Expand Up @@ -5,7 +5,6 @@ use crate::{
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -185,9 +184,7 @@ impl<'a> StatefulWidget for List<'a> {
state.offset = start;

let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
let blank_symbol = " ".repeat(highlight_symbol.width());

let mut current_height = 0;
let has_selection = state.selected.is_some();
Expand Down
9 changes: 2 additions & 7 deletions src/widgets/table.rs
Expand Up @@ -10,10 +10,7 @@ use cassowary::{
WeightedRelation::*,
{Expression, Solver},
};
use std::{
collections::HashMap,
iter::{self, Iterator},
};
use std::collections::HashMap;
use unicode_width::UnicodeWidthStr;

/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
Expand Down Expand Up @@ -427,9 +424,7 @@ impl<'a> StatefulWidget for Table<'a> {
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;

Expand Down
72 changes: 72 additions & 0 deletions tests/widgets_chart.rs
Expand Up @@ -47,6 +47,78 @@ fn widgets_chart_can_render_on_small_areas() {
test_case(2, 2);
}

#[test]
fn widgets_chart_handles_long_labels() {
let test_case = |x_labels, y_labels, lines| {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(2.0, 2.0)])];
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = x_labels {
x_axis = x_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
}
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = y_labels {
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
}
let chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
f.render_widget(chart, f.size());
})
.unwrap();
let expected = Buffer::with_lines(lines);
terminal.backend().assert_buffer(&expected);
};
test_case(
Some(("AAAA", "B")),
None,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "BBBB")),
None,
vec![
" ",
" ",
" ",
" ─────────",
"A BBBB",
],
);
test_case(
Some(("AAAAAAAAAAA", "B")),
None,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "B")),
Some(("CCCCCCC", "D")),
vec![
"D │ ",
" │ ",
"CCC│ ",
" └──────",
" A B",
],
);
}

#[test]
fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let backend = TestBackend::new(100, 100);
Expand Down