use kuva::backend::svg::SvgBackend;
use kuva::plot::diceplot::DicePlot;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;
use kuva::render::render::render_multiple;
use kuva::render::theme::Theme;
#[test]
fn test_dice_categorical_basic() {
let organs = vec!["Lung".into(), "Liver".into(), "Brain".into()];
let data = vec![
("miR-1", "Cpd1", "Lung", "#b2182b"),
("miR-1", "Cpd1", "Liver", "#2166ac"),
("miR-1", "Cpd1", "Brain", "#cccccc"),
("miR-1", "Cpd2", "Lung", "#cccccc"),
("miR-1", "Cpd2", "Brain", "#b2182b"),
("miR-2", "Cpd1", "Lung", "#2166ac"),
("miR-2", "Cpd1", "Liver", "#b2182b"),
("miR-2", "Cpd2", "Lung", "#cccccc"),
("miR-2", "Cpd2", "Liver", "#2166ac"),
("miR-2", "Cpd2", "Brain", "#b2182b"),
];
let dice = DicePlot::new(3)
.with_category_labels(organs)
.with_records(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots)
.with_title("Dice Categorical")
.with_x_label("miRNA")
.with_y_label("Compound");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_categorical_basic.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<rect"));
assert_eq!(svg.matches("<circle").count(), 10);
assert!(svg.contains("Dice Categorical"));
}
#[test]
fn test_dice_categorical_absent_dots_omitted() {
let cats = vec!["A".into(), "B".into(), "C".into(), "D".into()];
let data = vec![("X1", "Y1", "A", "#ff0000"), ("X1", "Y1", "C", "#0000ff")];
let dice = DicePlot::new(4)
.with_category_labels(cats)
.with_records(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert_eq!(svg.matches("<circle").count(), 2);
}
#[test]
fn test_dice_continuous_tile() {
let data = vec![
("G1", "S1", vec![0, 1, 2, 3], Some(0.8), Some(5.0)),
("G1", "S2", vec![0, 2], Some(0.3), Some(2.0)),
("G2", "S1", vec![1, 3], Some(0.6), Some(8.0)),
("G2", "S2", vec![0, 1, 2, 3], Some(0.1), Some(3.0)),
];
let dice = DicePlot::new(4).with_points(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots).with_title("Dice Continuous");
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_continuous_tile.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<rect"));
assert!(svg.contains("<circle"));
assert!(svg.contains("<path"));
}
#[test]
fn test_dice_per_dot_continuous() {
let cats = vec!["C1".into(), "C2".into(), "C3".into()];
let data = vec![
("X1", "Y1", 0_usize, Some(1.5), Some(3.0)),
("X1", "Y1", 1, Some(-0.8), Some(1.5)),
("X1", "Y1", 2, Some(0.2), Some(4.0)),
("X1", "Y2", 0, Some(-1.2), Some(2.0)),
("X1", "Y2", 2, Some(0.9), Some(5.0)),
("X2", "Y1", 1, Some(2.0), Some(3.5)),
("X2", "Y1", 2, Some(-0.3), Some(1.0)),
];
let dice = DicePlot::new(3)
.with_category_labels(cats)
.with_dot_data(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_per_dot.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("#ffffff"));
assert_eq!(svg.matches("<circle").count(), 7);
}
#[test]
fn test_dice_position_legend() {
let organs = vec!["Lung".into(), "Liver".into(), "Brain".into()];
let data = vec![
("X1", "Y1", "Lung", "#ff0000"),
("X1", "Y1", "Brain", "#0000ff"),
];
let dice = DicePlot::new(3)
.with_category_labels(organs)
.with_records(data)
.with_position_legend("Organ");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_position_legend.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("Organ"));
assert!(svg.contains("Lung"));
assert!(svg.contains("Liver"));
assert!(svg.contains("Brain"));
}
#[test]
fn test_dice_dot_legend() {
let organs = vec!["Lung".into(), "Liver".into()];
let data = vec![
("X1", "Y1", "Lung", "#b2182b"),
("X1", "Y1", "Liver", "#2166ac"),
];
let dice = DicePlot::new(2)
.with_category_labels(organs)
.with_records(data)
.with_dot_legend(vec![("Down", "#2166ac"), ("Up", "#b2182b")]);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_dot_legend.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("Down"));
assert!(svg.contains("Up"));
}
#[test]
fn test_dice_size_legend() {
let cats = vec!["A".into(), "B".into()];
let data = vec![
("X1", "Y1", 0_usize, Some(1.0), Some(2.0)),
("X1", "Y1", 1, Some(0.5), Some(8.0)),
];
let dice = DicePlot::new(2)
.with_category_labels(cats)
.with_dot_data(data)
.with_size_legend("-log10(FDR)");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_size_legend.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("-log10(FDR)"));
}
#[test]
fn test_dice_colorbar() {
let data = vec![
("X1", "Y1", vec![0, 1], Some(0.2), None),
("X1", "Y2", vec![0], Some(0.9), None),
("X2", "Y1", vec![1], Some(0.5), None),
];
let dice = DicePlot::new(2)
.with_points(data)
.with_fill_legend("Expression");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_colorbar.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("Expression"));
assert!(svg.matches("<rect").count() > 10);
}
#[test]
fn test_dice_empty_data() {
let dice = DicePlot::new(4);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(svg.contains("<svg"));
assert_eq!(svg.matches("<circle").count(), 0);
}
#[test]
fn test_dice_all_ndots_variants() {
for n in 1..=6 {
let mut data = Vec::new();
for k in 0..n {
data.push(("X", "Y", format!("Cat{k}"), "#444444"));
}
let labels: Vec<String> = (0..n).map(|k| format!("Cat{k}")).collect();
let dice = DicePlot::new(n)
.with_category_labels(labels)
.with_records(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(svg.contains("<svg"), "ndots={n} should produce valid SVG");
assert_eq!(
svg.matches("<circle").count(),
n,
"ndots={n} should have {n} circles"
);
}
}
#[test]
fn test_dice_stacked_legends() {
let cats = vec!["A".into(), "B".into(), "C".into()];
let data = vec![
("X1", "Y1", 0_usize, Some(1.0), Some(3.0)),
("X1", "Y1", 1, Some(-0.5), Some(1.0)),
("X1", "Y1", 2, Some(0.8), Some(5.0)),
];
let dice = DicePlot::new(3)
.with_category_labels(cats)
.with_dot_data(data)
.with_position_legend("Category")
.with_fill_legend("logFC")
.with_size_legend("Significance");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_stacked_legends.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("Category"));
assert!(svg.contains("logFC"));
assert!(svg.contains("Significance"));
assert!(svg.contains(">A<"));
assert!(svg.contains(">B<"));
assert!(svg.contains(">C<"));
}
#[test]
fn test_dice_position_legend_dark_theme() {
let organs = vec!["Lung".into(), "Liver".into()];
let data = vec![("X1", "Y1", "Lung", "#ff0000")];
let dice = DicePlot::new(2)
.with_category_labels(organs)
.with_records(data)
.with_position_legend("Organ");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots).with_theme(Theme::dark());
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(svg.contains("<svg"));
assert!(svg.contains("Organ"));
assert!(svg.contains("#1e1e1e") || svg.contains("background"));
}
#[test]
fn test_dice_single_dot_positions() {
for (ndots, expected_circles) in [(1_usize, 1_usize), (6, 6)] {
let labels: Vec<String> = (0..ndots).map(|k| format!("Cat{k}")).collect();
let colors: Vec<(&str, &str, String, &str)> = (0..ndots)
.map(|k| ("X", "Y", format!("Cat{k}"), "#444444"))
.collect();
let dice = DicePlot::new(ndots)
.with_category_labels(labels)
.with_records(colors);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert_eq!(
svg.matches("<circle").count(),
expected_circles,
"ndots={ndots}: expected {expected_circles} circles"
);
}
}
#[test]
fn test_dice_long_legend_title_fits_box() {
let long_title = "A Very Long Legend Title String";
let cats: Vec<String> = vec!["Cat A".into(), "Cat B".into()];
let data = vec![
("X1", "Y1", 0_usize, Some(1.0), Some(5.0)),
("X1", "Y1", 1, Some(0.5), Some(2.0)),
];
let dice = DicePlot::new(2)
.with_category_labels(cats)
.with_dot_data(data)
.with_position_legend(long_title)
.with_size_legend(long_title);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(svg.contains("<svg"));
assert!(svg.contains(long_title));
}
#[test]
fn test_dice_large_grid_with_position_legend() {
let organs: Vec<String> = vec![
"Lung".into(),
"Liver".into(),
"Brain".into(),
"Kidney".into(),
];
const UP: &str = "#d73027"; const DN: &str = "#4575b4"; const NS: &str = "#aaaaaa";
let combos: &[(&str, &str, &str, &str)] = &[
("miR-1", "CpdA", "Lung", UP),
("miR-1", "CpdA", "Liver", UP),
("miR-1", "CpdA", "Brain", NS),
("miR-1", "CpdB", "Lung", UP),
("miR-1", "CpdB", "Kidney", NS),
("miR-1", "CpdC", "Lung", UP),
("miR-1", "CpdC", "Liver", UP),
("miR-1", "CpdD", "Liver", UP),
("miR-1", "CpdD", "Brain", NS),
("miR-1", "CpdE", "Lung", UP),
("miR-1", "CpdF", "Liver", UP),
("miR-1", "CpdF", "Brain", DN),
("miR-1", "CpdG", "Lung", UP),
("miR-1", "CpdG", "Liver", NS),
("miR-1", "CpdH", "Lung", UP),
("miR-1", "CpdH", "Liver", UP),
("miR-2", "CpdA", "Brain", DN),
("miR-2", "CpdA", "Kidney", DN),
("miR-2", "CpdB", "Lung", NS),
("miR-2", "CpdB", "Kidney", DN),
("miR-2", "CpdC", "Kidney", DN),
("miR-2", "CpdD", "Lung", NS),
("miR-2", "CpdD", "Kidney", DN),
("miR-2", "CpdE", "Liver", NS),
("miR-2", "CpdE", "Kidney", DN),
("miR-2", "CpdF", "Brain", DN),
("miR-2", "CpdG", "Liver", UP),
("miR-2", "CpdG", "Kidney", DN),
("miR-2", "CpdH", "Brain", NS),
("miR-2", "CpdH", "Kidney", DN),
("miR-3", "CpdA", "Brain", UP),
("miR-3", "CpdA", "Lung", NS),
("miR-3", "CpdB", "Brain", UP),
("miR-3", "CpdB", "Liver", NS),
("miR-3", "CpdC", "Brain", UP),
("miR-3", "CpdD", "Brain", UP),
("miR-3", "CpdD", "Lung", NS),
("miR-3", "CpdE", "Brain", UP),
("miR-3", "CpdE", "Kidney", NS),
("miR-3", "CpdF", "Brain", UP),
("miR-3", "CpdG", "Brain", UP),
("miR-3", "CpdG", "Liver", DN),
("miR-3", "CpdH", "Brain", UP),
("miR-4", "CpdA", "Lung", DN),
("miR-4", "CpdA", "Liver", NS),
("miR-4", "CpdB", "Lung", DN),
("miR-4", "CpdC", "Lung", DN),
("miR-4", "CpdC", "Kidney", UP),
("miR-4", "CpdD", "Lung", DN),
("miR-4", "CpdE", "Lung", DN),
("miR-4", "CpdE", "Brain", NS),
("miR-4", "CpdF", "Lung", DN),
("miR-4", "CpdF", "Liver", UP),
("miR-4", "CpdG", "Lung", DN),
("miR-4", "CpdH", "Lung", DN),
("miR-4", "CpdH", "Kidney", NS),
("miR-5", "CpdA", "Lung", NS),
("miR-5", "CpdB", "Brain", NS),
("miR-5", "CpdC", "Liver", NS),
("miR-5", "CpdC", "Lung", NS),
("miR-5", "CpdD", "Kidney", NS),
("miR-5", "CpdE", "Brain", NS),
("miR-5", "CpdF", "Lung", NS),
("miR-5", "CpdG", "Liver", NS),
("miR-5", "CpdH", "Kidney", NS),
];
let dice = DicePlot::new(4)
.with_category_labels(organs)
.with_records(combos.iter().copied())
.with_position_legend("Organ")
.with_dot_legend(vec![
("Upregulated", UP),
("Downregulated", DN),
("Not significant", NS),
])
.with_grid_lines(true);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots)
.with_title("miRNA dysregulation by organ and direction")
.with_x_label("miRNA")
.with_y_label("Compound")
.with_height(520.0);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
std::fs::write("test_outputs/dice_large_grid.svg", svg.clone()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("<rect"));
assert!(svg.contains("Organ"));
assert!(svg.contains("Lung"));
assert!(svg.contains("Liver"));
assert!(svg.contains("Brain"));
assert!(svg.contains("Kidney"));
assert!(svg.contains("Upregulated"));
assert!(svg.contains("Downregulated"));
assert!(svg.contains("<line"));
}
#[test]
fn test_dice_grid_lines_off_by_default() {
let organs = vec!["A".into(), "B".into(), "C".into()];
let data = vec![("X1", "Y1", "A", "#ff0000")];
let dice = DicePlot::new(3)
.with_category_labels(organs)
.with_records(data);
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(!svg.contains("stroke-dasharray"));
}
#[test]
fn test_dice_fill_colorbar_range() {
let data = vec![
("G1", "S1", vec![0], Some(0.0_f64), None),
("G1", "S2", vec![0], Some(1.0), None),
];
let dice = DicePlot::new(1)
.with_points(data)
.with_fill_range(0.0, 5.0) .with_fill_legend("Score");
let plots = vec![Plot::DicePlot(dice)];
let layout = Layout::auto_from_plots(&plots);
let scene = render_multiple(plots, layout);
let svg = SvgBackend.render_scene(&scene);
assert!(svg.contains("Score"));
assert!(svg.contains('5') || svg.contains("5.0"));
}