milliams / plotlib
1
//! A module for plotting graphs
2

3
use std;
4
use std::collections::HashMap;
5

6
use crate::axis;
7
use crate::repr;
8
use crate::style;
9
use 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
13 1
fn value_to_axis_cell_offset(value: f64, axis: &axis::ContinuousAxis, face_cells: u32) -> i32 {
14 1
    let data_per_cell = (axis.max() - axis.min()) / f64::from(face_cells);
15 1
    ((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
22 1
fn tick_offset_map(axis: &axis::ContinuousAxis, face_width: u32) -> HashMap<i32, f64> {
23 1
    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
33 1
fn bound_cell_offsets(
34
    hist: &repr::Histogram,
35
    x_axis: &axis::ContinuousAxis,
36
    face_width: u32,
37
) -> Vec<i32> {
38 1
    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`.
48 1
fn bins_for_cells(bound_cell_offsets: &[i32], face_width: u32) -> Vec<Option<i32>> {
49 1
    let bound_cells = bound_cell_offsets;
50

51 1
    let bin_width_in_cells = bound_cells.pairwise().map(|(&a, &b)| b - a);
52 1
    let bins_cell_offset = bound_cells.first().unwrap();
53

54 1
    let mut cell_bins: Vec<Option<i32>> = vec![None]; // start with a prepended negative null
55 1
    for (bin, width) in bin_width_in_cells.enumerate() {
56
        // repeat bin, width times
57 1
        for _ in 0..width {
58 1
            cell_bins.push(Some(bin as i32));
59
        }
60
    }
61 1
    cell_bins.push(None); // end with an appended positive null
62

63 1
    if *bins_cell_offset <= 0 {
64 1
        cell_bins = cell_bins
65
            .iter()
66 1
            .skip(bins_cell_offset.wrapping_abs() as usize)
67
            .cloned()
68
            .collect();
69
    } else {
70 1
        let mut new_bins = vec![None; (*bins_cell_offset) as usize];
71 1
        new_bins.extend(cell_bins.iter());
72 1
        cell_bins = new_bins;
73
    }
74

75 1
    if cell_bins.len() <= face_width as usize + 2 {
76 1
        let deficit = face_width as usize + 2 - cell_bins.len();
77 1
        let mut new_bins = cell_bins;
78 1
        new_bins.extend(vec![None; deficit].iter());
79 1
        cell_bins = new_bins;
80
    } else {
81 1
        let new_bins = cell_bins;
82 1
        cell_bins = new_bins
83
            .iter()
84 1
            .take(face_width as usize + 2)
85
            .cloned()
86
            .collect();
87
    }
88

89 1
    cell_bins
90
}
91

92
/// An x-axis label for the text output renderer
93
#[derive(Debug)]
94
struct XAxisLabel {
95
    text: String,
96
    offset: i32,
97
}
98

99
impl XAxisLabel {
100 1
    fn len(&self) -> usize {
101 1
        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 1
    fn footprint(&self) -> usize {
107 1
        if self.len() % 2 == 0 {
108 1
            self.len() + 1
109
        } else {
110 1
            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 1
    fn start_offset(&self) -> i32 {
116 1
        self.offset as i32 - self.footprint() as i32 / 2
117
    }
118
}
119

120 1
fn create_x_axis_labels(x_tick_map: &HashMap<i32, f64>) -> Vec<XAxisLabel> {
121 1
    let mut ls: Vec<_> = x_tick_map
122
        .iter()
123 1
        .map(|(&offset, &tick)| XAxisLabel {
124 1
            text: tick.to_string(),
125 1
            offset,
126
        })
127
        .collect();
128 1
    ls.sort_by_key(|l| l.offset);
129 1
    ls
130
}
131

132 1
pub 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 1
    let y_tick_map = tick_offset_map(y_axis, face_height);
135

136
    // Find a minimum size for the left gutter
137 1
    let longest_y_label_width = y_tick_map
138
        .values()
139 1
        .map(|n| n.to_string().len())
140
        .max()
141
        .expect("ERROR: There are no y-axis ticks");
142

143 1
    let y_axis_label = format!(
144 1
        "{: ^width$}",
145 1
        y_axis.get_label(),
146 1
        width = face_height as usize + 1
147
    );
148 1
    let y_axis_label: Vec<_> = y_axis_label.chars().rev().collect();
149

150
    // Generate a list of strings to label the y-axis
151 1
    let y_label_strings: Vec<_> = (0..=face_height)
152 1
        .map(|line| match y_tick_map.get(&(line as i32)) {
153 1
            Some(v) => v.to_string(),
154 1
            None => "".to_string(),
155
        })
156
        .collect();
157

158
    // Generate a list of strings to tick the y-axis
159 1
    let y_tick_strings: Vec<_> = (0..=face_height)
160 1
        .map(|line| match y_tick_map.get(&(line as i32)) {
161 1
            Some(_) => "-".to_string(),
162 1
            None => " ".to_string(),
163
        })
164
        .collect();
165

166
    // Generate a list of strings to be the y-axis line itself
167 1
    let y_axis_line_strings: Vec<String> = std::iter::repeat('+')
168
        .take(1)
169 1
        .chain(std::iter::repeat('|').take(face_height as usize))
170 1
        .map(|s| s.to_string())
171
        .collect();
172

173 1
    let iter = y_axis_label
174
        .iter()
175 1
        .zip(y_label_strings.iter())
176 1
        .zip(y_tick_strings.iter())
177 1
        .zip(y_axis_line_strings.iter())
178 1
        .map(|(((a, x), y), z)| (a, x, y, z));
179

180 1
    let axis_string: Vec<String> = iter
181
        .rev()
182 1
        .map(|(l, ls, t, a)| {
183 1
            format!(
184 1
                "{} {:>num_width$}{}{}",
185
                l,
186
                ls,
187
                t,
188
                a,
189 1
                num_width = longest_y_label_width
190
            )
191
        })
192
        .collect();
193

194 1
    let axis_string = axis_string.join("\n");
195

196 1
    (axis_string, longest_y_label_width as i32)
197
}
198

199 1
pub 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 1
    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 1
    let x_axis_tick_string: String = (0..=face_width)
205 1
        .map(|cell| match x_tick_map.get(&(cell as i32)) {
206 1
            Some(_) => '|',
207 1
            None => ' ',
208
        })
209
        .collect();
210

211
    // Create a string which will be printed to give the x-axis labels
212 1
    let x_labels = create_x_axis_labels(&x_tick_map);
213 1
    let start_offset = x_labels
214
        .iter()
215 1
        .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 1
    let mut x_axis_label_string = "".to_string();
221 1
    for label in (&x_labels).iter() {
222
        let spaces_to_append =
223 1
            label.start_offset() - start_offset - x_axis_label_string.len() as i32;
224 1
        if spaces_to_append.is_positive() {
225 1
            for _ in 0..spaces_to_append {
226 1
                x_axis_label_string.push(' ');
227
            }
228
        } else {
229 1
            for _ in 0..spaces_to_append.wrapping_neg() {
230 0
                x_axis_label_string.pop();
231
            }
232
        }
233 1
        let formatted_label = format!("{: ^footprint$}", label.text, footprint = label.footprint());
234 1
        x_axis_label_string.push_str(&formatted_label);
235
    }
236

237
    // Generate a list of strings to be the y-axis line itself
238 1
    let x_axis_line_string: String = std::iter::repeat('+')
239
        .take(1)
240 1
        .chain(std::iter::repeat('-').take(face_width as usize))
241
        .collect();
242

243 1
    let x_axis_label = format!(
244 1
        "{: ^width$}",
245 1
        x_axis.get_label(),
246 1
        width = face_width as usize
247
    );
248

249 1
    let x_axis_string = if start_offset.is_positive() {
250 0
        let padding = (0..start_offset).map(|_| " ").collect::<String>();
251 0
        format!(
252 0
            "{}\n{}\n{}{}\n{}",
253
            x_axis_line_string, x_axis_tick_string, padding, x_axis_label_string, x_axis_label
254
        )
255
    } else {
256 1
        let padding = (0..start_offset.wrapping_neg())
257 0
            .map(|_| " ")
258
            .collect::<String>();
259 1
        format!(
260 1
            "{}{}\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 1
    (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
278 1
pub 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 1
    let bound_cells = bound_cell_offsets(h, x_axis, face_width);
286

287 1
    let cell_bins = bins_for_cells(&bound_cells, face_width);
288

289
    // counts per bin converted to rows per column
290 1
    let cell_heights: Vec<_> = cell_bins
291
        .iter()
292 1
        .map(|&bin| match bin {
293 1
            None => 0,
294 1
            Some(b) => value_to_axis_cell_offset(h.get_values()[b as usize], y_axis, face_height),
295
        })
296
        .collect();
297

298 1
    let mut face_strings: Vec<String> = vec![];
299

300 1
    for line in 1..=face_height {
301 1
        let mut line_string = String::new();
302 1
        for column in 1..=face_width as usize {
303
            // maybe use a HashSet for faster `contains()`?
304 1
            line_string.push(if bound_cells.contains(&(column as i32)) {
305
                // The value of the column _below_ this one
306 1
                let b = cell_heights[column - 1].cmp(&(line as i32));
307
                // The value of the column _above_ this one
308 1
                let a = cell_heights[column + 1].cmp(&(line as i32));
309 0
                match b {
310 1
                    std::cmp::Ordering::Less => {
311 1
                        match a {
312 1
                            std::cmp::Ordering::Less => ' ',
313 1
                            std::cmp::Ordering::Equal => '-', // or 'r'-shaped corner
314 1
                            std::cmp::Ordering::Greater => '|',
315
                        }
316
                    }
317
                    std::cmp::Ordering::Equal => {
318 1
                        match a {
319 1
                            std::cmp::Ordering::Less => '-',    // or backwards 'r'
320 1
                            std::cmp::Ordering::Equal => '-',   // or 'T'-shaped
321 0
                            std::cmp::Ordering::Greater => '|', // or '-|'
322
                        }
323
                    }
324
                    std::cmp::Ordering::Greater => {
325 1
                        match a {
326 1
                            std::cmp::Ordering::Less => '|',
327 1
                            std::cmp::Ordering::Equal => '|', // or '|-'
328 1
                            std::cmp::Ordering::Greater => '|',
329
                        }
330
                    }
331
                }
332
            } else {
333 1
                let bin_height_cells = cell_heights[column];
334

335 1
                if bin_height_cells == line as i32 {
336 1
                    '-' // bar cap
337
                } else {
338 1
                    ' ' //
339
                }
340
            });
341
        }
342 1
        face_strings.push(line_string);
343
    }
344 1
    let face_strings: Vec<String> = face_strings.iter().rev().cloned().collect();
345 1
    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
352 1
pub 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 1
    let points: Vec<_> = s
361
        .iter()
362 1
        .map(|&(x, y)| {
363
            (
364 1
                value_to_axis_cell_offset(x, x_axis, face_width),
365 1
                value_to_axis_cell_offset(y, y_axis, face_height),
366
            )
367
        })
368
        .collect();
369

370 1
    let marker = match style.get_marker() {
371 1
        style::PointMarker::Circle => '●',
372 0
        style::PointMarker::Square => '■',
373 0
        style::PointMarker::Cross => '×',
374
    };
375

376 1
    let mut face_strings: Vec<String> = vec![];
377 1
    for line in 1..=face_height {
378 1
        let mut line_string = String::new();
379 1
        for column in 1..=face_width as usize {
380 1
            line_string.push(if points.contains(&(column as i32, line as i32)) {
381 1
                marker
382
            } else {
383 1
                ' '
384
            });
385
        }
386 1
        face_strings.push(line_string);
387
    }
388 1
    let face_strings: Vec<String> = face_strings.iter().rev().cloned().collect();
389 1
    face_strings.join("\n")
390
}
391

392
/// Given two 'rectangular' strings, overlay the second on the first offset by `x` and `y`
393 1
pub fn overlay(under: &str, over: &str, x: i32, y: i32) -> String {
394 1
    let split_under: Vec<_> = under.split('\n').collect();
395 1
    let under_width = split_under.iter().map(|s| s.len()).max().unwrap();
396 1
    let under_height = split_under.len();
397

398 1
    let split_over: Vec<String> = over.split('\n').map(|s| s.to_string()).collect();
399 1
    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 1
    let split_over: Vec<String> = if y.is_negative() {
405 1
        split_over.iter().skip(y.abs() as usize).cloned().collect()
406 1
    } else if y.is_positive() {
407 1
        (0..y)
408 1
            .map(|_| (0..over_width).map(|_| ' ').collect())
409 1
            .chain(split_over.iter().map(|s| s.to_string()))
410 1
            .collect()
411
    } else {
412 1
        split_over
413
    };
414

415
    // Trim/add chars at beginning
416 1
    let split_over: Vec<String> = if x.is_negative() {
417 1
        split_over
418
            .iter()
419 1
            .map(|l| l.chars().skip(x.abs() as usize).collect())
420 1
            .collect()
421 1
    } else if x.is_positive() {
422 1
        split_over
423
            .iter()
424 1
            .map(|s| (0..x).map(|_| ' ').chain(s.chars()).collect())
425 1
            .collect()
426
    } else {
427 1
        split_over
428
    };
429

430
    // pad out end of vector
431 1
    let over_width = split_over.iter().map(|s| s.len()).max().unwrap();
432 1
    let over_height = split_over.len();
433 1
    let lines_deficit = under_height as i32 - over_height as i32;
434 1
    let split_over: Vec<String> = if lines_deficit.is_positive() {
435 1
        let new_lines: Vec<String> = (0..lines_deficit)
436 1
            .map(|_| (0..over_width).map(|_| ' ').collect::<String>())
437
            .collect();
438 1
        let mut temp = split_over;
439 1
        for new_line in new_lines {
440 1
            temp.push(new_line);
441
        }
442 1
        temp
443
    } else {
444 1
        split_over
445
    };
446

447
    // pad out end of each line
448 1
    let line_width_deficit = under_width as i32 - over_width as i32;
449 1
    let split_over: Vec<String> = if line_width_deficit.is_positive() {
450 1
        split_over
451
            .iter()
452 1
            .map(|l| {
453 1
                l.chars()
454 1
                    .chain((0..line_width_deficit).map(|_| ' '))
455
                    .collect()
456
            })
457
            .collect()
458
    } else {
459 1
        split_over
460
    };
461

462
    // Now that the dimensions match, overlay them
463 1
    let mut out: Vec<String> = vec![];
464 1
    for (l, ol) in split_under.iter().zip(split_over.iter()) {
465 1
        let mut new_line = "".to_string();
466 1
        for (c, oc) in l.chars().zip(ol.chars()) {
467 1
            new_line.push(if oc == ' ' { c } else { oc });
468
        }
469 1
        out.push(new_line);
470
    }
471

472 1
    out.join("\n")
473
}
474

475
#[cfg(test)]
476
mod tests {
477
    use super::*;
478

479
    #[test]
480 1
    fn test_bins_for_cells() {
481 1
        let face_width = 10;
482 1
        let n = i32::max_value();
483 1
        let run_bins_for_cells = |bound_cell_offsets: &[i32]| -> Vec<_> {
484 1
            bins_for_cells(&bound_cell_offsets, face_width)
485
                .iter()
486 1
                .map(|&a| a.unwrap_or(n))
487
                .collect()
488
        };
489

490 1
        assert_eq!(
491 1
            run_bins_for_cells(&vec![-4, -1, 4, 7, 10]),
492 1
            [1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, n]
493
        );
494 1
        assert_eq!(
495 1
            run_bins_for_cells(&vec![0, 2, 4, 8, 10]),
496 1
            [n, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, n]
497
        );
498 1
        assert_eq!(
499 1
            run_bins_for_cells(&vec![3, 5, 7, 9, 10]),
500 1
            [n, n, n, n, 0, 0, 1, 1, 2, 2, 3, n]
501
        );
502 1
        assert_eq!(
503 1
            run_bins_for_cells(&vec![0, 2, 4, 6, 8]),
504 1
            [n, 0, 0, 1, 1, 2, 2, 3, 3, n, n, n]
505
        );
506 1
        assert_eq!(
507 1
            run_bins_for_cells(&vec![0, 3, 6, 9, 12]),
508 1
            [n, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3]
509
        );
510

511 1
        assert_eq!(
512 1
            run_bins_for_cells(&vec![-5, -4, -3, -1, 0]),
513 1
            [3, n, n, n, n, n, n, n, n, n, n, n]
514
        );
515 1
        assert_eq!(
516 1
            run_bins_for_cells(&vec![10, 12, 14, 16, 18]),
517 1
            [n, n, n, n, n, n, n, n, n, n, n, 0]
518
        );
519

520 1
        assert_eq!(
521 1
            run_bins_for_cells(&vec![15, 16, 17, 18, 19]),
522 1
            [n, n, n, n, n, n, n, n, n, n, n, n]
523
        );
524 1
        assert_eq!(
525 1
            run_bins_for_cells(&vec![-19, -18, -17, -16, -1]),
526 1
            [n, n, n, n, n, n, n, n, n, n, n, n]
527
        );
528
    }
529

530
    #[test]
531 1
    fn test_value_to_axis_cell_offset() {
532 1
        assert_eq!(
533 1
            value_to_axis_cell_offset(3.0, &axis::ContinuousAxis::new(5.0, 10.0, 6), 10),
534
            -4
535
        );
536
    }
537

538
    #[test]
539 1
    fn test_x_axis_label() {
540 1
        let l = XAxisLabel {
541 1
            text: "3".to_string(),
542
            offset: 2,
543
        };
544 1
        assert_eq!(l.len(), 1);
545 1
        assert!(l.footprint() % 2 != 0);
546 1
        assert_eq!(l.start_offset(), 2);
547

548 1
        let l = XAxisLabel {
549 1
            text: "34".to_string(),
550
            offset: 2,
551
        };
552 1
        assert_eq!(l.len(), 2);
553 1
        assert!(l.footprint() % 2 != 0);
554 1
        assert_eq!(l.start_offset(), 1);
555

556 1
        let l = XAxisLabel {
557 1
            text: "345".to_string(),
558
            offset: 2,
559
        };
560 1
        assert_eq!(l.len(), 3);
561 1
        assert!(l.footprint() % 2 != 0);
562 1
        assert_eq!(l.start_offset(), 1);
563

564 1
        let l = XAxisLabel {
565 1
            text: "3454".to_string(),
566
            offset: 1,
567
        };
568 1
        assert_eq!(l.len(), 4);
569 1
        assert!(l.footprint() % 2 != 0);
570 1
        assert_eq!(l.start_offset(), -1);
571
    }
572

573
    #[test]
574 1
    fn test_render_y_axis_strings() {
575 1
        let y_axis = axis::ContinuousAxis::new(0.0, 10.0, 6);
576

577 1
        let (y_axis_string, longest_y_label_width) = render_y_axis_strings(&y_axis, 10);
578

579 1
        assert!(y_axis_string.contains(&"0".to_string()));
580 1
        assert!(y_axis_string.contains(&"6".to_string()));
581 1
        assert!(y_axis_string.contains(&"10".to_string()));
582 1
        assert_eq!(longest_y_label_width, 2);
583
    }
584

585
    #[test]
586 1
    fn test_render_x_axis_strings() {
587 1
        let x_axis = axis::ContinuousAxis::new(0.0, 10.0, 6);
588

589 1
        let (x_axis_string, start_offset) = render_x_axis_strings(&x_axis, 20);
590

591 1
        assert!(x_axis_string.contains("0 "));
592 1
        assert!(x_axis_string.contains(" 6 "));
593 1
        assert!(x_axis_string.contains(" 10"));
594 1
        assert_eq!(x_axis_string.chars().filter(|&c| c == '|').count(), 6);
595 1
        assert_eq!(start_offset, 0);
596
    }
597

598
    #[test]
599 1
    fn test_render_face_bars() {
600 1
        let data = vec![0.3, 0.5, 6.4, 5.3, 3.6, 3.6, 3.5, 7.5, 4.0];
601 1
        let h = repr::Histogram::from_slice(&data, repr::HistogramBins::Count(10));
602 1
        let x_axis = axis::ContinuousAxis::new(0.3, 7.5, 6);
603 1
        let y_axis = axis::ContinuousAxis::new(0., 3., 6);
604 1
        let strings = render_face_bars(&h, &x_axis, &y_axis, 20, 10);
605 1
        assert_eq!(strings.lines().count(), 10);
606 1
        assert!(strings.lines().all(|s| s.chars().count() == 20));
607

608 1
        let comp = vec![
609
            "       ---          ",
610
            "       | |          ",
611
            "       | |          ",
612
            "--     | |          ",
613
            " |     | |          ",
614
            " |     | |          ",
615
            " |     | |          ",
616
            " |     | |---- -----",
617
            " |     | | | | | | |",
618
            " |     | | | | | | |",
619
        ]
620 0
        .join("\n");
621

622 1
        assert_eq!(&strings, &comp);
623
    }
624

625
    #[test]
626 1
    fn test_render_face_points() {
627
        use crate::style::PointStyle;
628 1
        let data = vec![
629 1
            (-3.0, 2.3),
630 1
            (-1.6, 5.3),
631 1
            (0.3, 0.7),
632 1
            (4.3, -1.4),
633 1
            (6.4, 4.3),
634 1
            (8.5, 3.7),
635
        ];
636 1
        let x_axis = axis::ContinuousAxis::new(-3.575, 9.075, 6);
637 1
        let y_axis = axis::ContinuousAxis::new(-1.735, 5.635, 6);
638 1
        let style = PointStyle::new();
639
        //TODO NEXT
640 1
        let strings = render_face_points(&data, &x_axis, &y_axis, 20, 10, &style);
641 1
        assert_eq!(strings.lines().count(), 10);
642 1
        assert!(strings.lines().all(|s| s.chars().count() == 20));
643

644 1
        let comp = vec![
645
            "  ●                 ",
646
            "                    ",
647
            "               ●    ",
648
            "                  ● ",
649
            "                    ",
650
            "●                   ",
651
            "                    ",
652
            "     ●              ",
653
            "                    ",
654
            "                    ",
655
        ]
656 0
        .join("\n");
657

658 1
        assert_eq!(&strings, &comp);
659
    }
660

661
    #[test]
662 1
    fn test_overlay() {
663 1
        let a = " ooo ";
664 1
        let b = "  #  ";
665 1
        let r = " o#o ";
666 1
        assert_eq!(overlay(a, b, 0, 0), r);
667

668 1
        let a = " o o o o o o o o o o ";
669 1
        let b = "# # # # #";
670 1
        let r = " o#o#o#o#o#o o o o o ";
671 1
        assert_eq!(overlay(a, b, 2, 0), r);
672

673 1
        let a = "     \n   o \n o  o\nooooo\no o o";
674 1
        let b = "  #  \n   # \n     \n  ## \n   ##";
675 1
        let r = "  #  \n   # \n o  o\noo##o\no o##";
676 1
        assert_eq!(overlay(a, b, 0, 0), r);
677

678 1
        let a = "     \n   o \n o  o\nooooo\no o o";
679 1
        let b = "  #\n## ";
680 1
        let r = "     \n   o \n o #o\no##oo\no o o";
681 1
        assert_eq!(overlay(a, b, 1, 2), r);
682

683 1
        let a = "     \n   o \n o  o\nooooo\no o o";
684 1
        let b = "###\n###\n###";
685 1
        let r = "##   \n## o \n o  o\nooooo\no o o";
686 1
        assert_eq!(overlay(a, b, -1, -1), r);
687

688 1
        let a = "oo\noo";
689 1
        let b = "    \n  # \n #  \n    ";
690 1
        let r = "o#\n#o";
691 1
        assert_eq!(overlay(a, b, -1, -1), r);
692
    }
693
}

Read our documentation on viewing source code .

Loading