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 pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
55 self.bounds = bounds;
56 self
57 }
58
59 pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
60 self.labels = Some(labels);
61 self
62 }
63
64 pub fn style(mut self, style: Style) -> Axis<'a> {
65 self.style = style;
66 self
67 }
68
69 pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
74 self.labels_alignment = alignment;
75 self
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
81pub enum GraphType {
82 Scatter,
84 Line,
86}
87
88#[derive(Debug, Clone)]
90pub struct Dataset<'a> {
91 name: Cow<'a, str>,
93 data: &'a [(f64, f64)],
95 marker: symbols::Marker,
97 graph_type: GraphType,
99 style: Style,
101}
102
103impl<'a> Default for Dataset<'a> {
104 fn default() -> Dataset<'a> {
105 Dataset {
106 name: Cow::from(""),
107 data: &[],
108 marker: symbols::Marker::Dot,
109 graph_type: GraphType::Scatter,
110 style: Style::default(),
111 }
112 }
113}
114
115impl<'a> Dataset<'a> {
116 pub fn name<S>(mut self, name: S) -> Dataset<'a>
117 where
118 S: Into<Cow<'a, str>>,
119 {
120 self.name = name.into();
121 self
122 }
123
124 pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
125 self.data = data;
126 self
127 }
128
129 pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
130 self.marker = marker;
131 self
132 }
133
134 pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
135 self.graph_type = graph_type;
136 self
137 }
138
139 pub fn style(mut self, style: Style) -> Dataset<'a> {
140 self.style = style;
141 self
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Default)]
148struct ChartLayout {
149 title_x: Option<(u16, u16)>,
151 title_y: Option<(u16, u16)>,
153 label_x: Option<u16>,
155 label_y: Option<u16>,
157 axis_x: Option<u16>,
159 axis_y: Option<u16>,
161 legend_area: Option<Rect>,
163 graph_area: Rect,
165}
166
167#[derive(Debug, Clone)]
204pub struct Chart<'a> {
205 block: Option<Block<'a>>,
207 x_axis: Axis<'a>,
209 y_axis: Axis<'a>,
211 datasets: Vec<Dataset<'a>>,
213 style: Style,
215 hidden_legend_constraints: (Constraint, Constraint),
217}
218
219impl<'a> Chart<'a> {
220 pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
221 Chart {
222 block: None,
223 x_axis: Axis::default(),
224 y_axis: Axis::default(),
225 style: Default::default(),
226 datasets,
227 hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
228 }
229 }
230
231 pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
232 self.block = Some(block);
233 self
234 }
235
236 pub fn style(mut self, style: Style) -> Chart<'a> {
237 self.style = style;
238 self
239 }
240
241 pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
242 self.x_axis = axis;
243 self
244 }
245
246 pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
247 self.y_axis = axis;
248 self
249 }
250
251 pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
268 self.hidden_legend_constraints = constraints;
269 self
270 }
271
272 fn layout(&self, area: Rect) -> ChartLayout {
275 let mut layout = ChartLayout::default();
276 if area.height == 0 || area.width == 0 {
277 return layout;
278 }
279 let mut x = area.left();
280 let mut y = area.bottom() - 1;
281
282 if self.x_axis.labels.is_some() && y > area.top() {
283 layout.label_x = Some(y);
284 y -= 1;
285 }
286
287 layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
288 x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
289
290 if self.x_axis.labels.is_some() && y > area.top() {
291 layout.axis_x = Some(y);
292 y -= 1;
293 }
294
295 if self.y_axis.labels.is_some() && x + 1 < area.right() {
296 layout.axis_y = Some(x);
297 x += 1;
298 }
299
300 if x < area.right() && y > 1 {
301 layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
302 }
303
304 if let Some(ref title) = self.x_axis.title {
305 let w = title.width() as u16;
306 if w < layout.graph_area.width && layout.graph_area.height > 2 {
307 layout.title_x = Some((x + layout.graph_area.width - w, y));
308 }
309 }
310
311 if let Some(ref title) = self.y_axis.title {
312 let w = title.width() as u16;
313 if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
314 layout.title_y = Some((x, area.top()));
315 }
316 }
317
318 if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
319 let legend_width = inner_width + 2;
320 let legend_height = self.datasets.len() as u16 + 2;
321 let max_legend_width = self
322 .hidden_legend_constraints
323 .0
324 .apply(layout.graph_area.width);
325 let max_legend_height = self
326 .hidden_legend_constraints
327 .1
328 .apply(layout.graph_area.height);
329 if inner_width > 0
330 && legend_width < max_legend_width
331 && legend_height < max_legend_height
332 {
333 layout.legend_area = Some(Rect::new(
334 layout.graph_area.right() - legend_width,
335 layout.graph_area.top(),
336 legend_width,
337 legend_height,
338 ));
339 }
340 }
341 layout
342 }
343
344 fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
345 let mut max_width = self
346 .y_axis
347 .labels
348 .as_ref()
349 .map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
350 .unwrap_or_default();
351
352 if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
353 let first_label_width = first_x_label.content.width() as u16;
354 let width_left_of_y_axis = match self.x_axis.labels_alignment {
355 Alignment::Left => {
356 let y_axis_offset = if has_y_axis { 1 } else { 0 };
358 first_label_width.saturating_sub(y_axis_offset)
359 }
360 Alignment::Center => first_label_width / 2,
361 Alignment::Right => 0,
362 };
363 max_width = max(max_width, width_left_of_y_axis);
364 }
365 max_width.min(area.width / 3)
367 }
368
369 fn render_x_labels(
370 &mut self,
371 buf: &mut Buffer,
372 layout: &ChartLayout,
373 chart_area: Rect,
374 graph_area: Rect,
375 ) {
376 let y = match layout.label_x {
377 Some(y) => y,
378 None => return,
379 };
380 let labels = self.x_axis.labels.as_ref().unwrap();
381 let labels_len = labels.len() as u16;
382 if labels_len < 2 {
383 return;
384 }
385
386 let width_between_ticks = graph_area.width / labels_len;
387
388 let label_area = self.first_x_label_area(
389 y,
390 labels.first().unwrap().width() as u16,
391 width_between_ticks,
392 chart_area,
393 graph_area,
394 );
395
396 let label_alignment = match self.x_axis.labels_alignment {
397 Alignment::Left => Alignment::Right,
398 Alignment::Center => Alignment::Center,
399 Alignment::Right => Alignment::Left,
400 };
401
402 Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
403
404 for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
405 let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
407 let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
408
409 Self::render_label(buf, label, label_area, Alignment::Center);
410 }
411
412 let x = graph_area.right() - width_between_ticks;
413 let label_area = Rect::new(x, y, width_between_ticks, 1);
414 Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
416 }
417
418 fn first_x_label_area(
419 &self,
420 y: u16,
421 label_width: u16,
422 max_width_after_y_axis: u16,
423 chart_area: Rect,
424 graph_area: Rect,
425 ) -> Rect {
426 let (min_x, max_x) = match self.x_axis.labels_alignment {
427 Alignment::Left => (chart_area.left(), graph_area.left()),
428 Alignment::Center => (
429 chart_area.left(),
430 graph_area.left() + max_width_after_y_axis.min(label_width),
431 ),
432 Alignment::Right => (
433 graph_area.left().saturating_sub(1),
434 graph_area.left() + max_width_after_y_axis,
435 ),
436 };
437
438 Rect::new(min_x, y, max_x - min_x, 1)
439 }
440
441 fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
442 let label_width = label.width() as u16;
443 let bounded_label_width = label_area.width.min(label_width);
444
445 let x = match alignment {
446 Alignment::Left => label_area.left(),
447 Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
448 Alignment::Right => label_area.right() - bounded_label_width,
449 };
450
451 buf.set_span(x, label_area.top(), label, bounded_label_width);
452 }
453
454 fn render_y_labels(
455 &mut self,
456 buf: &mut Buffer,
457 layout: &ChartLayout,
458 chart_area: Rect,
459 graph_area: Rect,
460 ) {
461 let x = match layout.label_y {
462 Some(x) => x,
463 None => return,
464 };
465 let labels = self.y_axis.labels.as_ref().unwrap();
466 let labels_len = labels.len() as u16;
467 for (i, label) in labels.iter().enumerate() {
468 let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
469 if dy < graph_area.bottom() {
470 let label_area = Rect::new(
471 x,
472 graph_area.bottom().saturating_sub(1) - dy,
473 (graph_area.left() - chart_area.left()).saturating_sub(1),
474 1,
475 );
476 Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
477 }
478 }
479 }
480}
481
482impl<'a> Widget for Chart<'a> {
483 fn render(&mut self, area: Rect, buf: &mut Buffer) {
484 if area.area() == 0 {
485 return;
486 }
487 buf.set_style(area, self.style);
488 let original_style = buf.get(area.left(), area.top()).style();
492
493 let chart_area = match self.block.as_mut() {
494 Some(b) => {
495 let inner_area = b.inner(area);
496 b.render(area, buf);
497 inner_area
498 }
499 None => area,
500 };
501
502 let layout = self.layout(chart_area);
503 let graph_area = layout.graph_area;
504 if graph_area.width < 1 || graph_area.height < 1 {
505 return;
506 }
507
508 self.render_x_labels(buf, &layout, chart_area, graph_area);
509 self.render_y_labels(buf, &layout, chart_area, graph_area);
510
511 if let Some(y) = layout.axis_x {
512 for x in graph_area.left()..graph_area.right() {
513 buf.get_mut(x, y)
514 .set_symbol(symbols::line::HORIZONTAL)
515 .set_style(self.x_axis.style);
516 }
517 }
518
519 if let Some(x) = layout.axis_y {
520 for y in graph_area.top()..graph_area.bottom() {
521 buf.get_mut(x, y)
522 .set_symbol(symbols::line::VERTICAL)
523 .set_style(self.y_axis.style);
524 }
525 }
526
527 if let Some(y) = layout.axis_x {
528 if let Some(x) = layout.axis_y {
529 buf.get_mut(x, y)
530 .set_symbol(symbols::line::BOTTOM_LEFT)
531 .set_style(self.x_axis.style);
532 }
533 }
534
535 for dataset in &self.datasets {
536 Canvas::default()
537 .background_color(self.style.bg.unwrap_or(Color::Reset))
538 .x_bounds(self.x_axis.bounds)
539 .y_bounds(self.y_axis.bounds)
540 .marker(dataset.marker)
541 .paint(|ctx| {
542 ctx.draw(&Points {
543 coords: dataset.data,
544 color: dataset.style.fg.unwrap_or(Color::Reset),
545 });
546 if let GraphType::Line = dataset.graph_type {
547 for data in dataset.data.windows(2) {
548 ctx.draw(&Line {
549 x1: data[0].0,
550 y1: data[0].1,
551 x2: data[1].0,
552 y2: data[1].1,
553 color: dataset.style.fg.unwrap_or(Color::Reset),
554 })
555 }
556 }
557 })
558 .render(graph_area, buf);
559 }
560
561 if let Some(legend_area) = layout.legend_area {
562 buf.set_style(legend_area, original_style);
563 Block::default()
564 .borders(Borders::ALL)
565 .render(legend_area, buf);
566 for (i, dataset) in self.datasets.iter().enumerate() {
567 buf.set_string(
568 legend_area.x + 1,
569 legend_area.y + 1 + i as u16,
570 &dataset.name,
571 dataset.style,
572 );
573 }
574 }
575
576 if let Some((x, y)) = layout.title_x {
577 let title = self.x_axis.title.as_mut().unwrap();
578 let width = graph_area.right().saturating_sub(x);
579 buf.set_style(
580 Rect {
581 x,
582 y,
583 width,
584 height: 1,
585 },
586 original_style,
587 );
588 buf.set_spans(x, y, &title, width);
589 }
590
591 if let Some((x, y)) = layout.title_y {
592 let title = self.y_axis.title.as_mut().unwrap();
593 let width = graph_area.right().saturating_sub(x);
594 buf.set_style(
595 Rect {
596 x,
597 y,
598 width,
599 height: 1,
600 },
601 original_style,
602 );
603 buf.set_spans(x, y, &title, width);
604 }
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 struct LegendTestCase {
613 chart_area: Rect,
614 hidden_legend_constraints: (Constraint, Constraint),
615 legend_area: Option<Rect>,
616 }
617
618 #[test]
619 fn it_should_hide_the_legend() {
620 let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
621 let cases = [
622 LegendTestCase {
623 chart_area: Rect::new(0, 0, 100, 100),
624 hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
625 legend_area: Some(Rect::new(88, 0, 12, 12)),
626 },
627 LegendTestCase {
628 chart_area: Rect::new(0, 0, 100, 100),
629 hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),
630 legend_area: None,
631 },
632 ];
633 for case in &cases {
634 let datasets = (0..10)
635 .map(|i| {
636 let name = format!("Dataset #{}", i);
637 Dataset::default().name(name).data(&data)
638 })
639 .collect::<Vec<_>>();
640 let chart = Chart::new(datasets)
641 .x_axis(Axis::default().title("X axis"))
642 .y_axis(Axis::default().title("Y axis"))
643 .hidden_legend_constraints(case.hidden_legend_constraints);
644 let layout = chart.layout(case.chart_area);
645 assert_eq!(layout.legend_area, case.legend_area);
646 }
647 }
648}