=============================

DASHBOARD OUTPUT

=============================

—- KPIs —-

cur <- kpi_latest |> filter(year == latest_year) |> pull(offences) prev <- kpi_latest |> filter(year == prev_year) |> pull(offences) pct <- ifelse(prev > 0, round(100 * (cur - prev) / prev, 1), NA)

flexdashboard::valueBox(fmt_int(cur), glue(“Total offences — {latest_year}”), icon = “fa-balance-scale”) flexdashboard::valueBox( ifelse(is.na(pct), “n/a”, paste0(ifelse(pct >= 0, “+”, ““), pct,”%“)), glue(”Change vs {prev_year}“), icon = ifelse(pct >= 0,”fa-arrow-up”, “fa-arrow-down”), color = ifelse(pct >= 0, “danger”, “success”) )

top_lga <- csa_sum |> filter(year == latest_year) |> arrange(desc(offences)) |> slice_head(n = 1) flexdashboard::valueBox(fmt_int(top_lga\(offences), glue("Top LGA — {top_lga\)lga}: {fmt_int(top_lga$offences)}“), icon =”fa-city”)

—- Trend —-

plt_trend <- ggplot(csa_total_year, aes(year, offences)) + geom_line(size = 1.2, color = “#2c3e50”) + geom_point(size = 2, color = “#2c3e50”) + scale_y_continuous(labels = label_number(big.mark = “,”)) + labs(title = “Total Recorded Offences in Victoria”, x = NULL, y = “Offences”) + theme_minimal(base_size = 12) print(plotly::ggplotly(plt_trend, tooltip = c(“x”, “y”)))

—- LGA Explorer —-

crosstalk::filter_select(“f_lga”, “LGA”, sd, ~lga, multiple = TRUE)

plot_data <- sd$data(withFilter = FALSE) |> group_by(year, lga) |> summarise(offences = sum(offences), rate_per_100k = sum(rate_per_100k), .groups = “drop”)

plt_lga <- ggplot(plot_data, aes(year, offences, color = lga)) + geom_line() + geom_point(size = 1.5) + scale_y_continuous(labels = label_number(big.mark = “,”)) + labs(title = “Offences by LGA (filter above)”, x = NULL, y = “Offences”, color = “LGA”) + theme_minimal(base_size = 12) + theme(legend.position = “none”)

crosstalk::bscols( widths = c(12, 12), plotly::ggplotly(plt_lga, tooltip = c(“x”, “y”, “colour”)), DT::datatable( sd$data(withFilter = TRUE) |> select(year, lga, offences, rate_per_100k) |> arrange(desc(year)), options = list(pageLength = 6, scrollX = TRUE), rownames = FALSE ) )

—- SEIFA Scatter —-

if (!is.null(seifa) && any(!is.na(csa_context$rate_per_100k))) { latest_rates <- csa_context |> filter(year == latest_year) |> select(lga, rate_per_100k) |> left_join(seifa, by = “lga”) num_cols <- latest_rates |> select(where(is.numeric)) |> names() seifa_col <- num_cols[grepl(“seifa|irsad|ier|index”, num_cols, ignore.case = TRUE)][1]

if (!is.na(seifa_col)) { p <- ggplot(latest_rates, aes(.data[[seifa_col]], rate_per_100k)) + geom_point(alpha = 0.7, color = “#3498db”) + geom_smooth(method = “lm”, se = TRUE, color = “red”) + labs(title = “Crime Rate vs Socio-Economic Advantage”, x = str_to_title(gsub(“_“,” “, seifa_col)), y =”Rate per 100,000”) + theme_minimal(base_size = 12) print(plotly::ggplotly(p, tooltip = c(“x”, “y”))) } }

—- Map: Total + Rate per 100k —-

if (file.exists(params\(file_lga_geojson)) { lga_geo <- geojsonio::geojson_read(params\)file_lga_geojson, what = “sp”) |> sf::st_as_sf()

latest_tot <- csa_sum |> filter(year == latest_year) |> select(lga, offences) latest_rate <- csa_sum |> filter(year == latest_year) |> select(lga, rate_per_100k)

map_total <- lga_geo |> mutate(lga = str_replace(NAME, ” City\(| Shire\)| Council$“,”“) |> str_squish()) |> left_join(latest_tot, by =”lga”)

map_rate <- lga_geo |> mutate(lga = str_replace(NAME, ” City\(| Shire\)| Council$“,”“) |> str_squish()) |> left_join(latest_rate, by =”lga”)

pal_total <- colorNumeric(“Blues”, domain = map_total\(offences, na.color = "lightgray") pal_rate <- colorNumeric("Reds", domain = map_rate\)rate_per_100k, na.color = “lightgray”)

m <- leaflet(map_total, options = leafletOptions(minZoom = 6)) |> addProviderTiles(providers$CartoDB.Positron) |> addPolygons(fillColor = ~pal_total(offences), weight = 0.8, color = “#555”, fillOpacity = 0.8, label = ~paste0(lga, “:”, comma(offences)), group = “Total Offences”) |> addPolygons(data = map_rate, fillColor = ~pal_rate(rate_per_100k), weight = 0.8, color = “#555”, fillOpacity = 0.8, label = ~paste0(lga, “:”, fmt_rate(rate_per_100k), ” per 100k”), group = “Rate per 100k”) |> addLayersControl(overlayGroups = c(“Total Offences”, “Rate per 100k”), options = layersControlOptions(collapsed = FALSE)) |> addLegend(pal = pal_total, values = ~offences, title = glue(“Offences ({latest_year})”), position = “bottomright”) |> addLegend(pal = pal_rate, values = ~rate_per_100k, title = “Rate per 100k”, position = “bottomright”) |> hideGroup(“Rate per 100k”) print(m) }

—- About —-

cat(htmltools::htmlPreserve(” Story question. Is crime really rising post-pandemic, or are patterns shifting by region?

Notes on methods. - Data from CSA Table 01 (total offences by LGA). - Rates already provided in source. - Optional SEIFA and population for context.

References - Crime Statistics Agency. (2025). Recorded offences by LGA. https://www.crimestatistics.vic.gov.au/

Created by Sahil Bedwal (S4143314) · RMIT University

“), sep =”“)