1use std::{borrow::Cow, cmp::max};
2
3use unicode_width::UnicodeWidthStr;
4
5use crate::layout::Alignment;
6use crate::{
7 buffer::Buffer,
8 layout::{Constraint, Rect},
9 style::{Color, Style},
10 symbols,
11 text::{Span, Spans},
12 widgets::{
13 canvas::{Canvas, Line, Points},
14 Block, Borders, Widget,
15 },
16};
17
18#[derive(Debug, Clone)]
20pub struct Axis<'a> {
21 title: Option<Spans<'a>>,
23 bounds: [f64; 2],
25 labels: Option<Vec<Span<'a>>>,
27 style: Style,
29 labels_alignment: Alignment,
31}
32
33impl<'a> Default for Axis<'a> {
34 fn default() -> Axis<'a> {
35 Axis {
36 title: None,
37 bounds: [0.0, 0.0],
38 labels: None,
39 style: Default::default(),
40 labels_alignment: Alignment::Left,
41 }
42 }
43}
44
45impl<'a> Axis<'a> {
46 pub fn title<T>(mut self, title: T) -> Axis<'a>
47 where
48 T: Into<Spans<'a>>,
49 {
50 self.title = Some(title.into());
51 self
52 }
53
54 #[deprecated(
55 since = "0.10.0",
56 note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
57 )]
58 pub fn title_style(mut self, style: Style) -> Axis<'a> {
59 if let Some(t) = self.title {
60 let title = String::from(t);
61 self.title = Some(Spans::from(Span::styled(title, style)));
62 }
63 self
64 }
65
66 pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
67 self.bounds = bounds;
68 self
69 }
70
71 pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
72 self.labels = Some(labels);
73 self
74 }
75
76 pub fn style(mut self, style: Style) -> Axis<'a> {
77 self.style = style;
78 self
79 }
80
81 pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
86 self.labels_alignment = alignment;
87 self
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
93pub enum GraphType {
94 Scatter,
96 Line,
98}
99
100#[derive(Debug, Clone)]
102pub struct Dataset<'a> {
103 name: Cow<'a, str>,
105 data: &'a [(f64, f64)],
107 marker: symbols::Marker,
109 graph_type: GraphType,
111 style: Style,
113}
114
115impl<'a> Default for Dataset<'a> {
116 fn default() -> Dataset<'a> {
117 Dataset {
118 name: Cow::from(""),
119 data: &[],
120 marker: symbols::Marker::Dot,
121 graph_type: GraphType::Scatter,
122 style: Style::default(),
123 }
124 }
125}
126
127impl<'a> Dataset<'a> {
128 pub fn name<S>(mut self, name: S) -> Dataset<'a>
129 where
130 S: Into<Cow<'a, str>>,
131 {
132 self.name = name.into();
133 self
134 }
135
136 pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
137 self.data = data;
138 self
139 }
140
141 pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
142 self.marker = marker;
143 self
144 }
145
146 pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
147 self.graph_type = graph_type;
148 self
149 }
150
151 pub fn style(mut self, style: Style) -> Dataset<'a> {
152 self.style = style;
153 self
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Default)]
160struct ChartLayout {
161 title_x: Option<(u16, u16)>,
163 title_y: Option<(u16, u16)>,
165 label_x: Option<u16>,
167 label_y: Option<u16>,
169 axis_x: Option<u16>,
171 axis_y: Option<u16>,
173 legend_area: Option<Rect>,
175 graph_area: Rect,
177}
178
179#[derive(Debug, Clone)]
216pub struct Chart<'a> {
217 block: Option<Block<'a>>,
219 x_axis: Axis<'a>,
221 y_axis: Axis<'a>,
223 datasets: Vec<Dataset<'a>>,
225 style: Style,
227 hidden_legend_constraints: (Constraint, Constraint),
229}
230
231impl<'a> Chart<'a> {
232 pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
233 Chart {
234 block: None,
235 x_axis: Axis::default(),
236 y_axis: Axis::default(),
237 style: Default::default(),
238 datasets,
239 hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
240 }
241 }
242
243 pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
244 self.block = Some(block);
245 self
246 }
247
248 pub fn style(mut self, style: Style) -> Chart<'a> {
249 self.style = style;
250 self
251 }
252
253 pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
254 self.x_axis = axis;
255 self
256 }
257
258 pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
259 self.y_axis = axis;
260 self
261 }
262
263 pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
280 self.hidden_legend_constraints = constraints;
281 self
282 }
283
284 fn layout(&self, area: Rect) -> ChartLayout {
287 let mut layout = ChartLayout::default();
288 if area.height == 0 || area.width == 0 {
289 return layout;
290 }
291 let mut x = area.left();
292 let mut y = area.bottom() - 1;
293
294 if self.x_axis.labels.is_some() && y > area.top() {
295 layout.label_x = Some(y);
296 y -= 1;
297 }
298
299 layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
300 x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
301
302 if self.x_axis.labels.is_some() && y > area.top() {
303 layout.axis_x = Some(y);
304 y -= 1;
305 }
306
307 if self.y_axis.labels.is_some() && x + 1 < area.right() {
308 layout.axis_y = Some(x);
309 x += 1;
310 }
311
312 if x < area.right() && y > 1 {
313 layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
314 }
315
316 if let Some(ref title) = self.x_axis.title {
317 let w = title.width() as u16;
318 if w < layout.graph_area.width && layout.graph_area.height > 2 {
319 layout.title_x = Some((x + layout.graph_area.width - w, y));
320 }
321 }
322
323 if let Some(ref title) = self.y_axis.title {
324 let w = title.width() as u16;
325 if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
326 layout.title_y = Some((x, area.top()));
327 }
328 }
329
330 if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
331 let legend_width = inner_width + 2;
332 let legend_height = self.datasets.len() as u16 + 2;
333 let max_legend_width = self
334 .hidden_legend_constraints
335 .0
336 .apply(layout.graph_area.width);
337 let max_legend_height = self
338 .hidden_legend_constraints
339 .1
340 .apply(layout.graph_area.height);
341 if inner_width > 0
342 && legend_width < max_legend_width
343 && legend_height < max_legend_height
344 {
345 layout.legend_area = Some(Rect::new(
346 layout.graph_area.right() - legend_width,
347 layout.graph_area.top(),
348 legend_width,
349 legend_height,
350 ));
351 }
352 }
353 layout
354 }
355
356 fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
357 let mut max_width = self
358 .y_axis
359 .labels
360 .as_ref()
361 .map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
362 .unwrap_or_default();
363
364 if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
365 let first_label_width = first_x_label.content.width() as u16;
366 let width_left_of_y_axis = match self.x_axis.labels_alignment {
367 Alignment::Left => {
368 let y_axis_offset = if has_y_axis { 1 } else { 0 };
370 first_label_width.saturating_sub(y_axis_offset)
371 }
372 Alignment::Center => first_label_width / 2,
373 Alignment::Right => 0,
374 };
375 max_width = max(max_width, width_left_of_y_axis);
376 }
377 max_width.min(area.width / 3)
379 }
380
381 fn render_x_labels(
382 &mut self,
383 buf: &mut Buffer,
384 layout: &ChartLayout,
385 chart_area: Rect,
386 graph_area: Rect,
387 ) {
388 let y = match layout.label_x {
389 Some(y) => y,
390 None => return,
391 };
392 let labels = self.x_axis.labels.as_ref().unwrap();
393 let labels_len = labels.len() as u16;
394 if labels_len < 2 {
395 return;
396 }
397
398 let width_between_ticks = graph_area.width / labels_len;
399
400 let label_area = self.first_x_label_area(
401 y,
402 labels.first().unwrap().width() as u16,
403 width_between_ticks,
404 chart_area,
405 graph_area,
406 );
407
408 let label_alignment = match self.x_axis.labels_alignment {
409 Alignment::Left => Alignment::Right,
410 Alignment::Center => Alignment::Center,
411 Alignment::Right => Alignment::Left,
412 };
413
414 Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
415
416 for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
417 let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
419 let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
420
421 Self::render_label(buf, label, label_area, Alignment::Center);
422 }
423
424 let x = graph_area.right() - width_between_ticks;
425 let label_area = Rect::new(x, y, width_between_ticks, 1);
426 Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
428 }
429
430 fn first_x_label_area(
431 &self,
432 y: u16,
433 label_width: u16,
434 max_width_after_y_axis: u16,
435 chart_area: Rect,
436 graph_area: Rect,
437 ) -> Rect {
438 let (min_x, max_x) = match self.x_axis.labels_alignment {
439 Alignment::Left => (chart_area.left(), graph_area.left()),
440 Alignment::Center => (
441 chart_area.left(),
442 graph_area.left() + max_width_after_y_axis.min(label_width),
443 ),
444 Alignment::Right => (
445 graph_area.left().saturating_sub(1),
446 graph_area.left() + max_width_after_y_axis,
447 ),
448 };
449
450 Rect::new(min_x, y, max_x - min_x, 1)
451 }
452
453 fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
454 let label_width = label.width() as u16;
455 let bounded_label_width = label_area.width.min(label_width);
456
457 let x = match alignment {
458 Alignment::Left => label_area.left(),
459 Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
460 Alignment::Right => label_area.right() - bounded_label_width,
461 };
462
463 buf.set_span(x, label_area.top(), label, bounded_label_width);
464 }
465
466 fn render_y_labels(
467 &mut self,
468 buf: &mut Buffer,
469 layout: &ChartLayout,
470 chart_area: Rect,
471 graph_area: Rect,
472 ) {
473 let x = match layout.label_y {
474 Some(x) => x,
475 None => return,
476 };
477 let labels = self.y_axis.labels.as_ref().unwrap();
478 let labels_len = labels.len() as u16;
479 for (i, label) in labels.iter().enumerate() {
480 let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
481 if dy < graph_area.bottom() {
482 let label_area = Rect::new(
483 x,
484 graph_area.bottom().saturating_sub(1) - dy,
485 (graph_area.left() - chart_area.left()).saturating_sub(1),
486 1,
487 );
488 Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
489 }
490 }
491 }
492}
493
494impl<'a> Widget for Chart<'a> {
495 fn render(mut self, area: Rect, buf: &mut Buffer) {
496 if area.area() == 0 {
497 return;
498 }
499 buf.set_style(area, self.style);
500 let original_style = buf.get(area.left(), area.top()).style();
504
505 let chart_area = match self.block.take() {
506 Some(b) => {
507 let inner_area = b.inner(area);
508 b.render(area, buf);
509 inner_area
510 }
511 None => area,
512 };
513
514 let layout = self.layout(chart_area);
515 let graph_area = layout.graph_area;
516 if graph_area.width < 1 || graph_area.height < 1 {
517 return;
518 }
519
520 self.render_x_labels(buf, &layout, chart_area, graph_area);
521 self.render_y_labels(buf, &layout, chart_area, graph_area);
522
523 if let Some(y) = layout.axis_x {
524 for x in graph_area.left()..graph_area.right() {
525 buf.get_mut(x, y)
526 .set_symbol(symbols::line::HORIZONTAL)
527 .set_style(self.x_axis.style);
528 }
529 }
530
531 if let Some(x) = layout.axis_y {
532 for y in graph_area.top()..graph_area.bottom() {
533 buf.get_mut(x, y)
534 .set_symbol(symbols::line::VERTICAL)
535 .set_style(self.y_axis.style);
536 }
537 }
538
539 if let Some(y) = layout.axis_x {
540 if let Some(x) = layout.axis_y {
541 buf.get_mut(x, y)
542 .set_symbol(symbols::line::BOTTOM_LEFT)
543 .set_style(self.x_axis.style);
544 }
545 }
546
547 for dataset in &self.datasets {
548 Canvas::default()
549 .background_color(self.style.bg.unwrap_or(Color::Reset))
550 .x_bounds(self.x_axis.bounds)
551 .y_bounds(self.y_axis.bounds)
552 .marker(dataset.marker)
553 .paint(|ctx| {
554 ctx.draw(&Points {
555 coords: dataset.data,
556 color: dataset.style.fg.unwrap_or(Color::Reset),
557 });
558 if let GraphType::Line = dataset.graph_type {
559 for data in dataset.data.windows(2) {
560 ctx.draw(&Line {
561 x1: data[0].0,
562 y1: data[0].1,
563 x2: data[1].0,
564 y2: data[1].1,
565 color: dataset.style.fg.unwrap_or(Color::Reset),
566 })
567 }
568 }
569 })
570 .render(graph_area, buf);
571 }
572
573 if let Some(legend_area) = layout.legend_area {
574 buf.set_style(legend_area, original_style);
575 Block::default()
576 .borders(Borders::ALL)
577 .render(legend_area, buf);
578 for (i, dataset) in self.datasets.iter().enumerate() {
579 buf.set_string(
580 legend_area.x + 1,
581 legend_area.y + 1 + i as u16,
582 &dataset.name,
583 dataset.style,
584 );
585 }
586 }
587
588 if let Some((x, y)) = layout.title_x {
589 let title = self.x_axis.title.unwrap();
590 let width = graph_area.right().saturating_sub(x);
591 buf.set_style(
592 Rect {
593 x,
594 y,
595 width,
596 height: 1,
597 },
598 original_style,
599 );
600 buf.set_spans(x, y, &title, width);
601 }
602
603 if let Some((x, y)) = layout.title_y {
604 let title = self.y_axis.title.unwrap();
605 let width = graph_area.right().saturating_sub(x);
606 buf.set_style(
607 Rect {
608 x,
609 y,
610 width,
611 height: 1,
612 },
613 original_style,
614 );
615 buf.set_spans(x, y, &title, width);
616 }
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 struct LegendTestCase {
625 chart_area: Rect,
626 hidden_legend_constraints: (Constraint, Constraint),
627 legend_area: Option<Rect>,
628 }
629
630 #[test]
631 fn it_should_hide_the_legend() {
632 let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
633 let cases = [
634 LegendTestCase {
635 chart_area: Rect::new(0, 0, 100, 100),
636 hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
637 legend_area: Some(Rect::new(88, 0, 12, 12)),
638 },
639 LegendTestCase {
640 chart_area: Rect::new(0, 0, 100, 100),
641 hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
642 legend_area: None,
643 },
644 ];
645 for case in &cases {
646 let datasets = (0..10)
647 .map(|i| {
648 let name = format!("Dataset #{}", i);
649 Dataset::default().name(name).data(&data)
650 })
651 .collect::<Vec<_>>();
652 let chart = Chart::new(datasets)
653 .x_axis(Axis::default().title("X axis"))
654 .y_axis(Axis::default().title("Y axis"))
655 .hidden_legend_constraints(case.hidden_legend_constraints);
656 let layout = chart.layout(case.chart_area);
657 assert_eq!(layout.legend_area, case.legend_area);
658 }
659 }
660}