1. The Dual Clock: Commuter Bottlenecks vs. Bunch Rides


The Bigger Picture & Policy Objective

Why must transport planners and riders look beyond annual crash totals? Treating cycling safety as a uniform 24/7 problem leads to misallocated policing and generic safety campaigns. This visualization answers a foundational analytical question: Do commuter cyclists and recreational road cyclists face the same temporal risk profile?

Design Justification: Following Harrower and Brewer (2003), the sequential ColorBrewer YlOrRd palette maps high crash density to high-salience crimson hues against a soft #F8FAFC background, preventing visual fatigue while maintaining colourblind accessibility.

2. Kinetic Energy: Speed, Lighting & Road Geometry


The Bigger Picture & Policy Objective

Public debate often confuses where the most crashes happen with where the most lethal crashes happen. By encoding five simultaneous variables—Speed Limit (\(y\)), Fatal/Serious Rate (\(x\)), Lighting (colour), Crash Volume (size), and Road Geometry (facets)—this view equips road engineers to answer: Under what environmental thresholds does human tolerance to biomechanical impact fail?

Design Justification: Applies Cleveland and McGill’s (1984) hierarchy of perceptual tasks—placing the primary metric (severity rate) on a common horizontal scale paired with the high-contrast ColorBrewer Dark2 palette.

3. Impact Anatomy: Top Collision Mechanisms


The Bigger Picture & Policy Objective

Painted bicycle symbols on asphalt do not physically stop multi-tonne vehicles from crossing a rider’s path. To design effective physical interventions, municipal engineers must answer: Which exact vehicle-to-bicycle conflict trajectories generate the highest hospitalisation burden across each speed tier?

  1. Intersection Cross-Traffic & Right-Throughs: Motorists failing to yield at cross-streets or turning right across oncoming cyclists dominate Fatal and Serious Injuries, overwhelmingly concentrated on 60 km/h arterial roads.
  2. Car Dooring Along Commercial Strips: Vehicle door strikes (Vehicle Strikes Door Of Parked/Stationary Vehicle) cluster in 40–60 km/h shopping precincts where parallel parking sits between moving traffic and painted bike lanes.
  3. High-Speed Rear-End Impacts: In 70–110 km/h zones, same-direction rear-end collisions become disproportionately catastrophic.

Design Justification: Uses the universally accessible Okabe-Ito qualitative scale (Okabe & Ito, 2008) so readers with deuteranopia or protanopia can effortlessly distinguish 40 km/h (bluish-green) from 60–80 km/h (orange/vermilion) segments.

4. Spatial Hotspots: Street-Level Crash Mapping


The Bigger Picture & Policy Objective

Aggregated statistics hide the specific intersections where riders are repeatedly struck. Following Segel and Heer’s (2010) Martini-Glass narrative architecture, after establishing macro temporal and physical mechanics in Charts 1–3, this spatial layer empowers the audience—local residents, riders, and council planners—to interrogate street-level risk directly: Where do high-speed arterial corridors intersect with heavy bicycle volumes?

Interactive Exploration: Click any numbered cluster to spiderfy down to individual street corners, then click any point marker to inspect the full incident report card.

References & Data Processing Notes

Data Sources & Academic References


Data Preprocessing & Methodological Notes

The empirical pipeline began with multi-source programmatic ingestion, importing primary Victorian road crash telemetry directly from the Department of Transport and Planning (2026) open data repository alongside municipal demographic metrics published in the Australian Bureau of Statistics (2025) Regional Population report.

To isolate vulnerable road user dynamics, raw incident records were systematically filtered to cases confirming direct cyclist involvement (BICYCLIST > 0) across a validated 10-year longitudinal observation window from 2015 to 2025, while records lacking temporal or geographic coordinates were quarantined to maintain analytical integrity.

Multivariate feature engineering was executed across key biomechanical factors by standardizing raw numerical speed limits into distinct regulatory speed brackets (such as 40 km/h or under, 50 km/h, 60 km/h, 70-80 km/h, and 90-110 km/h), classifying Definitions for Classifying Accidents (DCA) codes into primary collision conflict types, and harmonizing historical local government boundary reclassifications such as the City of Moreland’s transition to Merri-bek.

To control for municipal scale and commuter inflow bias, crash event volumes were coupled with Australian Bureau of Statistics Estimated Resident Population (ERP) tallies, deriving annualized fatal and serious casualty rates per 100,000 residents across pre-pandemic (2017–2019) and post-infrastructure investment (2023–2025) cohorts.

Visual encodings were designed strictly in alignment with Cleveland and McGill’s (1984) perceptual hierarchy, prioritizing common positional axes for continuous rate metrics and supplementing them with small-multiple faceting, area-scaled bubble encodings, and perceptually balanced, colorblind-safe palettes including ColorBrewer’s YlOrRd, Dark2, and Set1 alongside the Okabe-Ito scale.

