Skip to main content

rich_ext/chart/
bar.rs

1//! Bar charts, horizontal and vertical, in groups or not, and histograms.
2
3use 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
17/// Partial cells for [`Charset::Blocks`]: 1/8 to 7/8 of a cell.
18const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
19const FULL_BLOCK: char = '█';
20/// [`Charset::Braille`]: a full cell and its left half.
21const BRAILLE_FULL: char = '⣿';
22const BRAILLE_HALF: char = '⡇';
23/// [`Charset::Ascii`]: a full cell and a half.
24const ASCII_FULL: char = '#';
25const ASCII_HALF: char = '=';
26
27/// Vertical bars, bottom up: 1/8 to 7/8 of a cell.
28const LOWER_EIGHTHS: [char; 7] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇'];
29/// Vertical [`Charset::Braille`] and [`Charset::Ascii`] half cells.
30const BRAILLE_LOWER_HALF: char = '⣤';
31const ASCII_LOWER_HALF: char = '.';
32/// Rows for the tallest vertical bar when [`BarChart::bar_width`] is not
33/// set and the chart is given no height.
34const DEFAULT_COLUMN: usize = 8;
35/// The tallest vertical bar drawn, in rows, whatever
36/// [`BarChart::bar_width`] asks for.
37const MAX_COLUMN: usize = u16::MAX as usize;
38/// The thickest vertical bar the chart picks itself, in cells.
39const MAX_THICKNESS: usize = 3;
40/// The thickest bar [`BarChart::thickness`] allows, in cells or rows.
41const MAX_SIDE: usize = u16::MAX as usize;
42
43/// Which way a [`BarChart`]'s bars run.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum Orientation {
46    /// Left to right, one bar per line with its label before it and its
47    /// value after (the default).
48    #[default]
49    Horizontal,
50    /// Bottom to top, side by side, with labels under the bars and values
51    /// just above them (below, for negative values).
52    Vertical,
53}
54
55/// How a vertical chart fits its width.
56#[derive(Clone, Copy, Debug, PartialEq)]
57struct Columns {
58    /// Cells per bar, its label and its value.
59    slot: usize,
60    /// Cells between slots.
61    gap: usize,
62    /// Cells between groups, on top of `gap`.
63    group_gap: usize,
64    /// Cells of bar glyphs, centred in the slot.
65    thickness: usize,
66    labels: bool,
67    values: bool,
68    /// How many bars fit, from the first.
69    shown: usize,
70}
71
72/// The narrowest bar drawn before labels and values give way.
73const MIN_BAR: usize = 4;
74/// Bar cells when [`BarChart::bar_width`] is not set.
75const DEFAULT_BAR: usize = 40;
76
77/// One bar of a [`BarChart`].
78#[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    /// Written before the bar (under it, when vertical).
82    pub label: String,
83    /// The bar's length on the chart's scale.
84    pub value: f64,
85    /// A theme key or style definition for this bar, instead of
86    /// `chart.bar` (`chart.negative` below zero).
87    pub style: Option<String>,
88    /// Written instead of the value, such as `76°` or `8.5M` (ratatui's
89    /// `text_value`).
90    pub text: Option<String>,
91    /// A theme key or style definition for this bar's value, over the
92    /// chart's.
93    pub value_style: Option<String>,
94    /// A theme key or style definition for this bar's label, over the
95    /// chart's.
96    pub label_style: Option<String>,
97}
98
99impl Bar {
100    /// A bar called `label` of `value`.
101    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    /// This bar's style: a theme key or a definition.
113    pub fn style(mut self, style: impl Into<String>) -> Self {
114        self.style = Some(style.into());
115        self
116    }
117
118    /// Write `text` instead of the value (ratatui's `text_value`). The
119    /// value still sets the bar's length.
120    pub fn text(mut self, text: impl Into<String>) -> Self {
121        self.text = Some(text.into());
122        self
123    }
124
125    /// The value's style, over the chart's
126    /// [`value_style`](BarChart::value_style).
127    pub fn value_style(mut self, style: impl Into<String>) -> Self {
128        self.value_style = Some(style.into());
129        self
130    }
131
132    /// The label's style, over the chart's
133    /// [`label_style`](BarChart::label_style).
134    pub fn label_style(mut self, style: impl Into<String>) -> Self {
135        self.label_style = Some(style.into());
136        self
137    }
138}
139
140/// Bars that belong together, such as one quarter's revenue for each
141/// company, with a label: ratatui's `BarGroup`. A vertical chart writes the
142/// label centred under the group; a horizontal one on the row after it.
143///
144/// ```
145/// use rich_ext::chart::{Bar, BarChart, BarGroup};
146///
147/// let chart = BarChart::new()
148///     .group(BarGroup::new("Q1").bar(Bar::new("a", 3.0)).bar(Bar::new("b", 5.0)))
149///     .group(BarGroup::new("Q2").bars([Bar::new("a", 4.0), Bar::new("b", 2.0)]));
150/// assert_eq!(chart.bars().len(), 4);
151/// ```
152///
153/// A [`stacked`](Self::stacked) group is one bar made of its bars end to
154/// end, labelled with the group's label (see [`BarChart`]'s stacked bars).
155#[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    /// A group called `label` (empty for none), without bars yet.
165    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    /// Draw the group as one bar: its bars end to end, each a part in its
174    /// own style (`chart.series.N` by position, or [`Bar::style`]), with the
175    /// group's label and the total written. Parts above zero stack up from
176    /// zero and parts below it down from zero.
177    ///
178    /// A chart with a stacked group draws every group that way (other bars
179    /// are stacks of one) and adds a legend naming the parts
180    /// ([`BarChart::legend`]). With colour each part is its style; without
181    /// it each part has its own fill (`█▓░▒`, `⣿⣤⠿⠶` in Braille, `#=+:` in
182    /// ASCII). Part boundaries fall on whole cells; with colour the last
183    /// part ends in a partial cell. Values on the bars
184    /// ([`BarChart::values_inside`]) are not drawn on stacks; the total is
185    /// written after (or above) each.
186    ///
187    /// ```
188    /// use rich::Console;
189    /// use rich_ext::chart::{Bar, BarChart, BarGroup};
190    ///
191    /// let console = Console::builder().width(40).color_system(None).build();
192    /// let chart = BarChart::new()
193    ///     .group(BarGroup::new("Q1").stacked().bar(Bar::new("a", 4.0)).bar(Bar::new("b", 2.0)))
194    ///     .group(BarGroup::new("Q2").stacked().bar(Bar::new("a", 3.0)).bar(Bar::new("b", 3.0)))
195    ///     .bar_width(12);
196    /// assert_eq!(
197    ///     console.render_export(&chart),
198    ///     "Q1 ████████▓▓▓▓ 6\nQ2 ██████▓▓▓▓▓▓ 6\n█ a  ▓ b         \n"
199    /// );
200    /// ```
201    pub fn stacked(mut self) -> Self {
202        self.stacked = true;
203        self
204    }
205
206    /// Add a bar.
207    pub fn bar(mut self, bar: Bar) -> Self {
208        self.bars.push(bar);
209        self
210    }
211
212    /// Add bars.
213    pub fn bars(mut self, bars: impl IntoIterator<Item = Bar>) -> Self {
214        self.bars.extend(bars);
215        self
216    }
217}
218
219/// A run of a chart's bars in one group.
220#[derive(Clone, Debug, PartialEq)]
221struct Group {
222    label: String,
223    len: usize,
224    /// Made of bars added one by one, so the next one joins it.
225    loose: bool,
226    /// Drawn as one bar of parts ([`BarGroup::stacked`]).
227    stacked: bool,
228}
229
230/// Bars: by default horizontal, a label, a bar and the value on each line;
231/// with [`Orientation::Vertical`], columns side by side.
232///
233/// - Lengths are measured from zero on a scale that runs from the smallest
234///   to the largest value and always includes zero (override with
235///   [`range`](Self::range)). Negative values extend left of zero, at
236///   whole-cell precision, in `chart.negative`; their written value keeps
237///   its minus sign.
238/// - [`Charset::Blocks`] draws with eighth-cell precision (`█▉▊▋▌▍▎▏`),
239///   [`Charset::Braille`] with half cells (`⣿⡇`), [`Charset::Ascii`] with
240///   `#` and a half `=`. A value above zero always shows at least a sliver.
241/// - It measures to the label, the bar ([`bar_width`](Self::bar_width),
242///   40 by default) and the value. Given less, the bar shrinks to 4 cells,
243///   then the values go, then the labels are cut (with `…`, or `.` in
244///   ASCII), then the bar takes whatever is left.
245/// - [`Orientation::Vertical`] draws columns bottom up, as tall as
246///   [`bar_width`](Self::bar_width) rows, or the height it is given (a
247///   `Layout` region, an intuiTUIve node) less its labels and values, or 8
248///   rows. Blocks are eighth-cell (`▁▂▃▄▅▆▇█`), Braille half-cell (`⣤⣿`),
249///   ASCII `#` and a half `.`. Each bar has a slot as wide as the widest
250///   label or value (or its [`thickness`](Self::thickness)), with a cell
251///   between slots ([`bar_gap`](Self::bar_gap)), and is up to 3 cells
252///   thick unless a thickness is set. Labels go under the bars and each
253///   value just above its bar; negative values hang below the zero line at
254///   whole-cell precision, their values under them. Given less width the
255///   values go, then the labels are cut (and dropped below two cells),
256///   then the gaps, and bars that still do not fit are left off the right.
257///   An explicit [`thickness`](Self::thickness), [`bar_gap`](Self::bar_gap)
258///   or [`group_gap`](Self::group_gap) is kept, as ratatui keeps them: the
259///   slots narrow to the thickness, then as many whole bars as fit are
260///   drawn, from the first, and the rest are left off.
261///
262/// What ratatui's `BarChart` has, by our names:
263///
264/// - [`group`](Self::group) adds a [`BarGroup`]: bars with a label, set
265///   apart by [`group_gap`](Self::group_gap) (1 cell vertical, 2 rows
266///   horizontal, by default).
267/// - [`thickness`](Self::thickness) is ratatui's `bar_width`: how many
268///   cells wide a vertical bar is, or rows tall a horizontal one.
269///   [`bar_width`](Self::bar_width) keeps its meaning here: the longest
270///   bar's length, or the tallest column's height.
271/// - [`bar_gap`](Self::bar_gap) is the space between bars (1 cell
272///   vertical, no rows horizontal, by default).
273/// - [`Bar::text`] writes text instead of the value (`76°`).
274/// - [`label_style`](Self::label_style) and
275///   [`value_style`](Self::value_style), and [`Bar::label_style`] and
276///   [`Bar::value_style`] over them, style the labels and values.
277/// - [`values_inside`](Self::values_inside) writes each value on its bar,
278///   as ratatui draws them.
279///
280/// ```
281/// use rich::Console;
282/// use rich_ext::chart::{BarChart, Charset};
283///
284/// let console = Console::builder().width(40).color_system(None).build();
285/// let chart = BarChart::new()
286///     .bar("api", 31.0)
287///     .bar("web", 12.5)
288///     .bar("worker", 4.0)
289///     .bar_width(8);
290/// assert_eq!(
291///     console.render_export(&chart),
292///     "api    ████████   31\n\
293///      web    ███▎     12.5\n\
294///      worker █           4\n"
295/// );
296/// let ascii = chart.charset(Charset::Ascii);
297/// assert_eq!(
298///     console.render_export(&ascii),
299///     "api    ########   31\n\
300///      web    ###      12.5\n\
301///      worker #           4\n"
302/// );
303/// ```
304///
305/// Vertical:
306///
307/// ```
308/// use rich::Console;
309/// use rich_ext::chart::{BarChart, Orientation};
310///
311/// let console = Console::builder().width(40).color_system(None).build();
312/// let chart = BarChart::from_pairs([("mon", 4.0), ("tue", 7.5), ("wed", -2.0)])
313///     .orientation(Orientation::Vertical)
314///     .bar_width(4);
315/// assert_eq!(
316///     console.render_export(&chart),
317///     concat!(
318///         "    7.5    \n",
319///         " 4  ███    \n",
320///         "▅▅▅ ███    \n",
321///         "███ ███    \n",
322///         "        ███\n",
323///         "        -2 \n",
324///         "mon tue wed\n",
325///     )
326/// );
327/// ```
328///
329/// Grouped, with the values on the bars:
330///
331/// ```
332/// use rich::Console;
333/// use rich_ext::chart::{Bar, BarChart, BarGroup, Orientation};
334///
335/// let console = Console::builder().width(40).color_system(None).build();
336/// let chart = BarChart::new()
337///     .group(BarGroup::new("Q1").bar(Bar::new("a", 4.0)).bar(Bar::new("b", 2.0)))
338///     .group(BarGroup::new("Q2").bar(Bar::new("a", 3.0)).bar(Bar::new("b", 1.0)))
339///     .orientation(Orientation::Vertical)
340///     .thickness(3)
341///     .values_inside(true)
342///     .bar_width(4);
343/// assert_eq!(
344///     console.render_export(&chart),
345///     concat!(
346///         "███             \n",
347///         "███      ███    \n",
348///         "███ ███  ███    \n",
349///         "█4█ █2█  █3█ █1█\n",
350///         " a   b    a   b \n",
351///         "  Q1       Q2   \n",
352///     )
353/// );
354/// ```
355#[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    /// An empty chart; add rows with [`bar`](Self::bar).
385    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    /// A chart of `(label, value)` pairs.
408    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    /// Add a bar.
415    pub fn bar(self, label: impl Into<String>, value: f64) -> Self {
416        self.push(Bar::new(label, value))
417    }
418
419    /// Add a [`Bar`] built with its own style, text or value style.
420    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    /// Add a [`BarGroup`]: its bars, set apart from the others, with its
436    /// label. A group without bars is left out, as in ratatui.
437    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    /// The bars, in order, across their groups.
460    pub fn bars(&self) -> &[Bar] {
461        &self.bars
462    }
463
464    /// Fix the scale (default: the values' range, stretched to include
465    /// zero). Values outside are clamped.
466    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    /// Fix only the top of the scale, such as `100.0` for percentages.
473    pub fn max(mut self, max: f64) -> Self {
474        self.max = Some(max);
475        self
476    }
477
478    /// Glyphs to draw with (default [`Charset::Auto`]: blocks).
479    pub fn charset(mut self, charset: Charset) -> Self {
480        self.charset = charset;
481        self
482    }
483
484    /// Which way the bars run (default [`Orientation::Horizontal`]).
485    pub fn orientation(mut self, orientation: Orientation) -> Self {
486        self.orientation = orientation;
487        self
488    }
489
490    /// Cells for the longest bar (default 40, at least 1); rows for the
491    /// tallest when [`Orientation::Vertical`] (default: the height given,
492    /// less labels and values, or 8; at most 65535). For how thick a bar
493    /// is, ratatui's `bar_width`, see [`thickness`](Self::thickness).
494    pub fn bar_width(mut self, width: usize) -> Self {
495        self.bar_width = Some(width.max(1));
496        self
497    }
498
499    /// How thick each bar is (ratatui's `bar_width`): cells across a
500    /// vertical bar, rows down a horizontal one (at least 1). By default a
501    /// horizontal bar is a row and a vertical one as thick as its slot, up
502    /// to 3 cells. A horizontal bar's label and value sit on its middle
503    /// row.
504    pub fn thickness(mut self, thickness: usize) -> Self {
505        self.thickness = Some(thickness.clamp(1, MAX_SIDE));
506        self
507    }
508
509    /// The space between two bars: cells when vertical (default 1), rows
510    /// when horizontal (default none).
511    pub fn bar_gap(mut self, gap: usize) -> Self {
512        self.bar_gap = Some(gap.min(MAX_SIDE));
513        self
514    }
515
516    /// The space between two groups, on top of the
517    /// [`bar_gap`](Self::bar_gap): cells when vertical (default 1), rows
518    /// when horizontal (default 2). A horizontal group's label takes the
519    /// first of those rows, or a row of its own when there are none.
520    pub fn group_gap(mut self, gap: usize) -> Self {
521        self.group_gap = Some(gap.min(MAX_SIDE));
522        self
523    }
524
525    /// Write each value after its bar (default on).
526    pub fn show_values(mut self, show: bool) -> Self {
527        self.show_values = show;
528        self
529    }
530
531    /// Write each value on its bar, as ratatui draws values: from the start
532    /// of a horizontal bar, past its end when it is longer; on the bottom
533    /// row of a vertical one, centred and cut to its thickness. Values on
534    /// bars take no room of their own. Without a value style they are the
535    /// bar's colour reversed, so they read on the bar.
536    pub fn values_inside(mut self, inside: bool) -> Self {
537        self.values_inside = inside;
538        self
539    }
540
541    /// How values are written (default [`ValueFormat::Compact`]).
542    pub fn format(mut self, format: ValueFormat) -> Self {
543        self.format = format;
544        self
545    }
546
547    /// Show the legend of a chart with [stacked](BarGroup::stacked) groups:
548    /// each part's marker and name (default on). Other charts have none.
549    pub fn legend(mut self, legend: bool) -> Self {
550        self.legend = legend;
551        self
552    }
553
554    /// The style of every bar: a theme key or a definition (default
555    /// `chart.bar`).
556    pub fn style(mut self, style: impl Into<String>) -> Self {
557        self.style = Some(style.into());
558        self
559    }
560
561    /// The labels' style, over `chart.label`: a theme key or a definition.
562    /// It styles the group labels too.
563    pub fn label_style(mut self, style: impl Into<String>) -> Self {
564        self.label_style = Some(style.into());
565        self
566    }
567
568    /// The values' style, over `chart.value`: a theme key or a definition.
569    pub fn value_style(mut self, style: impl Into<String>) -> Self {
570        self.value_style = Some(style.into());
571        self
572    }
573
574    /// The groups, each a label (empty for none) and its bars' indices.
575    /// Without groups, all the bars are one.
576    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    /// What a bar's value is written as.
607    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    /// The room values take beside or above the bars: none when they are
625    /// on the bars.
626    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    /// The space between groups, on top of the gap; none with one group.
649    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    /// The vertical layout for `width` cells.
660    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        // An explicit thickness or gap is kept, as ratatui keeps them: the
693        // slots narrow to the thickness, then the bars that do not fit are
694        // left off the right.
695        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            // A label cut to one cell would be only the `…`.
721            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    /// A vertical bar's cells, bottom up, for `length` rows (a fraction
735    /// of a row allowed).
736    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    /// `base` with the chart's style spec and then the bar's over it.
776    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    /// The style of a value written on its bar: the value styles when any
808    /// is set, the bar's reversed otherwise.
809    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    /// The vertical chart for `width` cells, as tall as `bar_width`, or as
822    /// `height` less its labels and values, or 8 rows.
823    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        // Each shown bar's left column.
859        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        // Rows counted from the bottom of the plot; a value may sit one row
876        // above it or one below.
877        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            // Draw the bar; the row its value goes in, and the row of its
893            // base (next to zero), when it has one.
894            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                // On the bar's base, centred and cut to its thickness.
928                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        // Only the rows above and below the plot that hold a value stay.
956        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    /// (label, bar, value) widths for `width` cells, after shrinking.
1008    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        // Cut labels down, leaving the bar its floor.
1025        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    /// The glyphs for a bar of `length` cells (a fraction of a cell
1034    /// allowed).
1035    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    /// A horizontal bar's `bar_w` cells, where it starts, and how many
1072    /// whole cells it covers.
1073    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                            // Right-aligned, so the digits line up.
1188                            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            // The group's label on the row after its bars, then the space
1199            // to the next group.
1200            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
1303/// `Bar chart, 4 bars, values from 12 to 412`, then every bar (up to six)
1304/// or the three largest and the smallest. A grouped bar is named with its
1305/// group's label first.
1306impl 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    /// The scale's range (zero included, fixed bounds kept), when there is
1317    /// a value or a bound: what a shared scale unifies.
1318    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    /// `what`, the count, the range and the bars ranked.
1332    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/// Raw values counted into equal-width bins and drawn as a [`BarChart`],
1386/// one bar per bin labelled with its range.
1387///
1388/// Bins split the values' range (or [`range`](Self::range)) into
1389/// [`bins`](Self::bins) equal parts (10 by default). Each label is
1390/// `[low, high)`, the last `[low, high]`, so every value falls in exactly
1391/// one, the one its label names; NaN and infinite values are not counted,
1392/// and values outside an explicit range are left out. The edges are
1393/// written exactly: in full (`[1000, 1250)`) when the compact form would
1394/// round one of them.
1395///
1396/// ```
1397/// use rich::Console;
1398/// use rich_ext::chart::Histogram;
1399///
1400/// let console = Console::builder().width(40).color_system(None).build();
1401/// let latencies = [12.0, 14.0, 15.0, 21.0, 22.0, 23.0, 24.0, 38.0];
1402/// let histogram = Histogram::new(latencies).bins(3).range(10.0, 40.0).bar_width(8);
1403/// assert_eq!(
1404///     console.render_export(&histogram),
1405///     "[10, 20) ██████   3\n\
1406///      [20, 30) ████████ 4\n\
1407///      [30, 40] ██       1\n"
1408/// );
1409/// ```
1410#[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    /// A histogram of `values`.
1422    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    /// How many bins (default 10, at least 1).
1433    pub fn bins(mut self, bins: usize) -> Self {
1434        self.bins = bins.max(1);
1435        self
1436    }
1437
1438    /// The range the bins cover (default: the values' range).
1439    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    /// Glyphs to draw with (default [`Charset::Auto`]: blocks).
1446    pub fn charset(mut self, charset: Charset) -> Self {
1447        self.chart = self.chart.charset(charset);
1448        self
1449    }
1450
1451    /// Which way the bars run (default [`Orientation::Horizontal`]).
1452    pub fn orientation(mut self, orientation: Orientation) -> Self {
1453        self.chart = self.chart.orientation(orientation);
1454        self
1455    }
1456
1457    /// Cells for the tallest bin (default 40); rows when vertical
1458    /// (default 8).
1459    pub fn bar_width(mut self, width: usize) -> Self {
1460        self.chart = self.chart.bar_width(width);
1461        self
1462    }
1463
1464    /// Write each count after its bar (default on).
1465    pub fn show_values(mut self, show: bool) -> Self {
1466        self.chart = self.chart.show_values(show);
1467        self
1468    }
1469
1470    /// The bars' style: a theme key or a definition (default `chart.bar`).
1471    pub fn style(mut self, style: impl Into<String>) -> Self {
1472        self.chart = self.chart.style(style);
1473        self
1474    }
1475
1476    /// The bin edges, `bins + 1` of them, low to high, without
1477    /// floating-point noise (`0.3`, not `0.30000000000000004`).
1478    ///
1479    /// With an explicit [`range`](Self::range) the bins split it exactly.
1480    /// Otherwise the bin width is rounded up to 1, 2, 2.5 or 5 times a power
1481    /// of ten and the first edge down to a multiple of it, so the labels are
1482    /// round numbers; the last bins may then be empty. A range too wide for
1483    /// round bins (near `±f64::MAX`) is split exactly.
1484    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        // Rounding the start down can leave the top uncovered: widen.
1505        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    /// How many values fall in each bin: `[low, high)`, the last
1518    /// `[low, high]`, against the edges [`edges`](Self::edges) gives.
1519    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            // The bins whose low edge is at or below `v`, past the first.
1529            let bin = inner.partition_point(|&edge| edge <= v);
1530            counts[bin] += 1;
1531        }
1532        counts
1533    }
1534
1535    /// The [`BarChart`] this histogram draws.
1536    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
1569/// The bin edges as the labels write them: exactly, in full when the
1570/// compact form would round one.
1571fn 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
1582/// `Histogram, 8 values in 3 bins from 10 to 40`, then each bin's count
1583/// (up to six bins) or the three largest bins and the smallest.
1584impl 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
1618/// The smallest of 1, 2, 2.5 and 5 times a power of ten that is at least `x`.
1619fn 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        // 24 bars 3 cells thick, 2 apart, in 40 cells: the first 8.
1648        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        // Without them, every bar is squeezed in, one cell each.
1659        let squeezed = bars(BarChart::new());
1660        assert_eq!(rows(40, &squeezed)[0], "█".repeat(24));
1661        // Room for every bar: the slots narrow, the thickness stays.
1662        let narrow = bars(BarChart::new()).thickness(1).bar_gap(1);
1663        assert_eq!(rows(47, &narrow)[0], ["█"; 24].join(" "));
1664        // Group gaps are kept too: two whole groups of three fit 20 cells.
1665        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    /// The style of the cell at `col` of a rendered line.
1752    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        // Bars added on their own after a group make a group of their own.
1784        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        // An empty group is left out.
1794        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        // Text longer than its bar runs on past it in the bar's colour.
1835        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        // A bar one cell thick has no room for a cut value: none is drawn.
1860        let thin = chart.clone().thickness(1);
1861        assert_eq!(rows(40, &thin), vec!["█ █", "█ 7", "a b"]);
1862        // Above the bars, the slot is as wide as the widest value.
1863        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        // Horizontal: five rows, the label and value on the middle one.
1881        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        // The default space between horizontal groups: the label's row
1940        // and one more.
1941        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        // Text values are written in place of the number, and in ASCII on
1976        // an ASCII console.
1977        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        // Four rows of bars and their values and labels: as `bar_width(4)`.
2000        assert_eq!(
2001            text,
2002            vec![
2003                "    7.5    ",
2004                " 4  ███    ",
2005                "▅▅▅ ███    ",
2006                "███ ███    ",
2007                "        ███",
2008                "        -2 ",
2009                "mon tue wed",
2010            ]
2011        );
2012        // `bar_width` wins; with no height at all, eight rows of bars.
2013        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        // Horizontal charts keep a row a bar.
2018        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}