1#![warn(unused_lifetimes, missing_docs)]
59
60use colored::Colorize;
61
62pub mod params {
64
65 #[derive(Debug)]
66 #[allow(missing_docs)]
67 pub enum ViewPreference {
68 Bottom,
72 Top,
76 }
77
78 #[derive(Debug)]
79 #[allow(missing_docs)]
80 pub enum DisplayMode<'a> {
81 Compact,
83 Portrait { labels: &'a [&'a str] },
85 }
86
87 #[derive(Debug)]
88 #[allow(missing_docs)]
89 pub struct ChartOptions<'a> {
90 pub height: u16,
92 pub view: ViewPreference,
94 pub display: DisplayMode<'a>,
96 }
97
98 impl<'a> Default for ChartOptions<'a> {
99 fn default() -> Self {
100 Self {
101 height: 8,
102 view: ViewPreference::Top,
103 display: DisplayMode::Compact,
104 }
105 }
106 }
107
108 #[derive(Debug)]
109 #[allow(missing_docs)]
110 pub struct ChartComparison<'a> {
111 pub data: &'a [u32],
113 }
114}
115
116use params::*;
117
118pub struct Chart<'a> {
120 data: &'a [u32],
121 compare: Option<ChartComparison<'a>>,
122 options: ChartOptions<'a>,
123}
124
125impl<'a> Chart<'a> {
126 pub fn new(
128 data: &'a [u32],
129 compare: Option<ChartComparison<'a>>,
130 options: ChartOptions<'a>,
131 ) -> Self {
132 assert!(
133 (1..=100).contains(&data.len()),
134 "data should contain no more than 100 values"
136 );
137 if let Some(ref compare) = compare {
138 assert_eq!(
139 compare.data.len(),
140 data.len(),
141 "compare data length should equal primary data length",
142 )
143 }
144 if let DisplayMode::Portrait { labels } = options.display {
145 assert_eq!(
146 labels.len(),
147 data.len(),
148 "label count should equal data length",
149 );
150 }
151
152 Self {
153 data,
154 compare,
155 options,
156 }
157 }
158
159 fn render(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
160 let ChartOptions {
161 height: height_lines,
162 display,
163 ..
164 } = &self.options;
165
166 let bar_width_chars = if self.data.len() <= 10 { 1 } else { 2 };
169
170 let chars = " ▁▂▃▄▅▆▇█🢃🢁⨯".chars().collect::<Vec<_>>();
173
174 let (data_steps, cmp_data_steps) = self.scale_to_steps();
175 let steps_zipped: Vec<(&i16, Option<&i16>)> = match cmp_data_steps {
176 Some(ref cmp_data_steps) => data_steps
177 .iter()
178 .zip(cmp_data_steps.iter())
179 .map(|(a, b)| (a, Some(b)))
180 .collect(),
181 None => data_steps.iter().map(|a| (a, None)).collect(),
182 };
183
184 let find_min_max = |data: &[u32], steps: &Vec<i16>| -> (u32, u32) {
187 let mut min = u32::MAX;
188 let mut max = u32::MIN;
189 for i in 0..data.len() {
190 if steps[i] > 0 {
191 if data[i] < min {
192 min = data[i];
193 }
194 if data[i] > max {
195 max = data[i];
196 }
197 }
198 }
199 (min, max)
200 };
201 let (mut min_visible, mut max_visible) = find_min_max(self.data, &data_steps);
202 if let (Some(c), Some(steps)) = (self.compare.as_ref(), &cmp_data_steps) {
203 let (min_visible_cmp, max_visible_cmp) = find_min_max(c.data, steps);
204 min_visible = std::cmp::min(min_visible, min_visible_cmp);
205 max_visible = std::cmp::max(max_visible, max_visible_cmp);
206 }
207
208 let tick_spacer = max_visible
209 .to_string()
210 .chars()
211 .map(|_| " ")
212 .collect::<String>();
213
214 for layer_num in (0..*height_lines).rev() {
217 if layer_num == height_lines - 1 {
218 write!(f, "{max_visible}│")?;
219 } else if layer_num == 0 {
220 let gap = (0..tick_spacer.len() - min_visible.to_string().len())
221 .map(|_| " ")
222 .collect::<String>();
223 write!(f, "{gap}{min_visible}│")?;
224 } else {
225 write!(f, "{tick_spacer}│")?;
226 };
227
228 let print_steps_start = (layer_num * 8) as i16;
231 let print_steps_end = ((layer_num + 1) * 8) as i16;
232
233 let to_print_char = |steps_count: i16| -> char {
235 match steps_count {
236 0 if layer_num == 0 => chars[11],
237 -1 if layer_num == 0 => chars[9],
238 -2 => chars[10],
239 below if below <= print_steps_start => chars[0],
240 above if above > print_steps_end => chars[8],
241 value => chars[(value - print_steps_start) as usize],
242 }
243 };
244 for (i, &(&pri_steps, cmp_steps)) in steps_zipped.iter().enumerate() {
245 match cmp_steps {
246 None => {
247 let pri_char = if i % 2 == 0 {
248 to_print_char(pri_steps).to_string().bright_white()
249 } else {
250 to_print_char(pri_steps).to_string().white()
251 };
252 for _ in 0..bar_width_chars {
253 write!(f, "{pri_char}")?;
254 }
255 }
256 Some(&cmp_steps) => {
259 write!(f, "{}", to_print_char(pri_steps).to_string().bright_white())
260 .unwrap();
261
262 let pri_value = self.data[i];
263 let cmp_value = self.compare.as_ref().unwrap().data[i];
264 let cmp_char = if cmp_value <= pri_value {
265 to_print_char(cmp_steps).to_string().bright_green()
266 } else {
267 to_print_char(cmp_steps).to_string().bright_red()
268 };
269 write!(f, "{cmp_char} ",).unwrap();
270 }
271 }
272 }
273
274 writeln!(f)?;
276 }
277
278 write!(f, "{tick_spacer} ")?;
280 let mut chart_width = tick_spacer.len() as u16;
281 for i in 0..self.data.len() {
282 write!(f, "{i}")?;
283 let label_width = i.to_string().len() as u16;
284 chart_width += label_width;
285 let offset_width = match (self.compare.is_some(), bar_width_chars) {
286 (false, width) => width,
287 (true, _) => 3,
288 };
289 for _ in label_width..offset_width {
290 write!(f, " ")?;
291 chart_width += 1;
292 }
293 }
294
295 if let DisplayMode::Portrait { labels } = display {
296 writeln!(f)?;
297
298 let col_count = std::cmp::max((chart_width as f32 / 17f32).floor() as usize, 1usize);
302 let col_length = labels.len().div_ceil(col_count);
303 let enumerated_labels = labels.iter().enumerate().collect::<Vec<_>>();
304 let label_cols = enumerated_labels.chunks(col_length).collect::<Vec<_>>();
305 let max_rows = label_cols.iter().map(|c| c.len()).max().unwrap();
306
307 for i in 0..max_rows {
308 for col in &label_cols {
309 if let Some((offset, label)) = col.get(i) {
310 write!(
311 f,
312 "{offset:>2}: {:<12} ",
314 label.chars().take(12).collect::<String>()
315 )?;
316 }
317 }
318 writeln!(f)?;
319 }
320 } else {
321 writeln!(f)?;
322 }
323
324 Ok(())
325 }
326
327 fn scale_to_steps(&self) -> (Vec<i16>, Option<Vec<i16>>) {
328 let max_step_count: u16 = self.options.height * 8;
331
332 let all_measurements = self
335 .data
336 .iter()
337 .chain(self.compare.as_ref().map(|c| c.data).unwrap_or(&[]).iter())
338 .filter(|&&m| m > 0);
339 let all_max = all_measurements.clone().max().unwrap();
340 let unit_height_steps: u16 = std::cmp::max(
341 (max_step_count as f32 / *all_max as f32).floor() as u16,
342 1u16,
343 );
344
345 let (excessive, unexcessive) = all_measurements.clone().partition::<Vec<&u32>, _>(|&&m| {
348 m > u16::MAX as u32 || m as u16 * unit_height_steps > max_step_count
349 });
350 let low_max = unexcessive.iter().max();
351 let high_max = excessive.iter().max();
352
353 let (show_excessive, scale_factor) = match (&self.options.view, low_max, high_max) {
355 (ViewPreference::Bottom, Some(&&low_max), _)
357 | (ViewPreference::Top, Some(&&low_max), None) => {
358 (false, max_step_count as f32 / low_max as f32)
359 }
360 (ViewPreference::Top, _, Some(&&high_max))
362 | (ViewPreference::Bottom, _, Some(&&high_max)) => {
363 (true, max_step_count as f32 / high_max as f32)
364 }
365 _ => unimplemented!(),
366 };
367
368 let measurement_to_step_count = |m: &u32| -> i16 {
371 if *m == 0 {
372 return 0;
373 }
374 match (excessive.is_empty(), show_excessive, excessive.contains(&m)) {
375 (false, false, true) => -2i16,
377 (false, true, false) => -1i16,
379 _ => {
381 let step_count = (*m as f32 * scale_factor) as i16;
382 if step_count == 0 {
383 -1i16
385 } else {
386 step_count
387 }
388 }
389 }
390 };
391 let scaled_data: Vec<i16> = self.data.iter().map(measurement_to_step_count).collect();
392 let scaled_data_cmp: Option<Vec<i16>> = self
393 .compare
394 .as_ref()
395 .map(|c| c.data.iter().map(measurement_to_step_count).collect());
396
397 (scaled_data, scaled_data_cmp)
398 }
399}
400
401impl<'a> std::fmt::Display for Chart<'a> {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 self.render(f)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
412
413 #[test]
414 fn test_two_digit_width() {
415 let chart = Chart::new(
416 &[23, 32, 44, 0, 2, 44, 5, 23, 42, 29, 16],
417 None,
418 ChartOptions::default(),
419 );
420 println!("\ntwo_digit_width\n{chart}");
421 }
422
423 #[test]
424 fn test_excessive_value_too_small_for_height() {
425 let chart = Chart::new(
426 &[0, 6837, 18067, 352038],
427 None,
428 ChartOptions {
429 height: 5,
430 view: ViewPreference::Top,
431 display: DisplayMode::Compact,
432 },
433 );
434 println!("\nexcessive_value_too_small_for_height\n{chart}");
435 }
436
437 #[test]
438 fn test_prefer_small_but_only_large() {
439 let chart = Chart::new(
440 &[2332, 3232, 3244, 0],
441 None,
442 ChartOptions {
443 height: 5,
444 view: ViewPreference::Bottom,
445 display: DisplayMode::Compact,
446 },
447 );
448 println!("\nprefer_small_but_large\n{chart}");
449 }
450
451 #[test]
452 fn test_prefer_large_but_only_small() {
453 let chart = Chart::new(
454 &[23, 32, 44, 0],
455 None,
456 ChartOptions {
457 height: 10,
458 view: ViewPreference::Top,
459 display: DisplayMode::Compact,
460 },
461 );
462 println!("\nprefer_large_but_small\n{chart}");
463 }
464
465 #[test]
466 fn test_comparison_portrait() {
467 let chart = Chart::new(
468 &[
469 0, 22, 2, 9, 223, 34, 33, 66, 76, 122, 199, 33, 12, 89, 1222, 100,
470 ],
471 Some(ChartComparison {
472 data: &[
473 14, 20, 1, 8, 223, 12, 56, 79, 69, 100, 1122, 33, 45, 9, 9000, 78,
474 ],
475 }),
476 ChartOptions {
477 height: 16,
478 view: ViewPreference::Bottom,
479 display: DisplayMode::Portrait {
480 labels: &[
481 "first",
482 "second",
483 "third",
484 "fourth",
485 "fifth",
486 "sixth",
487 "seventh",
488 "eighth",
489 "nineth",
490 "tenth",
491 "eleventh",
492 "twelfth",
493 "thirteenth",
494 "fourteenth",
495 "fifteenth",
496 "sixteenth",
497 ],
498 },
499 },
500 );
501 println!("\ncomparison_portrait\n{chart}");
502 }
503}