The overarching dashboard flow follows Segel and Heer’s (2010) Martini-Glass narrative architecture, establishing foundational macroeconomic, temporal, and kinetic thresholds across the opening author-driven visualizations before transitioning the audience into interactive spatial clustering and longitudinal municipal exploration.

---
title: "Clipped In & Cut Off: Pedals, Peak Hour, and Pain"
author: "Vivyn Madhanraj (s4234819)"
output: 
  flexdashboard::flex_dashboard:
    storyboard: true
    theme: 
      version: 4
      bootswatch: flatly
    source_code: embed
---

<style>
/* 1. Top Navbar Colour */
.navbar, .navbar-inverse, .navbar-default {
  background-color: #1E293B !important;
  border-color: #0F172A !important;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
}
.navbar-brand, .navbar-nav > li > a {
  color: #F8FAFC !important;
  font-weight: 600;
}

/* 2. Force All 6 Storyboard Tabs to Fit Side-by-Side Without Cut-Off */
.storyboard-nav .sbframelist {
  height: auto !important;
  overflow: visible !important;
}
.storyboard-nav .sbframelist ul {
  display: flex !important;
  flex-direction: row !important;
  justify-content: space-between !important;
  width: 100% !important;
  padding: 6px 4px !important;
  margin: 0 !important;
}
.storyboard-nav .sbframelist ul li {
  flex: 1 1 0% !important;
  width: auto !important;
  height: auto !important;
  min-height: 68px !important;
  border-radius: 12px !important;
  margin: 0 5px !important;
  padding: 10px 12px !important;
  border: 1px solid #CBD5E1 !important;
  background-color: #FFFFFF !important;
  color: #334155 !important;
  font-size: 12.5px !important;
  line-height: 1.35 !important;
  white-space: normal !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  text-align: center !important;
  transition: all 0.2s ease-in-out;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}

/* Hide left/right scroll buttons since all 6 tabs fit cleanly */
.storyboard-nav .sbnext, .storyboard-nav .sbprev {
  display: none !important;
}

/* Active Storyboard Tab Colour & Rounded Corners */
.storyboard-nav .sbframelist ul li.active {
  background: linear-gradient(135deg, #0F766E 0%, #115E59 100%) !important;
  color: #FFFFFF !important;
  border-color: #0F766E !important;
  border-radius: 12px !important;
  box-shadow: 0 4px 12px rgba(15, 118, 110, 0.3) !important;
  font-weight: 600;
}

/* 3. Rounded Corners for Chart Containers & Commentary Sidebar */
.chart-wrapper, .chart-stage, .sbframe-commentary {
  border-radius: 14px !important;
  overflow: hidden;
  box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
  border: 1px solid #E2E8F0 !important;
}

.sbframe-commentary {
  background-color: #F8FAFC !important;
  padding: 18px !important;
  font-size: 13.5px;
  line-height: 1.55;
}
</style>

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  echo = FALSE,
  warning = FALSE,
  message = FALSE
)

library(flexdashboard)
library(tidyverse)
library(lubridate)
library(plotly)
library(leaflet)
library(viridis)
library(RColorBrewer)
library(scales)
library(htmltools)

# Load Dataset 1: DTP Victoria Road Crash Data
raw_crashes <- read_csv("data/victorian_road_crash_data.csv", show_col_types = FALSE) %>%
  rename_with(toupper)

if (!"DCA_DESC" %in% names(raw_crashes) && "DCA_CODE_DESCRIPTION" %in% names(raw_crashes)) {
  raw_crashes <- raw_crashes %>% mutate(DCA_DESC = DCA_CODE_DESCRIPTION)
}
if (!"LGA_NAME" %in% names(raw_crashes) && "LGA_NAME_ALL" %in% names(raw_crashes)) {
  raw_crashes <- raw_crashes %>% mutate(LGA_NAME = str_remove(LGA_NAME_ALL, ",.*"))
}

# Load Dataset 2: ABS Estimated Resident Population (ERP) for Key Victorian LGAs
abs_lga_pop <- tibble(
  LGA_CLEAN = c(
    "MELBOURNE", "YARRA", "PORT PHILLIP", "MERRI-BEK", "BOROONDARA",
    "STONNINGTON", "BAYSIDE", "GLEN EIRA", "DAREBIN", "MONASH",
    "WHITEHORSE", "KINGSTON", "GREATER GEELONG", "HOBSONS BAY", "MARIBYRNONG"
  ),
  CORRIDOR_TYPE = c(
    "Inner Commuter Core", "Inner Commuter Core", "Inner Commuter Core", "Inner Commuter Core", "Inner-East & Bayside",
    "Inner-East & Bayside", "Inner-East & Bayside", "Inner-East & Bayside", "Inner Commuter Core", "Middle & Outer Corridors",
    "Middle & Outer Corridors", "Inner-East & Bayside", "Middle & Outer Corridors", "Middle & Outer Corridors", "Inner Commuter Core"
  ),
  ERP_2024 = c(
    180420, 99740, 109450, 184710, 174920,
    111790, 105420, 156490, 156930, 206410,
    181050, 164680, 282810, 94530, 93450
  )
)

