1use std::ops::Range;
4
5use rich::measure::Measurement;
6use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
7
8use super::axis::exact_labels;
9use super::scale::{along, clean};
10use super::{
11 Charset, Line, Scale, ValueFormat, cell_units, cells, has_colour, lines_to_segments,
12 theme_style, truncate, user_style,
13};
14
15mod stacked;
16
17const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
19const FULL_BLOCK: char = '█';
20const BRAILLE_FULL: char = '⣿';
22const BRAILLE_HALF: char = '⡇';
23const ASCII_FULL: char = '#';
25const ASCII_HALF: char = '=';
26
27const LOWER_EIGHTHS: [char; 7] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇'];
29const BRAILLE_LOWER_HALF: char = '⣤';
31const ASCII_LOWER_HALF: char = '.';
32const DEFAULT_COLUMN: usize = 8;
35const MAX_COLUMN: usize = u16::MAX as usize;
38const MAX_THICKNESS: usize = 3;
40const MAX_SIDE: usize = u16::MAX as usize;
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum Orientation {
46 #[default]
49 Horizontal,
50 Vertical,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
57struct Columns {
58 slot: usize,
60 gap: usize,
62 group_gap: usize,
64 thickness: usize,
66 labels: bool,
67 values: bool,
68 shown: usize,
70}
71
72const MIN_BAR: usize = 4;
74const DEFAULT_BAR: usize = 40;
76
77#[derive(Clone, Debug, PartialEq)]
79#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
80pub struct Bar {
81 pub label: String,
83 pub value: f64,
85 pub style: Option<String>,
88 pub text: Option<String>,
91 pub value_style: Option<String>,
94 pub label_style: Option<String>,
97}
98
99impl Bar {
100 pub fn new(label: impl Into<String>, value: f64) -> Self {
102 Bar {
103 label: label.into(),
104 value,
105 style: None,
106 text: None,
107 value_style: None,
108 label_style: None,
109 }
110 }
111
112 pub fn style(mut self, style: impl Into<String>) -> Self {
114 self.style = Some(style.into());
115 self
116 }
117
118 pub fn text(mut self, text: impl Into<String>) -> Self {
121 self.text = Some(text.into());
122 self
123 }
124
125 pub fn value_style(mut self, style: impl Into<String>) -> Self {
128 self.value_style = Some(style.into());
129 self
130 }
131
132 pub fn label_style(mut self, style: impl Into<String>) -> Self {
135 self.label_style = Some(style.into());
136 self
137 }
138}
139
140#[derive(Clone, Debug, Default, PartialEq)]
156#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
157pub struct BarGroup {
158 label: String,
159 bars: Vec<Bar>,
160 stacked: bool,
161}
162
163impl BarGroup {
164 pub fn new(label: impl Into<String>) -> Self {
166 BarGroup {
167 label: label.into(),
168 bars: Vec::new(),
169 stacked: false,
170 }
171 }
172
173 pub fn stacked(mut self) -> Self {
202 self.stacked = true;
203 self
204 }
205
206 pub fn bar(mut self, bar: Bar) -> Self {
208 self.bars.push(bar);
209 self
210 }
211
212 pub fn bars(mut self, bars: impl IntoIterator<Item = Bar>) -> Self {
214 self.bars.extend(bars);
215 self
216 }
217}
218
219#[derive(Clone, Debug, PartialEq)]
221struct Group {
222 label: String,
223 len: usize,
224 loose: bool,
226 stacked: bool,
228}
229
230#[derive(Clone, Debug, PartialEq)]
356#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
357pub struct BarChart {
358 bars: Vec<Bar>,
359 groups: Vec<Group>,
360 min: Option<f64>,
361 max: Option<f64>,
362 charset: Charset,
363 bar_width: Option<usize>,
364 show_values: bool,
365 format: ValueFormat,
366 style: Option<String>,
367 orientation: Orientation,
368 thickness: Option<usize>,
369 bar_gap: Option<usize>,
370 group_gap: Option<usize>,
371 label_style: Option<String>,
372 value_style: Option<String>,
373 values_inside: bool,
374 legend: bool,
375}
376
377impl Default for BarChart {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383impl BarChart {
384 pub fn new() -> Self {
386 BarChart {
387 bars: Vec::new(),
388 groups: Vec::new(),
389 min: None,
390 max: None,
391 charset: Charset::Auto,
392 bar_width: None,
393 show_values: true,
394 format: ValueFormat::Compact,
395 style: None,
396 orientation: Orientation::Horizontal,
397 thickness: None,
398 bar_gap: None,
399 group_gap: None,
400 label_style: None,
401 value_style: None,
402 values_inside: false,
403 legend: true,
404 }
405 }
406
407 pub fn from_pairs<L: Into<String>>(pairs: impl IntoIterator<Item = (L, f64)>) -> Self {
409 pairs
410 .into_iter()
411 .fold(Self::new(), |chart, (label, value)| chart.bar(label, value))
412 }
413
414 pub fn bar(self, label: impl Into<String>, value: f64) -> Self {
416 self.push(Bar::new(label, value))
417 }
418
419 pub fn push(mut self, bar: Bar) -> Self {
421 self.bars.push(bar);
422 match self.groups.last_mut() {
423 Some(group) if group.loose => group.len += 1,
424 Some(_) => self.groups.push(Group {
425 label: String::new(),
426 len: 1,
427 loose: true,
428 stacked: false,
429 }),
430 None => {}
431 }
432 self
433 }
434
435 pub fn group(mut self, group: BarGroup) -> Self {
438 if group.bars.is_empty() {
439 return self;
440 }
441 if self.groups.is_empty() && !self.bars.is_empty() {
442 self.groups.push(Group {
443 label: String::new(),
444 len: self.bars.len(),
445 loose: false,
446 stacked: false,
447 });
448 }
449 self.groups.push(Group {
450 label: group.label,
451 len: group.bars.len(),
452 loose: false,
453 stacked: group.stacked,
454 });
455 self.bars.extend(group.bars);
456 self
457 }
458
459 pub fn bars(&self) -> &[Bar] {
461 &self.bars
462 }
463
464 pub fn range(mut self, min: f64, max: f64) -> Self {
467 self.min = Some(min);
468 self.max = Some(max);
469 self
470 }
471
472 pub fn max(mut self, max: f64) -> Self {
474 self.max = Some(max);
475 self
476 }
477
478 pub fn charset(mut self, charset: Charset) -> Self {
480 self.charset = charset;
481 self
482 }
483
484 pub fn orientation(mut self, orientation: Orientation) -> Self {
486 self.orientation = orientation;
487 self
488 }
489
490 pub fn bar_width(mut self, width: usize) -> Self {
495 self.bar_width = Some(width.max(1));
496 self
497 }
498
499 pub fn thickness(mut self, thickness: usize) -> Self {
505 self.thickness = Some(thickness.clamp(1, MAX_SIDE));
506 self
507 }
508
509 pub fn bar_gap(mut self, gap: usize) -> Self {
512 self.bar_gap = Some(gap.min(MAX_SIDE));
513 self
514 }
515
516 pub fn group_gap(mut self, gap: usize) -> Self {
521 self.group_gap = Some(gap.min(MAX_SIDE));
522 self
523 }
524
525 pub fn show_values(mut self, show: bool) -> Self {
527 self.show_values = show;
528 self
529 }
530
531 pub fn values_inside(mut self, inside: bool) -> Self {
537 self.values_inside = inside;
538 self
539 }
540
541 pub fn format(mut self, format: ValueFormat) -> Self {
543 self.format = format;
544 self
545 }
546
547 pub fn legend(mut self, legend: bool) -> Self {
550 self.legend = legend;
551 self
552 }
553
554 pub fn style(mut self, style: impl Into<String>) -> Self {
557 self.style = Some(style.into());
558 self
559 }
560
561 pub fn label_style(mut self, style: impl Into<String>) -> Self {
564 self.label_style = Some(style.into());
565 self
566 }
567
568 pub fn value_style(mut self, style: impl Into<String>) -> Self {
570 self.value_style = Some(style.into());
571 self
572 }
573
574 fn spans(&self) -> Vec<(&str, Range<usize>)> {
577 let mut out = Vec::new();
578 let mut at = 0;
579 for group in &self.groups {
580 let end = (at + group.len).min(self.bars.len());
581 out.push((group.label.as_str(), at..end));
582 at = end;
583 }
584 if at < self.bars.len() {
585 out.push(("", at..self.bars.len()));
586 }
587 out.retain(|(_, range)| !range.is_empty());
588 out
589 }
590
591 fn scale(&self) -> Scale {
592 let values = self.bars.iter().map(|b| b.value);
593 let data = Scale::from_values(values);
594 let data = if self.bars.iter().any(|b| b.value.is_finite()) {
595 data.include_zero()
596 } else {
597 data
598 };
599 data.bounds(self.min, self.max)
600 }
601
602 fn label_width(&self) -> usize {
603 self.bars.iter().map(|b| cells(&b.label)).max().unwrap_or(0)
604 }
605
606 fn value_text(&self, bar: &Bar) -> String {
608 bar.text
609 .clone()
610 .unwrap_or_else(|| self.format.format(bar.value))
611 }
612
613 fn value_width(&self) -> usize {
614 if !self.show_values {
615 return 0;
616 }
617 self.bars
618 .iter()
619 .map(|b| cells(&self.value_text(b)))
620 .max()
621 .unwrap_or(0)
622 }
623
624 fn value_room(&self) -> usize {
627 if self.values_inside {
628 0
629 } else {
630 self.value_width()
631 }
632 }
633
634 fn natural_bar(&self) -> usize {
635 self.bar_width.unwrap_or(match self.orientation {
636 Orientation::Horizontal => DEFAULT_BAR,
637 Orientation::Vertical => DEFAULT_COLUMN,
638 })
639 }
640
641 fn gap(&self) -> usize {
642 self.bar_gap.unwrap_or(match self.orientation {
643 Orientation::Horizontal => 0,
644 Orientation::Vertical => 1,
645 })
646 }
647
648 fn group_space(&self, groups: usize) -> usize {
650 if groups < 2 {
651 return 0;
652 }
653 self.group_gap.unwrap_or(match self.orientation {
654 Orientation::Horizontal => 2,
655 Orientation::Vertical => 1,
656 })
657 }
658
659 fn columns(&self, width: usize) -> Columns {
661 let n = self.bars.len().max(1);
662 let groups = self.spans().len().max(1);
663 let label = self.label_width();
664 let value = self.value_room();
665 let gap = self.gap();
666 let group_gap = self.group_space(groups);
667 let floor = self.thickness.unwrap_or(1);
668 let gaps = (n - 1)
669 .saturating_mul(gap)
670 .saturating_add((groups - 1).saturating_mul(group_gap));
671 let fits = |slot: usize| n.saturating_mul(slot).saturating_add(gaps) <= width;
672 let full = |slot: usize, labels: bool, values: bool| Columns {
673 slot,
674 gap,
675 group_gap,
676 thickness: self
677 .thickness
678 .map_or(slot.min(MAX_THICKNESS), |t| t.min(slot)),
679 labels,
680 values,
681 shown: n,
682 };
683 let values = self.value_width() > 0;
684 let with_values = label.max(value).max(floor);
685 if fits(with_values) {
686 return full(with_values, label > 0, values);
687 }
688 let without = label.max(floor);
689 if fits(without) {
690 return full(without, label > 0, values && self.values_inside);
691 }
692 if self.thickness.is_some() || self.bar_gap.is_some() || self.group_gap.is_some() {
696 if fits(floor) {
697 let slot = (width - gaps) / n;
698 return full(slot, label > 0 && slot > 1, values && self.values_inside);
699 }
700 let mut shown = 0;
701 for (group, (_, range)) in self.spans().iter().enumerate() {
702 for index in range.clone() {
703 let left = index
704 .saturating_mul(floor.saturating_add(gap))
705 .saturating_add(group.saturating_mul(group_gap));
706 if left.saturating_add(floor) > width {
707 break;
708 }
709 shown = index + 1;
710 }
711 }
712 let shows_values = self.values_inside || value <= floor;
713 return Columns {
714 shown: shown.max(1).min(self.bars.len()),
715 ..full(floor, label > 0 && floor > 1, values && shows_values)
716 };
717 }
718 if width >= gaps.saturating_add(n) {
719 let slot = (width - gaps) / n;
720 return full(slot, label > 0 && slot > 1, false);
722 }
723 Columns {
724 slot: 1,
725 gap: 0,
726 group_gap: 0,
727 thickness: 1,
728 labels: false,
729 values: false,
730 shown: n.min(width.max(1)),
731 }
732 }
733
734 fn column_glyphs(charset: Charset, length: f64, sliver: bool) -> Vec<char> {
737 let full = length.floor() as usize;
738 let rest = length - full as f64;
739 let mut out = Vec::new();
740 match charset {
741 Charset::Ascii | Charset::Braille => {
742 let (whole, half) = if charset == Charset::Ascii {
743 (ASCII_FULL, ASCII_LOWER_HALF)
744 } else {
745 (BRAILLE_FULL, BRAILLE_LOWER_HALF)
746 };
747 let halves = (rest * 2.0).round() as usize;
748 out.extend(std::iter::repeat_n(whole, full + halves / 2));
749 if halves == 1 || (out.is_empty() && sliver) {
750 out.push(half);
751 }
752 }
753 _ => {
754 let eighths = (rest * 8.0).round() as usize;
755 out.extend(std::iter::repeat_n(FULL_BLOCK, full + eighths / 8));
756 if !eighths.is_multiple_of(8) {
757 out.push(LOWER_EIGHTHS[eighths % 8 - 1]);
758 } else if out.is_empty() && sliver {
759 out.push(LOWER_EIGHTHS[0]);
760 }
761 }
762 }
763 out
764 }
765
766 fn bar_style(&self, console: &Console, bar: &Bar, negative: bool) -> Style {
767 match (&bar.style, &self.style) {
768 (Some(s), _) => user_style(console, s),
769 (None, _) if negative => theme_style(console, "chart.negative"),
770 (None, Some(s)) => user_style(console, s),
771 (None, None) => theme_style(console, "chart.bar"),
772 }
773 }
774
775 fn layered(
777 console: &Console,
778 base: &str,
779 chart: &Option<String>,
780 bar: Option<&Option<String>>,
781 ) -> Style {
782 let mut style = theme_style(console, base);
783 for spec in [Some(chart), bar].into_iter().flatten().flatten() {
784 style = style.combine(&user_style(console, spec));
785 }
786 style
787 }
788
789 fn label_style_of(&self, console: &Console, bar: &Bar) -> Style {
790 Self::layered(
791 console,
792 "chart.label",
793 &self.label_style,
794 Some(&bar.label_style),
795 )
796 }
797
798 fn value_style_of(&self, console: &Console, bar: &Bar) -> Style {
799 Self::layered(
800 console,
801 "chart.value",
802 &self.value_style,
803 Some(&bar.value_style),
804 )
805 }
806
807 fn inside_style_of(&self, console: &Console, bar: &Bar, bar_style: &Style) -> Style {
810 if self.value_style.is_none() && bar.value_style.is_none() {
811 bar_style.combine(&Style::parse("reverse").unwrap_or_default())
812 } else {
813 self.value_style_of(console, bar)
814 }
815 }
816
817 fn group_style(&self, console: &Console) -> Style {
818 Self::layered(console, "chart.label", &self.label_style, None)
819 }
820
821 fn vertical_fit(
824 &self,
825 console: &Console,
826 width: usize,
827 charset: Charset,
828 height: Option<usize>,
829 ) -> Vec<Line> {
830 match (self.bar_width, height) {
831 (Some(rows), _) => self.vertical_lines(console, width, charset, rows.min(MAX_COLUMN)),
832 (None, None) => self.vertical_lines(console, width, charset, DEFAULT_COLUMN),
833 (None, Some(total)) => {
834 let total = total.min(MAX_COLUMN);
835 let mut rows = total.max(1);
836 loop {
837 let lines = self.vertical_lines(console, width, charset, rows);
838 if lines.len() <= total || rows == 1 {
839 return lines;
840 }
841 rows -= (lines.len() - total).min(rows - 1);
842 }
843 }
844 }
845 }
846
847 fn vertical_lines(
848 &self,
849 console: &Console,
850 width: usize,
851 charset: Charset,
852 height: usize,
853 ) -> Vec<Line> {
854 let ascii = charset == Charset::Ascii;
855 let colour = has_colour(console);
856 let layout = self.columns(width);
857 let spans = self.spans();
858 let mut lefts = Vec::with_capacity(layout.shown);
860 for (group, (_, range)) in spans.iter().enumerate() {
861 for index in range.clone().take_while(|i| *i < layout.shown) {
862 lefts.push(index * (layout.slot + layout.gap) + group * layout.group_gap);
863 }
864 }
865 let width = lefts.last().map_or(0, |l| l + layout.slot).min(width);
866 let scale = self.scale();
867 let zero = scale.normalize(0.0).unwrap_or(0.0) * height as f64;
868 let zero_row = (zero.round() as usize).min(height);
869 let full = match charset {
870 Charset::Ascii => ASCII_FULL,
871 Charset::Braille => BRAILLE_FULL,
872 _ => FULL_BLOCK,
873 };
874
875 let (bottom, top) = (-1i64, height as i64);
878 let rows = (top - bottom + 1) as usize;
879 let mut grid: Vec<Vec<(String, Option<Style>)>> =
880 vec![vec![(" ".to_string(), None); width]; rows];
881 let mut put = |row: i64, col: usize, cell: &str, style: Option<Style>| {
882 if (bottom..=top).contains(&row) && col < width {
883 grid[(top - row) as usize][col] = (cell.to_string(), style);
884 }
885 };
886 let mut used = (0i64, height as i64 - 1);
887 for (index, bar) in self.bars.iter().take(layout.shown).enumerate() {
888 let left = lefts[index];
889 let bar_left = left + (layout.slot - layout.thickness) / 2;
890 let negative = bar.value < 0.0;
891 let style = colour.then(|| self.bar_style(console, bar, negative));
892 let (value_row, base_row) = match scale.normalize(bar.value) {
895 None => (zero_row as i64, None),
896 Some(n) if negative => {
897 let start = (n * height as f64).round() as usize;
898 let len = zero_row.saturating_sub(start).max(1).min(zero_row);
899 let glyph = full.to_string();
900 for row in zero_row - len..zero_row {
901 for col in bar_left..bar_left + layout.thickness {
902 put(row as i64, col, &glyph, style.clone());
903 }
904 }
905 let base = (len > 0).then_some(zero_row as i64 - 1);
906 ((zero_row - len) as i64 - 1, base)
907 }
908 Some(n) => {
909 let length = (n * height as f64 - zero).max(0.0);
910 let glyphs = Self::column_glyphs(charset, length, bar.value > 0.0);
911 let drawn = glyphs.len().min(height - zero_row);
912 for (i, glyph) in glyphs.iter().take(drawn).enumerate() {
913 let glyph = glyph.to_string();
914 for col in bar_left..bar_left + layout.thickness {
915 put((zero_row + i) as i64, col, &glyph, style.clone());
916 }
917 }
918 let base = (drawn > 0).then_some(zero_row as i64);
919 ((zero_row + drawn) as i64, base)
920 }
921 };
922 if !layout.values {
923 continue;
924 }
925 let value = truncate(&self.value_text(bar), usize::MAX, ascii);
926 if self.values_inside {
927 let Some(row) = base_row else {
929 continue;
930 };
931 let text = if cells(&value) <= layout.thickness {
932 value
933 } else if layout.thickness > 1 {
934 truncate(&value, layout.thickness, ascii)
935 } else {
936 continue;
937 };
938 let start = bar_left + (layout.thickness - cells(&text)) / 2;
939 let value_style = colour.then(|| {
940 self.inside_style_of(console, bar, style.as_ref().unwrap_or(&Style::new()))
941 });
942 for (i, unit) in cell_units(&text).iter().enumerate() {
943 put(row, start + i, unit, value_style.clone());
944 }
945 } else {
946 let start = left + (layout.slot - cells(&value)) / 2;
947 let value_style = colour.then(|| self.value_style_of(console, bar));
948 for (i, unit) in cell_units(&value).iter().enumerate() {
949 put(value_row, start + i, unit, value_style.clone());
950 }
951 used = (used.0.min(value_row), used.1.max(value_row));
952 }
953 }
954
955 let mut out: Vec<Line> = grid
957 .into_iter()
958 .enumerate()
959 .filter(|(i, _)| (used.0..=used.1).contains(&(top - *i as i64)))
960 .map(|(_, row)| {
961 let mut line = Line::new();
962 for (cell, style) in row {
963 line.push(&cell, style);
964 }
965 line
966 })
967 .collect();
968 if layout.labels {
969 let mut line = Line::new();
970 for (index, bar) in self.bars.iter().take(layout.shown).enumerate() {
971 line.pad(lefts[index].saturating_sub(line.width()));
972 let label = truncate(&bar.label, layout.slot, ascii);
973 let pad = layout.slot - cells(&label);
974 line.pad(pad / 2);
975 line.push(&label, colour.then(|| self.label_style_of(console, bar)));
976 line.pad(pad - pad / 2);
977 }
978 line.pad(width.saturating_sub(line.width()));
979 out.push(line);
980 }
981 if spans.iter().any(|(label, _)| !label.is_empty()) {
982 let style = colour.then(|| self.group_style(console));
983 let mut line = Line::new();
984 for (label, range) in &spans {
985 let shown: Vec<usize> = range.clone().take_while(|i| *i < layout.shown).collect();
986 let (Some(&first), Some(&last)) = (shown.first(), shown.last()) else {
987 continue;
988 };
989 let (start, end) = (lefts[first], lefts[last] + layout.slot);
990 let room = end - start;
991 if room < 2 && cells(label) > room {
992 continue;
993 }
994 let text = truncate(label, room, ascii);
995 line.pad(start.saturating_sub(line.width()));
996 let pad = room - cells(&text);
997 line.pad(pad / 2);
998 line.push(&text, style.clone());
999 line.pad(pad - pad / 2);
1000 }
1001 line.pad(width.saturating_sub(line.width()));
1002 out.push(line);
1003 }
1004 out
1005 }
1006
1007 fn layout(&self, width: usize) -> (usize, usize, usize) {
1009 let mut label = self.label_width();
1010 let mut value = self.value_room();
1011 let gap = |w: usize| usize::from(w > 0);
1012 let fixed = |l: usize, v: usize| l + gap(l) + v + gap(v);
1013 let natural = self.natural_bar();
1014 let floor = MIN_BAR.min(natural);
1015 if width >= fixed(label, value) + floor {
1016 let bar = natural.min(width - fixed(label, value));
1017 return (label, bar, value);
1018 }
1019 value = 0;
1020 if width >= fixed(label, 0) + floor {
1021 let bar = natural.min(width - fixed(label, 0));
1022 return (label, bar, value);
1023 }
1024 label = width.saturating_sub(floor + 1).min(label);
1026 let bar = width
1027 .saturating_sub(fixed(label, 0))
1028 .max(1)
1029 .min(width.max(1));
1030 (label, bar, value)
1031 }
1032
1033 pub(crate) fn glyphs(charset: Charset, length: f64, sliver: bool) -> String {
1036 let full = length.floor() as usize;
1037 let rest = length - full as f64;
1038 let mut out = String::new();
1039 match charset {
1040 Charset::Ascii | Charset::Braille => {
1041 let (whole, half) = if charset == Charset::Ascii {
1042 (ASCII_FULL, ASCII_HALF)
1043 } else {
1044 (BRAILLE_FULL, BRAILLE_HALF)
1045 };
1046 let halves = (rest * 2.0).round() as usize;
1047 let full = full + halves / 2;
1048 out.extend(std::iter::repeat_n(whole, full));
1049 if halves == 1 {
1050 out.push(half);
1051 }
1052 if out.is_empty() && sliver {
1053 out.push(half);
1054 }
1055 }
1056 _ => {
1057 let eighths = (rest * 8.0).round() as usize;
1058 let full = full + eighths / 8;
1059 out.extend(std::iter::repeat_n(FULL_BLOCK, full));
1060 if !eighths.is_multiple_of(8) {
1061 out.push(EIGHTHS[eighths % 8 - 1]);
1062 }
1063 if out.is_empty() && sliver {
1064 out.push(EIGHTHS[0]);
1065 }
1066 }
1067 }
1068 out
1069 }
1070
1071 fn bar_cells(
1074 &self,
1075 bar: &Bar,
1076 scale: &Scale,
1077 charset: Charset,
1078 (bar_w, zero, zero_cell): (usize, f64, usize),
1079 style: Option<Style>,
1080 ) -> (Vec<(String, Option<Style>)>, usize, usize) {
1081 let blank = || (" ".to_string(), None);
1082 let mut row = vec![blank(); bar_w];
1083 match scale.normalize(bar.value) {
1084 None => (row, zero_cell.min(bar_w), 0),
1085 Some(n) if bar.value < 0.0 => {
1086 let start = (n * bar_w as f64).round() as usize;
1087 let len = zero_cell.saturating_sub(start).max(1).min(zero_cell);
1088 let start = zero_cell - len;
1089 let glyph = match charset {
1090 Charset::Ascii => ASCII_FULL,
1091 Charset::Braille => BRAILLE_FULL,
1092 _ => FULL_BLOCK,
1093 };
1094 for cell in &mut row[start..zero_cell.min(bar_w)] {
1095 *cell = (glyph.to_string(), style.clone());
1096 }
1097 (row, start, len)
1098 }
1099 Some(n) => {
1100 let length = (n * bar_w as f64 - zero).max(0.0);
1101 let glyphs = Self::glyphs(charset, length, bar.value > 0.0);
1102 let start = zero_cell.min(bar_w);
1103 let drawn = cells(&glyphs).min(bar_w - start);
1104 for (i, glyph) in glyphs.chars().take(drawn).enumerate() {
1105 row[start + i] = (glyph.to_string(), style.clone());
1106 }
1107 (row, start, (length.floor() as usize).min(drawn))
1108 }
1109 }
1110 }
1111
1112 fn horizontal_lines(&self, console: &Console, width: usize, charset: Charset) -> Vec<Line> {
1113 let ascii = charset == Charset::Ascii;
1114 let colour = has_colour(console);
1115 let (label_w, bar_w, value_w) = self.layout(width);
1116 let scale = self.scale();
1117 let zero = scale.normalize(0.0).unwrap_or(0.0) * bar_w as f64;
1118 let zero_cell = zero.round() as usize;
1119 let thickness = self.thickness.unwrap_or(1);
1120 let middle = thickness / 2;
1121 let spans = self.spans();
1122 let gap = self.gap();
1123 let group_gap = self.group_space(spans.len());
1124 let mut out = Vec::new();
1125 for (group, (group_label, range)) in spans.iter().enumerate() {
1126 for (nth, bar) in self.bars[range.clone()].iter().enumerate() {
1127 if nth > 0 {
1128 out.extend((0..gap).map(|_| Line::new()));
1129 }
1130 let negative = bar.value < 0.0;
1131 let style = colour.then(|| self.bar_style(console, bar, negative));
1132 let (cells_, start, whole) = self.bar_cells(
1133 bar,
1134 &scale,
1135 charset,
1136 (bar_w, zero, zero_cell),
1137 style.clone(),
1138 );
1139 for row in 0..thickness {
1140 let here = row == middle;
1141 let mut line = Line::new();
1142 if label_w > 0 {
1143 if here {
1144 let label = truncate(&bar.label, label_w, ascii);
1145 let pad = label_w - cells(&label);
1146 line.push(&label, colour.then(|| self.label_style_of(console, bar)));
1147 line.pad(pad + 1);
1148 } else {
1149 line.pad(label_w + 1);
1150 }
1151 }
1152 let mut cells_ = cells_.clone();
1153 if here && self.values_inside && self.show_values && bar.value.is_finite() {
1154 let text = truncate(&self.value_text(bar), usize::MAX, ascii);
1155 let on_bar = colour.then(|| {
1156 self.inside_style_of(
1157 console,
1158 bar,
1159 style.as_ref().unwrap_or(&Style::new()),
1160 )
1161 });
1162 let units = cell_units(&text);
1163 for (i, unit) in units.iter().enumerate() {
1164 let col = start + i;
1165 if col >= bar_w {
1166 break;
1167 }
1168 let wide =
1169 units.get(i + 1).is_some_and(String::is_empty) && !unit.is_empty();
1170 let cell_style = if i < whole {
1171 on_bar.clone()
1172 } else {
1173 style.clone()
1174 };
1175 if wide && col + 1 >= bar_w {
1176 cells_[col] = (" ".to_string(), cell_style);
1177 break;
1178 }
1179 cells_[col] = (unit.clone(), cell_style);
1180 }
1181 }
1182 for (cell, cell_style) in cells_ {
1183 line.push(&cell, cell_style);
1184 }
1185 if value_w > 0 {
1186 if here {
1187 let value = truncate(&self.value_text(bar), value_w, ascii);
1189 line.pad(1 + value_w - cells(&value));
1190 line.push(&value, colour.then(|| self.value_style_of(console, bar)));
1191 } else {
1192 line.pad(1 + value_w);
1193 }
1194 }
1195 out.push(line);
1196 }
1197 }
1198 let mut space = if group + 1 < spans.len() {
1201 gap + group_gap
1202 } else {
1203 0
1204 };
1205 if !group_label.is_empty() {
1206 let mut line = Line::new();
1207 if label_w > 0 {
1208 line.pad(label_w + 1);
1209 }
1210 let room = bar_w + if value_w > 0 { value_w + 1 } else { 0 };
1211 line.push(
1212 &truncate(group_label, room, ascii),
1213 colour.then(|| self.group_style(console)),
1214 );
1215 out.push(line);
1216 space = space.saturating_sub(1);
1217 }
1218 out.extend((0..space).map(|_| Line::new()));
1219 }
1220 out
1221 }
1222
1223 pub(crate) fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Line> {
1224 let width = options.max_width;
1225 let charset = self.charset.resolve(console, options, Charset::Blocks);
1226 let ascii = charset == Charset::Ascii;
1227 if self.bars.is_empty() {
1228 let mut line = Line::new();
1229 line.push(&truncate("no data", width, ascii), None);
1230 return vec![line];
1231 }
1232 if self.is_stacked() {
1233 return self.stacked_lines(console, width, charset, options.height);
1234 }
1235 match self.orientation {
1236 Orientation::Vertical => self.vertical_fit(console, width, charset, options.height),
1237 Orientation::Horizontal => self.horizontal_lines(console, width, charset),
1238 }
1239 }
1240
1241 fn natural_width(&self) -> usize {
1242 if self.is_stacked() {
1243 return self.totals().natural_width();
1244 }
1245 if self.orientation == Orientation::Vertical {
1246 let n = self.bars.len();
1247 let groups = self.spans().len().max(1);
1248 let slot = self
1249 .label_width()
1250 .max(self.value_room())
1251 .max(self.thickness.unwrap_or(1));
1252 let gaps = n
1253 .saturating_sub(1)
1254 .saturating_mul(self.gap())
1255 .saturating_add((groups - 1).saturating_mul(self.group_space(groups)));
1256 return n.saturating_mul(slot).saturating_add(gaps);
1257 }
1258 let l = self.label_width();
1259 let v = self.value_room();
1260 (l + usize::from(l > 0) + v + usize::from(v > 0)).saturating_add(self.natural_bar())
1261 }
1262}
1263
1264impl crate::surface::Draw for BarChart {
1265 fn draw(
1266 &self,
1267 console: &Console,
1268 options: &ConsoleOptions,
1269 screen: &mut crate::surface::Screen,
1270 rect: crate::surface::Rect,
1271 ) {
1272 super::draw_lines(
1273 self.render_lines(console, options),
1274 options.max_width,
1275 options,
1276 screen,
1277 rect,
1278 );
1279 }
1280}
1281
1282impl Renderable for BarChart {
1283 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1284 lines_to_segments(self.render_lines(console, options), options.max_width)
1285 }
1286
1287 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
1288 if self.bars.is_empty() {
1289 return Measurement::new(7, 7).with_maximum(options.max_width);
1290 }
1291 let max = self.natural_width();
1292 let min = match self.orientation {
1293 Orientation::Horizontal => MIN_BAR,
1294 Orientation::Vertical if self.is_stacked() => self.stacks().len(),
1295 Orientation::Vertical => self.bars.len(),
1296 };
1297 Measurement::new(min.min(max), max)
1298 .with_maximum(options.max_width)
1299 .normalize()
1300 }
1301}
1302
1303impl crate::a11y::AccessibleText for BarChart {
1307 fn accessible_text(&self, _width: usize) -> String {
1308 if self.is_stacked() {
1309 return self.stacked_facts();
1310 }
1311 self.facts("Bar chart", ("bar", "bars"))
1312 }
1313}
1314
1315impl BarChart {
1316 pub(crate) fn value_extent(&self) -> Option<(f64, f64)> {
1319 let any = self.bars.iter().any(|b| b.value.is_finite());
1320 if !any && self.min.is_none() && self.max.is_none() {
1321 return None;
1322 }
1323 let scale = if self.is_stacked() {
1324 self.stacked_scale()
1325 } else {
1326 self.scale()
1327 };
1328 Some((scale.min(), scale.max()))
1329 }
1330
1331 fn facts(&self, what: &str, noun: (&str, &str)) -> String {
1333 use super::describe::{counted, extent, range, ranked};
1334 if self.bars.is_empty() {
1335 return format!("{what}, no {}", noun.1);
1336 }
1337 let mut head = format!("{what}, {}", counted(self.bars.len(), noun.0, noun.1));
1338 let groups: Vec<(&str, Range<usize>)> = self
1339 .spans()
1340 .into_iter()
1341 .filter(|_| !self.groups.is_empty())
1342 .collect();
1343 if groups.len() > 1 {
1344 head.push_str(&format!(" in {} groups", groups.len()));
1345 }
1346 if let Some((lo, hi)) = extent(self.bars.iter().map(|b| b.value)) {
1347 head.push_str(&format!(", values {}", range(lo, hi, self.format)));
1348 }
1349 let missing = self.bars.iter().filter(|b| !b.value.is_finite()).count();
1350 if missing > 0 {
1351 head.push_str(&format!(
1352 ", {} without a value",
1353 super::describe::count(missing)
1354 ));
1355 }
1356 let group_of = |i: usize| {
1357 groups
1358 .iter()
1359 .find(|(_, r)| r.contains(&i))
1360 .map_or("", |(label, _)| label)
1361 };
1362 let entries: Vec<(String, String, f64)> = self
1363 .bars
1364 .iter()
1365 .enumerate()
1366 .map(|(i, bar)| {
1367 let name = match group_of(i) {
1368 "" => bar.label.clone(),
1369 group => format!("{group} {}", bar.label).trim_end().to_string(),
1370 };
1371 let text = if bar.value.is_finite() {
1372 self.value_text(bar)
1373 } else {
1374 "no value".to_string()
1375 };
1376 (name, text, bar.value)
1377 })
1378 .collect();
1379 let mut out = vec![head];
1380 out.extend(ranked(&entries, 6));
1381 out.join("\n")
1382 }
1383}
1384
1385#[derive(Clone, Debug, PartialEq)]
1411#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
1412pub struct Histogram {
1413 values: Vec<f64>,
1414 bins: usize,
1415 min: Option<f64>,
1416 max: Option<f64>,
1417 chart: BarChart,
1418}
1419
1420impl Histogram {
1421 pub fn new(values: impl IntoIterator<Item = f64>) -> Self {
1423 Histogram {
1424 values: values.into_iter().collect(),
1425 bins: 10,
1426 min: None,
1427 max: None,
1428 chart: BarChart::new(),
1429 }
1430 }
1431
1432 pub fn bins(mut self, bins: usize) -> Self {
1434 self.bins = bins.max(1);
1435 self
1436 }
1437
1438 pub fn range(mut self, min: f64, max: f64) -> Self {
1440 self.min = Some(min);
1441 self.max = Some(max);
1442 self
1443 }
1444
1445 pub fn charset(mut self, charset: Charset) -> Self {
1447 self.chart = self.chart.charset(charset);
1448 self
1449 }
1450
1451 pub fn orientation(mut self, orientation: Orientation) -> Self {
1453 self.chart = self.chart.orientation(orientation);
1454 self
1455 }
1456
1457 pub fn bar_width(mut self, width: usize) -> Self {
1460 self.chart = self.chart.bar_width(width);
1461 self
1462 }
1463
1464 pub fn show_values(mut self, show: bool) -> Self {
1466 self.chart = self.chart.show_values(show);
1467 self
1468 }
1469
1470 pub fn style(mut self, style: impl Into<String>) -> Self {
1472 self.chart = self.chart.style(style);
1473 self
1474 }
1475
1476 pub fn edges(&self) -> Vec<f64> {
1485 let data = Scale::from_values(self.values.iter().copied());
1486 let bins = self.bins as f64;
1487 let split = |scale: Scale| -> Vec<f64> {
1488 (0..=self.bins)
1489 .map(|i| clean(scale.lerp(i as f64 / bins)))
1490 .collect()
1491 };
1492 if self.min.is_some() || self.max.is_some() {
1493 let scale = data.bounds(self.min, self.max);
1494 let step = scale.span() / bins;
1495 if (scale.max() - scale.min()).is_finite() {
1496 return (0..=self.bins)
1497 .map(|i| clean(scale.min() + step * i as f64))
1498 .collect();
1499 }
1500 return split(scale);
1501 }
1502 let mut step = bin_step(data.span() / bins);
1503 let mut start = (data.min() / step).floor() * step;
1504 while step.is_finite() && along(start, bins, step) < data.max() {
1506 step = bin_step(step * 1.001);
1507 start = (data.min() / step).floor() * step;
1508 }
1509 if !(start.is_finite() && along(start, bins, step).is_finite()) {
1510 return split(data);
1511 }
1512 (0..=self.bins)
1513 .map(|i| clean(along(start, i as f64, step)))
1514 .collect()
1515 }
1516
1517 pub fn counts(&self) -> Vec<usize> {
1520 let edges = self.edges();
1521 let (lo, hi) = (edges[0], edges[self.bins]);
1522 let inner = &edges[1..self.bins];
1523 let mut counts = vec![0; self.bins];
1524 for v in self.values.iter().copied().filter(|v| v.is_finite()) {
1525 if v < lo || v > hi {
1526 continue;
1527 }
1528 let bin = inner.partition_point(|&edge| edge <= v);
1530 counts[bin] += 1;
1531 }
1532 counts
1533 }
1534
1535 pub fn to_bar_chart(&self) -> BarChart {
1537 if !self.values.iter().any(|v| v.is_finite()) {
1538 return BarChart {
1539 bars: Vec::new(),
1540 ..self.chart.clone()
1541 };
1542 }
1543 let edges = self.edges();
1544 let written = edge_labels(&edges);
1545 let counts = self.counts();
1546 let mut chart = BarChart {
1547 bars: Vec::new(),
1548 ..self.chart.clone()
1549 };
1550 for (i, count) in counts.into_iter().enumerate() {
1551 let close = if i + 1 == self.bins { ']' } else { ')' };
1552 let label = format!("[{}, {}{close}", written[i], written[i + 1]);
1553 chart.bars.push(Bar::new(label, count as f64));
1554 }
1555 chart
1556 }
1557}
1558
1559impl Renderable for Histogram {
1560 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1561 self.to_bar_chart().rich_render(console, options)
1562 }
1563
1564 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1565 self.to_bar_chart().measure(console, options)
1566 }
1567}
1568
1569fn edge_labels(edges: &[f64]) -> Vec<String> {
1572 exact_labels(ValueFormat::Compact, edges)
1573 .map(|(labels, _)| labels)
1574 .unwrap_or_else(|| {
1575 edges
1576 .iter()
1577 .map(|&e| ValueFormat::Compact.format(e))
1578 .collect()
1579 })
1580}
1581
1582impl crate::a11y::AccessibleText for Histogram {
1585 fn accessible_text(&self, _width: usize) -> String {
1586 use super::describe::{count, counted, ranked};
1587 let finite = self.values.iter().filter(|v| v.is_finite()).count();
1588 let ignored = self.values.len() - finite;
1589 let mut head = format!("Histogram, {}", counted(finite, "value", "values"));
1590 if finite == 0 {
1591 if ignored > 0 {
1592 head.push_str(&format!(", {} ignored", count(ignored)));
1593 }
1594 return head;
1595 }
1596 let written = edge_labels(&self.edges());
1597 let chart = self.to_bar_chart();
1598 head.push_str(&format!(
1599 " in {} from {} to {}",
1600 counted(self.bins, "bin", "bins"),
1601 written[0],
1602 written[self.bins]
1603 ));
1604 if ignored > 0 {
1605 head.push_str(&format!(", {} ignored", count(ignored)));
1606 }
1607 let entries: Vec<(String, String, f64)> = chart
1608 .bars
1609 .iter()
1610 .map(|b| (b.label.clone(), count(b.value as usize), b.value))
1611 .collect();
1612 let mut out = vec![head];
1613 out.extend(ranked(&entries, 6));
1614 out.join("\n")
1615 }
1616}
1617
1618fn bin_step(x: f64) -> f64 {
1620 if !(x.is_finite() && x > 0.0) {
1621 return 1.0;
1622 }
1623 let power = 10f64.powf(x.log10().floor());
1624 [1.0, 2.0, 2.5, 5.0, 10.0]
1625 .into_iter()
1626 .map(|m| m * power)
1627 .find(|step| *step >= x * (1.0 - 1e-12))
1628 .unwrap_or(10.0 * power)
1629}
1630
1631#[cfg(test)]
1632mod tests {
1633 use super::*;
1634
1635 #[test]
1636 fn an_explicit_thickness_and_gap_keep_and_leave_bars_off() {
1637 let labels = "abcdefghijklmnopqrstuvwx";
1638 let bars = |chart: BarChart| {
1639 labels
1640 .chars()
1641 .fold(chart, |c, l| c.bar(l.to_string(), 5.0))
1642 .max(5.0)
1643 .orientation(Orientation::Vertical)
1644 .show_values(false)
1645 .bar_width(2)
1646 };
1647 let kept = bars(BarChart::new()).thickness(3).bar_gap(2);
1649 let row = ["███"; 8].join(" ");
1650 assert_eq!(
1651 rows(40, &kept),
1652 vec![
1653 row.clone(),
1654 row,
1655 " a b c d e f g h ".to_string(),
1656 ]
1657 );
1658 let squeezed = bars(BarChart::new());
1660 assert_eq!(rows(40, &squeezed)[0], "█".repeat(24));
1661 let narrow = bars(BarChart::new()).thickness(1).bar_gap(1);
1663 assert_eq!(rows(47, &narrow)[0], ["█"; 24].join(" "));
1664 let group = |label: &str| {
1666 BarGroup::new("").bars((0..3).map(|i| Bar::new(format!("{label}{i}"), 1.0)))
1667 };
1668 let grouped = ["p", "q", "r", "s"]
1669 .into_iter()
1670 .fold(BarChart::new(), |c, l| c.group(group(l)))
1671 .max(1.0)
1672 .orientation(Orientation::Vertical)
1673 .show_values(false)
1674 .bar_width(1)
1675 .thickness(2)
1676 .bar_gap(1)
1677 .group_gap(3);
1678 assert_eq!(rows(20, &grouped)[0], "██ ██ ██ ██ ██ ██");
1679 }
1680
1681 #[test]
1682 fn accessible_text_lists_few_bars_and_ranks_many() {
1683 use crate::a11y::AccessibleText;
1684 let chart = BarChart::new()
1685 .bar("api", 412.0)
1686 .bar("web", 268.5)
1687 .bar("worker", 97.0)
1688 .push(Bar::new("cron", 12.0).text("12 jobs"));
1689 assert_eq!(
1690 chart.accessible_text(80),
1691 "Bar chart, 4 bars, values from 12 to 412\n\
1692 api 412, web 268.5, worker 97, cron 12 jobs"
1693 );
1694 let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
1695 let values = [30.0, 42.0, 35.0, 51.0, 48.0, 20.0, 44.0];
1696 let many = BarChart::from_pairs(months.into_iter().zip(values));
1697 assert_eq!(
1698 many.accessible_text(80),
1699 "Bar chart, 7 bars, values from 20 to 51\n\
1700 Largest: Apr 51, May 48, Jul 44\n\
1701 Smallest: Jun 20"
1702 );
1703 let grouped = BarChart::new()
1704 .group(
1705 BarGroup::new("Q1")
1706 .bar(Bar::new("a", 4.0))
1707 .bar(Bar::new("b", 2.0)),
1708 )
1709 .group(
1710 BarGroup::new("Q2")
1711 .bar(Bar::new("a", 3.0))
1712 .bar(Bar::new("b", f64::NAN)),
1713 );
1714 assert_eq!(
1715 grouped.accessible_text(80),
1716 "Bar chart, 4 bars in 2 groups, values from 2 to 4, 1 without a value\n\
1717 Q1 a 4, Q1 b 2, Q2 a 3, Q2 b no value"
1718 );
1719 assert_eq!(BarChart::new().accessible_text(80), "Bar chart, no bars");
1720 let latencies = [12.0, 14.0, 15.0, 21.0, 22.0, 23.0, 24.0, 38.0, f64::NAN];
1721 let histogram = Histogram::new(latencies).bins(3).range(10.0, 40.0);
1722 assert_eq!(
1723 histogram.accessible_text(80),
1724 "Histogram, 8 values in 3 bins from 10 to 40, 1 ignored\n\
1725 [10, 20) 3, [20, 30) 4, [30, 40] 1"
1726 );
1727 assert_eq!(
1728 Histogram::new([f64::NAN]).accessible_text(80),
1729 "Histogram, 0 values, 1 ignored"
1730 );
1731 }
1732
1733 fn rows(width: usize, chart: &BarChart) -> Vec<String> {
1734 let console = Console::builder().width(width).color_system(None).build();
1735 console
1736 .render_export(chart)
1737 .lines()
1738 .map(str::to_string)
1739 .collect()
1740 }
1741
1742 fn coloured(width: usize) -> Console {
1743 Console::builder()
1744 .width(width)
1745 .force_terminal(true)
1746 .color_system(Some(rich::ColorSystem::Truecolor))
1747 .no_color(false)
1748 .build()
1749 }
1750
1751 fn style_at(line: &[Segment], col: usize) -> Option<Style> {
1753 let mut seen = 0;
1754 for segment in line {
1755 let n = cells(&segment.text);
1756 if col < seen + n {
1757 return segment.style.clone();
1758 }
1759 seen += n;
1760 }
1761 None
1762 }
1763
1764 #[test]
1765 fn grouped_vertical_bars_have_group_labels() {
1766 let chart = BarChart::new()
1767 .group(
1768 BarGroup::new("Q1")
1769 .bar(Bar::new("a", 2.0))
1770 .bar(Bar::new("b", 4.0)),
1771 )
1772 .group(
1773 BarGroup::new("Q2")
1774 .bar(Bar::new("a", 3.0))
1775 .bar(Bar::new("b", 1.0)),
1776 )
1777 .orientation(Orientation::Vertical)
1778 .bar_width(2);
1779 assert_eq!(
1780 rows(40, &chart),
1781 vec![" 4 3 ", "2 █ ▄ 1", "█ █ █ ▄", "a b a b", "Q1 Q2 "]
1782 );
1783 let mixed = BarChart::new()
1785 .bar("x", 1.0)
1786 .group(BarGroup::new("g").bar(Bar::new("y", 1.0)))
1787 .bar("z", 1.0)
1788 .orientation(Orientation::Vertical)
1789 .show_values(false)
1790 .max(1.0)
1791 .bar_width(1);
1792 assert_eq!(rows(40, &mixed), vec!["█ █ █", "x y z", " g "]);
1793 let empty = BarChart::new().group(BarGroup::new("none"));
1795 assert!(empty.bars().is_empty());
1796 assert_eq!(rows(10, &empty), vec!["no data"]);
1797 }
1798
1799 #[test]
1800 fn values_inside_horizontal_bars_as_demo2_draws_the_seasons() {
1801 let seasons = BarChart::new()
1802 .push(Bar::new("", 51.0).text("Winter 37-51"))
1803 .push(Bar::new("", 65.0).text("Spring 40-65"))
1804 .push(Bar::new("", 77.0).text("Summer 54-77"))
1805 .push(Bar::new("", 71.0).text("Fall 41-71").value_style("bold"))
1806 .bar_gap(1)
1807 .style("#203060")
1808 .value_style("grey70 on #203060")
1809 .values_inside(true)
1810 .bar_width(20);
1811 assert_eq!(
1812 rows(40, &seasons),
1813 vec![
1814 "Winter 37-51█▎ ",
1815 " ",
1816 "Spring 40-65████▉ ",
1817 " ",
1818 "Summer 54-77████████",
1819 " ",
1820 "Fall 41-71████████▌ ",
1821 ]
1822 );
1823 let console = coloured(40);
1824 let lines = console.render_lines(&seasons, &console.options(), false);
1825 assert_eq!(
1826 style_at(&lines[0], 0),
1827 Style::parse("grey70 on #203060").ok()
1828 );
1829 assert_eq!(style_at(&lines[0], 12), Style::parse("#203060").ok());
1830 assert_eq!(
1831 style_at(&lines[6], 0),
1832 Style::parse("grey70 on #203060 bold").ok()
1833 );
1834 let short = BarChart::new()
1836 .push(Bar::new("", 1.0).text("long text"))
1837 .push(Bar::new("", 10.0))
1838 .style("green")
1839 .values_inside(true)
1840 .bar_width(10);
1841 let lines = console.render_lines(&short, &console.options(), false);
1842 let text: String = lines[0].iter().map(|s| s.text.as_str()).collect();
1843 assert_eq!(text, "long text ");
1844 assert_eq!(style_at(&lines[0], 0), Style::parse("green reverse").ok());
1845 assert_eq!(style_at(&lines[0], 4), Style::parse("green").ok());
1846 }
1847
1848 #[test]
1849 fn text_values_wider_than_a_bar_are_centred_and_cut() {
1850 let chart = BarChart::new()
1851 .push(Bar::new("a", 4.0).text("1234"))
1852 .push(Bar::new("b", 4.0).text("7"))
1853 .orientation(Orientation::Vertical)
1854 .thickness(3)
1855 .values_inside(true)
1856 .max(4.0)
1857 .bar_width(2);
1858 assert_eq!(rows(40, &chart), vec!["███ ███", "12… █7█", " a b "]);
1859 let thin = chart.clone().thickness(1);
1861 assert_eq!(rows(40, &thin), vec!["█ █", "█ 7", "a b"]);
1862 let above = chart.thickness(1).values_inside(false);
1864 assert_eq!(
1865 rows(40, &above),
1866 vec!["1234 7 ", " █ █ ", " █ █ ", " a b "]
1867 );
1868 }
1869
1870 #[test]
1871 fn thick_bars() {
1872 let vertical = BarChart::from_pairs([("x", 2.0), ("y", 1.0)])
1873 .orientation(Orientation::Vertical)
1874 .thickness(5)
1875 .bar_width(2);
1876 assert_eq!(
1877 rows(40, &vertical),
1878 vec![" 2 ", "█████ 1 ", "█████ █████", " x y "]
1879 );
1880 let horizontal = BarChart::new()
1882 .bar("a", 2.0)
1883 .bar("b", 1.0)
1884 .thickness(5)
1885 .bar_width(4);
1886 assert_eq!(
1887 rows(40, &horizontal),
1888 vec![
1889 " ████ ",
1890 " ████ ",
1891 "a ████ 2",
1892 " ████ ",
1893 " ████ ",
1894 " ██ ",
1895 " ██ ",
1896 "b ██ 1",
1897 " ██ ",
1898 " ██ ",
1899 ]
1900 );
1901 }
1902
1903 #[test]
1904 fn gaps_between_bars_and_groups() {
1905 let group = |a: &str, b: &str| BarGroup::new("").bars([Bar::new(a, 1.0), Bar::new(b, 1.0)]);
1906 let vertical = BarChart::new()
1907 .group(group("a", "b"))
1908 .group(group("c", "d"))
1909 .orientation(Orientation::Vertical)
1910 .bar_gap(2)
1911 .group_gap(3)
1912 .show_values(false)
1913 .max(1.0)
1914 .bar_width(1);
1915 assert_eq!(rows(40, &vertical), vec!["█ █ █ █", "a b c d"]);
1916 let horizontal = BarChart::new()
1917 .group(
1918 BarGroup::new("one")
1919 .bar(Bar::new("a", 1.0))
1920 .bar(Bar::new("b", 2.0)),
1921 )
1922 .group(BarGroup::new("two").bar(Bar::new("c", 3.0)))
1923 .bar_gap(1)
1924 .group_gap(1)
1925 .show_values(false)
1926 .bar_width(3);
1927 assert_eq!(
1928 rows(40, &horizontal),
1929 vec![
1930 "a █ ",
1931 " ",
1932 "b ██ ",
1933 " one",
1934 " ",
1935 "c ███",
1936 " two"
1937 ]
1938 );
1939 let default = horizontal.clone();
1942 let default = BarChart {
1943 bar_gap: None,
1944 group_gap: None,
1945 ..default
1946 };
1947 assert_eq!(
1948 rows(40, &default),
1949 vec!["a █ ", "b ██ ", " one", " ", "c ███", " two"]
1950 );
1951 }
1952
1953 #[test]
1954 fn labels_and_values_take_the_chart_and_bar_styles() {
1955 let chart = BarChart::new()
1956 .push(Bar::new("Mon", 76.0).text("76°").value_style("bold"))
1957 .push(Bar::new("Tue", 65.0).label_style("underline"))
1958 .label_style("yellow")
1959 .value_style("italic")
1960 .bar_width(10);
1961 let console = coloured(40);
1962 let lines = console.render_lines(&chart, &console.options(), false);
1963 assert_eq!(style_at(&lines[0], 0), Style::parse("yellow").ok());
1964 assert_eq!(
1965 style_at(&lines[1], 0),
1966 Style::parse("yellow underline").ok()
1967 );
1968 let value_at = |line: &[Segment], text: &str| {
1969 line.iter()
1970 .find(|s| s.text.contains(text))
1971 .and_then(|s| s.style.clone())
1972 };
1973 assert_eq!(value_at(&lines[0], "76°"), Style::parse("italic bold").ok());
1974 assert_eq!(value_at(&lines[1], "65"), Style::parse("italic").ok());
1975 assert_eq!(rows(40, &chart)[0], "Mon ██████████ 76°");
1978 let ascii = Console::builder()
1979 .width(40)
1980 .color_system(None)
1981 .ascii_only(true)
1982 .build();
1983 assert_eq!(
1984 ascii.render_export(&chart).lines().next(),
1985 Some("Mon ########## 76?")
1986 );
1987 }
1988
1989 #[test]
1990 fn vertical_bars_fill_the_height_they_are_given() {
1991 let chart = BarChart::from_pairs([("mon", 4.0), ("tue", 7.5), ("wed", -2.0)])
1992 .orientation(Orientation::Vertical);
1993 let console = Console::builder().width(40).color_system(None).build();
1994 let lines = console.render_lines(&chart, &console.options().update_height(7), false);
1995 let text: Vec<String> = lines
1996 .iter()
1997 .map(|l| l.iter().map(|s| s.text.as_str()).collect())
1998 .collect();
1999 assert_eq!(
2001 text,
2002 vec![
2003 " 7.5 ",
2004 " 4 ███ ",
2005 "▅▅▅ ███ ",
2006 "███ ███ ",
2007 " ███",
2008 " -2 ",
2009 "mon tue wed",
2010 ]
2011 );
2012 let fixed = chart.clone().bar_width(2);
2014 let lines = console.render_lines(&fixed, &console.options().update_height(7), false);
2015 assert_eq!(lines.iter().filter(|l| !l.is_empty()).count(), 5);
2016 assert_eq!(console.render_export(&chart).lines().count(), 11);
2017 let horizontal = BarChart::from_pairs([("a", 1.0)]);
2019 let lines = console.render_lines(&horizontal, &console.options().update_height(7), false);
2020 assert_eq!(lines.iter().filter(|l| !l.is_empty()).count(), 1);
2021 }
2022}