diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index df6905f..e74c19f 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -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); @@ -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> { @@ -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::() 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() { diff --git a/tests/widgets_chart.rs b/tests/widgets_chart.rs index b5f43a3..8bbe080 100644 --- a/tests/widgets_chart.rs +++ b/tests/widgets_chart.rs @@ -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);