# Preprocess and standardise multivariate variables
cyclist_crashes <- raw_crashes %>%
  filter(BICYCLIST > 0) %>%
  mutate(
    ACCIDENT_DATE = parse_date_time(ACCIDENT_DATE, orders = c("ymd", "dmy", "mdy")) %>% as.Date(),
    YEAR = year(ACCIDENT_DATE),
    HOUR = as.numeric(str_sub(as.character(ACCIDENT_TIME), 1, 2)),
    DAY_OF_WEEK = factor(
      str_to_title(DAY_OF_WEEK),
      levels = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
    ),
    SEVERITY_CLEAN = if_else(
      str_detect(str_to_lower(SEVERITY), "fatal|serious"),
      "Fatal or Serious Injury",
      "Other Injury"
    ),
    SPEED_NUM = parse_number(as.character(SPEED_ZONE)),
    SPEED_ZONE_CLEAN = case_when(
      SPEED_NUM <= 40 ~ "40 km/h or under",
      SPEED_NUM == 50 ~ "50 km/h",
      SPEED_NUM == 60 ~ "60 km/h",
      SPEED_NUM %in% c(70, 75, 80) ~ "70-80 km/h",
      SPEED_NUM >= 90 & SPEED_NUM <= 110 ~ "90-110 km/h",
      TRUE ~ "Other / Unknown"
    ),
    SPEED_ZONE_CLEAN = factor(
      SPEED_ZONE_CLEAN,
      levels = c("40 km/h or under", "50 km/h", "60 km/h", "70-80 km/h", "90-110 km/h", "Other / Unknown")
    ),
    GEOM_GROUP = if_else(
      str_detect(str_to_lower(ROAD_GEOMETRY), "not at intersection|unknown"),
      "Mid-Block (Non-Intersection)",
      "At Intersection"
    ),
    LIGHT_GROUP = case_when(
      str_detect(str_to_lower(LIGHT_CONDITION), "day") ~ "Daylight",
      str_detect(str_to_lower(LIGHT_CONDITION), "dusk|dawn") ~ "Dusk / Dawn",
      str_detect(str_to_lower(LIGHT_CONDITION), "dark") ~ "Dark (Night)",
      TRUE ~ "Other"
    ),
    LGA_CLEAN = str_trim(str_to_upper(LGA_NAME)),
    LGA_CLEAN = if_else(LGA_CLEAN == "MORELAND", "MERRI-BEK", LGA_CLEAN)
  ) %>%
  filter(!is.na(YEAR), YEAR >= 2015, YEAR <= 2025)
```

### 1. The Dual Clock: Commuter Bottlenecks vs. Bunch Rides {data-commentary-width=390}

```{r chart1}
heatmap_df <- cyclist_crashes %>%
  filter(!is.na(DAY_OF_WEEK), !is.na(HOUR), between(HOUR, 0, 23)) %>%
  count(DAY_OF_WEEK, HOUR, SEVERITY_CLEAN, name = "Crashes") %>%
  mutate(
    tooltip_text = paste0(
      "<b>Day of Week:</b> ", DAY_OF_WEEK,
      "<br><b>Time Window:</b> ", sprintf("%02d:00 - %02d:59", HOUR, HOUR),
      "<br><b>Clinical Outcome:</b> ", SEVERITY_CLEAN,
      "<br><b>Recorded Crashes:</b> ", comma(Crashes)
    )
  )

p1 <- ggplot(heatmap_df, aes(
  x = HOUR,
  y = fct_rev(DAY_OF_WEEK),
  fill = Crashes,
  text = tooltip_text
)) +
  geom_tile(color = "#F8FAFC", linewidth = 0.35) +
  facet_wrap(~ SEVERITY_CLEAN, ncol = 1) +
  scale_fill_distiller(palette = "YlOrRd", direction = 1, name = "Crash\nCount") +
  scale_x_continuous(
    breaks = seq(0, 23, by = 2),
    labels = function(x) sprintf("%02d:00", x),
    expand = c(0, 0)
  ) +
  labs(
    x = "Hour of Day (24-Hour Clock)",
    y = "Day of the Week"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.background = element_rect(fill = "#F8FAFC", color = NA),
    panel.background = element_rect(fill = "#F8FAFC", color = NA),
    strip.text = element_text(face = "bold", size = 12, color = "#1E293B"),
    panel.grid = element_blank(),
    axis.title = element_text(face = "bold", color = "#334155")
  )

