use bland::{
bode, gamma_from_z, multi_panel, qq_plot, Basemap, BasemapOpts, BodeOpts, ColorbarPosition,
Figure, GraticuleOpts, Hatch, Marker, Normalize, PanelGridOpts, PaperSize, PolarGridOpts,
Projection, QqOpts, Resolution, SmithGridOpts, Stroke, TextAnchor, TitleBlock,
};
#[test]
fn renders_minimum_figure() {
let fig = Figure::new();
let svg = fig.to_svg();
assert!(svg.starts_with("<?xml"));
assert!(svg.contains("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn renders_line_with_legend() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [0.0, 1.0, 4.0, 9.0];
let svg = Figure::new()
.size(PaperSize::A5Landscape)
.title("Quadratic")
.xlabel("n")
.ylabel("n²")
.line(&xs, &ys, |s| s.label("n²").stroke(Stroke::Solid))
.legend_top_right()
.to_svg();
assert!(svg.contains("Quadratic") || svg.contains("QUADRATIC"));
assert!(svg.contains("polyline"));
assert!(svg.contains("n²"));
}
#[test]
fn renders_all_series_types() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [0.0, 1.0, 4.0, 9.0];
let cats = ["A", "B"];
let bar_vals = [3.0, 7.0];
let grid = vec![vec![0.0, 1.0, 2.0], vec![3.0, 4.0, 5.0]];
let svg = Figure::new()
.size(PaperSize::A4Landscape)
.title("Everything")
.line(&xs, &ys, |s| s.label("L"))
.scatter(&xs, &ys, |s| s.label("S").marker(Marker::CircleOpen))
.area(&xs, &ys, |s| s.label("A").hatch(Hatch::Diagonal))
.bar(&cats, &bar_vals, |b| b.label("B"))
.histogram(&[1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 4.0], |h| h.bins(4).label("H"))
.heatmap(grid, |h| h.label("M"))
.hline(2.0, |s| s.stroke(Stroke::Dotted))
.vline(1.5, |s| s.stroke(Stroke::DashDot))
.legend_top_right()
.to_svg();
assert!(svg.contains("<svg"));
assert!(svg.contains("bland-pattern-"));
assert!(svg.matches("<rect").count() > 10);
}
#[test]
fn renders_polygon_with_hatch() {
let svg = Figure::new()
.polygon(&[0.0, 1.0, 1.0, 0.0], &[0.0, 0.0, 1.0, 1.0], |p| {
p.label("box").hatch(Hatch::Diagonal)
})
.legend_top_right()
.to_svg();
assert!(svg.contains("<polygon"));
assert!(svg.contains("bland-pattern-diagonal"));
}
#[test]
fn renders_errorbar_with_yerr() {
let xs = [1.0, 2.0, 3.0];
let ys = [1.0, 2.5, 4.0];
let svg = Figure::new()
.errorbar(&xs, &ys, |e| {
e.yerr(&[0.1, 0.2, 0.3]).label("σ")
})
.to_svg();
assert!(svg.contains("<line"));
}
#[test]
fn renders_boxplot() {
let groups = vec![
("a".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]),
("b".to_string(), vec![2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 100.0]),
];
let svg = Figure::new()
.boxplot(&groups, |b| b.label("dist"))
.legend_top_right()
.to_svg();
assert!(svg.contains("<rect"));
}
#[test]
fn renders_stem() {
let xs: Vec<f64> = (0..8).map(|i| i as f64).collect();
let ys: Vec<f64> = xs.iter().map(|n| (n * 0.5).sin()).collect();
let svg = Figure::new()
.stem(&xs, &ys, |s| s.label("x[n]").marker(Marker::CircleFilled))
.to_svg();
assert!(svg.contains("<line"));
assert!(svg.contains("<circle"));
}
#[test]
fn renders_quiver() {
let svg = Figure::new()
.quiver(
&[0.0, 1.0],
&[0.0, 1.0],
&[1.0, -1.0],
&[1.0, 1.0],
|q| q.scale(0.5).head_size(4.0).label("F"),
)
.legend_top_right()
.to_svg();
assert!(svg.contains("<polygon"));
}
#[test]
fn renders_contour() {
let grid = vec![
vec![0.0, 1.0, 0.0],
vec![1.0, 2.0, 1.0],
vec![0.0, 1.0, 0.0],
];
let svg = Figure::new()
.contour(grid, |c| c.levels(vec![0.5, 1.5]).label("iso"))
.to_svg();
assert!(svg.contains("<line"));
}
#[test]
fn renders_histogram_cmf_as_line() {
let svg = Figure::new()
.histogram(&[1.0, 2.0, 2.0, 3.0, 3.0, 3.0], |h| {
h.bins(5).normalize(Normalize::Cmf).label("F")
})
.to_svg();
assert!(svg.contains("<polyline"));
}
#[test]
fn renders_polar() {
let thetas: Vec<f64> = (0..=64).map(|i| i as f64 / 64.0 * std::f64::consts::TAU).collect();
let rs: Vec<f64> = thetas.iter().map(|t| 1.0 + t.cos()).collect();
let svg = Figure::polar(2.0)
.polar_grid(PolarGridOpts::default())
.line(&thetas, &rs, |s| s.label("cardioid"))
.to_svg();
assert!(svg.contains("<svg"));
}
#[test]
fn renders_smith_grid() {
let (gr, gi) = gamma_from_z(1.0, 0.5);
let svg = Figure::smith()
.smith_grid(SmithGridOpts::default())
.scatter(&[gr], &[gi], |s| s.label("Γ").marker(Marker::CircleFilled))
.to_svg();
assert!(svg.contains("<svg"));
assert!(svg.contains("<polyline"));
}
#[test]
fn renders_mercator_with_graticule() {
let svg = Figure::new()
.size(PaperSize::A5Landscape)
.projection(Projection::Mercator)
.xlim(-180.0, 180.0)
.ylim(-70.0, 70.0)
.graticule(GraticuleOpts::default())
.line(&[-30.0, 30.0], &[0.0, 0.0], |s| s.stroke(Stroke::Solid))
.to_svg();
assert!(svg.contains("<svg"));
}
#[test]
fn renders_bode() {
let omegas: Vec<f64> = (0..50).map(|i| 10f64.powf(-1.0 + 4.0 * i as f64 / 49.0)).collect();
let mag: Vec<f64> = omegas.iter().map(|w| -10.0 * (1.0 + w * w / 100.0).log10()).collect();
let phase: Vec<f64> = omegas.iter().map(|w| -(w / 10.0).atan() * 180.0 / std::f64::consts::PI).collect();
let svg = bode(&omegas, &mag, &phase, BodeOpts::default());
assert!(svg.starts_with("<?xml"));
}
#[test]
fn renders_qq_plot() {
let samples: Vec<f64> = (0..200)
.map(|i| ((i as f64 - 100.0) / 30.0))
.collect();
let fig = qq_plot(&samples, QqOpts::default().label("residuals"));
let svg = fig.to_svg();
assert!(svg.contains("<svg"));
}
#[test]
fn renders_panels() {
let a = Figure::new().line(&[0.0, 1.0], &[0.0, 1.0], |s| s);
let b = Figure::new().line(&[0.0, 1.0], &[1.0, 0.0], |s| s);
let svg = multi_panel(&[a, b], PanelGridOpts::default().columns(2).title("paired"));
assert!(svg.contains("<svg"));
assert!(svg.contains("paired"));
}
#[test]
fn renders_annotations() {
let svg = Figure::new()
.line(&[0.0, 1.0], &[0.0, 1.0], |s| s)
.annotate_text(0.5, 0.5, "midpoint", TextAnchor::Middle)
.annotate_arrow((0.2, 0.2), (0.5, 0.5))
.to_svg();
assert!(svg.contains("midpoint"));
assert!(svg.contains("<polygon"));
}
#[test]
fn renders_colorbar_for_heatmap() {
let grid = vec![vec![0.0, 1.0], vec![2.0, 3.0]];
let svg = Figure::new()
.heatmap(grid, |h| h.label("density"))
.colorbar(ColorbarPosition::Right)
.to_svg();
assert!(svg.contains("density"));
assert!(svg.matches("<line").count() > 5);
}
#[test]
fn schematic_earth_basemap_loads() {
let coastlines = bland::basemaps::features(Basemap::EarthCoastlines, Resolution::Schematic);
assert!(coastlines.iter().any(|f| f.name == "Africa"));
let borders = bland::basemaps::features(Basemap::EarthBorders, Resolution::Schematic);
assert!(borders.iter().any(|f| f.name == "USA (contiguous)"));
}
#[test]
fn natural_earth_110m_basemap_loads() {
let coastlines = bland::basemaps::features(Basemap::EarthCoastlines, Resolution::Low);
assert!(coastlines.len() > 50);
let borders = bland::basemaps::features(Basemap::EarthBorders, Resolution::Low);
assert!(borders.len() > 100);
}
#[test]
fn moon_maria_basemap_loads() {
let maria = bland::basemaps::features(Basemap::MoonMaria, Resolution::Low);
assert!(maria.iter().any(|f| f.name == "Mare Imbrium"));
assert!(maria.iter().any(|f| f.name == "Oceanus Procellarum"));
}
#[test]
fn earth_tropics_includes_equator() {
let tropics = bland::basemaps::features(Basemap::EarthTropics, Resolution::Low);
assert!(tropics.iter().any(|f| f.name == "Equator"));
assert!(tropics.iter().any(|f| f.name == "Tropic of Cancer"));
}
#[test]
fn renders_world_map_with_basemap() {
let svg = Figure::new()
.size(PaperSize::A5Landscape)
.projection(Projection::Mercator)
.xlim(-180.0, 180.0)
.ylim(-65.0, 75.0)
.basemap(Basemap::EarthCoastlines, |b| b)
.basemap(Basemap::EarthBorders, |b| b.stroke(Stroke::Dashed))
.basemap(Basemap::EarthTropics, |b| b.stroke(Stroke::Dotted))
.to_svg();
assert!(svg.starts_with("<?xml"));
assert!(svg.matches("<polyline").count() > 100);
assert!(svg.matches("<polygon").count() > 100);
}
#[test]
fn basemap_only_filter_keeps_subset() {
let unfiltered = Figure::new()
.basemap(Basemap::EarthCoastlines, |b| b.resolution(Resolution::Schematic))
.to_svg();
let filtered = Figure::new()
.basemap(Basemap::EarthCoastlines, |b| {
b.resolution(Resolution::Schematic).only(["Africa", "Eurasia"])
})
.to_svg();
assert!(unfiltered.matches("<polygon").count() > filtered.matches("<polygon").count());
let _ = BasemapOpts::default();
}
#[test]
fn renders_title_block() {
let svg = Figure::new()
.title("With block")
.line(&[0.0, 1.0], &[0.0, 1.0], |s| s)
.title_block(
TitleBlock::new()
.project("BLAND")
.title("Test")
.drawn_by("JM")
.date("2026-04-28")
.scale("1:1")
.sheet("1 of 1")
.rev("A"),
)
.to_svg();
assert!(svg.contains("PROJECT"));
assert!(svg.contains("DRAWN"));
assert!(svg.contains("BLAND"));
}