use kuva::prelude::*;
use kuva::backend::svg::SvgBackend;
use kuva::render::figure::Figure;
use kuva::render::layout::Layout;
use kuva::plot::volcano::VolcanoPlot;
fn make_scatter_svg(interactive: bool) -> String {
let mut layout = Layout::auto_from_plots(&[Plot::Scatter(
ScatterPlot::new()
.with_data(vec![(1.0_f64, 2.0), (3.0, 4.0), (5.0, 1.0)]),
)]);
if interactive {
layout = layout.with_interactive();
}
let plots = vec![Plot::Scatter(
ScatterPlot::new()
.with_data(vec![(1.0_f64, 2.0), (3.0, 4.0), (5.0, 1.0)]),
)];
let scene = render_multiple(plots, layout);
SvgBackend::new().render_scene(&scene)
}
fn make_multi_group_scatter_svg() -> String {
let plots: Vec<Plot> = vec![
ScatterPlot::new()
.with_data(vec![(1.0_f64, 2.0), (2.0, 3.0)])
.with_color("steelblue")
.with_legend("Group A")
.into(),
ScatterPlot::new()
.with_data(vec![(3.0_f64, 4.0), (4.0, 5.0)])
.with_color("tomato")
.with_legend("Group B")
.into(),
];
let layout = Layout::auto_from_plots(&plots).with_interactive();
let scene = render_multiple(plots, layout);
SvgBackend::new().render_scene(&scene)
}
fn make_volcano_svg(interactive: bool) -> String {
let mut vp = VolcanoPlot::new();
vp = vp
.with_point("gene1", 2.5_f64, 0.001_f64)
.with_point("gene2", -3.0_f64, 0.0001_f64)
.with_point("gene3", 0.5_f64, 0.5_f64);
let plots = vec![Plot::Volcano(vp)];
let mut layout = Layout::auto_from_plots(&plots);
if interactive {
layout = layout.with_interactive();
}
let scene = render_multiple(plots, layout);
SvgBackend::new().render_scene(&scene)
}
#[test]
fn test_interactive_off_by_default() {
let svg = make_scatter_svg(false);
std::fs::write("test_outputs/interactive_off.svg", &svg).unwrap();
assert!(!svg.contains("data-xmin"), "non-interactive SVG must not have data-xmin");
assert!(!svg.contains("<script"), "non-interactive SVG must not have <script");
assert!(!svg.contains("<foreignObject"), "non-interactive SVG must not have <foreignObject");
}
#[test]
fn test_interactive_root_attrs() {
let svg = make_scatter_svg(true);
std::fs::write("test_outputs/interactive_root_attrs.svg", &svg).unwrap();
assert!(svg.contains("data-xmin="), "missing data-xmin");
assert!(svg.contains("data-xmax="), "missing data-xmax");
assert!(svg.contains("data-ymin="), "missing data-ymin");
assert!(svg.contains("data-ymax="), "missing data-ymax");
assert!(svg.contains("data-plot-left="), "missing data-plot-left");
assert!(svg.contains("data-plot-top="), "missing data-plot-top");
assert!(svg.contains("data-plot-right="), "missing data-plot-right");
assert!(svg.contains("data-plot-bottom="), "missing data-plot-bottom");
}
#[test]
fn test_interactive_log_axis_flag() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(1.0_f64, 2.0), (10.0, 100.0)]),
)];
let layout = Layout::auto_from_plots(&plots)
.with_interactive()
.with_log_y();
let scene = render_multiple(plots, layout);
let svg = SvgBackend::new().render_scene(&scene);
std::fs::write("test_outputs/interactive_log_y.svg", &svg).unwrap();
assert!(svg.contains(r#"data-log-y="1""#), "expected data-log-y=\"1\" in SVG root");
}
#[test]
fn test_interactive_scatter_data_attrs() {
let svg = make_scatter_svg(true);
std::fs::write("test_outputs/interactive_scatter_data_attrs.svg", &svg).unwrap();
assert!(svg.contains("data-x="), "expected data-x= on scatter point groups");
assert!(svg.contains("data-y="), "expected data-y= on scatter point groups");
}
#[test]
fn test_interactive_no_circle_batch() {
let svg = make_scatter_svg(true);
assert!(svg.contains("data-x="), "scatter points should be individually wrapped in interactive mode");
let svg_off = make_scatter_svg(false);
assert!(!svg_off.contains("data-x="), "non-interactive scatter must not have data-x");
}
#[test]
fn test_interactive_volcano_threshold() {
let svg = make_volcano_svg(true);
std::fs::write("test_outputs/interactive_volcano_threshold.svg", &svg).unwrap();
assert!(
svg.contains(r#"class="kuva-threshold""#),
"expected class=\"kuva-threshold\" on volcano threshold lines"
);
}
#[test]
fn test_interactive_volcano_point_logfc() {
let svg = make_volcano_svg(true);
assert!(svg.contains("data-logfc="), "expected data-logfc= on volcano point groups");
}
#[test]
fn test_interactive_legend_data_group() {
let svg = make_multi_group_scatter_svg();
std::fs::write("test_outputs/interactive_legend_data_group.svg", &svg).unwrap();
assert!(
svg.contains(r#"class="legend-entry""#),
"expected class=\"legend-entry\" on legend entry groups"
);
assert!(svg.contains("data-group="), "expected data-group= on legend entry groups");
}
#[test]
fn test_interactive_script_present() {
let svg = make_scatter_svg(true);
assert!(svg.contains("<script"), "expected <script in interactive SVG");
assert!(svg.contains("CDATA"), "expected CDATA section in script");
}
#[test]
fn test_interactive_foreignobject() {
let svg = make_scatter_svg(true);
assert!(svg.contains("<foreignObject"), "expected <foreignObject in interactive SVG");
assert!(svg.contains("kuva-search"), "expected search input in foreignObject");
}
#[test]
fn test_interactive_css_pinned() {
let svg = make_scatter_svg(true);
assert!(svg.contains(".pinned"), "expected .pinned CSS rule in interactive SVG");
}
#[test]
fn test_noninteractive_unchanged() {
let svg = make_scatter_svg(false);
assert!(svg.contains("<svg"), "SVG root missing");
assert!(svg.contains("<circle"), "expected circles in scatter");
assert!(!svg.contains("data-xmin"), "non-interactive must not have axis metadata");
assert!(!svg.contains(".pinned"), "non-interactive must not have pinned CSS");
}
#[test]
fn test_interactive_figure_propagates() {
let make_scatter = || vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(1.0_f64, 2.0), (3.0, 4.0)]),
)];
let make_layout = || {
let plots = make_scatter();
Layout::auto_from_plots(&plots).with_interactive()
};
let figure = Figure::new(1, 2)
.with_plots(vec![make_scatter(), make_scatter()])
.with_layouts(vec![make_layout(), make_layout()]);
let scene = figure.render();
let svg = SvgBackend::new().render_scene(&scene);
std::fs::write("test_outputs/interactive_figure.svg", &svg).unwrap();
assert!(svg.contains("data-x="), "figure panels should have data-x on scatter points when interactive");
}