1use std;
4use std::collections::HashMap;
5
6use crate::axis;
7use crate::repr;
8use crate::style;
9use crate::utils::PairWise;
10
11fn value_to_axis_cell_offset(value: f64, axis: &axis::ContinuousAxis, face_cells: u32) -> i32 {
14 let data_per_cell = (axis.max() - axis.min()) / f64::from(face_cells);
15 ((value - axis.min()) / data_per_cell).round() as i32
16}
17
18fn tick_offset_map(axis: &axis::ContinuousAxis, face_width: u32) -> HashMap<i32, f64> {
23 axis.ticks()
24 .iter()
25 .map(|&tick| (value_to_axis_cell_offset(tick, axis, face_width), tick))
26 .collect()
27}
28
29fn bound_cell_offsets(
34 hist: &repr::Histogram,
35 x_axis: &axis::ContinuousAxis,
36 face_width: u32,
37) -> Vec<i32> {
38 hist.bin_bounds
39 .iter()
40 .map(|&bound| value_to_axis_cell_offset(bound, x_axis, face_width))
41 .collect()
42}
43
44fn bins_for_cells(bound_cell_offsets: &[i32], face_width: u32) -> Vec<Option<i32>> {
49 let bound_cells = bound_cell_offsets;
50
51 let bin_width_in_cells = bound_cells.pairwise().map(|(&a, &b)| b - a);
52 let bins_cell_offset = bound_cells.first().unwrap();
53
54 let mut cell_bins: Vec<Option<i32>> = vec![None]; for (bin, width) in bin_width_in_cells.enumerate() {
56 for _ in 0..width {
58 cell_bins.push(Some(bin as i32));
59 }
60 }
61 cell_bins.push(None); if *bins_cell_offset <= 0 {
64 cell_bins = cell_bins
65 .iter()
66 .skip(bins_cell_offset.wrapping_abs() as usize)
67 .cloned()
68 .collect();
69 } else {
70 let mut new_bins = vec![None; (*bins_cell_offset) as usize];
71 new_bins.extend(cell_bins.iter());
72 cell_bins = new_bins;
73 }
74
75 if cell_bins.len() <= face_width as usize + 2 {
76 let deficit = face_width as usize + 2 - cell_bins.len();
77 let mut new_bins = cell_bins;
78 new_bins.extend(vec![None; deficit].iter());
79 cell_bins = new_bins;
80 } else {
81 let new_bins = cell_bins;
82 cell_bins = new_bins
83 .iter()
84 .take(face_width as usize + 2)
85 .cloned()
86 .collect();
87 }
88
89 cell_bins
90}
91
92#[derive(Debug)]
94struct XAxisLabel {
95 text: String,
96 offset: i32,
97}
98
99impl XAxisLabel {
100 fn len(&self) -> usize {
101 self.text.len()
102 }
103
104 fn footprint(&self) -> usize {
107 if self.len() % 2 == 0 {
108 self.len() + 1
109 } else {
110 self.len()
111 }
112 }
113
114 fn start_offset(&self) -> i32 {
116 self.offset as i32 - self.footprint() as i32 / 2
117 }
118}
119
120fn create_x_axis_labels(x_tick_map: &HashMap<i32, f64>) -> Vec<XAxisLabel> {
121 let mut ls: Vec<_> = x_tick_map
122 .iter()
123 .map(|(&offset, &tick)| XAxisLabel {
124 text: tick.to_string(),
125 offset,
126 })
127 .collect();
128 ls.sort_by_key(|l| l.offset);
129 ls
130}
131
132pub fn render_y_axis_strings(y_axis: &axis::ContinuousAxis, face_height: u32) -> (String, i32) {
133 let y_tick_map = tick_offset_map(y_axis, face_height);
135
136 let longest_y_label_width = y_tick_map
138 .values()
139 .map(|n| n.to_string().len())
140 .max()
141 .expect("ERROR: There are no y-axis ticks");
142
143 let y_axis_label = format!(
144 "{: ^width$}",
145 y_axis.get_label(),
146 width = face_height as usize + 1
147 );
148 let y_axis_label: Vec<_> = y_axis_label.chars().rev().collect();
149
150 let y_label_strings: Vec<_> = (0..=face_height)
152 .map(|line| match y_tick_map.get(&(line as i32)) {
153 Some(v) => v.to_string(),
154 None => "".to_string(),
155 })
156 .collect();
157
158 let y_tick_strings: Vec<_> = (0..=face_height)
160 .map(|line| match y_tick_map.get(&(line as i32)) {
161 Some(_) => "-".to_string(),
162 None => " ".to_string(),
163 })
164 .collect();
165
166 let y_axis_line_strings: Vec<String> = std::iter::repeat('+')
168 .take(1)
169 .chain(std::iter::repeat('|').take(face_height as usize))
170 .map(|s| s.to_string())
171 .collect();
172
173 let iter = y_axis_label
174 .iter()
175 .zip(y_label_strings.iter())
176 .zip(y_tick_strings.iter())
177 .zip(y_axis_line_strings.iter())
178 .map(|(((a, x), y), z)| (a, x, y, z));
179
180 let axis_string: Vec<String> = iter
181 .rev()
182 .map(|(l, ls, t, a)| {
183 format!(
184 "{} {:>num_width$}{}{}",
185 l,
186 ls,
187 t,
188 a,
189 num_width = longest_y_label_width
190 )
191 })
192 .collect();
193
194 let axis_string = axis_string.join("\n");
195
196 (axis_string, longest_y_label_width as i32)
197}
198
199pub fn render_x_axis_strings(x_axis: &axis::ContinuousAxis, face_width: u32) -> (String, i32) {
200 let x_tick_map = tick_offset_map(x_axis, face_width as u32);
202
203 let x_axis_tick_string: String = (0..=face_width)
205 .map(|cell| match x_tick_map.get(&(cell as i32)) {
206 Some(_) => '|',
207 None => ' ',
208 })
209 .collect();
210
211 let x_labels = create_x_axis_labels(&x_tick_map);
213 let start_offset = x_labels
214 .iter()
215 .map(|label| label.start_offset())
216 .min()
217 .expect("ERROR: Could not compute start offset of x-axis");
218
219 let mut x_axis_label_string = "".to_string();
221 for label in (&x_labels).iter() {
222 let spaces_to_append =
223 label.start_offset() - start_offset - x_axis_label_string.len() as i32;
224 if spaces_to_append.is_positive() {
225 for _ in 0..spaces_to_append {
226 x_axis_label_string.push(' ');
227 }
228 } else {
229 for _ in 0..spaces_to_append.wrapping_neg() {
230 x_axis_label_string.pop();
231 }
232 }
233 let formatted_label = format!("{: ^footprint$}", label.text, footprint = label.footprint());
234 x_axis_label_string.push_str(&formatted_label);
235 }
236
237 let x_axis_line_string: String = std::iter::repeat('+')
239 .take(1)
240 .chain(std::iter::repeat('-').take(face_width as usize))
241 .collect();
242
243 let x_axis_label = format!(
244 "{: ^width$}",
245 x_axis.get_label(),
246 width = face_width as usize
247 );
248
249 let x_axis_string = if start_offset.is_positive() {
250 let padding = (0..start_offset).map(|_| " ").collect::<String>();
251 format!(
252 "{}\n{}\n{}{}\n{}",
253 x_axis_line_string, x_axis_tick_string, padding, x_axis_label_string, x_axis_label
254 )
255 } else {
256 let padding = (0..start_offset.wrapping_neg())
257 .map(|_| " ")
258 .collect::<String>();
259 format!(
260 "{}{}\n{}{}\n{}\n{}{}",
261 padding,
262 x_axis_line_string,
263 padding,
264 x_axis_tick_string,
265 x_axis_label_string,
266 padding,
267 x_axis_label
268 )
269 };
270
271 (x_axis_string, start_offset)
272}
273
274pub fn render_face_bars(
279 h: &repr::Histogram,
280 x_axis: &axis::ContinuousAxis,
281 y_axis: &axis::ContinuousAxis,
282 face_width: u32,
283 face_height: u32,
284) -> String {
285 let bound_cells = bound_cell_offsets(h, x_axis, face_width);
286
287 let cell_bins = bins_for_cells(&bound_cells, face_width);
288
289 let cell_heights: Vec<_> = cell_bins
291 .iter()
292 .map(|&bin| match bin {
293 None => 0,
294 Some(b) => value_to_axis_cell_offset(h.get_values()[b as usize], y_axis, face_height),
295 })
296 .collect();
297
298 let mut face_strings: Vec<String> = vec![];
299
300 for line in 1..=face_height {
301 let mut line_string = String::new();
302 for column in 1..=face_width as usize {
303 line_string.push(if bound_cells.contains(&(column as i32)) {
305 let b = cell_heights[column - 1].cmp(&(line as i32));
307 let a = cell_heights[column + 1].cmp(&(line as i32));
309 match b {
310 std::cmp::Ordering::Less => {
311 match a {
312 std::cmp::Ordering::Less => ' ',
313 std::cmp::Ordering::Equal => '-', std::cmp::Ordering::Greater => '|',
315 }
316 }
317 std::cmp::Ordering::Equal => {
318 match a {
319 std::cmp::Ordering::Less => '-', std::cmp::Ordering::Equal => '-', std::cmp::Ordering::Greater => '|', }
323 }
324 std::cmp::Ordering::Greater => {
325 match a {
326 std::cmp::Ordering::Less => '|',
327 std::cmp::Ordering::Equal => '|', std::cmp::Ordering::Greater => '|',
329 }
330 }
331 }
332 } else {
333 let bin_height_cells = cell_heights[column];
334
335 if bin_height_cells == line as i32 {
336 '-' } else {
338 ' ' }
340 });
341 }
342 face_strings.push(line_string);
343 }
344 let face_strings: Vec<String> = face_strings.iter().rev().cloned().collect();
345 face_strings.join("\n")
346}
347
348pub fn render_face_points(
353 s: &[(f64, f64)],
354 x_axis: &axis::ContinuousAxis,
355 y_axis: &axis::ContinuousAxis,
356 face_width: u32,
357 face_height: u32,
358 style: &style::PointStyle,
359) -> String {
360 let points: Vec<_> = s
361 .iter()
362 .map(|&(x, y)| {
363 (
364 value_to_axis_cell_offset(x, x_axis, face_width),
365 value_to_axis_cell_offset(y, y_axis, face_height),
366 )
367 })
368 .collect();
369
370 let marker = match style.get_marker() {
371 style::PointMarker::Circle => '●',
372 style::PointMarker::Square => '■',
373 style::PointMarker::Cross => '×',
374 };
375
376 let mut face_strings: Vec<String> = vec![];
377 for line in 1..=face_height {
378 let mut line_string = String::new();
379 for column in 1..=face_width as usize {
380 line_string.push(if points.contains(&(column as i32, line as i32)) {
381 marker
382 } else {
383 ' '
384 });
385 }
386 face_strings.push(line_string);
387 }
388 let face_strings: Vec<String> = face_strings.iter().rev().cloned().collect();
389 face_strings.join("\n")
390}
391
392pub fn overlay(under: &str, over: &str, x: i32, y: i32) -> String {
394 let split_under: Vec<_> = under.split('\n').collect();
395 let under_width = split_under.iter().map(|s| s.len()).max().unwrap();
396 let under_height = split_under.len();
397
398 let split_over: Vec<String> = over.split('\n').map(|s| s.to_string()).collect();
399 let over_width = split_over.iter().map(|s| s.len()).max().unwrap();
400
401 let split_over: Vec<String> = if y.is_negative() {
405 split_over.iter().skip(y.abs() as usize).cloned().collect()
406 } else if y.is_positive() {
407 (0..y)
408 .map(|_| (0..over_width).map(|_| ' ').collect())
409 .chain(split_over.iter().map(|s| s.to_string()))
410 .collect()
411 } else {
412 split_over
413 };
414
415 let split_over: Vec<String> = if x.is_negative() {
417 split_over
418 .iter()
419 .map(|l| l.chars().skip(x.abs() as usize).collect())
420 .collect()
421 } else if x.is_positive() {
422 split_over
423 .iter()
424 .map(|s| (0..x).map(|_| ' ').chain(s.chars()).collect())
425 .collect()
426 } else {
427 split_over
428 };
429
430 let over_width = split_over.iter().map(|s| s.len()).max().unwrap();
432 let over_height = split_over.len();
433 let lines_deficit = under_height as i32 - over_height as i32;
434 let split_over: Vec<String> = if lines_deficit.is_positive() {
435 let new_lines: Vec<String> = (0..lines_deficit)
436 .map(|_| (0..over_width).map(|_| ' ').collect::<String>())
437 .collect();
438 let mut temp = split_over;
439 for new_line in new_lines {
440 temp.push(new_line);
441 }
442 temp
443 } else {
444 split_over
445 };
446
447 let line_width_deficit = under_width as i32 - over_width as i32;
449 let split_over: Vec<String> = if line_width_deficit.is_positive() {
450 split_over
451 .iter()
452 .map(|l| {
453 l.chars()
454 .chain((0..line_width_deficit).map(|_| ' '))
455 .collect()
456 })
457 .collect()
458 } else {
459 split_over
460 };
461
462 let mut out: Vec<String> = vec![];
464 for (l, ol) in split_under.iter().zip(split_over.iter()) {
465 let mut new_line = "".to_string();
466 for (c, oc) in l.chars().zip(ol.chars()) {
467 new_line.push(if oc == ' ' { c } else { oc });
468 }
469 out.push(new_line);
470 }
471
472 out.join("\n")
473}
474
475pub fn empty_face(width: u32, height: u32) -> String {
476 (0..height)
477 .map(|_| " ".repeat(width as usize))
478 .collect::<Vec<String>>()
479 .join("\n")
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn test_bins_for_cells() {
488 let face_width = 10;
489 let n = i32::max_value();
490 let run_bins_for_cells = |bound_cell_offsets: &[i32]| -> Vec<_> {
491 bins_for_cells(&bound_cell_offsets, face_width)
492 .iter()
493 .map(|&a| a.unwrap_or(n))
494 .collect()
495 };
496
497 assert_eq!(
498 run_bins_for_cells(&[-4, -1, 4, 7, 10]),
499 [1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, n]
500 );
501 assert_eq!(
502 run_bins_for_cells(&[0, 2, 4, 8, 10]),
503 [n, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, n]
504 );
505 assert_eq!(
506 run_bins_for_cells(&[3, 5, 7, 9, 10]),
507 [n, n, n, n, 0, 0, 1, 1, 2, 2, 3, n]
508 );
509 assert_eq!(
510 run_bins_for_cells(&[0, 2, 4, 6, 8]),
511 [n, 0, 0, 1, 1, 2, 2, 3, 3, n, n, n]
512 );
513 assert_eq!(
514 run_bins_for_cells(&[0, 3, 6, 9, 12]),
515 [n, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3]
516 );
517
518 assert_eq!(
519 run_bins_for_cells(&[-5, -4, -3, -1, 0]),
520 [3, n, n, n, n, n, n, n, n, n, n, n]
521 );
522 assert_eq!(
523 run_bins_for_cells(&[10, 12, 14, 16, 18]),
524 [n, n, n, n, n, n, n, n, n, n, n, 0]
525 );
526
527 assert_eq!(
528 run_bins_for_cells(&[15, 16, 17, 18, 19]),
529 [n, n, n, n, n, n, n, n, n, n, n, n]
530 );
531 assert_eq!(
532 run_bins_for_cells(&[-19, -18, -17, -16, -1]),
533 [n, n, n, n, n, n, n, n, n, n, n, n]
534 );
535 }
536
537 #[test]
538 fn test_value_to_axis_cell_offset() {
539 assert_eq!(
540 value_to_axis_cell_offset(3.0, &axis::ContinuousAxis::new(5.0, 10.0, 6), 10),
541 -4
542 );
543 }
544
545 #[test]
546 fn test_x_axis_label() {
547 let l = XAxisLabel {
548 text: "3".to_string(),
549 offset: 2,
550 };
551 assert_eq!(l.len(), 1);
552 assert_ne!(l.footprint() % 2, 0);
553 assert_eq!(l.start_offset(), 2);
554
555 let l = XAxisLabel {
556 text: "34".to_string(),
557 offset: 2,
558 };
559 assert_eq!(l.len(), 2);
560 assert_ne!(l.footprint() % 2, 0);
561 assert_eq!(l.start_offset(), 1);
562
563 let l = XAxisLabel {
564 text: "345".to_string(),
565 offset: 2,
566 };
567 assert_eq!(l.len(), 3);
568 assert_ne!(l.footprint() % 2, 0);
569 assert_eq!(l.start_offset(), 1);
570
571 let l = XAxisLabel {
572 text: "3454".to_string(),
573 offset: 1,
574 };
575 assert_eq!(l.len(), 4);
576 assert_ne!(l.footprint() % 2, 0);
577 assert_eq!(l.start_offset(), -1);
578 }
579
580 #[test]
581 fn test_render_y_axis_strings() {
582 let y_axis = axis::ContinuousAxis::new(0.0, 10.0, 6);
583
584 let (y_axis_string, longest_y_label_width) = render_y_axis_strings(&y_axis, 10);
585
586 assert!(y_axis_string.contains(&"0".to_string()));
587 assert!(y_axis_string.contains(&"6".to_string()));
588 assert!(y_axis_string.contains(&"10".to_string()));
589 assert_eq!(longest_y_label_width, 2);
590 }
591
592 #[test]
593 fn test_render_x_axis_strings() {
594 let x_axis = axis::ContinuousAxis::new(0.0, 10.0, 6);
595
596 let (x_axis_string, start_offset) = render_x_axis_strings(&x_axis, 20);
597
598 assert!(x_axis_string.contains("0 "));
599 assert!(x_axis_string.contains(" 6 "));
600 assert!(x_axis_string.contains(" 10"));
601 assert_eq!(x_axis_string.chars().filter(|&c| c == '|').count(), 6);
602 assert_eq!(start_offset, 0);
603 }
604
605 #[test]
606 fn test_render_face_bars() {
607 let data = vec![0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0];
608 let h = repr::Histogram::from_slice(&data, repr::HistogramBins::Count(10));
609 let x_axis = axis::ContinuousAxis::new(0.3, 7.5, 6);
610 let y_axis = axis::ContinuousAxis::new(0., 3., 6);
611 let strings = render_face_bars(&h, &x_axis, &y_axis, 20, 10);
612 assert_eq!(strings.lines().count(), 10);
613 assert!(strings.lines().all(|s| s.chars().count() == 20));
614
615 let comp = vec![
616 " --- ",
617 " | | ",
618 " | | ",
619 "-- | | ",
620 " | | | ",
621 " | | | ",
622 " | | | ",
623 " | | |---- -----",
624 " | | | | | | | |",
625 " | | | | | | | |",
626 ]
627 .join("\n");
628
629 assert_eq!(&strings, &comp);
630 }
631
632 #[test]
633 fn test_render_face_points() {
634 use crate::style::PointStyle;
635 let data = vec![
636 (-3.0, 2.3),
637 (-1.6, 5.3),
638 (0.3, 0.7),
639 (4.3, -1.4),
640 (6.4, 4.3),
641 (8.5, 3.7),
642 ];
643 let x_axis = axis::ContinuousAxis::new(-3.575, 9.075, 6);
644 let y_axis = axis::ContinuousAxis::new(-1.735, 5.635, 6);
645 let style = PointStyle::new();
646 let strings = render_face_points(&data, &x_axis, &y_axis, 20, 10, &style);
648 assert_eq!(strings.lines().count(), 10);
649 assert!(strings.lines().all(|s| s.chars().count() == 20));
650
651 let comp = vec![
652 " ● ",
653 " ",
654 " ● ",
655 " ● ",
656 " ",
657 "● ",
658 " ",
659 " ● ",
660 " ",
661 " ",
662 ]
663 .join("\n");
664
665 assert_eq!(&strings, &comp);
666 }
667
668 #[test]
669 fn test_overlay() {
670 let a = " ooo ";
671 let b = " # ";
672 let r = " o#o ";
673 assert_eq!(overlay(a, b, 0, 0), r);
674
675 let a = " o o o o o o o o o o ";
676 let b = "# # # # #";
677 let r = " o#o#o#o#o#o o o o o ";
678 assert_eq!(overlay(a, b, 2, 0), r);
679
680 let a = " \n o \n o o\nooooo\no o o";
681 let b = " # \n # \n \n ## \n ##";
682 let r = " # \n # \n o o\noo##o\no o##";
683 assert_eq!(overlay(a, b, 0, 0), r);
684
685 let a = " \n o \n o o\nooooo\no o o";
686 let b = " #\n## ";
687 let r = " \n o \n o #o\no##oo\no o o";
688 assert_eq!(overlay(a, b, 1, 2), r);
689
690 let a = " \n o \n o o\nooooo\no o o";
691 let b = "###\n###\n###";
692 let r = "## \n## o \n o o\nooooo\no o o";
693 assert_eq!(overlay(a, b, -1, -1), r);
694
695 let a = "oo\noo";
696 let b = " \n # \n # \n ";
697 let r = "o#\n#o";
698 assert_eq!(overlay(a, b, -1, -1), r);
699 }
700
701 #[test]
702 fn test_empty_face() {
703 assert_eq!(empty_face(0, 0), "");
704 assert_eq!(empty_face(1, 1), " ");
705 assert_eq!(empty_face(2, 2), " \n ");
706 assert_eq!(empty_face(2, 3), " \n \n ");
707 assert_eq!(empty_face(4, 2), " \n ");
708 }
709}