use kuva::plot::ScatterPlot;
use kuva::render::{plots::Plot, layout::Layout, render::render_multiple};
use kuva::backend::svg::SvgBackend;
fn scatter_svg(layout: Layout) -> String {
let plot = ScatterPlot::new()
.with_data(vec![(0.0f64, 0.0f64), (13.0, 5.0)]);
let plots = vec![Plot::Scatter(plot)];
SvgBackend.render_scene(&render_multiple(plots, layout))
}
#[test]
fn test_axis_range_override() {
let plots = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (13.0, 5.0)]))];
let layout_auto = Layout::auto_from_plots(&plots);
let svg_auto = SvgBackend.render_scene(&render_multiple(plots, layout_auto));
assert!(svg_auto.contains("15"), "auto range should include tick 15");
let plots2 = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (13.0, 5.0)]))];
let layout_override = Layout::auto_from_plots(&plots2)
.with_x_axis_min(0.0)
.with_x_axis_max(10.0);
let svg_override = scatter_svg(layout_override);
std::fs::write("test_outputs/tick_control_range.svg", &svg_override).unwrap();
assert!(svg_override.contains("10"), "overridden range should include tick 10");
assert!(!svg_override.contains(">15<"), "overridden range should not show tick 15");
}
#[test]
fn test_explicit_tick_step() {
let plots = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]))];
let layout = Layout::auto_from_plots(&plots)
.with_x_axis_min(0.0)
.with_x_axis_max(10.0)
.with_x_tick_step(2.5);
let svg = scatter_svg(layout);
std::fs::write("test_outputs/tick_control_step.svg", &svg).unwrap();
assert!(svg.contains(">0<") || svg.contains(">0.0<") || svg.contains("\"0\"") || svg.contains(">0"),
"tick 0 should appear");
assert!(svg.contains("2.5"), "tick 2.5 should appear");
assert!(svg.contains(">5<") || svg.contains("5.0") || svg.contains(">5"),
"tick 5 should appear");
assert!(svg.contains("7.5"), "tick 7.5 should appear");
assert!(svg.contains(">10<") || svg.contains(">10"),
"tick 10 should appear");
}
#[test]
fn test_minor_ticks() {
let plots = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]))];
let layout_no_minor = Layout::auto_from_plots(&plots);
let svg_no_minor = scatter_svg(layout_no_minor);
let plots2 = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]))];
let layout_minor = Layout::auto_from_plots(&plots2).with_minor_ticks(5);
let svg_minor = scatter_svg(layout_minor);
std::fs::write("test_outputs/tick_control_minor.svg", &svg_minor).unwrap();
let lines_without = svg_no_minor.matches("<line").count();
let lines_with = svg_minor.matches("<line").count();
assert!(lines_with > lines_without,
"minor ticks should add more line elements ({} vs {})", lines_with, lines_without);
}
#[test]
fn test_minor_grid() {
let plots = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]))];
let layout_minor = Layout::auto_from_plots(&plots).with_minor_ticks(5);
let svg_minor = scatter_svg(layout_minor);
let plots2 = vec![Plot::Scatter(ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]))];
let layout_grid = Layout::auto_from_plots(&plots2)
.with_minor_ticks(5)
.with_show_minor_grid(true);
let svg_grid = scatter_svg(layout_grid);
std::fs::write("test_outputs/tick_control_minor_grid.svg", &svg_grid).unwrap();
let lines_minor = svg_minor.matches("<line").count();
let lines_grid = svg_grid.matches("<line").count();
assert!(lines_grid > lines_minor,
"minor grid should add even more line elements ({} vs {})", lines_grid, lines_minor);
}
#[test]
fn test_axis_line_width() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots).with_axis_line_width(4.0);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/axis_line_width.svg", &svg).unwrap();
assert!(
svg.contains(r#"stroke-width="4""#),
"axis lines should carry stroke-width=\"4\""
);
}
#[test]
fn test_axis_line_width_default() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(
!svg.contains(r#"stroke-width="4""#),
"default rendering should not have stroke-width=\"4\""
);
}
#[test]
fn test_tick_stroke_width() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots).with_tick_width(3.5);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/tick_stroke_width.svg", &svg).unwrap();
assert!(
svg.contains(r#"stroke-width="3.5""#),
"tick marks should carry stroke-width=\"3.5\""
);
}
#[test]
fn test_tick_stroke_width_default() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(
!svg.contains(r#"stroke-width="3.5""#),
"default rendering should not have stroke-width=\"3.5\""
);
}
#[test]
fn test_tick_length() {
let make_plots = || {
vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)]
};
let plots_default = make_plots();
let layout_default = Layout::auto_from_plots(&plots_default);
let svg_default = SvgBackend.render_scene(&render_multiple(plots_default, layout_default));
let plots_long = make_plots();
let layout_long = Layout::auto_from_plots(&plots_long).with_tick_length(150.0);
let svg_long = SvgBackend.render_scene(&render_multiple(plots_long, layout_long));
std::fs::write("test_outputs/tick_length.svg", &svg_long).unwrap();
assert_ne!(
svg_default, svg_long,
"with_tick_length(15.0) should produce different tick coordinates than the default"
);
}
#[test]
fn test_grid_line_width() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots).with_grid_line_width(2.5);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/grid_line_width.svg", &svg).unwrap();
assert!(
svg.contains(r#"stroke-width="2.5""#),
"grid lines should carry stroke-width=\"2.5\""
);
}
#[test]
fn test_grid_line_width_default() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
assert!(
!svg.contains(r#"stroke-width="2.5""#),
"default rendering should not have stroke-width=\"2.5\""
);
}
#[test]
fn test_tick_and_grid_width_independent() {
let make_plots = || {
vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)]
};
let plots_both = make_plots();
let layout_both = Layout::auto_from_plots(&plots_both)
.with_tick_width(3.0)
.with_grid_line_width(7.0);
let svg_both = SvgBackend.render_scene(&render_multiple(plots_both, layout_both));
std::fs::write("test_outputs/tick_grid_independent.svg", &svg_both).unwrap();
assert!(
svg_both.contains(r#"stroke-width="3""#),
"tick width=3 should appear when set independently of grid"
);
assert!(
svg_both.contains(r#"stroke-width="7""#),
"grid line width=7 should appear when set independently of ticks"
);
let plots_tick_only = make_plots();
let layout_tick_only = Layout::auto_from_plots(&plots_tick_only).with_tick_width(3.0);
let svg_tick_only = SvgBackend.render_scene(&render_multiple(plots_tick_only, layout_tick_only));
let count_tick_only = svg_tick_only.matches(r#"stroke-width="3""#).count();
let plots_tick_and_grid = make_plots();
let layout_tick_and_grid = Layout::auto_from_plots(&plots_tick_and_grid)
.with_tick_width(3.0)
.with_grid_line_width(3.0);
let svg_tick_and_grid = SvgBackend.render_scene(&render_multiple(plots_tick_and_grid, layout_tick_and_grid));
let count_tick_and_grid = svg_tick_and_grid.matches(r#"stroke-width="3""#).count();
assert!(
count_tick_and_grid > count_tick_only,
"adding grid_line_width=3 on top of tick_width=3 should increase the stroke-width=\"3\" \
count (grid lines join in); got tick_only={} vs tick+grid={}",
count_tick_only, count_tick_and_grid
);
}
#[test]
fn test_axis_controls_combined() {
let plots = vec![Plot::Scatter(
ScatterPlot::new().with_data(vec![(0.0f64, 0.0f64), (10.0, 5.0)]),
)];
let layout = Layout::auto_from_plots(&plots)
.with_axis_line_width(5.0)
.with_tick_width(2.5)
.with_tick_length(8.0)
.with_grid_line_width(0.5);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/axis_controls_combined.svg", &svg).unwrap();
assert!(svg.contains(r#"stroke-width="5""#), "axis line width=5 should appear");
assert!(svg.contains(r#"stroke-width="2.5""#), "tick width=2.5 should appear");
assert!(svg.contains(r#"stroke-width="0.5""#), "grid line width=0.5 should appear");
}
#[test]
fn test_no_negative_zero_tick_label() {
use kuva::render::layout::TickFormat;
assert_ne!(TickFormat::Auto.format(-0.0_f64), "-0");
assert_ne!(TickFormat::Integer.format(-0.0_f64), "-0");
assert_ne!(TickFormat::Fixed(1).format(-0.0_f64), "-0.0");
assert_ne!(TickFormat::Percent.format(-0.0_f64), "-0.0%");
use kuva::plot::DensityPlot;
use kuva::render::plots::Plot;
let dp = DensityPlot::new()
.with_data(vec![0.5_f64; 20])
.with_x_range(0.0, 1.0);
let plots = vec![Plot::Density(dp)];
let layout = Layout::auto_from_plots(&plots);
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("test_outputs/tick_no_negative_zero.svg", &svg).unwrap();
assert!(!svg.contains(">-0<"), "SVG must not contain a '-0' tick label");
}