use std::time::{Duration, Instant};
use ggplot_rs::prelude::*;
fn col(name: &str, v: Vec<Value>) -> (String, Vec<Value>) {
(name.to_string(), v)
}
fn floats(v: &[f64]) -> Vec<Value> {
v.iter().map(|&f| Value::Float(f)).collect()
}
fn strs(v: &[&str]) -> Vec<Value> {
v.iter().map(|s| Value::Str(s.to_string())).collect()
}
#[test]
fn hover_attributes_escape_quotes() {
let evil = "a\" onmouseover=\"alert(1)";
let svg = GGPlot::new(vec![
col("x", strs(&[evil, "b"])),
col("y", floats(&[1.0, 2.0])),
col("lab", strs(&["x'y", "<b>"])),
])
.aes(Aes::new().x("x").y("y").label("lab"))
.geom_col()
.render_svg_native_with_size(300, 200)
.expect("render");
assert!(
!svg.contains("onmouseover=\""),
"attribute injection: {svg}"
);
assert!(svg.contains("""), "quote escaped");
assert!(!svg.contains("<b>"), "markup escaped");
}
#[test]
fn control_characters_are_dropped() {
let svg = GGPlot::new(vec![
col("x", strs(&["a\u{1}b", "c"])),
col("y", floats(&[1.0, 2.0])),
])
.aes(Aes::new().x("x").y("y"))
.geom_col()
.title("t\u{7}itle")
.render_svg_native_with_size(300, 200)
.expect("render");
assert!(!svg
.chars()
.any(|c| (c as u32) < 0x20 && !"\t\n\r".contains(c)));
}
#[test]
fn near_constant_data_terminates() {
let start = Instant::now();
for ys in [
vec![1234.5, 1234.5, 1_234.500_000_000_000_2],
vec![1e15, 1e15 + 0.125, 1e15],
vec![-7.0, -7.0 + 1e-14, -7.0],
] {
let x = floats(&[0.0, 1.0, 2.0]);
GGPlot::new(vec![col("x", x.clone()), col("y", floats(&ys))])
.aes(Aes::new().x("x").y("y"))
.geom_line()
.render_svg_native_with_size(300, 200)
.expect("line render");
GGPlot::new(vec![
col("x", x.clone()),
col("y", x),
col("c", floats(&ys)),
])
.aes(Aes::new().x("x").y("y").color("c"))
.geom_point()
.render_svg_native_with_size(300, 200)
.expect("colour render");
}
assert!(start.elapsed() < Duration::from_secs(10));
}
#[test]
fn extreme_datetimes_terminate() {
let start = Instant::now();
let x = vec![Value::DateTime(0), Value::DateTime(i64::MAX / 2)];
let _ = GGPlot::new(vec![col("x", x), col("y", floats(&[1.0, 2.0]))])
.aes(Aes::new().x("x").y("y"))
.geom_line()
.render_svg_native_with_size(300, 200);
assert!(start.elapsed() < Duration::from_secs(10));
}
#[test]
fn nan_x_does_not_panic() {
let xs: Vec<f64> = (0..200)
.map(|i| {
if i % 7 == 0 {
f64::NAN
} else {
(i * 37 % 101) as f64
}
})
.collect();
let ys: Vec<f64> = (0..200).map(|i| i as f64).collect();
for layer in ["line", "area", "step"] {
let p = GGPlot::new(vec![col("x", floats(&xs)), col("y", floats(&ys))])
.aes(Aes::new().x("x").y("y"));
let p = match layer {
"line" => p.geom_line(),
"area" => p.geom_area(),
_ => p.geom_step(),
};
let _ = p.render_svg_native_with_size(300, 200);
}
}
#[test]
fn many_categories_render_in_linear_time() {
let n = 20_000;
let x: Vec<Value> = (0..n).map(|i| Value::Str(format!("c{i}"))).collect();
let y: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
let fill: Vec<Value> = (0..n)
.map(|i| Value::Str(format!("g{}", i % 500)))
.collect();
let start = Instant::now();
GGPlot::new(vec![col("x", x), col("y", y), col("f", fill)])
.aes(Aes::new().x("x").y("y").fill("f"))
.geom_col()
.theme_void()
.render_svg_native_with_size(800, 400)
.expect("render");
assert!(
start.elapsed() < Duration::from_secs(20),
"{:?}",
start.elapsed()
);
}
#[test]
fn jitter_is_deterministic() {
let render = || {
GGPlot::new(vec![
col("x", strs(&["a", "a", "b", "b", "c"])),
col("y", floats(&[1.0, 2.0, 3.0, 4.0, 5.0])),
])
.aes(Aes::new().x("x").y("y"))
.geom_jitter()
.render_svg_native_with_size(300, 200)
.expect("render")
};
assert_eq!(render(), render());
}
#[test]
fn continuous_bars_do_not_overlap() {
let n = 60;
let svg = GGPlot::new(vec![
col("x", floats(&(0..n).map(f64::from).collect::<Vec<_>>())),
col(
"y",
floats(&(0..n).map(|i| 1.0 + f64::from(i % 5)).collect::<Vec<_>>()),
),
])
.aes(Aes::new().x("x").y("y"))
.geom_col()
.render_svg_native_with_size(800, 300)
.expect("render");
let mut spans: Vec<(f64, f64)> = svg
.split("<rect ")
.skip(1)
.filter(|r| r.contains("data-x="))
.filter_map(|r| {
let r = format!(" {r}");
let attr = |k: &str| -> Option<f64> {
let s = r.split(&format!(" {k}=\"")).nth(1)?;
s.split('"').next()?.parse().ok()
};
Some((attr("x")?, attr("width")?))
})
.collect();
assert!(spans.len() >= n as usize, "found {} bars", spans.len());
spans.sort_by(|a, b| a.0.total_cmp(&b.0));
for w in spans.windows(2) {
assert!(w[0].0 + w[0].1 <= w[1].0 + 0.01, "overlap: {w:?}");
}
assert!(spans[0].1 > 4.0, "bars too thin: {}", spans[0].1);
}