use kuva::backend::svg::SvgBackend;
use kuva::plot::waffle::{CellShape, FillOrder, WafflePlot};
use kuva::render::render::render_waffle;
use kuva::render::{layout::Layout, plots::Plot, render::render_multiple};
fn render(wp: WafflePlot, title: &str) -> String {
let plots = vec![Plot::Waffle(wp)];
let layout = Layout::auto_from_plots(&plots).with_title(title);
SvgBackend.render_scene(&render_multiple(plots, layout))
}
fn write(name: &str, svg: &str) {
std::fs::create_dir_all("test_outputs").unwrap();
std::fs::write(format!("test_outputs/{name}"), svg).unwrap();
}
#[test]
fn test_waffle_basic() {
let wp = WafflePlot::new()
.with_category("Yes", 60.0, "steelblue")
.with_category("No", 40.0, "tomato");
let svg = render(wp, "Basic Waffle");
write("waffle_basic.svg", &svg);
assert!(svg.contains("<svg"), "should produce valid SVG");
assert!(svg.contains("<rect"), "should have rect elements");
}
#[test]
fn test_waffle_empty() {
let wp = WafflePlot::new();
let svg = render(wp, "Empty Waffle");
write("waffle_empty.svg", &svg);
assert!(svg.contains("<svg"), "should produce valid SVG");
}
#[test]
fn test_waffle_single_category() {
let wp = WafflePlot::new().with_category("Only", 100.0, "#3498db");
let svg = render(wp, "Single Category");
write("waffle_single_category.svg", &svg);
assert!(svg.contains("<rect"), "should have rect elements");
assert!(svg.contains("#3498db"), "should use the category color");
}
#[test]
fn test_waffle_largest_remainder_exact() {
use kuva::render::render::waffle_largest_remainder;
let counts = waffle_largest_remainder(&[50.0, 30.0, 20.0], 10);
assert_eq!(counts, vec![5, 3, 2]);
assert_eq!(counts.iter().sum::<usize>(), 10);
}
#[test]
fn test_waffle_largest_remainder_rounding() {
use kuva::render::render::waffle_largest_remainder;
let counts = waffle_largest_remainder(&[1.0, 1.0, 1.0], 100);
assert_eq!(counts.iter().sum::<usize>(), 100);
for &c in &counts {
assert!(c >= 33 && c <= 34);
}
}
#[test]
fn test_waffle_largest_remainder_single() {
use kuva::render::render::waffle_largest_remainder;
let counts = waffle_largest_remainder(&[100.0], 10);
assert_eq!(counts, vec![10]);
}
#[test]
fn test_waffle_largest_remainder_empty() {
use kuva::render::render::waffle_largest_remainder;
let counts = waffle_largest_remainder(&[], 10);
assert!(counts.is_empty());
}
#[test]
fn test_waffle_largest_remainder_zero_total() {
use kuva::render::render::waffle_largest_remainder;
let counts = waffle_largest_remainder(&[0.0, 0.0], 10);
assert_eq!(counts, vec![0, 0]);
}
#[test]
fn test_waffle_custom_grid() {
let wp = WafflePlot::new()
.with_grid(5, 20)
.with_category("A", 70.0, "steelblue")
.with_category("B", 30.0, "tomato");
let svg = render(wp, "5×20 Grid");
write("waffle_custom_grid.svg", &svg);
assert!(svg.contains("<rect"), "should have rect elements");
}
#[test]
fn test_waffle_rows_cols_builders() {
let wp = WafflePlot::new()
.with_rows(4)
.with_cols(25)
.with_category("A", 50.0, "#2ecc71")
.with_category("B", 50.0, "#e74c3c");
assert_eq!(wp.rows, 4);
assert_eq!(wp.cols, 25);
assert_eq!(wp.total_cells(), 100);
let svg = render(wp, "4×25 Grid");
write("waffle_rows_cols.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_fill_order_bottom_left() {
let wp = WafflePlot::new()
.with_fill_order(FillOrder::RowMajorBottomLeft)
.with_category("A", 60.0, "steelblue")
.with_category("B", 40.0, "tomato");
let svg = render(wp, "Bottom-Left Fill");
write("waffle_fill_bottom_left.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_fill_order_col_major() {
let wp = WafflePlot::new()
.with_fill_order(FillOrder::ColMajorTopLeft)
.with_category("A", 60.0, "steelblue")
.with_category("B", 40.0, "tomato");
let svg = render(wp, "Column-Major Fill");
write("waffle_fill_col_major.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_circle_shape() {
let wp = WafflePlot::new()
.with_shape(CellShape::Circle)
.with_category("A", 70.0, "#9b59b6")
.with_category("B", 30.0, "#f39c12");
let svg = render(wp, "Circle Cells");
write("waffle_circles.svg", &svg);
assert!(svg.contains("<circle"), "should have circle elements");
}
#[test]
fn test_waffle_no_gap() {
let wp = WafflePlot::new()
.with_gap(0.0)
.with_category("A", 50.0, "steelblue")
.with_category("B", 50.0, "tomato");
let svg = render(wp, "No Gap");
write("waffle_no_gap.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_large_gap() {
let wp = WafflePlot::new()
.with_gap(0.3)
.with_category("A", 50.0, "steelblue")
.with_category("B", 50.0, "tomato");
let svg = render(wp, "Large Gap");
write("waffle_large_gap.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_legend() {
let wp = WafflePlot::new()
.with_category("Treated", 45.0, "#3498db")
.with_category("Partial", 30.0, "#f1c40f")
.with_category("Untreated", 25.0, "#e74c3c")
.with_legend("Status");
let svg = render(wp, "With Legend");
write("waffle_legend.svg", &svg);
assert!(svg.contains("Treated"), "legend should contain 'Treated'");
assert!(svg.contains("Partial"), "legend should contain 'Partial'");
assert!(
svg.contains("Untreated"),
"legend should contain 'Untreated'"
);
}
#[test]
fn test_waffle_legend_with_percents() {
let wp = WafflePlot::new()
.with_category("Yes", 75.0, "steelblue")
.with_category("No", 25.0, "tomato")
.with_legend("Response")
.with_show_percents();
let svg = render(wp, "Legend + Percents");
write("waffle_legend_percents.svg", &svg);
assert!(svg.contains("75.0%"), "legend should show 75.0%");
assert!(svg.contains("25.0%"), "legend should show 25.0%");
}
#[test]
fn test_waffle_legend_with_counts() {
let wp = WafflePlot::new()
.with_category("A", 40.0, "steelblue")
.with_category("B", 60.0, "tomato")
.with_legend("Category")
.with_show_counts();
let svg = render(wp, "Legend + Counts");
write("waffle_legend_counts.svg", &svg);
assert!(
svg.contains("40 cells"),
"legend should show cell count for A"
);
assert!(
svg.contains("60 cells"),
"legend should show cell count for B"
);
}
#[test]
fn test_waffle_legend_percents_and_counts() {
let wp = WafflePlot::new()
.with_category("Alpha", 50.0, "#2ecc71")
.with_category("Beta", 50.0, "#e74c3c")
.with_legend("Group")
.with_show_percents()
.with_show_counts();
let svg = render(wp, "Legend + Both");
write("waffle_legend_both.svg", &svg);
assert!(svg.contains("50 cells"), "should show cell count");
assert!(svg.contains("50.0%"), "should show percentage");
}
#[test]
fn test_waffle_unit_label() {
let wp = WafflePlot::new()
.with_category("Vaccinated", 68.0, "#27ae60")
.with_category("Unvaccinated", 32.0, "#e74c3c")
.with_unit_label("■ = 1% of population");
let svg = render(wp, "With Unit Label");
write("waffle_unit_label.svg", &svg);
assert!(
svg.contains("1% of population"),
"should include unit annotation"
);
}
#[test]
fn test_render_waffle_convenience() {
let wp = WafflePlot::new()
.with_category("A", 60.0, "steelblue")
.with_category("B", 40.0, "tomato");
let layout =
Layout::auto_from_plots(&[Plot::Waffle(wp.clone())]).with_title("Convenience render");
let scene = render_waffle(wp, layout);
let svg = SvgBackend.render_scene(&scene);
write("waffle_render_convenience.svg", &svg);
assert!(svg.contains("<rect"));
}
#[test]
fn test_waffle_many_categories() {
let wp = WafflePlot::new()
.with_categories([
("A", 20.0, "#e74c3c"),
("B", 18.0, "#e67e22"),
("C", 15.0, "#f1c40f"),
("D", 14.0, "#2ecc71"),
("E", 12.0, "#1abc9c"),
("F", 11.0, "#3498db"),
("G", 6.0, "#9b59b6"),
("H", 4.0, "#34495e"),
])
.with_legend("Category")
.with_show_percents();
let svg = render(wp, "Many Categories");
write("waffle_many_categories.svg", &svg);
assert!(svg.contains("<rect"));
assert!(svg.contains("A"), "legend should contain category A");
}
#[test]
fn test_waffle_vaccination_showcase() {
let wp = WafflePlot::new()
.with_category("Fully vaccinated", 62.0, "#27ae60")
.with_category("Partially vaccinated", 13.0, "#f39c12")
.with_category("Unvaccinated", 25.0, "#e74c3c")
.with_shape(CellShape::Circle)
.with_fill_order(FillOrder::RowMajorBottomLeft)
.with_gap(0.15)
.with_legend("Vaccination status")
.with_show_percents()
.with_unit_label("■ = 1% of adult population");
let svg = render(wp, "COVID-19 Vaccination Coverage");
write("waffle_vaccination_showcase.svg", &svg);
assert!(svg.contains("<circle"), "should use circle cells");
assert!(
svg.contains("Fully vaccinated"),
"legend should contain 'Fully vaccinated'"
);
assert!(svg.contains("62.0%"), "legend should show 62.0%");
assert!(svg.contains("25.0%"), "legend should show 25.0%");
assert!(
svg.contains("adult population"),
"unit label should be present"
);
}
#[test]
fn test_waffle_budget_showcase() {
let wp = WafflePlot::new()
.with_categories([
("Healthcare", 32.0, "#3498db"),
("Education", 22.0, "#2ecc71"),
("Defence", 16.0, "#e74c3c"),
("Transport", 12.0, "#f39c12"),
("Social care", 9.0, "#9b59b6"),
("Other", 9.0, "#95a5a6"),
])
.with_fill_order(FillOrder::ColMajorTopLeft)
.with_gap(0.08)
.with_legend("Budget allocation")
.with_show_percents()
.with_show_counts()
.with_unit_label("■ = 1% of public spending");
let svg = render(wp, "Government Budget Allocation");
write("waffle_budget_showcase.svg", &svg);
assert!(svg.contains("<rect"), "should have rect elements");
assert!(
svg.contains("Healthcare"),
"legend should contain Healthcare"
);
assert!(svg.contains("Education"), "legend should contain Education");
assert!(
svg.contains("32.0%"),
"legend should show Healthcare percentage"
);
assert!(
svg.contains("1% of public spending"),
"unit label should be present"
);
assert!(
svg.contains("32 cells"),
"legend should show Healthcare cell count"
);
}
#[test]
fn test_waffle_survey_wide_grid() {
let wp = WafflePlot::new()
.with_grid(5, 20)
.with_categories([
("Strongly agree", 28.0, "#1a5276"),
("Agree", 35.0, "#2980b9"),
("Neutral", 17.0, "#aab7b8"),
("Disagree", 12.0, "#e67e22"),
("Strongly disagree", 8.0, "#c0392b"),
])
.with_gap(0.05)
.with_empty_color("#f2f3f4")
.with_legend("Agreement")
.with_show_percents();
let svg = render(wp, "Survey: \"The product meets my needs\"");
write("waffle_survey_wide.svg", &svg);
assert!(svg.contains("<rect"), "should have rect elements");
assert!(
svg.contains("Strongly agree"),
"legend should contain Strongly agree"
);
assert!(svg.contains("28.0%"), "legend should show 28.0%");
assert!(svg.contains("35.0%"), "legend should show 35.0%");
let wp2 = WafflePlot::new().with_grid(5, 20);
assert_eq!(wp2.total_cells(), 100);
}
#[test]
fn test_waffle_figure_small_multiples() {
use kuva::backend::svg::SvgBackend;
use kuva::render::figure::{Figure, LabelConfig};
let waffle_2020 = WafflePlot::new()
.with_category("Renewable", 29.0, "#27ae60")
.with_category("Nuclear", 19.0, "#8e44ad")
.with_category("Gas", 39.0, "#e67e22")
.with_category("Coal", 13.0, "#7f8c8d")
.with_legend("Source");
let waffle_2023 = WafflePlot::new()
.with_category("Renewable", 42.0, "#27ae60")
.with_category("Nuclear", 17.0, "#8e44ad")
.with_category("Gas", 35.0, "#e67e22")
.with_category("Coal", 6.0, "#7f8c8d")
.with_legend("Source");
let fig = Figure::new(1, 2)
.with_plots(vec![
vec![Plot::Waffle(waffle_2020)],
vec![Plot::Waffle(waffle_2023)],
])
.with_labels_custom(vec!["2020", "2023"], LabelConfig::default())
.with_title("UK Electricity Generation Mix");
let svg = SvgBackend.render_scene(&fig.render());
write("waffle_figure_small_multiples.svg", &svg);
assert!(svg.contains("2020"), "should contain first panel label");
assert!(svg.contains("2023"), "should contain second panel label");
assert!(svg.contains("Renewable"), "should contain category label");
assert!(svg.contains("<rect"), "should have rect elements");
}
#[test]
fn test_waffle_clinical_trial_showcase() {
let wp = WafflePlot::new()
.with_categories([
("Responders", 270.0, "#2980b9"),
("Non-responders", 130.0, "#e74c3c"),
])
.with_shape(CellShape::Circle)
.with_gap(0.12)
.with_legend("Outcome")
.with_show_percents()
.with_show_counts()
.with_unit_label("● = 4 patients (n = 400 total)");
let svg = render(wp, "Clinical Trial Response Rate");
write("waffle_clinical_trial.svg", &svg);
assert!(svg.contains("<circle"), "should use circle cells");
assert!(
svg.contains("Responders"),
"legend should contain Responders"
);
assert!(svg.contains("67.5%"), "legend should show response rate");
assert!(
svg.contains("68 cells"),
"LR should give 68 cells to responders"
);
assert!(
svg.contains("32 cells"),
"LR should give 32 cells to non-responders"
);
assert!(svg.contains("4 patients"), "unit label should be present");
}