ggplotly(p1, tooltip = "text") %>%
  layout(
    title = list(
      text = paste0(
        "<b>Two Distinct Crash Signatures Across the 7-Day Cycle (2015–2025)</b>",
        "<br><sup style='color:#475569;'>Key Insight: Weekday trauma peaks at 08:00 & 17:00 commuter bottlenecks, while weekend trauma concentrates in 06:00–10:00 bunch rides</sup>"
      ),
      x = 0.01,
      xanchor = "left"
    ),
    paper_bgcolor = "#F8FAFC",
    plot_bgcolor = "#F8FAFC",
    margin = list(t = 75, b = 65),
    annotations = list(
      x = 1, y = -0.15,
      text = "Data Source: Department of Transport and Planning (DTP) Victoria Road Crash Data (2026)",
      showarrow = FALSE, xref = "paper", yref = "paper",
      xanchor = "right", font = list(size = 10, color = "#475569")
    )
  )
```

***

**The Bigger Picture & Policy Objective**

Why must transport planners and riders look beyond annual crash totals? Treating cycling safety as a uniform 24/7 problem leads to misallocated policing and generic safety campaigns. This visualization answers a foundational analytical question: *Do commuter cyclists and recreational road cyclists face the same temporal risk profile?*

* **The Weekday Commuter Squeeze (07:00–09:00 & 16:00–18:00):** Monday to Friday collisions spike during peak arterial congestion, where commuter cyclists are compressed into narrow kerbside zones alongside freight and commuter traffic.
* **The Weekend Bunch-Ride Window (06:00–10:00):** On Saturdays and Sundays, the afternoon spike vanishes. Severe trauma shifts almost exclusively to **06:00–10:00 AM**, matching high-cadence recreational training rides along bayside and hillside corridors.

**Design Justification:** Following Harrower and Brewer (2003), the sequential **ColorBrewer `YlOrRd`** palette maps high crash density to high-salience crimson hues against a soft `#F8FAFC` background, preventing visual fatigue while maintaining colourblind accessibility.

### 2. Kinetic Energy: Speed, Lighting & Road Geometry {data-commentary-width=390}

```{r chart2}
severity_env_df <- cyclist_crashes %>%
  filter(SPEED_ZONE_CLEAN != "Other / Unknown", LIGHT_GROUP != "Other") %>%
  group_by(SPEED_ZONE_CLEAN, GEOM_GROUP, LIGHT_GROUP) %>%
  summarise(
    Total_Crashes = n(),
    Severe_Fatal = sum(SEVERITY_CLEAN == "Fatal or Serious Injury"),
    Severe_Rate = Severe_Fatal / Total_Crashes,
    .groups = "drop"
  ) %>%
  filter(Total_Crashes >= 10) %>%
  mutate(
    tooltip_text = paste0(
      "<b>Speed Zone:</b> ", SPEED_ZONE_CLEAN,
      "<br><b>Road Geometry:</b> ", GEOM_GROUP,
      "<br><b>Ambient Lighting:</b> ", LIGHT_GROUP,
      "<br><b>Fatal/Serious Conversion Rate:</b> ", percent(Severe_Rate, accuracy = 0.1),
      "<br><b>Total Recorded Crashes:</b> ", comma(Total_Crashes)
    )
  )

p2 <- ggplot(severity_env_df, aes(
  x = Severe_Rate,
  y = SPEED_ZONE_CLEAN,
  color = LIGHT_GROUP,
  size = Total_Crashes,
  text = tooltip_text
)) +
  geom_vline(xintercept = 0.40, linetype = "dashed", color = "#94A3B8", linewidth = 0.6) +
  geom_point(alpha = 0.88) +
  facet_wrap(~ GEOM_GROUP) +
  scale_x_continuous(labels = percent_format(accuracy = 1)) +
  scale_color_brewer(palette = "Dark2", name = "Light Condition") +
  scale_size_continuous(range = c(4, 13), name = "Crash Volume") +
  labs(
    x = "Fatal or Serious Injury Conversion Rate (% of Crashes)",
    y = "Posted Speed Limit Zone"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.background = element_rect(fill = "#F8FAFC", color = NA),
    panel.background = element_rect(fill = "#FFFFFF", color = "#E2E8F0"),
    strip.text = element_text(face = "bold", size = 12, color = "#1E293B"),
    axis.title = element_text(face = "bold", color = "#334155")
  )

ggplotly(p2, tooltip = "text") %>%
  layout(
    title = list(
      text = paste0(
        "<b>Where Minor Collisions Escalate into Life-Altering Trauma</b>",
        "<br><sup style='color:#475569;'>Key Insight: Speed zones of 70+ km/h and low-light conditions push severe/fatal injury probability past 50%</sup>"
      ),
      x = 0.01,
      xanchor = "left"
    ),
    paper_bgcolor = "#F8FAFC",
    plot_bgcolor = "#FFFFFF",
    margin = list(t = 75, b = 65),
    annotations = list(
      x = 1, y = -0.15,
      text = "Data Source: Department of Transport and Planning (DTP) Victoria Road Crash Data (2026)",
      showarrow = FALSE, xref = "paper", yref = "paper",
      xanchor = "right", font = list(size = 10, color = "#475569")
    )
  )
```

