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 { ``` 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 { ``` 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> { ``` 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> = 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) -> Vec { ``` 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 = 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 = 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::(); ``` 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::(); ``` 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = if lines_deficit.is_positive() { ``` 435 1 ``` let new_lines: Vec = (0..lines_deficit) ``` 436 1 ``` .map(|_| (0..over_width).map(|_| ' ').collect::()) ``` 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 = 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 = 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 .