use ggplot_rs::locale::Locale;
use ggplot_rs::prelude::*;
use ggplot_rs::scale::format::label_currency_locale;
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()
}
fn frame(cols: Vec<(&str, Vec<Value>)>) -> Vec<(String, Vec<Value>)> {
cols.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}
#[test]
fn a_template_fills_placeholders_formatted_through_the_columns_scale() {
let data = frame(vec![
("label", strs(&["Berlin"])),
("x", floats(&[1.0])),
("y", floats(&[1234.5])),
]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y").label("label"))
.geom_point()
.scale_y_continuous(
ScaleContinuous::new()
.with_value_formatter(label_currency_locale(&Locale::de_de(), Some(0.01))),
)
.tooltip("{label}: {y}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>Berlin: 1.234,50 €</title>"), "{svg}");
}
#[test]
fn a_hex_template_reads_the_stat_computed_count_column() {
let data = frame(vec![
("x", floats(&[1.0, 1.0, 1.0])),
("y", floats(&[1.0, 1.0, 1.0])),
]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_hex()
.tooltip("{count} points")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>3 points</title>"), "{svg}");
}
#[test]
fn double_braces_are_literal() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.tooltip("{{x}} = {x}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>{x} = 1</title>"), "{svg}");
}
#[test]
fn an_unknown_placeholder_renders_empty_not_literally() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.tooltip("[{nope}]")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>[]</title>"), "{svg}");
assert!(!svg.contains("nope"), "{svg}");
}
#[test]
fn the_rendered_tooltip_is_escaped() {
let data = frame(vec![
("label", strs(&["<b>&\"</b>"])),
("x", floats(&[1.0])),
("y", floats(&[2.0])),
]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y").label("label"))
.geom_point()
.tooltip("{label}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(!svg.contains("<b>"), "{svg}");
assert!(svg.contains("<b>&"</b>"), "{svg}");
}
#[test]
fn tooltip_applies_to_the_most_recently_added_layer_only() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data.clone())
.aes(Aes::new().x("x").y("y"))
.geom_line()
.geom_point()
.tooltip("point: {y}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>point: 2</title>"), "{svg}");
}
#[test]
fn without_a_tooltip_call_output_is_unaffected() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>(1, 2)</title>"), "{svg}");
}
#[test]
fn an_unterminated_brace_is_kept_literally() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.tooltip("end {unterminated")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>end {unterminated</title>"), "{svg}");
}
#[test]
fn a_placeholder_for_an_na_value_renders_empty() {
let data = frame(vec![
("x", floats(&[1.0, 2.0])),
("y", floats(&[1.0, 2.0])),
("label", vec![Value::Na, Value::Str("b".into())]),
]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y").label("label"))
.geom_point()
.tooltip("[{label}]")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>[]</title>"), "{svg}");
assert!(svg.contains("<title>[b]</title>"), "{svg}");
}
#[test]
fn a_template_on_a_geom_that_ignores_tooltips_warns() {
let data = frame(vec![("x", floats(&[1.0, 2.0])), ("y", floats(&[1.0, 2.0]))]);
let (svg, warnings) = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_area()
.tooltip("{y}")
.render_svg_native_with_warnings(300, 200)
.unwrap();
assert!(!svg.contains("<title>"), "{svg}");
assert!(
warnings
.iter()
.any(|w| w.contains("tooltip") && w.contains("no effect")),
"{warnings:?}"
);
}
#[test]
fn an_unmapped_lower_upper_placeholder_renders_the_raw_value() {
let data = frame(vec![
("x", floats(&[1.0])),
("y", floats(&[10.0])),
("lower", floats(&[9.0])),
("upper", floats(&[11.0])),
]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_line()
.geom_point()
.tooltip("{y} [{lower}, {upper}]")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>10 [9, 11]</title>"), "{svg}");
}
#[test]
fn a_template_on_a_geom_that_reads_tooltips_does_not_warn() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let (_, warnings) = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.tooltip("{y}")
.render_svg_native_with_warnings(300, 200)
.unwrap();
assert!(
!warnings.iter().any(|w| w.contains("tooltip")),
"{warnings:?}"
);
}
#[test]
fn geom_vline_tooltip_template_replaces_the_default_x_equals_label() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let (svg, warnings) = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.geom_vline(2.0)
.tooltip("{xintercept}")
.render_svg_native_with_warnings(300, 200)
.unwrap();
assert!(svg.contains("<title>2</title>"), "{svg}");
assert!(!svg.contains("x = 2"), "{svg}");
assert!(
!warnings.iter().any(|w| w.contains("tooltip")),
"{warnings:?}"
);
}
#[test]
fn geom_hline_tooltip_template_replaces_the_default_y_equals_label() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.geom_hline(3.0)
.tooltip("{yintercept}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>3</title>"), "{svg}");
assert!(!svg.contains("y = 3"), "{svg}");
}
#[test]
fn geom_abline_tooltip_template_replaces_the_default_label() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.geom_abline(1.0, 0.5)
.tooltip("slope {slope}")
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>slope 1</title>"), "{svg}");
}
#[test]
fn geom_vline_without_a_template_keeps_its_default_x_equals_label() {
let data = frame(vec![("x", floats(&[1.0])), ("y", floats(&[2.0]))]);
let svg = GGPlot::new(data)
.aes(Aes::new().x("x").y("y"))
.geom_point()
.geom_vline(2.0)
.render_svg_native_with_size(300, 200)
.unwrap();
assert!(svg.contains("<title>x = 2</title>"), "{svg}");
}