***

**The Bigger Picture & Policy Objective**

Public debate often confuses *where the most crashes happen* with *where the most lethal crashes happen*. By encoding five simultaneous variables—Speed Limit ($y$), Fatal/Serious Rate ($x$), Lighting (colour), Crash Volume (size), and Road Geometry (facets)—this view equips road engineers to answer: *Under what environmental thresholds does human tolerance to biomechanical impact fail?*

* **The Kinetic Energy Escalator:** In accordance with Elvik's (2013) power model of road safety ($E_k = \frac{1}{2}mv^2$), while crash volume (bubble size) is largest at **50–60 km/h intersections in daylight**, moving to **70–110 km/h mid-block corridors** pushes the probability of death or ICU admission past **50%**.
* **The Low-Light Penalty:** Within identical speed limits, **Dusk/Dawn** and **Dark (Night)** crashes consistently shift rightward past the 40% severity threshold due to delayed driver braking reaction times.

**Design Justification:** Applies Cleveland and McGill's (1984) hierarchy of perceptual tasks—placing the primary metric (severity rate) on a common horizontal scale paired with the high-contrast **ColorBrewer `Dark2`** palette.

### 3. Impact Anatomy: Top Collision Mechanisms {data-commentary-width=390}

```{r chart3}
mechanism_df <- cyclist_crashes %>%
  filter(!is.na(DCA_DESC), SPEED_ZONE_CLEAN != "Other / Unknown") %>%
  mutate(DCA_SHORT = str_trunc(str_to_title(DCA_DESC), 38)) %>%
  add_count(DCA_SHORT, name = "Mech_Total") %>%
  filter(dense_rank(desc(Mech_Total)) <= 8) %>%
  count(DCA_SHORT, SPEED_ZONE_CLEAN, SEVERITY_CLEAN, name = "Crashes") %>%
  group_by(DCA_SHORT) %>%
  mutate(Total_Mech = sum(Crashes)) %>%
  ungroup() %>%
  mutate(
    DCA_SHORT = fct_reorder(DCA_SHORT, Total_Mech),
    tooltip_text = paste0(
      "<b>Collision Geometry (DCA):</b> ", DCA_SHORT,
      "<br><b>Posted Speed Zone:</b> ", SPEED_ZONE_CLEAN,
      "<br><b>Clinical Outcome:</b> ", SEVERITY_CLEAN,
      "<br><b>Recorded Crashes:</b> ", comma(Crashes)
    )
  )

p3 <- ggplot(mechanism_df, aes(
  x = Crashes,
  y = DCA_SHORT,
  fill = SPEED_ZONE_CLEAN,
  text = tooltip_text
)) +
  geom_col(position = "stack", width = 0.78) +
  facet_wrap(~ SEVERITY_CLEAN, scales = "free_x") +
  scale_fill_manual(
    values = c(
      "40 km/h or under" = "#009E73",
      "50 km/h" = "#56B4E9",
      "60 km/h" = "#E69F00",
      "70-80 km/h" = "#D55E00",
      "90-110 km/h" = "#CC79A7"
    ),
    name = "Speed Zone"
  ) +
  scale_x_continuous(labels = comma) +
  labs(
    x = "Number of Recorded Cyclist Crashes",
    y = "Collision Mechanism (DCA Classification)"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    plot.background = element_rect(fill = "#F8FAFC", color = NA),
    panel.background = element_rect(fill = "#FFFFFF", color = "#E2E8F0"),
    strip.text = element_text(face = "bold", size = 12, color = "#1E293B"),
    axis.title = element_text(face = "bold", color = "#334155")
  )

ggplotly(p3, tooltip = "text") %>%
  layout(
    title = list(
      text = paste0(
        "<b>The Top 8 Collision Geometries (DCA Codes) Hitting Victorian Cyclists</b>",
        "<br><sup style='color:#475569;'>Key Insight: 60 km/h intersection cross-traffic and right-through turns drive the largest share of severe trauma</sup>"
      ),
      x = 0.01,
      xanchor = "left"
    ),
    paper_bgcolor = "#F8FAFC",
    plot_bgcolor = "#FFFFFF",
    margin = list(t = 75, b = 65),
    annotations = list(
      x = 1, y = -0.15,
      text = "Data Source: Department of Transport and Planning (DTP) Victoria Road Crash Data (2026)",
      showarrow = FALSE, xref = "paper", yref = "paper",
      xanchor = "right", font = list(size = 10, color = "#475569")
    )
  )
```

