1use rich::measure::Measurement;
6use rich::{Console, ConsoleOptions, HorizontalAlign, Renderable, Segment, Style};
7
8use super::plot_canvas::{CellGrid, Marker, Op, View, is_ascii, raster_into, spec_style};
9use super::{Scale, ValueFormat, cell_units, cells, lines_to_segments, series_key};
10use crate::fidelity::ascii_text;
11
12mod stacked;
13
14const DEFAULT_HEIGHT: usize = 15;
17const MAX_HEIGHT: usize = u16::MAX as usize;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22pub enum AxisScale {
23 #[default]
25 Linear,
26 Log,
29}
30
31#[derive(Clone, Debug, Default, PartialEq)]
33pub enum Labels {
34 #[default]
37 None,
38 Auto(usize),
42 Values(Vec<f64>),
45 Text(Vec<String>),
48 Markup(Vec<String>),
52}
53
54#[derive(Clone, Debug, Default, PartialEq)]
67#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
68pub struct Axis {
69 bounds: Option<[f64; 2]>,
70 title: Option<String>,
71 title_style: Option<String>,
72 labels: Labels,
73 labels_align: Option<HorizontalAlign>,
74 scale: AxisScale,
75 style: Option<String>,
76 label_format: ValueFormat,
77 grid: Option<Box<GridLines>>,
80}
81
82#[derive(Clone, Debug, Default, PartialEq)]
84struct GridLines {
85 values: Vec<f64>,
86 style: Option<String>,
87 marker: Option<Marker>,
88}
89
90impl Axis {
91 pub fn bounds(mut self, min: f64, max: f64) -> Self {
94 self.bounds = Some([min, max]);
95 self
96 }
97
98 pub fn title(mut self, title: impl Into<String>) -> Self {
101 self.title = Some(title.into());
102 self
103 }
104
105 pub fn title_style(mut self, style: impl Into<String>) -> Self {
108 self.title_style = Some(style.into());
109 self
110 }
111
112 pub fn labels(mut self, labels: Labels) -> Self {
114 self.labels = labels;
115 self
116 }
117
118 pub fn labels_align(mut self, align: HorizontalAlign) -> Self {
130 self.labels_align = Some(align);
131 self
132 }
133
134 pub fn scale(mut self, scale: AxisScale) -> Self {
136 self.scale = scale;
137 self
138 }
139
140 pub fn style(mut self, style: impl Into<String>) -> Self {
143 self.style = Some(style.into());
144 self
145 }
146
147 pub fn label_format(mut self, format: ValueFormat) -> Self {
150 self.label_format = format;
151 self
152 }
153
154 pub fn grid(mut self, values: impl IntoIterator<Item = f64>) -> Self {
181 self.grid_lines().values = values.into_iter().collect();
182 self
183 }
184
185 pub fn grid_style(mut self, style: impl Into<String>) -> Self {
215 self.grid_lines().style = Some(style.into());
216 self
217 }
218
219 pub fn grid_marker(mut self, marker: Marker) -> Self {
223 self.grid_lines().marker = Some(marker);
224 self
225 }
226
227 fn grid_lines(&mut self) -> &mut GridLines {
228 self.grid.get_or_insert_with(Default::default)
229 }
230
231 pub fn label_values(&self) -> Vec<f64> {
234 let scale = self.span(std::iter::empty());
235 self.values(&scale)
236 }
237
238 fn map(&self, value: f64) -> Option<f64> {
240 match self.scale {
241 _ if !value.is_finite() => None,
242 AxisScale::Linear => Some(value),
243 AxisScale::Log if value > 0.0 => Some(value.log10()),
244 AxisScale::Log => None,
245 }
246 }
247
248 fn span(&self, data: impl Iterator<Item = f64>) -> Scale {
250 let given = self.bounds.and_then(|[a, b]| {
251 let (a, b) = (self.map(a)?, self.map(b)?);
252 Some(Scale::new(a, b))
253 });
254 match given {
255 Some(scale) => scale,
256 None => Scale::from_values(data.filter_map(|v| self.map(v))),
257 }
258 }
259
260 fn values(&self, scale: &Scale) -> Vec<f64> {
263 let inside = |m: f64| {
264 let slack = scale.span() * 1e-9;
265 m >= scale.min() - slack && m <= scale.max() + slack
266 };
267 match (&self.labels, self.scale) {
268 (Labels::Values(values), _) => values
269 .iter()
270 .copied()
271 .filter(|v| self.map(*v).is_some_and(inside))
272 .collect(),
273 (Labels::Auto(0), _) => Vec::new(),
274 (Labels::Auto(n), AxisScale::Linear) => {
275 scale.ticks(*n).into_iter().filter(|v| inside(*v)).collect()
276 }
277 (Labels::Auto(n), AxisScale::Log) => log_ticks(scale, *n),
278 _ => Vec::new(),
279 }
280 }
281
282 fn resolve(&self, data: impl Iterator<Item = f64>, console: &Console, ascii: bool) -> Resolved {
283 let scale = self.span(data);
284 let text = |s: &str| {
285 if ascii && !s.is_ascii() {
286 ascii_text(s)
287 } else {
288 s.to_string()
289 }
290 };
291 let plain = |s: &str| Label::plain(text(s));
292 let labels = match &self.labels {
293 Labels::None => Placed::None,
294 Labels::Text(labels) => Placed::Even(labels.iter().map(|l| plain(l)).collect()),
295 Labels::Markup(labels) => Placed::Even(
296 labels
297 .iter()
298 .map(|l| Label::markup(console, l, ascii))
299 .collect(),
300 ),
301 _ => Placed::At(
302 self.values(&scale)
303 .into_iter()
304 .filter_map(|v| Some((self.map(v)?, plain(&self.label_format.format(v)))))
305 .collect(),
306 ),
307 };
308 Resolved {
309 scale,
310 labels,
311 title: self.title.as_deref().map(text).filter(|t| !t.is_empty()),
312 }
313 }
314}
315
316fn log_ticks(scale: &Scale, count: usize) -> Vec<f64> {
320 let (lo, hi) = (scale.min(), scale.max());
321 let slack = (hi - lo) * 1e-9;
322 let inside = |v: f64| (lo - slack..=hi + slack).contains(&v.log10());
323 let (first, last) = (lo.floor() as i32, hi.ceil() as i32);
325 let mut values: Vec<f64> = (first..=last)
326 .map(|k| 10f64.powi(k))
327 .filter(|v| inside(*v))
328 .collect();
329 if values.len() < 2 {
330 values = (first..=last)
331 .flat_map(|k| [1.0, 2.0, 5.0].map(|m| m * 10f64.powi(k)))
332 .filter(|v| inside(*v))
333 .collect();
334 }
335 let count = count.max(2);
336 if values.len() > count {
337 let step = values.len().div_ceil(count);
338 values = values.into_iter().step_by(step).collect();
339 }
340 values
341}
342
343struct Resolved {
345 scale: Scale,
346 labels: Placed,
347 title: Option<String>,
348}
349
350enum Placed {
351 None,
352 At(Vec<(f64, Label)>),
354 Even(Vec<Label>),
356}
357
358#[derive(Clone, Debug, Default)]
361struct Label {
362 runs: Vec<(String, Option<Style>)>,
363}
364
365impl Label {
366 fn plain(text: String) -> Label {
367 Label {
368 runs: vec![(text, None)],
369 }
370 }
371
372 fn markup(console: &Console, markup: &str, ascii: bool) -> Label {
375 let text = rich::Text::from_markup(markup).unwrap_or_else(|_| rich::Text::new(markup));
376 let mut options = console.options().update_width(cells(text.plain()).max(1));
377 options.no_wrap = Some(true);
378 let mut runs = Vec::new();
379 for segment in console.render(&text, Some(&options)) {
380 if segment.control || segment.text.is_empty() {
381 continue;
382 }
383 if segment.text.contains('\n') {
384 break;
385 }
386 let run = if ascii && !segment.text.is_ascii() {
387 ascii_text(&segment.text)
388 } else {
389 segment.text.into_string()
390 };
391 runs.push((run, segment.style));
392 }
393 Label { runs }
394 }
395
396 fn width(&self) -> usize {
397 self.runs.iter().map(|(text, _)| cells(text)).sum()
398 }
399}
400
401fn over(base: &Option<Style>, own: &Option<Style>) -> Option<Style> {
403 match (base, own) {
404 (_, None) => base.clone(),
405 (None, Some(own)) => Some(own.clone()),
406 (Some(base), Some(own)) => Some(base.combine(own)),
407 }
408}
409
410impl Resolved {
411 fn grid(&self, axis: &Axis) -> Vec<f64> {
413 let (lo, hi) = (self.scale.min(), self.scale.max());
414 let slack = (hi - lo).abs() * 1e-9;
415 axis.grid
416 .iter()
417 .flat_map(|lines| lines.values.iter())
418 .filter_map(|v| axis.map(*v))
419 .filter(|m| lo.min(hi) - slack <= *m && *m <= lo.max(hi) + slack)
420 .collect()
421 }
422
423 fn has_labels(&self) -> bool {
424 !matches!(self.labels, Placed::None)
425 }
426
427 fn texts(&self) -> Vec<&Label> {
428 match &self.labels {
429 Placed::None => Vec::new(),
430 Placed::At(labels) => labels.iter().map(|(_, l)| l).collect(),
431 Placed::Even(labels) => labels.iter().collect(),
432 }
433 }
434
435 fn cell(&self, mapped: f64, cells: usize, per: usize) -> usize {
438 let t = self.scale.normalize(mapped).unwrap_or(0.0);
439 let dots = (cells * per).saturating_sub(1);
440 ((t * dots as f64) as usize / per).min(cells.saturating_sub(1))
441 }
442}
443
444#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
446pub enum GraphType {
447 #[default]
449 Scatter,
450 Line,
453 Bar,
455 Area,
460}
461
462#[derive(Clone, Debug, PartialEq)]
464#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
465pub struct Dataset {
466 name: String,
467 points: Vec<(f64, f64)>,
468 graph_type: GraphType,
469 marker: Marker,
470 style: Option<String>,
471 fill_to: f64,
472}
473
474impl Dataset {
475 pub fn new(points: impl IntoIterator<Item = (f64, f64)>) -> Self {
478 Dataset {
479 name: String::new(),
480 points: points.into_iter().collect(),
481 graph_type: GraphType::Scatter,
482 marker: Marker::Dot,
483 style: None,
484 fill_to: 0.0,
485 }
486 }
487
488 pub fn name(mut self, name: impl Into<String>) -> Self {
490 self.name = name.into();
491 self
492 }
493
494 pub fn graph_type(mut self, graph_type: GraphType) -> Self {
496 self.graph_type = graph_type;
497 self
498 }
499
500 pub fn marker(mut self, marker: Marker) -> Self {
502 self.marker = marker;
503 self
504 }
505
506 pub fn style(mut self, style: impl Into<String>) -> Self {
509 self.style = Some(style.into());
510 self
511 }
512
513 pub fn fill_to(mut self, y: f64) -> Self {
517 self.fill_to = y;
518 self
519 }
520
521 pub fn points(&self) -> &[(f64, f64)] {
523 &self.points
524 }
525}
526
527#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
529pub enum LegendPosition {
530 #[default]
532 TopRight,
533 Top,
535 TopLeft,
537 Left,
539 Right,
541 BottomLeft,
543 Bottom,
545 BottomRight,
547 None,
549}
550
551#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
574pub enum OutOfBounds {
575 #[default]
578 Clip,
579 Drop,
582}
583
584#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
605pub enum Layering {
606 #[default]
609 Merge,
610 PerDataset,
614}
615
616#[derive(Clone, Debug, PartialEq)]
675#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
676pub struct Chart {
677 datasets: Vec<Dataset>,
678 x_axis: Axis,
679 y_axis: Axis,
680 legend: LegendPosition,
681 hidden_legend: (f64, f64),
682 legend_style: Option<Box<str>>,
684 style: Option<Box<str>>,
685 legend_clear: bool,
686 out_of_bounds: OutOfBounds,
687 layering: Layering,
688 marker: Option<Marker>,
689 width: Option<usize>,
690 height: Option<usize>,
691 stacked: bool,
692}
693
694impl Default for Chart {
695 fn default() -> Self {
696 Self::new(Vec::new())
697 }
698}
699
700impl Chart {
701 pub fn new(datasets: impl IntoIterator<Item = Dataset>) -> Self {
703 Chart {
704 datasets: datasets.into_iter().collect(),
705 x_axis: Axis::default(),
706 y_axis: Axis::default(),
707 legend: LegendPosition::TopRight,
708 hidden_legend: (0.25, 0.25),
709 legend_style: None,
710 style: None,
711 legend_clear: true,
712 out_of_bounds: OutOfBounds::Clip,
713 layering: Layering::Merge,
714 marker: None,
715 width: None,
716 height: None,
717 stacked: false,
718 }
719 }
720
721 pub fn dataset(mut self, dataset: Dataset) -> Self {
723 self.datasets.push(dataset);
724 self
725 }
726
727 pub fn datasets(&self) -> &[Dataset] {
729 &self.datasets
730 }
731
732 pub fn x_axis(mut self, axis: Axis) -> Self {
734 self.x_axis = axis;
735 self
736 }
737
738 pub fn y_axis(mut self, axis: Axis) -> Self {
740 self.y_axis = axis;
741 self
742 }
743
744 pub fn legend(mut self, position: LegendPosition) -> Self {
746 self.legend = position;
747 self
748 }
749
750 pub fn hidden_legend_fraction(mut self, width: f64, height: f64) -> Self {
754 let share = |v: f64| if v.is_nan() { 0.25 } else { v.clamp(0.0, 1.0) };
755 self.hidden_legend = (share(width), share(height));
756 self
757 }
758
759 pub fn style(mut self, style: impl Into<String>) -> Self {
785 self.style = Some(style.into().into_boxed_str());
786 self
787 }
788
789 pub fn legend_style(mut self, style: impl Into<String>) -> Self {
800 self.legend_style = Some(style.into().into_boxed_str());
801 self
802 }
803
804 pub fn legend_unstyled(self) -> Self {
826 self.legend_style("none")
827 }
828
829 pub fn legend_clear(mut self, clear: bool) -> Self {
856 self.legend_clear = clear;
857 self
858 }
859
860 pub fn out_of_bounds(mut self, out_of_bounds: OutOfBounds) -> Self {
863 self.out_of_bounds = out_of_bounds;
864 self
865 }
866
867 pub fn layering(mut self, layering: Layering) -> Self {
870 self.layering = layering;
871 self
872 }
873
874 pub fn marker(mut self, marker: Marker) -> Self {
895 self.marker = Some(marker);
896 self
897 }
898
899 pub fn width(mut self, width: usize) -> Self {
901 self.width = Some(width);
902 self
903 }
904
905 pub fn height(mut self, rows: usize) -> Self {
909 self.height = Some(rows.min(MAX_HEIGHT));
910 self
911 }
912
913 fn grid(&self, console: &Console, options: &ConsoleOptions) -> CellGrid {
914 let w = self
915 .width
916 .map_or(options.max_width, |v| v.min(options.max_width))
917 .min(MAX_HEIGHT);
918 let h = self
919 .height
920 .or(options.height)
921 .unwrap_or(DEFAULT_HEIGHT)
922 .min(MAX_HEIGHT);
923 let mut grid = CellGrid::new(w, h);
924 if w == 0 || h == 0 {
925 return grid;
926 }
927 let ascii = is_ascii(console, options);
928 let points = || self.datasets.iter().flat_map(|d| d.points.iter());
929 let x = self.x_axis.resolve(points().map(|p| p.0), console, ascii);
930 let y = self.y_axis.resolve(points().map(|p| p.1), console, ascii);
931 let style_of = |spec: &Option<String>, default: &str| {
932 spec_style(console, spec.as_deref().unwrap_or(default))
933 };
934 let x_style = style_of(&self.x_axis.style, "chart.axis");
935 let y_style = style_of(&self.y_axis.style, "chart.axis");
936 let (h_line, v_line, corner) = if ascii {
937 ('-', '|', '+')
938 } else {
939 ('─', '│', '└')
940 };
941
942 let mut bottom = h - 1;
945 let label_x = (!x.texts().is_empty() && bottom > 0).then(|| {
947 bottom -= 1;
948 bottom + 1
949 });
950 let x_align = self.x_axis.labels_align;
951 let mut left = left_width(w, &x, &y, x_align);
952 let axis_x = (x.has_labels() && bottom > 0).then(|| {
953 bottom -= 1;
954 bottom + 1
955 });
956 let axis_y = (y.has_labels() && left + 1 < w).then(|| {
957 left += 1;
958 left - 1
959 });
960 let (gx, gw, gh) = (left, w - left, bottom + 1);
961 let marker = self
962 .datasets
963 .first()
964 .map_or(Marker::Dot, |d| d.marker)
965 .resolve(ascii);
966 let (per_x, per_y) = marker.per_cell();
967
968 if let Some(row) = label_x {
970 match &x.labels {
971 Placed::At(labels) => {
972 let mut free = 0;
973 for (value, label) in labels {
974 let len = label.width();
975 if len > w {
976 continue;
977 }
978 let tick = gx + x.cell(*value, gw, per_x);
979 let start = match x_align {
980 Some(HorizontalAlign::Left) => tick,
981 Some(HorizontalAlign::Right) => (tick + 1).saturating_sub(len),
982 _ => tick.saturating_sub(len / 2),
983 }
984 .min(w - len);
985 if start < free {
986 continue;
987 }
988 put_runs(&mut grid, row, start, w, label, &x_style);
989 free = start + len + 1;
990 }
991 }
992 Placed::Even(labels) if labels.len() >= 2 => {
993 let n = labels.len();
994 let between = gw / n;
995 let put = |grid: &mut CellGrid, col, width, label: &Label, align| {
996 put_aligned(grid, row, col, width, label, &x_style, align);
997 };
998 let (col, width, align) = match x_align {
1001 Some(HorizontalAlign::Center) => {
1002 (0, gx + between.min(labels[0].width()), Align::Centre)
1003 }
1004 Some(HorizontalAlign::Right) => {
1005 (gx.saturating_sub(1), between + 1, Align::Left)
1006 }
1007 _ => (0, gx, Align::Right),
1008 };
1009 put(&mut grid, col, width, &labels[0], align);
1010 for (i, label) in labels[1..n - 1].iter().enumerate() {
1011 let col = gx + (i + 1) * between + 1;
1012 put(
1013 &mut grid,
1014 col,
1015 between.saturating_sub(1),
1016 label,
1017 Align::Centre,
1018 );
1019 }
1020 put(
1021 &mut grid,
1022 w - between,
1023 between,
1024 &labels[n - 1],
1025 Align::Right,
1026 );
1027 }
1028 _ => {}
1029 }
1030 }
1031 if y.has_labels() {
1032 let width = gx.saturating_sub(1);
1033 let align = self.y_axis.labels_align.map(Align::from);
1034 match &y.labels {
1035 Placed::At(labels) => {
1036 let mut used = vec![false; gh];
1037 for (value, label) in labels {
1038 let row = gh - 1 - y.cell(*value, gh, per_y);
1039 if !std::mem::replace(&mut used[row], true) {
1040 let align = align.unwrap_or(Align::Right);
1041 put_aligned(&mut grid, row, 0, width, label, &y_style, align);
1042 }
1043 }
1044 }
1045 Placed::Even(labels) => {
1046 let n = labels.len();
1047 for (i, label) in labels.iter().enumerate() {
1048 let dy = if n > 1 { i * (gh - 1) / (n - 1) } else { 0 };
1049 if dy < gh {
1050 let row = gh - 1 - dy;
1051 let align = align.unwrap_or(Align::Left);
1052 put_aligned(&mut grid, row, 0, width, label, &y_style, align);
1053 }
1054 }
1055 }
1056 Placed::None => {}
1057 }
1058 }
1059
1060 let (x_id, y_id) = (grid.intern(x_style.as_ref()), grid.intern(y_style.as_ref()));
1062 if let Some(row) = axis_x {
1063 for col in gx..w {
1064 grid.set_char(row, col, h_line, x_id);
1065 }
1066 }
1067 if let Some(col) = axis_y {
1068 for row in 0..gh {
1069 grid.set_char(row, col, v_line, y_id);
1070 }
1071 if let Some(row) = axis_x {
1072 grid.set_char(row, col, corner, x_id);
1073 }
1074 }
1075
1076 let view = View {
1078 x: [x.scale.min(), x.scale.max()],
1079 y: [y.scale.min(), y.scale.max()],
1080 width: gw,
1081 height: gh,
1082 marker,
1083 ascii,
1084 };
1085 raster_into(&self.ops(&x, &y), &view, console, &mut grid, gx);
1086
1087 let x_title = x.title.as_ref().map(|t| (t, cells(t)));
1089 let x_title = x_title
1090 .filter(|(_, tw)| *tw < gw && gh > 2)
1091 .map(|(title, tw)| {
1092 let col = gx + gw - tw;
1093 let style = style_of(&self.x_axis.title_style, "chart.label");
1094 grid.put_text(gh - 1, col, title, style, w);
1095 (gh - 1, col, col + tw)
1096 });
1097 let y_title = y.title.as_ref().map(|t| (t, cells(t)));
1098 let y_title = y_title
1099 .filter(|(_, tw)| tw + 1 < gw && gh > 2)
1100 .map(|(title, tw)| {
1101 let style = style_of(&self.y_axis.title_style, "chart.label");
1102 grid.put_text(0, gx, title, style, w);
1103 (0, gx, gx + tw)
1104 });
1105
1106 let named: Vec<(String, Option<Style>)> = self
1108 .datasets
1109 .iter()
1110 .enumerate()
1111 .filter(|(_, d)| !d.name.is_empty())
1112 .map(|(i, d)| {
1113 let name = if ascii && !d.name.is_ascii() {
1114 ascii_text(&d.name)
1115 } else {
1116 d.name.clone()
1117 };
1118 (name, self.dataset_style(console, i))
1119 })
1120 .collect();
1121 let inner = named.iter().map(|(n, _)| cells(n)).max().unwrap_or(0);
1122 let (lw, lh) = (inner + 2, named.len() + 2);
1123 let max_w = (gw as f64 * self.hidden_legend.0).round() as usize;
1124 let max_h = (gh as f64 * self.hidden_legend.1).round() as usize;
1125 if inner > 0 && lw <= max_w && lh <= max_h {
1126 let area = Area { gx, gw, gh };
1127 if let Some((top, col)) = area.legend(self.legend, lw, lh, x_title, y_title) {
1128 let border = if ascii {
1129 ['+', '+', '+', '+', '-', '|']
1130 } else {
1131 ['┌', '┐', '└', '┘', '─', '│']
1132 };
1133 let style = match (self.legend_style.as_deref(), self.style.as_deref()) {
1134 (Some(spec), _) | (None, Some(spec)) => spec_style(console, spec),
1135 (None, None) => x_style.clone(),
1136 };
1137 let box_ = LegendBox {
1138 at: (top, col),
1139 inner,
1140 border,
1141 style,
1142 clear: self.legend_clear,
1143 };
1144 box_.draw(&mut grid, &named);
1145 }
1146 }
1147 if let Some(style) = self.style.as_deref().and_then(|s| spec_style(console, s)) {
1148 grid.underlay(&style);
1149 }
1150 grid
1151 }
1152
1153 fn dataset_style(&self, console: &Console, index: usize) -> Option<Style> {
1155 spec_style(console, &self.dataset_spec(index))
1156 }
1157
1158 fn dataset_spec(&self, index: usize) -> String {
1159 self.datasets[index]
1160 .style
1161 .clone()
1162 .unwrap_or_else(|| series_key(index))
1163 }
1164
1165 fn ops(&self, x: &Resolved, y: &Resolved) -> Vec<Op> {
1167 let within = |v: f64, s: &Scale| {
1168 let (a, b) = (s.min(), s.max());
1169 a.min(b) <= v && v <= a.max(b)
1170 };
1171 let inside = |(px, py): (f64, f64)| within(px, &x.scale) && within(py, &y.scale);
1172 let keep = |a: (f64, f64), b: (f64, f64)| {
1174 self.out_of_bounds == OutOfBounds::Clip || (inside(a) && inside(b))
1175 };
1176 let mut ops = Vec::new();
1177 let fallback = self
1181 .marker
1182 .or(self.datasets.first().map(|d| d.marker))
1183 .unwrap_or(Marker::Braille);
1184 let lines = |axis: &Axis| {
1185 let grid = axis.grid.as_deref();
1186 let spec = grid
1187 .and_then(|g| g.style.clone())
1188 .or_else(|| axis.style.clone())
1189 .unwrap_or_else(|| "chart.axis".into());
1190 let marker = grid.and_then(|g| g.marker).unwrap_or(fallback);
1191 (spec, marker)
1192 };
1193 let (left, right) = (x.scale.min(), x.scale.max());
1194 let (bottom, top) = (y.scale.min(), y.scale.max());
1195 let (y_spec, y_marker) = lines(&self.y_axis);
1196 let y_spec: super::plot_canvas::Spec = y_spec.into();
1197 let (x_spec, x_marker) = lines(&self.x_axis);
1198 let x_spec: super::plot_canvas::Spec = x_spec.into();
1199 let across = y.grid(&self.y_axis).into_iter().map(|at| {
1200 let line = Op::Line((left, at), (right, at), y_spec.clone());
1201 (line, y_marker)
1202 });
1203 let up = x.grid(&self.x_axis).into_iter().map(|at| {
1204 let line = Op::Line((at, bottom), (at, top), x_spec.clone());
1205 (line, x_marker)
1206 });
1207 let mut marker = None;
1208 for (line, line_marker) in across.chain(up) {
1209 if marker != Some(line_marker) {
1210 ops.push(Op::Marker(line_marker));
1211 marker = Some(line_marker);
1212 }
1213 ops.push(line);
1214 if self.layering == Layering::PerDataset {
1215 ops.push(Op::Layer);
1216 }
1217 }
1218 for (nth, index) in self.draw_order().into_iter().enumerate() {
1219 let dataset = &self.datasets[index];
1220 if nth > 0 && self.layering == Layering::PerDataset {
1221 ops.push(Op::Layer);
1222 }
1223 let style: super::plot_canvas::Spec = self.dataset_spec(index).into();
1224 let mapped: Vec<Option<(f64, f64)>> = dataset
1225 .points
1226 .iter()
1227 .map(|&(px, py)| Some((self.x_axis.map(px)?, self.y_axis.map(py)?)))
1228 .collect();
1229 let shown = || mapped.iter().flatten().copied();
1230 ops.push(Op::Marker(dataset.marker));
1231 ops.push(Op::Points(shown().collect(), style.clone()));
1232 match dataset.graph_type {
1233 GraphType::Scatter => {}
1234 GraphType::Line => {
1235 for pair in mapped.windows(2) {
1236 if let [Some(a), Some(b)] = pair
1237 && keep(*a, *b)
1238 {
1239 ops.push(Op::Line(*a, *b, style.clone()));
1240 }
1241 }
1242 }
1243 GraphType::Bar => {
1244 let base = match self.y_axis.scale {
1245 AxisScale::Linear => 0.0,
1246 AxisScale::Log => y.scale.min(),
1247 };
1248 for (px, py) in shown().filter(|&p| keep((p.0, base), p)) {
1249 ops.push(Op::Line((px, base), (px, py), style.clone()));
1250 }
1251 }
1252 GraphType::Area => {
1253 let level = self.y_axis.map(dataset.fill_to).unwrap_or(y.scale.min());
1254 for pair in mapped.windows(2) {
1255 if let [Some(a), Some(b)] = pair
1256 && keep(*a, *b)
1257 {
1258 ops.push(Op::Area(*a, *b, level, style.clone()));
1259 }
1260 }
1261 }
1262 }
1263 }
1264 ops
1265 }
1266}
1267
1268fn left_width(width: usize, x: &Resolved, y: &Resolved, x_align: Option<HorizontalAlign>) -> usize {
1272 let mut widest = y.texts().iter().map(|l| l.width()).max().unwrap_or(0);
1273 if let Some(first) = x.texts().first() {
1274 let first = first.width();
1275 let hangs = match (&x.labels, x_align) {
1276 (Placed::Even(_), None | Some(HorizontalAlign::Left)) => {
1278 first.saturating_sub(usize::from(y.has_labels()))
1279 }
1280 (Placed::Even(_), Some(HorizontalAlign::Right)) => 0,
1281 (_, Some(HorizontalAlign::Left)) => 0,
1282 (_, Some(HorizontalAlign::Right)) => first.saturating_sub(1),
1283 _ => first / 2,
1284 };
1285 widest = widest.max(hangs);
1286 }
1287 widest.min(width / 3)
1288}
1289
1290#[derive(Clone, Copy)]
1291enum Align {
1292 Left,
1293 Centre,
1294 Right,
1295}
1296
1297impl From<HorizontalAlign> for Align {
1298 fn from(align: HorizontalAlign) -> Align {
1299 match align {
1300 HorizontalAlign::Left => Align::Left,
1301 HorizontalAlign::Center => Align::Centre,
1302 HorizontalAlign::Right => Align::Right,
1303 }
1304 }
1305}
1306
1307fn put_runs(
1309 grid: &mut CellGrid,
1310 row: usize,
1311 col: usize,
1312 end: usize,
1313 label: &Label,
1314 base: &Option<Style>,
1315) {
1316 let mut at = col;
1317 for (text, style) in &label.runs {
1318 if at >= end {
1319 break;
1320 }
1321 grid.put_text(row, at, text, over(base, style), end);
1322 at += cells(text);
1323 }
1324}
1325
1326fn put_aligned(
1329 grid: &mut CellGrid,
1330 row: usize,
1331 col: usize,
1332 width: usize,
1333 label: &Label,
1334 base: &Option<Style>,
1335 align: Align,
1336) {
1337 if width == 0 {
1338 return;
1339 }
1340 let len = label.width();
1341 let end = col + width;
1342 if len <= width {
1343 let offset = match align {
1344 Align::Left => 0,
1345 Align::Centre => (width - len) / 2,
1346 Align::Right => width - len,
1347 };
1348 put_runs(grid, row, col + offset, end, label, base);
1349 } else if let Align::Right = align {
1350 let units: Vec<(String, Option<Style>)> = label
1352 .runs
1353 .iter()
1354 .flat_map(|(text, style)| {
1355 let style = over(base, style);
1356 cell_units(text)
1357 .into_iter()
1358 .map(move |u| (u, style.clone()))
1359 })
1360 .collect();
1361 for (i, (unit, style)) in units[len - width..].iter().enumerate() {
1362 let unit = if i == 0 && unit.is_empty() { " " } else { unit };
1363 grid.set(row, col + i, unit.to_string(), style.clone());
1364 }
1365 } else {
1366 put_runs(grid, row, col, end, label, base);
1367 }
1368}
1369
1370struct Area {
1372 gx: usize,
1373 gw: usize,
1374 gh: usize,
1375}
1376
1377type TitleSpan = (usize, usize, usize);
1379
1380impl Area {
1381 fn legend(
1384 &self,
1385 position: LegendPosition,
1386 lw: usize,
1387 lh: usize,
1388 x_title: Option<TitleSpan>,
1389 y_title: Option<TitleSpan>,
1390 ) -> Option<(usize, usize)> {
1391 self.gh
1393 .checked_sub(lh + usize::from(x_title.is_some()) + usize::from(y_title.is_some()))?;
1394 if lw > self.gw {
1395 return None;
1396 }
1397 let (right, centre) = (self.gx + self.gw - lw, self.gx + (self.gw - lw) / 2);
1398 let (middle, bottom) = ((self.gh - lh) / 2, self.gh - lh);
1399 let (mut row, col) = match position {
1400 LegendPosition::TopLeft => (0, self.gx),
1401 LegendPosition::Top => (0, centre),
1402 LegendPosition::TopRight => (0, right),
1403 LegendPosition::Left => (middle, self.gx),
1404 LegendPosition::Right => (middle, right),
1405 LegendPosition::BottomLeft => (bottom, self.gx),
1406 LegendPosition::Bottom => (bottom, centre),
1407 LegendPosition::BottomRight => (bottom, right),
1408 LegendPosition::None => return None,
1409 };
1410 let covers = |row: usize, (r, c0, c1): TitleSpan| {
1411 row <= r && r < row + lh && col < c1 && c0 < col + lw
1412 };
1413 if y_title.is_some_and(|t| covers(row, t)) {
1414 row += 1;
1415 }
1416 if x_title.is_some_and(|t| covers(row, t)) {
1417 row = row.saturating_sub(1);
1418 }
1419 Some((row, col))
1420 }
1421}
1422
1423struct LegendBox {
1426 at: (usize, usize),
1427 inner: usize,
1428 border: [char; 6],
1429 style: Option<Style>,
1430 clear: bool,
1431}
1432
1433impl LegendBox {
1434 fn draw(&self, grid: &mut CellGrid, named: &[(String, Option<Style>)]) {
1436 let ((top, left), inner, border) = (self.at, self.inner, &self.style);
1437 let [tl, tr, bl, br, h, v] = self.border;
1438 let edge = |a: char, b: char| format!("{a}{}{b}", h.to_string().repeat(inner));
1439 let right = left + inner + 2;
1440 grid.put_text(top, left, &edge(tl, tr), border.clone(), right);
1441 for (i, (name, style)) in named.iter().enumerate() {
1442 let row = top + 1 + i;
1443 grid.set(row, left, v.to_string(), border.clone());
1444 for col in left + 1..right - 1 {
1445 if self.clear {
1446 grid.set(row, col, " ".to_string(), None);
1447 } else {
1448 grid.restyle(row, col, None);
1449 }
1450 }
1451 grid.put_text(row, left + 1, name, style.clone(), right - 1);
1452 grid.set(row, right - 1, v.to_string(), border.clone());
1453 }
1454 let bottom = top + named.len() + 1;
1455 grid.put_text(bottom, left, &edge(bl, br), border.clone(), right);
1456 }
1457}
1458
1459impl Axis {
1460 fn describe(&self, name: &str, data: impl Iterator<Item = f64>) -> String {
1463 let mut out = name.to_string();
1464 if let Some(title) = self.title.as_deref().filter(|t| !t.is_empty()) {
1465 out.push_str(&format!(" ({title})"));
1466 }
1467 let shown = data.filter(|v| self.map(*v).is_some());
1468 let span = match self.bounds {
1469 Some([a, b]) => Some((a.min(b), a.max(b))),
1470 None => super::describe::extent(shown),
1471 };
1472 if let Some((lo, hi)) = span {
1473 out.push(' ');
1474 out.push_str(&super::describe::range(lo, hi, self.label_format));
1475 }
1476 if self.scale == AxisScale::Log {
1477 out.push_str(", log scale");
1478 }
1479 out
1480 }
1481}
1482
1483impl crate::a11y::AccessibleText for Chart {
1487 fn accessible_text(&self, _width: usize) -> String {
1488 use super::describe::{counted, series};
1489 if self.datasets.is_empty() {
1490 return "Chart, no datasets".to_string();
1491 }
1492 let points = || self.datasets.iter().flat_map(|d| d.points.iter());
1493 let view = self.stacked_view();
1494 let stacked_points = || view.datasets.iter().flat_map(|d| d.points.iter());
1495 let mut out = vec![format!(
1496 "Chart, {}, {}, {}{}",
1497 counted(self.datasets.len(), "dataset", "datasets"),
1498 self.x_axis.describe("x", points().map(|p| p.0)),
1499 view.y_axis.describe("y", stacked_points().map(|p| p.1)),
1500 if self.stacks() { ", areas stacked" } else { "" },
1501 )];
1502 for (index, d) in self.datasets.iter().enumerate() {
1503 let name = if d.name.is_empty() {
1504 format!("dataset {}", index + 1)
1505 } else {
1506 d.name.clone()
1507 };
1508 let kind = match d.graph_type {
1509 GraphType::Scatter => "points",
1510 GraphType::Line => "line",
1511 GraphType::Bar => "bars",
1512 GraphType::Area => "area",
1513 };
1514 let facts = series(&d.points, self.y_axis.label_format, ("point", "points"));
1515 out.push(format!("{name}, {kind}: {facts}"));
1516 }
1517 out.join("\n")
1518 }
1519}
1520
1521impl Chart {
1522 pub(crate) fn y_extent(&self) -> Option<(f64, f64)> {
1525 if let Some([a, b]) = self.y_axis.bounds {
1526 return Some((a.min(b), a.max(b)));
1527 }
1528 let view = self.stacked_view();
1529 let ys = view
1530 .datasets
1531 .iter()
1532 .flat_map(|d| d.points.iter().map(|p| p.1));
1533 super::describe::extent(ys.filter(|v| self.y_axis.map(*v).is_some()))
1534 }
1535
1536 pub(crate) fn with_y_bounds(mut self, min: f64, max: f64) -> Self {
1538 self.y_axis.bounds = Some([min, max]);
1539 self
1540 }
1541}
1542
1543impl crate::surface::Draw for Chart {
1544 fn draw(
1545 &self,
1546 console: &Console,
1547 options: &ConsoleOptions,
1548 screen: &mut crate::surface::Screen,
1549 rect: crate::surface::Rect,
1550 ) {
1551 let view = self.stacked_view();
1552 super::draw_grid(view.grid(console, options), options, screen, rect);
1553 }
1554}
1555
1556impl Renderable for Chart {
1557 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1558 let view = self.stacked_view();
1559 lines_to_segments(view.grid(console, options).into_lines(), options.max_width)
1560 }
1561
1562 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
1563 let max = self.width.unwrap_or(options.max_width);
1564 Measurement::new(max.min(12), max)
1565 .with_maximum(options.max_width)
1566 .normalize()
1567 }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572 use super::*;
1573
1574 #[test]
1575 fn accessible_text_names_the_axes_and_each_dataset() {
1576 use crate::a11y::AccessibleText;
1577 let cpu = Dataset::new([(0.0, 0.0), (5.0, 4.0), (10.0, 1.0)])
1578 .name("cpu")
1579 .graph_type(GraphType::Line);
1580 let chart = Chart::new(vec![cpu, Dataset::new([(1.0, 2.0)])])
1581 .x_axis(Axis::default().bounds(0.0, 10.0).title("t"))
1582 .y_axis(Axis::default().bounds(0.0, 4.0).title("%"));
1583 assert_eq!(
1584 chart.accessible_text(80),
1585 "Chart, 2 datasets, x (t) from 0 to 10, y (%) from 0 to 4\n\
1586 cpu, line: 3 points, min 0, max 4, last 1, trend up\n\
1587 dataset 2, points: 1 point, min 2, max 2, last 2"
1588 );
1589 let log = Chart::new(vec![
1591 Dataset::new([(1.0, 1.0), (2.0, 100.0)])
1592 .name("hz")
1593 .graph_type(GraphType::Area),
1594 ])
1595 .y_axis(Axis::default().scale(AxisScale::Log));
1596 assert_eq!(
1597 log.accessible_text(80),
1598 "Chart, 1 dataset, x from 1 to 2, y from 1 to 100, log scale\n\
1599 hz, area: 2 points, min 1, max 100, last 100, trend up"
1600 );
1601 assert_eq!(Chart::default().accessible_text(80), "Chart, no datasets");
1602 }
1603
1604 fn rows(chart: &Chart, width: usize) -> Vec<String> {
1605 let console = Console::builder().width(width).color_system(None).build();
1606 console
1607 .render_export(chart)
1608 .lines()
1609 .map(str::to_string)
1610 .collect()
1611 }
1612
1613 fn dot(points: &[(f64, f64)]) -> Dataset {
1614 Dataset::new(points.to_vec()).marker(Marker::Ascii('o'))
1615 }
1616
1617 #[test]
1618 fn log_axes_map_by_powers_of_ten() {
1619 let axis = Axis::default().bounds(1.0, 1000.0).scale(AxisScale::Log);
1620 assert_eq!(axis.map(100.0), Some(2.0));
1621 assert_eq!(axis.map(0.0), None);
1622 assert_eq!(axis.map(-5.0), None);
1623 assert_eq!(axis.map(f64::NAN), None);
1624 let labels = |axis: Axis| axis.label_values();
1625 assert_eq!(
1626 labels(axis.clone().labels(Labels::Auto(4))),
1627 vec![1.0, 10.0, 100.0, 1000.0]
1628 );
1629 assert_eq!(
1631 labels(axis.clone().labels(Labels::Auto(2))),
1632 vec![1.0, 100.0]
1633 );
1634 let narrow = Axis::default()
1636 .bounds(2.0, 60.0)
1637 .scale(AxisScale::Log)
1638 .labels(Labels::Auto(6));
1639 assert_eq!(labels(narrow), vec![2.0, 5.0, 10.0, 20.0, 50.0]);
1640 let given = axis.labels(Labels::Values(vec![-1.0, 0.0, 5.0, 5000.0]));
1642 assert_eq!(labels(given), vec![5.0]);
1643 }
1644
1645 #[test]
1646 fn a_log_axis_places_points_by_decade() {
1647 let chart = Chart::new(vec![dot(&[
1648 (1.0, 0.0),
1649 (10.0, 0.0),
1650 (100.0, 0.0),
1651 (0.0, 0.0),
1652 ])])
1653 .x_axis(
1654 Axis::default()
1655 .bounds(1.0, 100.0)
1656 .scale(AxisScale::Log)
1657 .labels(Labels::Auto(3)),
1658 )
1659 .y_axis(Axis::default().bounds(0.0, 1.0))
1660 .height(3);
1661 assert_eq!(
1662 rows(&chart, 11),
1663 vec![" ", "o o o", "───────────", "1 10 100"][1..].to_vec()
1664 );
1665 }
1666
1667 #[test]
1668 fn text_labels_are_spread_as_ratatui_spreads_them() {
1669 let chart = Chart::new(Vec::new())
1670 .x_axis(Axis::default().labels(Labels::Text(vec!["a".into(), "b".into(), "c".into()])))
1671 .y_axis(Axis::default().labels(Labels::Text(vec!["lo".into(), "hi".into()])))
1672 .height(5);
1673 assert_eq!(
1674 rows(&chart, 14),
1675 vec![
1676 "hi│ ",
1677 " │ ",
1678 "lo│ ",
1679 " └───────────",
1680 " a b c",
1681 ]
1682 );
1683 }
1684
1685 #[test]
1686 fn bars_rise_from_zero() {
1687 let chart = Chart::new(vec![
1688 dot(&[(0.0, 2.0), (2.0, 4.0), (4.0, -1.0)]).graph_type(GraphType::Bar),
1689 ])
1690 .x_axis(Axis::default().bounds(0.0, 4.0))
1691 .y_axis(Axis::default().bounds(-1.0, 4.0))
1692 .height(6)
1693 .width(5);
1694 assert_eq!(
1695 rows(&chart, 5),
1696 vec![" o ", " o ", "o o ", "o o ", "o o o", " o"]
1697 );
1698 }
1699
1700 #[test]
1701 fn bars_on_a_log_axis_rise_from_the_bottom() {
1702 let chart = Chart::new(vec![dot(&[(1.0, 100.0)]).graph_type(GraphType::Bar)])
1703 .x_axis(Axis::default().bounds(0.0, 2.0))
1704 .y_axis(Axis::default().bounds(1.0, 1000.0).scale(AxisScale::Log))
1705 .height(4)
1706 .width(3);
1707 assert_eq!(rows(&chart, 3), vec![" ", " o ", " o ", " o "]);
1708 }
1709
1710 #[test]
1711 fn lines_break_at_points_the_axes_cannot_show() {
1712 let chart = Chart::new(vec![
1713 dot(&[
1714 (0.0, 0.0),
1715 (1.0, 0.0),
1716 (f64::NAN, 0.0),
1717 (3.0, f64::INFINITY),
1718 (4.0, 0.0),
1719 (5.0, 0.0),
1720 ])
1721 .graph_type(GraphType::Line),
1722 ])
1723 .x_axis(Axis::default().bounds(0.0, 5.0))
1724 .y_axis(Axis::default().bounds(0.0, 1.0))
1725 .height(1)
1726 .width(6);
1727 assert_eq!(rows(&chart, 6), vec!["oo oo"]);
1728 }
1729
1730 fn legend_chart(position: LegendPosition) -> Chart {
1731 Chart::new(vec![
1732 dot(&[]).name("one").style("red"),
1733 dot(&[]).name("two").style("blue"),
1734 dot(&[]),
1735 ])
1736 .legend(position)
1737 .hidden_legend_fraction(1.0, 1.0)
1738 .width(9)
1739 .height(6)
1740 }
1741
1742 #[test]
1743 fn legends_go_in_the_corner_asked_for() {
1744 assert_eq!(
1745 rows(&legend_chart(LegendPosition::TopRight), 9),
1746 vec![
1747 " ┌───┐",
1748 " │one│",
1749 " │two│",
1750 " └───┘",
1751 " ",
1752 " ",
1753 ]
1754 );
1755 assert_eq!(
1756 rows(&legend_chart(LegendPosition::BottomLeft), 9)[2..],
1757 ["┌───┐ ", "│one│ ", "│two│ ", "└───┘ "]
1758 );
1759 assert_eq!(
1760 rows(&legend_chart(LegendPosition::Right), 9)[1],
1761 " ┌───┐"
1762 );
1763 assert!(
1764 rows(&legend_chart(LegendPosition::None), 9)
1765 .iter()
1766 .all(|r| r.trim().is_empty())
1767 );
1768 }
1769
1770 #[test]
1771 fn legends_keep_clear_of_titles() {
1772 let chart = legend_chart(LegendPosition::TopLeft)
1773 .y_axis(Axis::default().title("y"))
1774 .x_axis(Axis::default().title("x"));
1775 assert_eq!(
1776 rows(&chart, 9),
1777 vec![
1778 "y ",
1779 "┌───┐ ",
1780 "│one│ ",
1781 "│two│ ",
1782 "└───┘ ",
1783 " x",
1784 ]
1785 );
1786 assert!(rows(&chart.height(5), 9).iter().all(|r| !r.contains('┌')));
1788 }
1789
1790 #[test]
1791 fn legends_hide_when_they_would_take_too_much() {
1792 let shown = |w: f64, h: f64| {
1793 let chart = legend_chart(LegendPosition::TopRight).hidden_legend_fraction(w, h);
1794 rows(&chart, 9).concat().contains("one")
1795 };
1796 assert!(shown(1.0, 1.0));
1797 assert!(shown(0.56, 0.67));
1799 assert!(!shown(0.4, 1.0));
1800 assert!(!shown(1.0, 0.5));
1801 let default = legend_chart(LegendPosition::TopRight);
1803 let default = Chart {
1804 hidden_legend: (0.25, 0.25),
1805 ..default
1806 };
1807 assert!(!rows(&default, 9).concat().contains("one"));
1808 }
1809
1810 #[test]
1811 fn the_legend_names_take_their_dataset_styles() {
1812 let console = Console::builder().width(9).build();
1813 let options = console.options();
1814 let grid = legend_chart(LegendPosition::TopRight).grid(&console, &options);
1815 assert_eq!(grid.style_at(1, 5), Style::parse("red").ok());
1816 assert_eq!(grid.style_at(2, 5), Style::parse("blue").ok());
1817 }
1818
1819 #[test]
1820 fn later_datasets_win_a_cell() {
1821 let chart = Chart::new(vec![
1822 Dataset::new([(0.0, 0.0)]).style("red"),
1823 Dataset::new([(0.0, 0.0)]).style("blue"),
1824 ])
1825 .x_axis(Axis::default().bounds(0.0, 1.0))
1826 .y_axis(Axis::default().bounds(0.0, 1.0))
1827 .height(2)
1828 .width(2);
1829 let console = Console::builder().width(2).build();
1830 let grid = chart.grid(&console, &console.options());
1831 assert_eq!(grid.row_string(1), "• ");
1832 assert_eq!(grid.style_at(1, 0), Style::parse("blue").ok());
1833 }
1834
1835 fn unit_chart(datasets: Vec<Dataset>) -> Chart {
1837 Chart::new(datasets)
1838 .x_axis(Axis::default().bounds(0.0, 4.0))
1839 .y_axis(Axis::default().bounds(0.0, 2.0))
1840 .width(5)
1841 .height(3)
1842 }
1843
1844 #[test]
1845 fn segments_leaving_the_bounds_are_clipped_by_default() {
1846 let line = dot(&[(0.0, 0.0), (8.0, 2.0)]).graph_type(GraphType::Line);
1847 let chart = unit_chart(vec![line]);
1848 assert_eq!(chart.out_of_bounds, OutOfBounds::Clip);
1849 assert_eq!(rows(&chart, 5), vec![" ", " ooo", "oo "]);
1850 let explicit = chart.clone().out_of_bounds(OutOfBounds::Clip);
1851 assert_eq!(rows(&explicit, 5), rows(&chart, 5));
1852 }
1853
1854 #[test]
1855 fn dropped_segments_leave_out_any_with_an_end_outside() {
1856 let drop = |d: Dataset| rows(&unit_chart(vec![d]).out_of_bounds(OutOfBounds::Drop), 5);
1857 let line = dot(&[(0.0, 0.0), (8.0, 2.0)]).graph_type(GraphType::Line);
1859 assert_eq!(drop(line), vec![" ", " ", "o "]);
1860 let line = dot(&[(0.0, 0.0), (4.0, 2.0), (4.0, 9.0)]).graph_type(GraphType::Line);
1862 assert_eq!(drop(line), vec![" oo", " oo ", "o "]);
1863 let across = dot(&[(-1.0, 1.0), (5.0, 1.0)]).graph_type(GraphType::Line);
1865 assert_eq!(drop(across.clone()), vec![" "; 3]);
1866 assert_eq!(
1867 rows(&unit_chart(vec![across]), 5),
1868 vec![" ", "ooooo", " "]
1869 );
1870 let bar = |d: Dataset| {
1872 let chart = unit_chart(vec![d.graph_type(GraphType::Bar)])
1873 .y_axis(Axis::default().bounds(1.0, 3.0));
1874 (
1875 rows(&chart.clone(), 5),
1876 rows(&chart.out_of_bounds(OutOfBounds::Drop), 5),
1877 )
1878 };
1879 let (clipped, dropped) = bar(dot(&[(2.0, 3.0)]));
1880 assert_eq!(clipped, vec![" o "; 3]);
1881 assert_eq!(dropped, vec![" o ", " ", " "]);
1882 let area = dot(&[(0.0, 2.0), (8.0, 2.0)]).graph_type(GraphType::Area);
1884 assert_eq!(drop(area), vec!["o ", " ", " "]);
1885 }
1886
1887 fn crossing() -> Chart {
1889 Chart::new(vec![
1890 Dataset::new([(0.0, 1.0), (4.0, 1.0)])
1891 .graph_type(GraphType::Line)
1892 .marker(Marker::Braille)
1893 .style("red"),
1894 Dataset::new([(2.0, 0.0), (2.0, 2.0)])
1895 .graph_type(GraphType::Line)
1896 .marker(Marker::Braille)
1897 .style("blue"),
1898 ])
1899 .x_axis(Axis::default().bounds(0.0, 4.0))
1900 .y_axis(Axis::default().bounds(0.0, 2.0))
1901 .width(3)
1902 .height(1)
1903 }
1904
1905 #[test]
1906 fn datasets_merge_their_dots_by_default() {
1907 let console = Console::builder().width(3).build();
1908 let chart = crossing();
1909 assert_eq!(chart.layering, Layering::Merge);
1910 let grid = chart.grid(&console, &console.options());
1911 assert_eq!(grid.row_string(0), "⠒⡗⠒");
1913 assert_eq!(grid.style_at(0, 1), Style::parse("blue").ok());
1914 assert_eq!(grid.style_at(0, 0), Style::parse("red").ok());
1915 }
1916
1917 #[test]
1918 fn per_dataset_layering_draws_each_over_the_ones_before() {
1919 let console = Console::builder().width(3).build();
1920 let chart = crossing().layering(Layering::PerDataset);
1921 let grid = chart.grid(&console, &console.options());
1922 assert_eq!(grid.row_string(0), "⠒⡇⠒");
1924 assert_eq!(grid.style_at(0, 1), Style::parse("blue").ok());
1925 assert_eq!(grid.style_at(0, 2), Style::parse("red").ok());
1926 let merged = crossing().grid(&console, &console.options());
1928 assert_eq!(grid.row_string(0)[..3], merged.row_string(0)[..3]);
1929 }
1930
1931 #[test]
1932 fn the_legend_box_takes_the_x_axis_style_unless_given_its_own() {
1933 let console = Console::builder().width(9).build();
1934 let options = console.options();
1935 let chart = legend_chart(LegendPosition::TopRight).x_axis(Axis::default().style("magenta"));
1936 let grid = chart.grid(&console, &options);
1937 assert_eq!(grid.style_at(0, 4), Style::parse("magenta").ok());
1938 assert_eq!(grid.style_at(1, 4), Style::parse("magenta").ok());
1939 let own = chart.legend_style("green").grid(&console, &options);
1940 for (row, col) in [(0, 4), (0, 8), (1, 4), (1, 8), (3, 6)] {
1941 assert_eq!(own.style_at(row, col), Style::parse("green").ok());
1942 }
1943 assert_eq!(own.style_at(1, 5), Style::parse("red").ok());
1945 assert_eq!(own.style_at(2, 5), Style::parse("blue").ok());
1946 let text = |g: &CellGrid| (0..6).map(|r| g.row_string(r)).collect::<Vec<_>>();
1948 assert_eq!(text(&own), text(&grid));
1949 }
1950
1951 fn walled() -> Chart {
1953 let wall = (0..8).flat_map(|x| (0..4).map(move |y| (f64::from(x), f64::from(y))));
1954 let dots = Dataset::new(wall)
1955 .name("a")
1956 .marker(Marker::Ascii('.'))
1957 .style("red");
1958 Chart::new(vec![dots, Dataset::new([]).name("bb").style("blue")])
1959 .x_axis(Axis::default().bounds(0.0, 7.0))
1960 .y_axis(Axis::default().bounds(0.0, 3.0))
1961 .hidden_legend_fraction(1.0, 1.0)
1962 .width(8)
1963 .height(4)
1964 }
1965
1966 #[test]
1967 fn the_legend_clears_the_plot_under_it_by_default() {
1968 let chart = walled();
1969 assert!(chart.legend_clear);
1970 assert_eq!(
1971 rows(&chart, 8),
1972 vec!["....┌──┐", "....│a │", "....│bb│", "....└──┘"]
1973 );
1974 assert_eq!(rows(&chart.clone().legend_clear(true), 8), rows(&chart, 8));
1975 }
1976
1977 #[test]
1978 fn an_uncleared_legend_leaves_the_plot_unstyled_around_the_names() {
1979 let chart = walled().legend_clear(false);
1980 assert_eq!(
1981 rows(&chart, 8),
1982 vec!["....┌──┐", "....│a.│", "....│bb│", "....└──┘"]
1983 );
1984 let console = Console::builder().width(8).build();
1985 let grid = chart.grid(&console, &console.options());
1986 assert_eq!(grid.style_at(1, 6), None);
1988 assert_eq!(grid.style_at(1, 5), Style::parse("red").ok());
1989 assert_eq!(grid.style_at(1, 2), Style::parse("red").ok());
1990 }
1991
1992 #[test]
1993 fn bounds_default_to_the_data() {
1994 let chart = Chart::new(vec![dot(&[(10.0, 5.0), (20.0, 7.0)])])
1995 .height(2)
1996 .width(3);
1997 assert_eq!(rows(&chart, 3), vec![" o", "o "]);
1998 }
1999
2000 #[test]
2001 fn tiny_charts_drop_labels_then_titles() {
2002 let chart = Chart::new(vec![
2003 dot(&[(0.0, 0.0), (1.0, 1.0)])
2004 .name("series")
2005 .graph_type(GraphType::Line),
2006 ])
2007 .x_axis(
2008 Axis::default()
2009 .bounds(0.0, 1.0)
2010 .title("time")
2011 .labels(Labels::Auto(5)),
2012 )
2013 .y_axis(
2014 Axis::default()
2015 .bounds(0.0, 1.0)
2016 .title("value")
2017 .labels(Labels::Auto(5)),
2018 );
2019 for width in 0..12 {
2020 for height in 0..6 {
2021 let rendered = rows(&chart.clone().width(width).height(height), 12);
2022 assert!(rendered.len() <= height);
2023 assert!(rendered.iter().all(|r| rich::cells::cell_len(r) <= width));
2024 }
2025 }
2026 assert_eq!(
2029 rows(&chart.clone().width(8).height(2), 8),
2030 vec![" 0│ooooo", " 0 0.8"]
2031 );
2032 assert_eq!(
2034 rows(&chart.clone().width(2).height(3), 2),
2035 vec!["│o", "└─", " 0"]
2036 );
2037 assert_eq!(
2039 rows(&chart.clone().width(12).height(5), 12),
2040 vec![
2041 " 1│value oo",
2042 "0.6│ oooo ",
2043 " 0│oo time",
2044 " └────────",
2045 " 0 0.6 1",
2046 ]
2047 );
2048 let short = rows(&chart.width(12).height(4), 12).concat();
2049 assert!(!short.contains("time") && !short.contains("value"));
2050 }
2051
2052 #[test]
2053 fn ascii_consoles_draw_ascii_axes_and_legends() {
2054 let console = Console::builder()
2055 .width(10)
2056 .color_system(None)
2057 .ascii_only(true)
2058 .build();
2059 let chart = Chart::new(vec![
2060 Dataset::new([(0.0, 0.0), (1.0, 1.0)])
2061 .name("é")
2062 .graph_type(GraphType::Line),
2063 ])
2064 .x_axis(
2065 Axis::default()
2066 .bounds(0.0, 1.0)
2067 .labels(Labels::Values(vec![])),
2068 )
2069 .y_axis(
2070 Axis::default()
2071 .bounds(0.0, 1.0)
2072 .labels(Labels::Values(vec![])),
2073 )
2074 .hidden_legend_fraction(1.0, 1.0)
2075 .height(6);
2076 assert_eq!(
2077 console.render_export(&chart),
2078 concat!(
2079 "| +-+\n",
2080 "| *|?|\n",
2081 "| ** +-+\n",
2082 "| ** \n",
2083 "|* \n",
2084 "+---------\n",
2085 )
2086 );
2087 }
2088
2089 #[test]
2090 fn area_charts_fill_to_their_level() {
2091 let points = [(0.0, 1.0), (2.0, 3.0), (4.0, 2.0), (6.0, 4.0)];
2092 let area = |fill: f64| {
2093 Chart::new(vec![
2094 Dataset::new(points)
2095 .graph_type(GraphType::Area)
2096 .marker(Marker::Ascii('#'))
2097 .fill_to(fill),
2098 ])
2099 .x_axis(Axis::default().bounds(0.0, 6.0))
2100 .y_axis(Axis::default().bounds(0.0, 4.0))
2101 .height(5)
2102 .width(7)
2103 };
2104 assert_eq!(
2105 rows(&area(0.0), 7),
2106 vec![" #", " # ##", " ######", "#######", "#######"]
2107 );
2108 assert_eq!(
2111 rows(&area(2.0), 7),
2112 vec![" #", " # ##", "#######", "# ", " "]
2113 );
2114 let log = Chart::new(vec![
2116 Dataset::new([(0.0, 10.0), (2.0, 10.0)])
2117 .graph_type(GraphType::Area)
2118 .marker(Marker::Ascii('#')),
2119 ])
2120 .x_axis(Axis::default().bounds(0.0, 2.0))
2121 .y_axis(Axis::default().bounds(1.0, 100.0).scale(AxisScale::Log))
2122 .height(3)
2123 .width(3);
2124 assert_eq!(rows(&log, 3), vec![" ", "###", "###"]);
2125 }
2126
2127 #[test]
2128 fn markup_labels_keep_their_styles() {
2129 let chart = Chart::new(Vec::new())
2130 .x_axis(Axis::default().style("blue").labels(Labels::Markup(vec![
2131 "[bold]0[/]".into(),
2132 "5".into(),
2133 "[bold]10[/]".into(),
2134 ])))
2135 .height(3)
2136 .width(12);
2137 assert_eq!(
2138 rows(&chart, 12),
2139 vec![" ", " ───────────", "0 5 10"]
2140 );
2141 let console = Console::builder().width(12).build();
2142 let grid = chart.grid(&console, &console.options());
2143 let bold_blue = Style::parse("bold blue").ok();
2144 assert_eq!(grid.style_at(2, 0), bold_blue);
2145 assert_eq!(grid.style_at(2, 5), Style::parse("blue").ok());
2146 assert_eq!(grid.style_at(2, 10), bold_blue);
2147 assert_eq!(grid.style_at(2, 11), bold_blue);
2148 let broken = Chart::new(Vec::new())
2150 .y_axis(Axis::default().labels(Labels::Markup(vec!["[/x]".into(), "b".into()])))
2151 .height(2)
2152 .width(8);
2153 assert_eq!(rows(&broken, 8), vec!["b │ ", "[/│ "]);
2154 }
2155
2156 #[test]
2157 fn labels_align_as_asked() {
2158 let chart = |align: Option<HorizontalAlign>| {
2159 let mut y = Axis::default()
2160 .bounds(0.0, 100.0)
2161 .labels(Labels::Values(vec![0.0, 50.0, 100.0]));
2162 if let Some(align) = align {
2163 y = y.labels_align(align);
2164 }
2165 Chart::new(Vec::new())
2166 .y_axis(y.labels(Labels::Text(vec!["0".into(), "fifty".into(), "100".into()])))
2167 .height(3)
2168 .width(15)
2169 };
2170 assert_eq!(
2172 rows(&chart(None), 15),
2173 vec!["100 │ ", "fifty│ ", "0 │ "]
2174 );
2175 assert_eq!(
2176 rows(&chart(Some(HorizontalAlign::Right)), 15),
2177 vec![" 100│ ", "fifty│ ", " 0│ "]
2178 );
2179 assert_eq!(
2180 rows(&chart(Some(HorizontalAlign::Center)), 15),
2181 vec![" 100 │ ", "fifty│ ", " 0 │ "]
2182 );
2183 let values = |align: Option<HorizontalAlign>| {
2185 let mut y = Axis::default()
2186 .bounds(0.0, 100.0)
2187 .labels(Labels::Values(vec![0.0, 100.0]));
2188 if let Some(align) = align {
2189 y = y.labels_align(align);
2190 }
2191 rows(&Chart::new(Vec::new()).y_axis(y).height(2).width(12), 12)
2192 };
2193 assert_eq!(values(None), vec!["100│ ", " 0│ "]);
2194 assert_eq!(
2195 values(Some(HorizontalAlign::Left)),
2196 vec!["100│ ", "0 │ "]
2197 );
2198 }
2199
2200 #[test]
2201 fn the_first_x_label_sits_against_the_y_axis_as_asked() {
2202 let chart = |align: Option<HorizontalAlign>| {
2203 let mut x = Axis::default().labels(Labels::Text(vec!["start".into(), "end".into()]));
2204 if let Some(align) = align {
2205 x = x.labels_align(align);
2206 }
2207 let y = Axis::default().labels(Labels::Text(vec!["abc".into(), "d".into()]));
2208 rows(
2209 &Chart::new(Vec::new())
2210 .x_axis(x)
2211 .y_axis(y)
2212 .height(4)
2213 .width(16),
2214 16,
2215 )
2216 };
2217 assert_eq!(chart(None)[2], " └───────────");
2219 assert_eq!(chart(None)[3], "start end");
2220 assert_eq!(chart(Some(HorizontalAlign::Center))[3], " start end");
2222 assert_eq!(chart(Some(HorizontalAlign::Right))[2], " └────────────");
2223 assert_eq!(chart(Some(HorizontalAlign::Right))[3], " start end");
2224 }
2225
2226 #[test]
2227 fn a_chart_fills_the_height_it_is_given() {
2228 let chart = Chart::new(vec![
2229 dot(&[(0.0, 0.0), (1.0, 1.0)]).graph_type(GraphType::Line),
2230 ])
2231 .x_axis(Axis::default().bounds(0.0, 1.0).labels(Labels::Auto(2)))
2232 .y_axis(Axis::default().bounds(0.0, 1.0).labels(Labels::Auto(2)));
2233 let console = Console::builder().width(20).color_system(None).build();
2234 let lines = console.render_lines(&chart, &console.options().update_height(7), false);
2235 assert_eq!(lines.len(), 7);
2236 let text: Vec<String> = lines
2237 .iter()
2238 .map(|l| l.iter().map(|s| s.text.as_str()).collect())
2239 .collect();
2240 assert!(
2242 text[5].starts_with(" └") || text[5].contains('└'),
2243 "{text:?}"
2244 );
2245 assert!(text[0].contains('1') && text[4].contains('0'), "{text:?}");
2246 let fixed = chart.clone().height(4);
2248 let lines = console.render_lines(&fixed, &console.options().update_height(7), false);
2249 assert_eq!(lines.iter().filter(|l| !l.is_empty()).count(), 4);
2250 assert_eq!(console.render_export(&chart).lines().count(), 15);
2251 }
2252
2253 #[test]
2254 fn axes_without_labels_reserve_no_room_for_them() {
2255 let chart = Chart::new(vec![dot(&[(0.0, 0.0)])])
2256 .x_axis(
2257 Axis::default()
2258 .bounds(0.0, 1.0)
2259 .labels(Labels::Values(vec![])),
2260 )
2261 .y_axis(
2262 Axis::default()
2263 .bounds(0.0, 1.0)
2264 .labels(Labels::Text(vec![])),
2265 )
2266 .width(10)
2267 .height(4);
2268 assert_eq!(
2269 rows(&chart, 10),
2270 vec!["│ ", "│ ", "│o ", "└─────────"]
2271 );
2272 }
2273
2274 #[test]
2275 fn grid_lines_go_under_the_datasets_and_within_the_bounds() {
2276 let trace = Dataset::new([(0.0, 1.0), (4.0, 1.0)])
2277 .graph_type(GraphType::Line)
2278 .marker(Marker::Ascii('o'));
2279 let chart = Chart::new(vec![trace])
2280 .x_axis(Axis::default().bounds(0.0, 4.0).grid([2.0, 9.0]))
2281 .y_axis(Axis::default().bounds(0.0, 2.0).grid([-1.0]))
2282 .layering(Layering::PerDataset)
2283 .width(5)
2284 .height(3);
2285 assert_eq!(rows(&chart, 5), [" o ", "ooooo", " o "]);
2286 let log = Chart::new(vec![Dataset::new([]).marker(Marker::Ascii('+'))])
2288 .x_axis(
2289 Axis::default()
2290 .bounds(1.0, 100.0)
2291 .scale(AxisScale::Log)
2292 .grid([0.0, 10.0]),
2293 )
2294 .y_axis(Axis::default().bounds(0.0, 1.0))
2295 .width(5)
2296 .height(2);
2297 assert_eq!(rows(&log, 5), [" + ", " + "]);
2298 }
2299
2300 #[test]
2301 fn grid_lines_take_the_axis_style() {
2302 let console = Console::builder().width(3).build();
2303 let chart = Chart::new(vec![Dataset::new([]).marker(Marker::Ascii('.'))])
2304 .x_axis(Axis::default().bounds(0.0, 2.0).grid([1.0]).style("red"))
2305 .y_axis(Axis::default().bounds(0.0, 1.0))
2306 .width(3)
2307 .height(1);
2308 let grid = chart.grid(&console, &console.options());
2309 assert_eq!(grid.row_string(0), " . ");
2310 assert_eq!(grid.style_at(0, 1), Style::parse("red").ok());
2311 }
2312
2313 #[test]
2314 fn the_chart_style_is_under_everything_and_the_legends() {
2315 let console = Console::builder().width(8).build();
2316 let chart = Chart::new(vec![Dataset::new([]).name("a")])
2317 .x_axis(Axis::default().bounds(0.0, 1.0).style("red"))
2318 .y_axis(Axis::default().bounds(0.0, 1.0))
2319 .hidden_legend_fraction(1.0, 1.0)
2320 .width(8)
2321 .height(3);
2322 let grid = chart.grid(&console, &console.options());
2324 assert_eq!(grid.style_at(0, 5), Style::parse("red").ok());
2325 assert_eq!(grid.style_at(2, 0), None);
2326 let styled = chart.clone().style("on blue");
2327 let grid = styled.grid(&console, &console.options());
2328 assert_eq!(grid.style_at(0, 5), Style::parse("on blue").ok());
2329 assert_eq!(grid.style_at(2, 0), Style::parse("on blue").ok());
2330 let grid = styled
2332 .legend_style("green")
2333 .grid(&console, &console.options());
2334 assert_eq!(grid.style_at(0, 5), Style::parse("on blue green").ok());
2335 }
2336 #[test]
2337 fn crossing_grid_lines_are_layers_of_their_own_the_y_axis_first() {
2338 let cross = |layering| {
2341 Chart::new(Vec::<Dataset>::new())
2342 .x_axis(Axis::default().bounds(-1.0, 1.0).grid([0.0]))
2343 .y_axis(Axis::default().bounds(-1.0, 1.0).grid([0.0]))
2344 .layering(layering)
2345 .width(3)
2346 .height(1)
2347 };
2348 let line = |points: [(f64, f64); 2]| {
2349 Dataset::new(points)
2350 .graph_type(GraphType::Line)
2351 .marker(Marker::Braille)
2352 };
2353 let datasets = Chart::new(vec![
2354 line([(-1.0, 0.0), (1.0, 0.0)]),
2355 line([(0.0, -1.0), (0.0, 1.0)]),
2356 ])
2357 .x_axis(Axis::default().bounds(-1.0, 1.0))
2358 .y_axis(Axis::default().bounds(-1.0, 1.0))
2359 .layering(Layering::PerDataset)
2360 .width(3)
2361 .height(1);
2362 let layered = rows(&cross(Layering::PerDataset), 3);
2363 assert_eq!(layered, rows(&datasets, 3));
2364 assert_eq!(layered, ["⠒⡇⠒"]);
2365 assert_eq!(rows(&cross(Layering::Merge), 3), ["⠒⡗⠒"]);
2367 }
2368
2369 #[test]
2370 fn a_grid_with_no_datasets_is_braille_unless_told() {
2371 let empty = Chart::new(Vec::<Dataset>::new())
2372 .x_axis(Axis::default().bounds(0.0, 1.0))
2373 .y_axis(Axis::default().bounds(-1.0, 1.0).grid([0.0]))
2374 .width(3)
2375 .height(1);
2376 assert_eq!(rows(&empty, 3), ["⠒⠒⠒"]);
2377 let dotted = Chart::new(vec![Dataset::new([]).marker(Marker::Dot)])
2380 .y_axis(Axis::default().bounds(-1.0, 1.0).grid([0.0]))
2381 .x_axis(Axis::default().bounds(0.0, 1.0))
2382 .width(3)
2383 .height(1);
2384 assert_eq!(rows(&dotted, 3), ["•••"]);
2385 assert_eq!(rows(&dotted.clone().marker(Marker::Ascii('=')), 3), ["==="]);
2386 let own = dotted.marker(Marker::Ascii('=')).y_axis(
2387 Axis::default()
2388 .bounds(-1.0, 1.0)
2389 .grid([0.0])
2390 .grid_marker(Marker::Ascii('~')),
2391 );
2392 assert_eq!(rows(&own, 3), ["~~~"]);
2393 }
2394
2395 #[test]
2396 fn grid_lines_take_their_own_style_over_the_axis_style() {
2397 let console = Console::builder().width(3).build();
2398 let chart = Chart::new(vec![Dataset::new([]).marker(Marker::Ascii('.'))])
2399 .x_axis(
2400 Axis::default()
2401 .bounds(0.0, 2.0)
2402 .grid([1.0])
2403 .style("red")
2404 .grid_style("blue"),
2405 )
2406 .y_axis(Axis::default().bounds(0.0, 1.0))
2407 .width(3)
2408 .height(1);
2409 let grid = chart.grid(&console, &console.options());
2410 assert_eq!(grid.row_string(0), " . ");
2411 assert_eq!(grid.style_at(0, 1), Style::parse("blue").ok());
2412 let none = Chart::new(Vec::<Dataset>::new())
2414 .x_axis(Axis::default().bounds(0.0, 2.0).grid_style("blue"))
2415 .y_axis(Axis::default().bounds(0.0, 1.0))
2416 .width(3)
2417 .height(1);
2418 assert_eq!(rows(&none, 3), [" "]);
2419 }
2420
2421 #[test]
2422 fn an_unstyled_legend_is_the_none_style_by_name() {
2423 let console = Console::builder().width(9).build();
2424 let options = console.options();
2425 let chart = legend_chart(LegendPosition::TopRight).x_axis(Axis::default().style("magenta"));
2426 let plain = chart.clone().legend_unstyled().grid(&console, &options);
2427 let none = chart.style("none").grid(&console, &options);
2428 for (row, col) in [(0, 4), (1, 4), (3, 6)] {
2429 assert_eq!(
2430 plain.style_at(row, col).unwrap_or_default(),
2431 Style::default()
2432 );
2433 assert_eq!(plain.style_at(row, col), none.style_at(row, col));
2434 }
2435 }
2436}