use kuva::plot::ridgeline::RidgelinePlot;
use kuva::render::plots::Plot;
use kuva::render::layout::Layout;
use kuva::render::render::render_multiple;
use kuva::backend::svg::SvgBackend;
fn lcg(state: &mut u64) -> f64 {
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((*state >> 33) as f64) / (u32::MAX as f64)
}
fn gaussian(state: &mut u64, mean: f64, std: f64) -> f64 {
let u1 = lcg(state).max(1e-10);
let u2 = lcg(state);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
mean + z * std
}
fn make_gaussian(seed: u64, n: usize, mean: f64, std: f64) -> Vec<f64> {
let mut state = seed;
(0..n).map(|_| gaussian(&mut state, mean, std)).collect()
}
fn make_data(seed: u64, n: usize, mean: f64) -> Vec<f64> {
let mut state = seed;
(0..n).map(|_| mean + (lcg(&mut state) - 0.5) * 4.0).collect()
}
fn render(plots: Vec<Plot>) -> String {
let layout = Layout::auto_from_plots(&plots);
SvgBackend.render_scene(&render_multiple(plots, layout))
}
fn write(name: &str, svg: &str) {
std::fs::create_dir_all("test_outputs").ok();
std::fs::write(format!("test_outputs/{name}.svg"), svg).unwrap();
}
#[test]
fn test_ridgeline_basic() {
let plot = RidgelinePlot::new()
.with_group("GroupA", make_data(1, 50, 2.0))
.with_group("GroupB", make_data(2, 50, 5.0));
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"), "expected path elements");
assert!(svg.contains("GroupA"), "expected group label");
assert!(svg.contains("GroupB"), "expected group label");
write("ridgeline_basic", &svg);
}
#[test]
fn test_ridgeline_filled() {
let plot = RidgelinePlot::new()
.with_group("A", make_data(1, 50, 2.0))
.with_filled(true);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains('Z'), "expected closed path for filled ridgeline");
}
#[test]
fn test_ridgeline_unfilled() {
let plot = RidgelinePlot::new()
.with_group("A", make_data(1, 50, 2.0))
.with_filled(false);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(!svg.contains('Z'), "unfilled ridgeline should have no Z");
}
#[test]
fn test_ridgeline_overlap_zero() {
let plot = RidgelinePlot::new()
.with_group("A", make_data(1, 50, 2.0))
.with_group("B", make_data(2, 50, 5.0))
.with_overlap(0.0);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"));
write("ridgeline_overlap_zero", &svg);
}
#[test]
fn test_ridgeline_large_overlap() {
let plot = RidgelinePlot::new()
.with_group("A", make_data(1, 60, 0.0))
.with_group("B", make_data(2, 60, 3.0))
.with_group("C", make_data(3, 60, 6.0))
.with_overlap(2.0);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"));
write("ridgeline_large_overlap", &svg);
}
#[test]
fn test_ridgeline_colors() {
let plot = RidgelinePlot::new()
.with_group_color("A", make_data(1, 50, 2.0), "#e74c3c");
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("e74c3c"), "expected explicit color in SVG");
}
#[test]
fn test_ridgeline_legend() {
let plot = RidgelinePlot::new()
.with_group("GroupA", make_data(1, 50, 2.0))
.with_group("GroupB", make_data(2, 50, 5.0))
.with_legend(true);
let layout = Layout::auto_from_plots(&[Plot::Ridgeline(plot.clone())]);
let svg = SvgBackend.render_scene(&render_multiple(vec![Plot::Ridgeline(plot)], layout));
assert!(svg.contains("GroupA"), "expected legend label in SVG");
}
#[test]
fn test_ridgeline_normalize() {
let plot = RidgelinePlot::new()
.with_group("A", make_data(1, 50, 2.0))
.with_group("B", make_data(2, 50, 5.0))
.with_normalize(true);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"));
}
#[test]
fn test_ridgeline_single_group() {
let plot = RidgelinePlot::new()
.with_group("Only", make_data(1, 40, 0.0));
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"));
assert!(svg.contains("Only"));
write("ridgeline_single_group", &svg);
}
#[test]
fn test_ridgeline_group_order() {
let plot = RidgelinePlot::new()
.with_group("Alpha", make_data(1, 40, 1.0))
.with_group("Beta", make_data(2, 40, 4.0))
.with_group("Gamma", make_data(3, 40, 7.0));
let svg = render(vec![Plot::Ridgeline(plot)]);
let alpha_pos = svg.find("Alpha").expect("Alpha label missing");
let gamma_pos = svg.find("Gamma").expect("Gamma label missing");
assert!(alpha_pos < svg.len());
assert!(gamma_pos < svg.len());
}
#[test]
fn test_ridgeline_with_title() {
let plot = RidgelinePlot::new()
.with_group("Low", make_gaussian(1, 80, -2.0, 1.0))
.with_group("Mid", make_gaussian(2, 80, 0.0, 1.0))
.with_group("High", make_gaussian(3, 80, 2.0, 1.0))
.with_overlap(0.5);
let layout = Layout::auto_from_plots(&[Plot::Ridgeline(plot.clone())])
.with_title("Ridge with Title")
.with_x_label("Value")
.with_y_label("Group");
let svg = SvgBackend.render_scene(&render_multiple(vec![Plot::Ridgeline(plot)], layout));
assert!(svg.contains("Ridge with Title"), "title missing from SVG");
assert!(svg.contains("<path"), "ridges missing from SVG");
write("ridgeline_with_title", &svg);
}
const MONTHS: [(&str, f64, f64); 12] = [
("January", -3.0, 5.0),
("February", -1.5, 5.5),
("March", 4.0, 5.0),
("April", 10.0, 4.0),
("May", 15.5, 3.5),
("June", 20.0, 3.0),
("July", 23.0, 2.5),
("August", 22.5, 2.5),
("September", 17.0, 3.0),
("October", 10.5, 4.0),
("November", 3.5, 5.0),
("December", -1.0, 5.5),
];
#[test]
fn test_ridgeline_temperature() {
let mut plot = RidgelinePlot::new()
.with_overlap(0.6)
.with_opacity(0.75);
let colors = [
"#3a7abf", "#4589c4", "#6ba3d4", "#a0bfdc",
"#d4b8a0", "#e8c97a", "#f0a830", "#e86820",
"#d44a10", "#c06030", "#9070a0", "#5060b0",
];
for (i, &(month, mean, std)) in MONTHS.iter().enumerate() {
let data = make_gaussian(i as u64 + 1, 200, mean, std);
plot = plot.with_group_color(month, data, colors[i]);
}
let layout = Layout::auto_from_plots(&[Plot::Ridgeline(plot.clone())])
.with_title("Daily Temperature Distributions by Month")
.with_x_label("Temperature (°C)")
.with_y_label("Month");
let svg = SvgBackend.render_scene(&render_multiple(vec![Plot::Ridgeline(plot)], layout));
assert!(svg.contains("<path"), "expected path elements");
for &(month, _, _) in &MONTHS {
assert!(svg.contains(month), "expected month label '{month}' in SVG");
}
let path_count = svg.matches("<path").count();
assert!(path_count >= 12, "expected at least 12 paths, got {path_count}");
write("ridgeline_temperature", &svg);
}
#[test]
fn test_ridgeline_temperature_no_fill() {
let mut plot = RidgelinePlot::new()
.with_filled(false)
.with_overlap(0.8)
.with_stroke_width(2.0);
for (i, &(month, mean, std)) in MONTHS.iter().enumerate() {
plot = plot.with_group(month, make_gaussian(i as u64 + 42, 150, mean, std));
}
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(!svg.contains('Z'), "unfilled should have no Z");
assert!(svg.contains("<path"));
write("ridgeline_temperature_outline", &svg);
}
#[test]
fn test_ridgeline_baseline_default_on() {
let n = 3usize;
let plot = RidgelinePlot::new()
.with_group("A", make_gaussian(1, 60, 0.0, 1.0))
.with_group("B", make_gaussian(2, 60, 3.0, 1.0))
.with_group("C", make_gaussian(3, 60, 6.0, 1.0));
let svg = render(vec![Plot::Ridgeline(plot)]);
let line_count = svg.matches("<line").count();
assert!(line_count >= n + 2, "expected at least {n} baselines + 2 axis lines, got {line_count}");
write("ridgeline_baseline_on", &svg);
}
#[test]
fn test_ridgeline_baseline_off() {
let plot_on = RidgelinePlot::new()
.with_group("A", make_gaussian(1, 60, 0.0, 1.0))
.with_group("B", make_gaussian(2, 60, 3.0, 1.0));
let plot_off = RidgelinePlot::new()
.with_group("A", make_gaussian(1, 60, 0.0, 1.0))
.with_group("B", make_gaussian(2, 60, 3.0, 1.0))
.with_baseline(false);
let svg_on = render(vec![Plot::Ridgeline(plot_on)]);
let svg_off = render(vec![Plot::Ridgeline(plot_off)]);
let on_lines = svg_on .matches("<line").count();
let off_lines = svg_off.matches("<line").count();
assert!(off_lines < on_lines,
"disabling baseline should reduce line count: on={on_lines} off={off_lines}");
}
#[test]
fn test_ridgeline_bandwidth_override() {
let plot = RidgelinePlot::new()
.with_group("Narrow", make_gaussian(1, 100, 0.0, 1.0))
.with_group("Wide", make_gaussian(2, 100, 5.0, 2.0))
.with_bandwidth(0.5); let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("<path"));
write("ridgeline_bandwidth", &svg);
}
#[test]
fn test_ridgeline_with_groups_builder() {
let groups = vec![
("Spring", make_gaussian(1, 80, 12.0, 3.0)),
("Summer", make_gaussian(2, 80, 24.0, 2.0)),
("Autumn", make_gaussian(3, 80, 10.0, 4.0)),
("Winter", make_gaussian(4, 80, -2.0, 5.0)),
];
let plot = RidgelinePlot::new()
.with_groups(groups)
.with_overlap(0.4);
let svg = render(vec![Plot::Ridgeline(plot)]);
assert!(svg.contains("Spring"));
assert!(svg.contains("Winter"));
write("ridgeline_seasons", &svg);
}