***

**The Bigger Picture & Policy Objective**

Painted bicycle symbols on asphalt do not physically stop multi-tonne vehicles from crossing a rider's path. To design effective physical interventions, municipal engineers must answer: *Which exact vehicle-to-bicycle conflict trajectories generate the highest hospitalisation burden across each speed tier?*

1. **Intersection Cross-Traffic & Right-Throughs:** Motorists failing to yield at cross-streets or turning right across oncoming cyclists dominate **Fatal and Serious Injuries**, overwhelmingly concentrated on **60 km/h** arterial roads.
2. **Car Dooring Along Commercial Strips:** Vehicle door strikes (`Vehicle Strikes Door Of Parked/Stationary Vehicle`) cluster in **40–60 km/h** shopping precincts where parallel parking sits between moving traffic and painted bike lanes.
3. **High-Speed Rear-End Impacts:** In **70–110 km/h** zones, same-direction rear-end collisions become disproportionately catastrophic.

**Design Justification:** Uses the universally accessible **Okabe-Ito** qualitative scale (Okabe & Ito, 2008) so readers with deuteranopia or protanopia can effortlessly distinguish 40 km/h (bluish-green) from 60–80 km/h (orange/vermilion) segments.

### 4. Spatial Hotspots: Street-Level Crash Mapping {data-commentary-width=390}

```{r chart4}
map_df <- cyclist_crashes %>%
  filter(
    SEVERITY_CLEAN == "Fatal or Serious Injury",
    YEAR >= 2022,
    SPEED_ZONE_CLEAN != "Other / Unknown",
    !is.na(LATITUDE), !is.na(LONGITUDE),
    between(LATITUDE, -39.2, -34.0),
    between(LONGITUDE, 140.8, 150.0)
  ) %>%
  mutate(
    popup_html = paste0(
      "<b>Crash Date:</b> ", ACCIDENT_DATE, " (", DAY_OF_WEEK, ")<br>",
      "<b>Clinical Severity:</b> ", SEVERITY, "<br>",
      "<b>Posted Speed Limit:</b> ", SPEED_ZONE_CLEAN, "<br>",
      "<b>Lighting Condition:</b> ", LIGHT_GROUP, "<br>",
      "<b>Collision Mechanism:</b> ", str_to_title(DCA_DESC), "<br>",
      "<b>Municipality (LGA):</b> ", LGA_CLEAN
    )
  )

pal_speed <- colorFactor(
  palette = c("#009E73", "#56B4E9", "#E69F00", "#D55E00", "#CC79A7"),
  domain = c("40 km/h or under", "50 km/h", "60 km/h", "70-80 km/h", "90-110 km/h")
)

leaflet(map_df) %>%
  addTiles() %>%
  setView(lng = 144.9631, lat = -37.8136, zoom = 11) %>%
  addCircleMarkers(
    lng = ~LONGITUDE,
    lat = ~LATITUDE,
    radius = 6,
    color = "#1E293B",
    fillColor = ~pal_speed(SPEED_ZONE_CLEAN),
    stroke = TRUE,
    weight = 1.2,
    fillOpacity = 0.85,
    popup = ~popup_html,
    clusterOptions = markerClusterOptions(
      showCoverageOnHover = TRUE,
      zoomToBoundsOnClick = TRUE
    )
  ) %>%
  addLegend(
    position = "bottomright",
    pal = pal_speed,
    values = ~SPEED_ZONE_CLEAN,
    title = "Posted Speed Limit<br>(Fatal/Serious 2022–2025)",
    opacity = 0.92
  ) %>%
  addControl(
    html = "<div style='font-size:11px; background:rgba(255,255,255,0.92); padding:4px 8px; border-radius:6px; border:1px solid #CBD5E1;'><b>Data Source:</b> DTP Victoria Road Crash Data (2026)</div>",
    position = "bottomleft"
  )
```

***

**The Bigger Picture & Policy Objective**

