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

Read our documentation on viewing source code .

Loading