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 (data_steps, cmp_data_steps) = self.scale_to_steps();
161 let steps_zipped: Vec<(&i16, Option<&i16>)> = match cmp_data_steps {
162 Some(ref cmp_data_steps) => data_steps
163 .iter()
164 .zip(cmp_data_steps.iter())
165 .map(|(a, b)| (a, Some(b)))
166 .collect(),
167 None => data_steps.iter().map(|a| (a, None)).collect(),
168 };
169
170 let find_min_max = |data: &[u32], steps: &Vec<i16>| -> (u32, u32) {
173 let mut min = u32::MAX;
174 let mut max = u32::MIN;
175 for i in 0..data.len() {
176 if steps[i] > 0 {
177 if data[i] < min {
178 min = data[i];
179 }
180 if data[i] > max {
181 max = data[i];
182 }
183 }
184 }
185 (min, max)
186 };
187 let (mut min_visible, mut max_visible) = find_min_max(self.data, &data_steps);
188 if let (Some(c), Some(steps)) = (self.compare.as_ref(), &cmp_data_steps) {
189 let (min_visible_cmp, max_visible_cmp) = find_min_max(c.data, steps);
190 min_visible = std::cmp::min(min_visible, min_visible_cmp);
191 max_visible = std::cmp::max(max_visible, max_visible_cmp);
192 }
193
194 let get_print_char = |layer_num: u16, steps_count: i16| -> char {
195 let chars = " ▁▂▃▄▅▆▇█🢃🢁⨯".chars().collect::<Vec<_>>();
199
200 let print_steps_start = (layer_num * 8) as i16;
203 let print_steps_end = ((layer_num + 1) * 8) as i16;
204
205 match steps_count {
206 0 if layer_num == 0 => chars[11],
207 -1 if layer_num == 0 => chars[9],
208 -2 => chars[10],
209 below if below <= print_steps_start => chars[0],
210 above if above > print_steps_end => chars[8],
211 value => chars[(value - print_steps_start) as usize],
212 }
213 };
214
215 let bar_width_chars = if self.data.len() <= 10 { 1 } else { 2 };
218
219 let tick_spacer = max_visible
220 .to_string()
221 .chars()
222 .map(|_| " ")
223 .collect::<String>();
224
225 let mut write_layer = |layer_num: u16| -> std::fmt::Result {
226 if layer_num == self.options.height - 1 {
228 write!(f, "{max_visible}│")?;
229 } else if layer_num == 0 {
230 let gap = (0..tick_spacer.len() - min_visible.to_string().len())
231 .map(|_| " ")
232 .collect::<String>();
233 write!(f, "{gap}{min_visible}│")?;
234 } else {
235 write!(f, "{tick_spacer}│")?;
236 };
237
238 for (i, &(&pri_steps, cmp_steps)) in steps_zipped.iter().enumerate() {
240 let pri_char = get_print_char(layer_num, pri_steps).to_string();
241 match cmp_steps {
242 None => {
243 let pri_char = if i % 2 == 0 {
244 pri_char.bright_white()
245 } else {
246 pri_char.white()
247 };
248 for _ in 0..bar_width_chars {
249 write!(f, "{pri_char}")?;
250 }
251 }
252 Some(&cmp_steps) => {
255 write!(f, "{}", pri_char.bright_white())?;
256
257 let cmp_char = get_print_char(layer_num, cmp_steps).to_string();
258 let pri_value = self.data[i];
259 let cmp_value = self.compare.as_ref().unwrap().data[i];
260 let cmp_char = if cmp_value <= pri_value {
261 cmp_char.bright_green()
262 } else {
263 cmp_char.bright_red()
264 };
265 write!(f, "{cmp_char} ",)?;
266 }
267 }
268 }
269
270 writeln!(f)
272 };
273
274 for layer_num in (0..self.options.height).rev() {
276 write_layer(layer_num)?;
277 }
278
279 write!(f, "{tick_spacer} ")?;
281 let mut chart_width = tick_spacer.len() as u16;
282 for i in 0..self.data.len() {
283 write!(f, "{i}")?;
284 let label_width = i.to_string().len() as u16;
285 chart_width += label_width;
286 let offset_width = match (self.compare.is_some(), bar_width_chars) {
287 (false, width) => width,
288 (true, _) => 3,
289 };
290 for _ in label_width..offset_width {
291 write!(f, " ")?;
292 chart_width += 1;
293 }
294 }
295
296 if let DisplayMode::Portrait { labels } = self.options.display {
297 writeln!(f)?;
298
299 let col_count = std::cmp::max((chart_width as f32 / 17f32).floor() as usize, 1usize);
303 let col_length = labels.len().div_ceil(col_count);
304 let enumerated_labels = labels.iter().enumerate().collect::<Vec<_>>();
305 let label_cols = enumerated_labels.chunks(col_length).collect::<Vec<_>>();
306 let max_rows = label_cols.iter().map(|c| c.len()).max().unwrap();
307
308 for i in 0..max_rows {
309 for col in &label_cols {
310 if let Some((offset, label)) = col.get(i) {
311 write!(
312 f,
313 "{offset:>2}: {:<12} ",
315 label.chars().take(12).collect::<String>()
316 )?;
317 }
318 }
319 writeln!(f)?;
320 }
321 } else {
322 writeln!(f)?;
323 }
324
325 Ok(())
326 }
327
328 fn scale_to_steps(&self) -> (Vec<i16>, Option<Vec<i16>>) {
329 let max_step_count: u16 = self.options.height * 8;
332
333 let all_measurements = self
336 .data
337 .iter()
338 .chain(self.compare.as_ref().map(|c| c.data).unwrap_or(&[]).iter())
339 .filter(|&&m| m > 0);
340 let all_max = all_measurements.clone().max().unwrap();
341 let unit_height_steps: u16 = std::cmp::max(
342 (max_step_count as f32 / *all_max as f32).floor() as u16,
343 1u16,
344 );
345
346 let (excessive, unexcessive) = all_measurements.clone().partition::<Vec<&u32>, _>(|&&m| {
349 m > u16::MAX as u32 || m as u16 * unit_height_steps > max_step_count
350 });
351 let low_max = unexcessive.iter().max();
352 let high_max = excessive.iter().max();
353
354 let (show_excessive, scale_factor) = match (&self.options.view, low_max, high_max) {
356 (ViewPreference::Bottom, Some(&&low_max), _)
358 | (ViewPreference::Top, Some(&&low_max), None) => {
359 (false, max_step_count as f32 / low_max as f32)
360 }
361 (ViewPreference::Top, _, Some(&&high_max))
363 | (ViewPreference::Bottom, None, Some(&&high_max)) => {
364 (true, max_step_count as f32 / high_max as f32)
365 }
366 _ => unimplemented!(),
367 };
368
369 let measurement_to_step_count = |m: &u32| -> i16 {
372 if *m == 0 {
373 return 0;
374 }
375 match (excessive.is_empty(), show_excessive, excessive.contains(&m)) {
376 (false, false, true) => -2i16,
378 (false, true, false) => -1i16,
380 _ => {
382 let step_count = (*m as f32 * scale_factor) as i16;
383 if step_count == 0 {
384 -1i16
386 } else {
387 step_count
388 }
389 }
390 }
391 };
392 let scaled_data: Vec<i16> = self.data.iter().map(measurement_to_step_count).collect();
393 let scaled_data_cmp: Option<Vec<i16>> = self
394 .compare
395 .as_ref()
396 .map(|c| c.data.iter().map(measurement_to_step_count).collect());
397
398 (scaled_data, scaled_data_cmp)
399 }
400}
401
402impl<'a> std::fmt::Display for Chart<'a> {
403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404 self.render(f)
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
413
414 #[test]
415 fn test_two_digit_width() {
416 let chart = Chart::new(
417 &[23, 32, 44, 0, 2, 44, 5, 23, 42, 29, 16],
418 None,
419 ChartOptions::default(),
420 );
421 println!("\ntwo_digit_width\n{chart}");
422 }
423
424 #[test]
425 fn test_value_too_small_for_top() {
426 let chart = Chart::new(
427 &[0, 6837, 18067, 352038],
428 None,
429 ChartOptions {
430 height: 5,
431 view: ViewPreference::Top,
432 display: DisplayMode::Compact,
433 },
434 );
435 println!("\nvalue_too_small_for_top\n{chart}");
436 }
437
438 #[test]
439 fn test_view_bottom_with_only_large() {
440 let chart = Chart::new(
441 &[2332, 3232, 3244, 0],
442 None,
443 ChartOptions {
444 height: 5,
445 view: ViewPreference::Bottom,
446 display: DisplayMode::Compact,
447 },
448 );
449 println!("\nview_bottom_with_only_large\n{chart}");
450 }
451
452 #[test]
453 fn test_view_top_with_only_small() {
454 let chart = Chart::new(
455 &[23, 32, 44, 0],
456 None,
457 ChartOptions {
458 height: 10,
459 view: ViewPreference::Top,
460 display: DisplayMode::Compact,
461 },
462 );
463 println!("\nview_top_with_only_small\n{chart}");
464 }
465
466 #[test]
467 fn test_comparison_portrait() {
468 let chart = Chart::new(
469 &[
470 0, 22, 2, 9, 223, 34, 33, 66, 76, 122, 199, 33, 12, 89, 1222, 100,
471 ],
472 Some(ChartComparison {
473 data: &[
474 14, 20, 1, 8, 223, 12, 56, 79, 69, 100, 1122, 33, 45, 9, 9000, 78,
475 ],
476 }),
477 ChartOptions {
478 height: 16,
479 view: ViewPreference::Bottom,
480 display: DisplayMode::Portrait {
481 labels: &[
482 "first",
483 "second",
484 "third",
485 "fourth",
486 "fifth",
487 "sixth",
488 "seventh",
489 "eighth",
490 "nineth",
491 "tenth",
492 "eleventh",
493 "twelfth",
494 "thirteenth",
495 "fourteenth",
496 "fifteenth",
497 "sixteenth",
498 ],
499 },
500 },
501 );
502 println!("\ncomparison_portrait\n{chart}");
503 }
504}