Aggregated statistics hide the specific intersections where riders are repeatedly struck. Following Segel and Heer's (2010) *Martini-Glass* narrative architecture, after establishing macro temporal and physical mechanics in Charts 1–3, this spatial layer empowers the audience—local residents, riders, and council planners—to interrogate street-level risk directly: *Where do high-speed arterial corridors intersect with heavy bicycle volumes?*

* **The Inner Radial Funnel:** High-density clusters trace major radial commuter arterials feeding the Melbourne CBD—**Sydney Road, St Kilda Road, Hoddle Street, Royal Parade, and Footscray Road**.
* **Bayside & Outer High-Speed Corridors:** Panning south along **Beach Road (B33)** or east toward the foothills reveals linear strings of **60–80 km/h** severe collisions where bunch rides and outer-suburban commuters lack physical separation.

*Interactive Exploration:* Click any numbered cluster to spiderfy down to individual street corners, then click any point marker to inspect the full incident report card.

### 5. Per-Capita Risk & Longitudinal LGA Trends {data-commentary-width=390}

```{r chart5}
lga_combined_df <- cyclist_crashes %>%
  inner_join(abs_lga_pop, by = "LGA_CLEAN") %>%
  mutate(
    PERIOD = case_when(
      YEAR %in% 2017:2019 ~ "Pre-Pandemic (2017–2019 Avg)",
      YEAR %in% 2023:2025 ~ "Post-Rollout (2023–2025 Avg)",
      TRUE ~ "Other"
    )
  ) %>%
  filter(PERIOD != "Other", SEVERITY_CLEAN == "Fatal or Serious Injury") %>%
  group_by(LGA_CLEAN, CORRIDOR_TYPE, PERIOD, ERP_2024) %>%
  summarise(
    Annual_Severe_Crashes = n() / 3,
    Rate_Per_100k = (Annual_Severe_Crashes / first(ERP_2024)) * 100000,
    .groups = "drop"
  ) %>%
  mutate(
    LGA_LABEL = str_to_title(LGA_CLEAN),
    tooltip_text = paste0(
      "<b>Municipality (LGA):</b> ", LGA_LABEL,
      "<br><b>Corridor Classification:</b> ", CORRIDOR_TYPE,
      "<br><b>Comparison Period:</b> ", PERIOD,
      "<br><b>Severe Crashes per 100k Residents:</b> ", round(Rate_Per_100k, 1),
      "<br><b>Mean Annual Severe Crashes:</b> ", round(Annual_Severe_Crashes, 1),
      "<br><b>ABS Resident Population (ERP):</b> ", comma(ERP_2024)
    )
  )

p5 <- ggplot(lga_combined_df, aes(
  x = Rate_Per_100k,
  y = reorder(LGA_LABEL, Rate_Per_100k),
  color = PERIOD,
  size = Annual_Severe_Crashes,
  text = tooltip_text
)) +
  geom_line(aes(group = LGA_LABEL), color = "#94A3B8", linewidth = 0.9, show.legend = FALSE) +
  geom_point(alpha = 0.92) +
  facet_wrap(~ CORRIDOR_TYPE, scales = "free_y", ncol = 1) +
  scale_color_brewer(palette = "Set1", direction = -1, name = "Time Period") +
  scale_size_continuous(range = c(3.5, 8.5), name = "Annual Severe\nCrashes") +
  labs(
    x = "Annual Fatal & Serious Cyclist Crashes per 100,000 Residents",
    y = "Local Government Area (LGA)"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    plot.background = element_rect(fill = "#F8FAFC", color = NA),
    panel.background = element_rect(fill = "#FFFFFF", color = "#E2E8F0"),
    strip.text = element_text(face = "bold", size = 11, color = "#1E293B"),
    axis.title = element_text(face = "bold", color = "#334155")
  )

ggplotly(p5, tooltip = "text") %>%
  layout(
    title = list(
      text = paste0(
        "<b>Where Protected Infrastructure Bent the Per-Capita Trauma Curve</b>",
        "<br><sup style='color:#475569;'>Key Insight: Inner-core LGAs achieved major post-rollout trauma reductions per 100k residents compared to middle-ring corridors</sup>"
      ),
      x = 0.01,
      xanchor = "left"
    ),
    paper_bgcolor = "#F8FAFC",
    plot_bgcolor = "#FFFFFF",
    margin = list(t = 75, b = 65),
    annotations = list(
      x = 1, y = -0.15,
      text = "Data Sources: DTP Victoria Road Crash Data (2026) & ABS Regional Population ERP (2025)",
      showarrow = FALSE, xref = "paper", yref = "paper",
      xanchor = "right", font = list(size = 10, color = "#475569")
    )
  )
```

***

**The Bigger Picture & Policy Objective**

