test_that("group colours are fixed, named and valid hex", { g <- btg_group_colours() expect_identical(names(g), c("Asian", "Black", "Mixed", "Other", "White", "Unknown")) expect_true(all(grepl("^#[0-9A-Fa-f]{6}$", g))) expect_equal(unname(g[["Black"]]), unname(btg_colours()[["bar"]])) }) test_that("a force's ratio trend carries every year, group and coverage flag", { met <- btg_get_data("force_ethnicity_year", force = "metropolitan") p <- btg_plot_trend(met) expect_s3_class(p, "ggplot") expect_silent(ggplot2::ggplot_build(p)) d <- btg_figure_data(p) expect_equal(nrow(d), 9 * 4) expect_setequal(unique(d$series), c("Asian", "Black", "Mixed", "Other")) expect_equal(d$value[d$series == "Black" & d$period == 2025], met$data$ratio[met$data$group == "Black" & met$data$year == 2025]) expect_true(all(is.finite(d$lower[d$series == "Black"]))) expect_false(any(d$complete[d$period == 2026])) expect_equal(unique(d$months_in_source[d$period == 2026]), 7L) expect_true(all(d$complete[d$period %in% 2018:2025])) expect_match(p$labels$subtitle, "2026") expect_match(p$labels$title, "Metropolitan Police") rate <- btg_plot_trend(met, measure = "rate", highlight = "Black") expect_silent(ggplot2::ggplot_build(rate)) r <- btg_figure_data(rate) expect_true("White" %in% r$series) expect_equal(nrow(r), 9 * 5) expect_true(all(is.na(r$lower))) gm <- btg_get_data("force_ethnicity_year", force = "greater-manchester", groups = "Black") g <- btg_figure_data(btg_plot_trend(gm)) expect_true(is.na(g$value[g$period == 2025])) expect_false(g$complete[g$period == 2025]) view <- attr(p, "btg_view") expect_equal(view$schema_version, 3L) expect_equal(view$chart, "trend") expect_equal(view$settings$measure, "ratio") expect_equal(view$selection$force, "metropolitan") }) test_that("national, hit-rate, Section 60 and council trends draw their measures", { m <- btg_get_data("national_month_ethnicity") share <- btg_plot_trend(m, measure = "share") expect_silent(ggplot2::ggplot_build(share)) s <- btg_figure_data(share) totals <- as.numeric(tapply(s$value, s$period, sum)) expect_equal(totals, rep(100, length(totals))) expect_equal(nrow(btg_figure_data(btg_plot_trend(m))), nrow(m$data)) n <- btg_get_data("national_year") for (measure in c("stops", "arrest_rate", "s60")) { q <- btg_plot_trend(n, measure = measure) expect_silent(ggplot2::ggplot_build(q)) expect_equal(btg_figure_data(q)$value, n$data[[measure]]) } expect_silent(ggplot2::ggplot_build(btg_plot_trend(btg_get_data("hit_rate_ethnicity_year")))) s60 <- btg_get_data("section60_force_year", force = "metropolitan") expect_silent(ggplot2::ggplot_build(btg_plot_trend(s60))) council <- btg_get_data("council_ethnicity_year", council = "E09000022") expect_silent(ggplot2::ggplot_build(btg_plot_trend(council, highlight = "Black"))) expect_equal(nrow(btg_figure_data(btg_plot_trend(council))), 9 * 4) }) test_that("trend charts refuse ambiguous or unsupported selections", { expect_error(btg_plot_trend(btg_get_data("force_ethnicity_year")), "Select one force") expect_error(btg_plot_trend(btg_get_data("council_ethnicity_year")), "Select one council") expect_error(btg_plot_trend(btg_get_data()), "single year") met <- btg_get_data("force_ethnicity_year", force = "metropolitan") expect_error(btg_plot_trend(met, measure = "stops"), "measure must be one of") expect_error(btg_plot_trend(met, highlight = "White"), "highlight must be") expect_error(btg_plot_trend(met$data), "documented dataset") }) test_that("year-on-year change matches a hand calculation and reports coverage", { y <- btg_get_data("force_ethnicity_year", force = "metropolitan", groups = "Black") ch <- btg_change(y) expect_equal(nrow(ch), 1) expect_equal(ch$year_from, 2024L) expect_equal(ch$year_to, 2025L) a <- y$data[y$data$year == 2024, ] b <- y$data[y$data$year == 2025, ] expect_equal(ch$change, b$ratio / a$ratio) se <- sqrt(1 / a$group_stops + 1 / a$white_stops + 1 / b$group_stops + 1 / b$white_stops) expect_equal(ch$change_lower, exp(log(ch$change) - qnorm(0.975) * se)) expect_equal(ch$change_upper, exp(log(ch$change) + qnorm(0.975) * se)) expect_true(ch$change_direction %in% c("higher", "lower", "unclear")) expect_true(ch$comparable_coverage) expect_equal(ch$coverage_to, "all_months_supplied") all_forces <- btg_change(btg_get_data("force_ethnicity_year"), from = 2023, to = 2024) expect_equal(nrow(all_forces), 44 * 4) expect_true(all(all_forces$year_from == 2023 & all_forces$year_to == 2024)) expect_true(all(is.na(all_forces$change_direction[!is.finite(all_forces$change)]))) gm <- all_forces[all_forces$force == "greater-manchester", ] expect_true(all(gm$comparable_coverage == (gm$coverage_from %in% c("all_months_supplied", "unverified") & gm$coverage_to %in% c("all_months_supplied", "unverified")))) partial <- btg_change(btg_get_data("force_ethnicity_year", force = "metropolitan"), from = 2025, to = 2026) expect_false(any(partial$comparable_coverage)) expect_equal(unique(partial$months_in_source_to), 7L) council <- btg_change(btg_get_data("council_ethnicity_year", council = "E09000022")) expect_equal(nrow(council), 4) expect_true(all(council$comparable_coverage)) expect_error(btg_change(y, from = 2025, to = 2025), "must differ") expect_error(btg_change(y, from = 2017), "calendar years") expect_error(btg_change(btg_get_data()), "compares years") }) test_that("the change chart shows the largest changes with intervals and views", { y <- btg_get_data("force_ethnicity_year") p <- btg_plot_change(y) expect_s3_class(p, "ggplot") expect_silent(ggplot2::ggplot_build(p)) d <- btg_figure_data(p) expect_equal(nrow(d), 10) expect_true(all(d$group == "Black" & d$year_from == 2024 & d$year_to == 2025)) expect_true(all(diff(abs(log(d$change))) <= 1e-12)) inc <- btg_figure_data(btg_plot_change(y, order = "increase", n = 5)) expect_true(all(diff(inc$change) <= 1e-12) && all(inc$change > 1)) dec <- btg_figure_data(btg_plot_change(y, from = 2023, to = 2025, group = "Asian", order = "decrease", n = 5)) expect_true(all(diff(dec$change) >= -1e-12) && all(dec$change < 1)) expect_match(p$labels$subtitle, "exclude no change") expect_error(btg_plot_change(y, group = "White"), "group must be") expect_error(btg_plot_change(y, order = "sideways"), "order must be") expect_error(btg_plot_change(btg_get_data("council_ethnicity_year")), "force_ethnicity_year") expect_error(btg_plot_change(y, n = 0), "integer") }) test_that("trend and change views save and replay through schema 3", { met <- btg_get_data("force_ethnicity_year", force = "metropolitan", years = 2020:2025) path <- tempfile(fileext = ".json") on.exit(unlink(path)) p <- btg_plot_trend(met, measure = "rate", highlight = "Black") btg_save_view(p, path) replay <- btg_from_view(path) expect_identical(btg_figure_data(replay), btg_figure_data(p)) expect_identical(attr(replay, "btg_view"), attr(p, "btg_view")) saved <- jsonlite::read_json(path, simplifyVector = TRUE) expect_equal(saved$schema_version, 3) expect_equal(saved$selection$years, 2020:2025) q <- btg_plot_change(btg_get_data("force_ethnicity_year"), from = 2022, to = 2024, group = "Mixed", n = 7, order = "increase") btg_save_view(q, path, overwrite = TRUE) expect_identical(btg_figure_data(btg_from_view(path)), btg_figure_data(q)) n <- btg_plot_trend(btg_get_data("national_month_ethnicity", groups = c("Black", "White")), measure = "share") btg_save_view(n, path, overwrite = TRUE) expect_identical(btg_figure_data(btg_from_view(path)), btg_figure_data(n)) bad <- attr(p, "btg_view") bad$settings$measure <- "stops" jsonlite::write_json(bad, path, auto_unbox = TRUE) expect_error(btg_from_view(path), "measure must be") bad <- attr(p, "btg_view") bad$settings$code <- "stop('never')" jsonlite::write_json(bad, path, auto_unbox = TRUE) expect_error(btg_from_view(path), "settings") bad <- attr(p, "btg_view") bad$selection$dataset <- "private" jsonlite::write_json(bad, path, auto_unbox = TRUE) expect_error(btg_from_view(path), "selection") bad <- attr(p, "btg_view") bad$chart <- "force_disparity_ranking" jsonlite::write_json(bad, path, auto_unbox = TRUE) expect_error(btg_from_view(path), "chart") bad <- attr(p, "btg_view") bad$data_sha256 <- "wrong" jsonlite::write_json(bad, path, auto_unbox = TRUE) expect_error(btg_from_view(path), "checksums") })