1use rich::measure::Measurement;
5use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
6
7use super::plot_canvas::spec_style;
8use super::{Charset, Line, lines_to_segments};
9
10const EIGHTHS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
12const DEFAULT_HEIGHT: usize = 8;
15const MAX_HEIGHT: usize = u16::MAX as usize;
17
18#[derive(Clone, Debug, PartialEq)]
52#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
53pub struct BarSpectrum {
54 left: Vec<f64>,
55 right: Option<Vec<f64>>,
56 mirror: bool,
57 max: Option<f64>,
58 bar_width: usize,
59 gap: usize,
60 style: Option<String>,
61 charset: Charset,
62 height: Option<usize>,
63}
64
65impl BarSpectrum {
66 pub fn new(bands: impl IntoIterator<Item = f64>) -> Self {
68 BarSpectrum {
69 left: bands.into_iter().collect(),
70 right: None,
71 mirror: false,
72 max: None,
73 bar_width: 1,
74 gap: 1,
75 style: None,
76 charset: Charset::Auto,
77 height: None,
78 }
79 }
80
81 pub fn stereo(
84 left: impl IntoIterator<Item = f64>,
85 right: impl IntoIterator<Item = f64>,
86 ) -> Self {
87 let mut bars = BarSpectrum::new(left);
88 bars.right = Some(right.into_iter().collect());
89 bars.mirror = true;
90 bars
91 }
92
93 pub fn mirror(mut self, mirror: bool) -> Self {
96 self.mirror = mirror || self.right.is_some();
97 self
98 }
99
100 pub fn range(mut self, max: f64) -> Self {
103 self.max = (max.is_finite() && max > 0.0).then_some(max);
104 self
105 }
106
107 pub fn bar_width(mut self, cells: usize) -> Self {
109 self.bar_width = cells.max(1);
110 self
111 }
112
113 pub fn gap(mut self, cells: usize) -> Self {
115 self.gap = cells;
116 self
117 }
118
119 pub fn style(mut self, style: impl Into<String>) -> Self {
122 self.style = Some(style.into());
123 self
124 }
125
126 pub fn charset(mut self, charset: Charset) -> Self {
129 self.charset = charset;
130 self
131 }
132
133 pub fn height(mut self, rows: usize) -> Self {
135 self.height = Some(rows.min(MAX_HEIGHT));
136 self
137 }
138
139 pub fn bands(&self) -> (&[f64], Option<&[f64]>) {
142 (&self.left, self.right.as_deref())
143 }
144
145 fn bars(&self) -> Vec<f64> {
147 let clean = |v: &f64| if v.is_nan() { 0.0 } else { *v };
148 let mut bars: Vec<f64> = Vec::new();
149 if self.mirror {
150 bars.extend(self.left.iter().rev().map(clean));
151 }
152 bars.extend(self.right.as_ref().unwrap_or(&self.left).iter().map(clean));
153 bars
154 }
155
156 fn top(&self, bars: &[f64]) -> f64 {
158 self.max.unwrap_or_else(|| {
159 let loudest = bars
160 .iter()
161 .copied()
162 .filter(|v| v.is_finite())
163 .fold(0.0, f64::max);
164 if loudest > 0.0 { loudest } else { 1.0 }
165 })
166 }
167
168 fn lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Line> {
169 let width = options.max_width;
170 let height = self
171 .height
172 .or(options.height)
173 .unwrap_or(DEFAULT_HEIGHT)
174 .min(MAX_HEIGHT);
175 let ascii = self.charset.resolve(console, options, Charset::Blocks) == Charset::Ascii;
176 let style: Option<Style> =
177 spec_style(console, self.style.as_deref().unwrap_or("chart.bar"));
178 let mut bars = self.bars();
179 let top = self.top(&bars);
180 let (mut bar_width, mut gap) = (self.bar_width, self.gap);
182 let span = |n: usize, w: usize, g: usize| n * w + n.saturating_sub(1) * g;
183 if span(bars.len(), bar_width, gap) > width {
184 gap = 0;
185 }
186 if span(bars.len(), bar_width, gap) > width {
187 bar_width = (width / bars.len().max(1)).max(1);
188 }
189 if bars.len() > width && width > 0 {
190 let n = bars.len();
191 bars = (0..width)
192 .map(|c| {
193 let (from, to) = (c * n / width, ((c + 1) * n / width).max(c * n / width + 1));
194 bars[from..to.min(n)].iter().copied().fold(0.0, f64::max)
195 })
196 .collect();
197 }
198 let used = span(bars.len(), bar_width, gap).min(width);
199 let left = (width - used) / 2;
200 let eighths: Vec<usize> = bars
202 .iter()
203 .map(|v| {
204 let share = (v / top).clamp(0.0, 1.0);
205 if ascii {
206 (share * height as f64).round() as usize * 8
207 } else {
208 (share * (height * 8) as f64).round() as usize
209 }
210 })
211 .collect();
212 (0..height)
213 .map(|r| {
214 let above = (height - 1 - r) * 8;
215 let mut line = Line::new();
216 line.pad(left);
217 for (i, &e) in eighths.iter().enumerate() {
218 if i > 0 {
219 line.pad(gap);
220 }
221 let glyph = match e.saturating_sub(above) {
222 0 => ' ',
223 n if n >= 8 => {
224 if ascii {
225 '#'
226 } else {
227 '█'
228 }
229 }
230 n => EIGHTHS[n],
231 };
232 if glyph == ' ' {
233 line.pad(bar_width);
234 } else {
235 line.push(&glyph.to_string().repeat(bar_width), style.clone());
236 }
237 }
238 line.pad(width.saturating_sub(line.width()));
239 line
240 })
241 .collect()
242 }
243}
244
245impl crate::surface::Draw for BarSpectrum {
246 fn draw(
247 &self,
248 console: &Console,
249 options: &ConsoleOptions,
250 screen: &mut crate::surface::Screen,
251 rect: crate::surface::Rect,
252 ) {
253 super::draw_lines(
254 self.lines(console, options),
255 options.max_width,
256 options,
257 screen,
258 rect,
259 );
260 }
261}
262
263impl Renderable for BarSpectrum {
264 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
265 lines_to_segments(self.lines(console, options), options.max_width)
266 }
267
268 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
269 let n = self.bars().len().max(1);
270 let natural = n * self.bar_width + (n - 1) * self.gap;
271 Measurement::new(n.min(natural), natural)
272 .with_maximum(options.max_width)
273 .normalize()
274 }
275}
276
277impl crate::a11y::AccessibleText for BarSpectrum {
281 fn accessible_text(&self, _width: usize) -> String {
282 use super::ValueFormat;
283 use super::describe::{counted, extent, range};
284 if self.left.is_empty() && self.right.as_ref().is_none_or(Vec::is_empty) {
285 return "Bar spectrum, no bands".to_string();
286 }
287 let mut head = format!(
288 "Bar spectrum, {}",
289 counted(self.left.len(), "band", "bands")
290 );
291 if self.right.is_some() {
292 head.push_str(", stereo");
293 } else if self.mirror {
294 head.push_str(", mirrored");
295 }
296 let all = self.left.iter().chain(self.right.iter().flatten()).copied();
297 if let Some((lo, hi)) = extent(all) {
298 head.push_str(&format!(", levels {}", range(lo, hi, ValueFormat::Compact)));
299 }
300 let mut out = vec![head];
301 let loudest = |what: &str, bands: &[f64]| {
302 let (at, level) = bands
303 .iter()
304 .enumerate()
305 .filter(|(_, v)| v.is_finite())
306 .max_by(|a, b| a.1.total_cmp(b.1))?;
307 Some(format!(
308 "{what}: band {} of {}, {}",
309 at + 1,
310 bands.len(),
311 ValueFormat::Compact.format(*level)
312 ))
313 };
314 match &self.right {
315 None => out.extend(loudest("Loudest", &self.left)),
316 Some(right) => {
317 out.extend(loudest("Left loudest", &self.left));
318 out.extend(loudest("Right loudest", right));
319 }
320 }
321 out.join("\n")
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::a11y::AccessibleText;
329
330 fn plain(width: usize, bars: &BarSpectrum) -> Vec<String> {
331 let console = Console::builder().width(width).color_system(None).build();
332 console
333 .render_export(bars)
334 .lines()
335 .map(str::to_string)
336 .collect()
337 }
338
339 #[test]
340 fn tops_are_eighths_of_a_cell() {
341 let bars = BarSpectrum::new([1.0, 0.5, 1.0 / 16.0])
342 .range(1.0)
343 .gap(0)
344 .height(1);
345 assert_eq!(plain(3, &bars), ["█▄▁"]);
346 }
347
348 #[test]
349 fn stereo_puts_a_channel_each_side() {
350 let bars = BarSpectrum::stereo([1.0, 0.0], [0.0, 1.0])
351 .range(1.0)
352 .gap(0)
353 .height(1);
354 assert_eq!(plain(4, &bars), [" █ █"]);
356 assert_eq!(plain(4, &bars.mirror(false)), [" █ █"]);
358 }
359
360 #[test]
361 fn too_many_bands_lose_gaps_then_share_columns() {
362 let bars = BarSpectrum::new([1.0, 0.0, 0.0, 1.0])
363 .range(1.0)
364 .bar_width(3)
365 .height(1);
366 assert_eq!(plain(12, &bars), ["███ ███"]);
368 assert_eq!(plain(6, &bars), [" █ █ "]);
370 assert_eq!(plain(2, &bars), ["██"]);
372 }
373
374 #[test]
375 fn levels_are_clamped_and_nan_is_silence() {
376 let bars = BarSpectrum::new([2.0, -1.0, f64::NAN]).range(1.0).height(1);
377 assert_eq!(plain(5, &bars), ["█ "]);
378 assert_eq!(plain(3, &BarSpectrum::new([0.0, 0.0]).height(1)), [" "]);
380 assert_eq!(plain(3, &BarSpectrum::new([2.0, 1.0]).height(1)), ["█ ▄"]);
381 }
382
383 #[test]
384 fn ascii_bars_round_to_whole_cells() {
385 let bars = BarSpectrum::new([1.0, 0.4])
386 .range(1.0)
387 .charset(Charset::Ascii)
388 .height(2);
389 assert_eq!(plain(3, &bars), ["# ", "# #"]);
390 }
391
392 #[test]
393 fn accessible_text_names_the_loudest_band() {
394 let bars = BarSpectrum::new([0.2, 0.8, 0.1]).mirror(true);
395 assert_eq!(
396 bars.accessible_text(80),
397 "Bar spectrum, 3 bands, mirrored, levels from 0.1 to 0.8\n\
398 Loudest: band 2 of 3, 0.8"
399 );
400 let stereo = BarSpectrum::stereo([1.0], [2.0]);
401 assert_eq!(
402 stereo.accessible_text(80),
403 "Bar spectrum, 1 band, stereo, levels from 1 to 2\n\
404 Left loudest: band 1 of 1, 1\nRight loudest: band 1 of 1, 2"
405 );
406 assert_eq!(
407 BarSpectrum::new([]).accessible_text(80),
408 "Bar spectrum, no bands"
409 );
410 }
411}