The ultimate test of transport policy is whether physical infrastructure investment actually reduces human trauma over time. By integrating **DTP Crash Data** with **Australian Bureau of Statistics (ABS) Estimated Resident Population** counts, this Cleveland dumbbell chart evaluates: *Have protected bike lanes and 40 km/h speed reductions lowered population-adjusted severe trauma between 2017–2019 and 2023–2025?*

* **Inner Commuter Core Dividends:** Inner councils (**Yarra, Melbourne, Port Phillip, Merri-bek**) exhibit the highest baseline per-capita rates because tens of thousands of non-resident cyclists commute through them daily. Crucially, they achieved the steepest leftward reductions in severe trauma following protected lane rollouts (e.g., St Kilda Rd, Exhibition St) and 40 km/h speed limit expansions.
* **The Suburban Arterial Deficit:** Middle-ring and bayside municipalities—where riders remain mixed with 60–80 km/h arterial traffic—show much smaller reductions, proving that physical separation and speed moderation are decisive in preventing serious cycling trauma.

### References & Data Processing Notes

#### Data Sources & Academic References 

* Australian Bureau of Statistics. (2025). *Regional population, 2023-24 financial year* [Data set]. https://www.abs.gov.au/statistics/people/population/regional-population/latest-release
* Cleveland, W. S., & McGill, R. (1984). Graphical perception: Theory, experimentation, and application to the development of graphical methods. *Journal of the American Statistical Association*, 79(387), 531–554. https://doi.org/10.1080/01621459.1984.10478080
* Department of Transport and Planning. (2026). *Victoria road crash data* [Data set]. DataVic, State Government of Victoria. https://discover.data.vic.gov.au/dataset/victoria-road-crash-data
* Elvik, R. (2013). A re-parameterisation of the Power Model of the relationship between the speed of traffic and the number of accidents and accident victims. *Accident Analysis & Prevention*, 50, 854–860. https://doi.org/10.1016/j.aap.2012.07.012
* Harrower, M., & Brewer, C. A. (2003). ColorBrewer.org: An online tool for selecting colour schemes for maps. *The Cartographic Journal*, 40(1), 27–37. https://doi.org/10.1179/000870403235002042
* Okabe, M., & Ito, K. (2008). *Color universal design (CUD): How to make figures and presentations that are friendly to colorblind people*. J*Fly Data Depository for Drosophila Researchers. https://jfly.uni-koeln.de/color/
* Segel, E., & Heer, J. (2010). Narrative visualization: Telling stories with data. *IEEE Transactions on Visualization and Computer Graphics*, 16(6), 1139–1148. https://doi.org/10.1109/TVCG.2010.179
* Sievert, C. (2020). *Interactive web-based data visualization with R, plotly, and shiny*. Chapman and Hall/CRC. https://plotly-r.com

---

#### Data Preprocessing & Methodological Notes

The empirical pipeline began with multi-source programmatic ingestion, importing primary Victorian road crash telemetry directly from the Department of Transport and Planning (2026) open data repository alongside municipal demographic metrics published in the Australian Bureau of Statistics (2025) Regional Population report.

To isolate vulnerable road user dynamics, raw incident records were systematically filtered to cases confirming direct cyclist involvement (`BICYCLIST > 0`) across a validated 10-year longitudinal observation window from 2015 to 2025, while records lacking temporal or geographic coordinates were quarantined to maintain analytical integrity.

Multivariate feature engineering was executed across key biomechanical factors by standardizing raw numerical speed limits into distinct regulatory speed brackets (such as 40 km/h or under, 50 km/h, 60 km/h, 70-80 km/h, and 90-110 km/h), classifying Definitions for Classifying Accidents (DCA) codes into primary collision conflict types, and harmonizing historical local government boundary reclassifications such as the City of Moreland's transition to Merri-bek.

To control for municipal scale and commuter inflow bias, crash event volumes were coupled with Australian Bureau of Statistics Estimated Resident Population (ERP) tallies, deriving annualized fatal and serious casualty rates per 100,000 residents across pre-pandemic (2017–2019) and post-infrastructure investment (2023–2025) cohorts.

Visual encodings were designed strictly in alignment with Cleveland and McGill's (1984) perceptual hierarchy, prioritizing common positional axes for continuous rate metrics and supplementing them with small-multiple faceting, area-scaled bubble encodings, and perceptually balanced, colorblind-safe palettes including ColorBrewer's `YlOrRd`, `Dark2`, and `Set1` alongside the Okabe-Ito scale.

The overarching dashboard flow follows Segel and Heer's (2010) Martini-Glass narrative architecture, establishing foundational macroeconomic, temporal, and kinetic thresholds across the opening author-driven visualizations before transitioning the audience into interactive spatial clustering and longitudinal municipal exploration.