use kuva::backend::svg::SvgBackend;
use kuva::plot::RaincloudPlot;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;
use kuva::render::render::render_multiple;
fn make_values(seed: u64, n: usize, shift: f64) -> Vec<f64> {
let mut state = seed ^ 0x9e3779b97f4a7c15u64;
(0..n)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let u = (state >> 11) as f64 * (1.0 / (1u64 << 53) as f64);
(u - 0.5) * 4.0 + shift
})
.collect()
}
#[test]
fn test_raincloud_basic() {
let plot = RaincloudPlot::new()
.with_group("Control", make_values(1, 30, 5.0))
.with_group("Treated", make_values(2, 30, 7.0));
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title("Raincloud Basic")
.with_x_label("Group")
.with_y_label("Value");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_basic.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"), "SVG should start with svg element");
assert!(
svg.contains("<path"),
"SVG should contain path elements (clouds)"
);
assert!(
svg.contains("<rect"),
"SVG should contain rect elements (boxes)"
);
assert!(
svg.contains("<circle"),
"SVG should contain circle elements (rain)"
);
assert!(
svg.contains("Control"),
"SVG should contain group label 'Control'"
);
assert!(
svg.contains("Treated"),
"SVG should contain group label 'Treated'"
);
}
#[test]
fn test_raincloud_single_group() {
let plot = RaincloudPlot::new().with_group("Only", make_values(3, 20, 3.0));
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud Single Group");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_single.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(
svg.contains("Only"),
"SVG should contain the single group label"
);
}
#[test]
fn test_raincloud_no_cloud() {
let plot = RaincloudPlot::new()
.with_group("A", make_values(4, 25, 5.0))
.with_group("B", make_values(5, 25, 7.0))
.with_cloud(false);
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud No Cloud");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_no_cloud.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<rect"), "should still have box rects");
assert!(svg.contains("<circle"), "should still have rain circles");
}
#[test]
fn test_raincloud_no_box() {
let plot = RaincloudPlot::new()
.with_group("A", make_values(6, 25, 5.0))
.with_group("B", make_values(7, 25, 7.0))
.with_box(false);
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud No Box");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_no_box.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<circle"), "should have rain points");
assert!(svg.contains("<path"), "should have cloud paths");
}
#[test]
fn test_raincloud_no_rain() {
let plot = RaincloudPlot::new()
.with_group("A", make_values(8, 25, 5.0))
.with_group("B", make_values(9, 25, 7.0))
.with_rain(false);
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud No Rain");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_no_rain.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(!svg.contains("<circle"), "should have no rain circles");
assert!(svg.contains("<rect"), "should still have box rects");
assert!(svg.contains("<path"), "should still have cloud paths");
}
#[test]
fn test_raincloud_group_colors() {
let plot = RaincloudPlot::new()
.with_group("A", make_values(10, 20, 4.0))
.with_group("B", make_values(11, 20, 6.0))
.with_group("C", make_values(12, 20, 8.0))
.with_group_colors(["tomato", "steelblue", "seagreen"]);
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud Group Colors");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_group_colors.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(
svg.contains("seagreen") || svg.contains("#2e8b57"),
"SVG should contain seagreen color"
);
}
#[test]
fn test_raincloud_flipped() {
let plot = RaincloudPlot::new()
.with_group("A", make_values(13, 30, 5.0))
.with_group("B", make_values(14, 30, 7.0))
.with_flip(true);
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud Flipped");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_flipped.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<path"), "cloud path present when flipped");
assert!(svg.contains("<circle"), "rain circles present when flipped");
}
#[test]
fn test_raincloud_legend() {
let plot = RaincloudPlot::new()
.with_group("Control", make_values(15, 20, 4.0))
.with_group("Treated", make_values(16, 20, 6.5))
.with_legend("Condition");
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("Raincloud With Legend");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_legend.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(
svg.contains("Control"),
"SVG should contain legend entry 'Control'"
);
assert!(
svg.contains("Treated"),
"SVG should contain legend entry 'Treated'"
);
}
#[test]
fn test_raincloud_five_groups() {
let labels = ["Mon", "Tue", "Wed", "Thu", "Fri"];
let mut plot = RaincloudPlot::new();
for (i, label) in labels.iter().enumerate() {
plot = plot.with_group(
*label,
make_values(i as u64 + 100, 40, i as f64 * 1.5 + 3.0),
);
}
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title("Raincloud Five Groups")
.with_x_label("Day")
.with_y_label("Score");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_five_groups.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
for label in &labels {
assert!(
svg.contains(label),
"SVG should contain group label '{label}'"
);
}
let circle_count = svg.matches("<circle").count();
assert!(
circle_count >= 5,
"expected at least 5 rain circles, got {circle_count}"
);
}
#[test]
fn test_raincloud_bandwidth_scale() {
let groups = [("A", 300u64, 5.0f64), ("B", 301, 7.5), ("C", 302, 10.0)];
let mut default_plot = RaincloudPlot::new();
let mut tight_plot = RaincloudPlot::new().with_bandwidth_scale(0.4);
for (label, seed, shift) in &groups {
let vals = make_values(*seed, 60, *shift);
default_plot = default_plot.with_group(*label, vals.clone());
tight_plot = tight_plot.with_group(*label, vals);
}
for (plot, name) in [(default_plot, "default"), (tight_plot, "tight")] {
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title(format!("Raincloud bandwidth_scale={name}"))
.with_x_label("Group")
.with_y_label("Value");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write(format!("test_outputs/raincloud_bw_{name}.svg"), svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(
svg.contains("<path"),
"cloud path should be present ({name})"
);
}
}
#[test]
fn test_raincloud_large_dataset() {
let plot = RaincloudPlot::new()
.with_group("Control", make_values(200, 1000, 5.0))
.with_group("Low dose", make_values(201, 1000, 6.5))
.with_group("High dose", make_values(202, 1000, 8.5));
let plots = vec![Plot::Raincloud(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title("Raincloud Large Dataset (1000 pts/group)")
.with_x_label("Treatment")
.with_y_label("Response");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/raincloud_large.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<path"), "cloud paths present");
assert!(svg.contains("<rect"), "box rects present");
let circle_count = svg.matches("<circle").count();
assert!(
circle_count >= 3000,
"expected 3000+ rain circles for 3×1000 pts, got {circle_count}"
);
}