plotlib/
text_render.rs

1//! A module for plotting graphs
2
3use std;
4use std::collections::HashMap;
5
6use crate::axis;
7use crate::repr;
8use crate::style;
9use crate::utils::PairWise;
10
11// Given a value like a tick label or a bin count,
12// calculate how far from the x-axis it should be plotted
13fn 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
18/// Given a list of ticks to display,
19/// the total scale of the axis
20/// and the number of face cells to work with,
21/// create a mapping of cell offset to tick value
22fn 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
29/// Given a histogram object,
30/// the total scale of the axis
31/// and the number of face cells to work with,
32/// return which cells will contain a bin bound
33fn 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
44/// calculate for each cell which bin it is representing
45/// Cells which straddle bins will return the bin just on the lower side of the centre of the cell
46/// Will return a vector with (`face_width + 2`) entries to represent underflow and overflow cells
47/// cells which do not map to a bin will return `None`.
48fn 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]; // start with a prepended negative null
55    for (bin, width) in bin_width_in_cells.enumerate() {
56        // repeat bin, width times
57        for _ in 0..width {
58            cell_bins.push(Some(bin as i32));
59        }
60    }
61    cell_bins.push(None); // end with an appended positive null
62
63    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/// An x-axis label for the text output renderer
93#[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    /// The number of cells the label will actually use
105    /// We want this to always be an odd number
106    fn footprint(&self) -> usize {
107        if self.len() % 2 == 0 {
108            self.len() + 1
109        } else {
110            self.len()
111        }
112    }
113
114    /// The offset, relative to the zero-point of the axis where the label should start to be drawn
115    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    // Get the strings and offsets we'll use for the y-axis
134    let y_tick_map = tick_offset_map(y_axis, face_height);
135
136    // Find a minimum size for the left gutter
137    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    // Generate a list of strings to label the y-axis
151    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    // Generate a list of strings to tick the y-axis
159    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    // Generate a list of strings to be the y-axis line itself
167    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    // Get the strings and offsets we'll use for the x-axis
201    let x_tick_map = tick_offset_map(x_axis, face_width as u32);
202
203    // Create a string which will be printed to give the x-axis tick marks
204    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    // Create a string which will be printed to give the x-axis labels
212    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    // This string will be printed, starting at start_offset relative to the x-axis zero cell
220    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    // Generate a list of strings to be the y-axis line itself
238    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
274/// Given a histogram,
275/// the x ands y-axes
276/// and the face height and width,
277/// create the strings to be drawn as the face
278pub 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    // counts per bin converted to rows per column
290    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            // maybe use a HashSet for faster `contains()`?
304            line_string.push(if bound_cells.contains(&(column as i32)) {
305                // The value of the column _below_ this one
306                let b = cell_heights[column - 1].cmp(&(line as i32));
307                // The value of the column _above_ this one
308                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 => '-', // or 'r'-shaped corner
314                            std::cmp::Ordering::Greater => '|',
315                        }
316                    }
317                    std::cmp::Ordering::Equal => {
318                        match a {
319                            std::cmp::Ordering::Less => '-',    // or backwards 'r'
320                            std::cmp::Ordering::Equal => '-',   // or 'T'-shaped
321                            std::cmp::Ordering::Greater => '|', // or '-|'
322                        }
323                    }
324                    std::cmp::Ordering::Greater => {
325                        match a {
326                            std::cmp::Ordering::Less => '|',
327                            std::cmp::Ordering::Equal => '|', // or '|-'
328                            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                    '-' // bar cap
337                } else {
338                    ' ' //
339                }
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
348/// Given a scatter plot,
349/// the x ands y-axes
350/// and the face height and width,
351/// create the strings to be drawn as the face
352pub 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
392/// Given two 'rectangular' strings, overlay the second on the first offset by `x` and `y`
393pub 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    // Take `over` and pad it so that it matches `under`'s dimensions
402
403    // Trim/add lines at beginning
404    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    // Trim/add chars at beginning
416    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    // pad out end of vector
431    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    // pad out end of each line
448    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    // Now that the dimensions match, overlay them
463    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        //TODO NEXT
647        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}