# ---- Loading the dataset ---------------------------------------------------------------
data_file <- "abs-export-data-collation-june-2026.xlsx"
# All columns are read as text and then converted to the appropriate type.
raw <- read_excel(data_file, sheet = "Export Records", col_types = "text")
needed <- c("Financial Year", "Tonnes", "Value ($)", "Destination",
"Material group", "Data scope", "State of origin",
"AHECC code", "AHECC description")
stopifnot(all(needed %in% names(raw)))
# ---- Cleaning the dataset --------------------------------------------------------------
waste <- raw %>%
transmute(
fy = `Financial Year`,
year_end = as.integer(substr(fy, 1, 4)) + 1L, # 2017-18 -> 2018
tonnes = as.numeric(Tonnes),
value = as.numeric(`Value ($)`), # declared value, nominal AUD
dest = recode(Destination,
"Korea, Republic of" = "South Korea",
"USA" = "United States"),
material = `Material group`,
scope = `Data scope`,
ahecc = as.character(`AHECC code`), # customs product code
ahecc_desc = `AHECC description`
)
# Quality checks that must pass before anything is plotted
stopifnot(!anyNA(waste$tonnes), !anyNA(waste$value), !anyNA(waste$dest))
# ---- Scope --------------------------------------------------------------
core <- waste %>% filter(scope == "Core waste + ash")
first_fy <- min(core$fy) # "2017-18"
last_fy <- max(core$fy) # "2025-26"
pal <- c("Metals" = "#4D4D4D",
"Paper & cardboard" = "#56B4E9",
"Plastics" = "#0072B2",
"Glass" = "#009E73",
"Textiles" = "#E69F00",
"Tyres" = "#D55E00",
"Hazardous (excl. tyres)" = "#CC79A7",
"Other core waste" = "#999999")
source_note <- "Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values."
# Left-aligned title and white background, used by every ggplotly() chart
left_title <- function(txt) list(text = paste0("<b>", txt, "</b>"), x = 0.01, xref = "container", xanchor = "left")
# Small formatting helpers used in the text and tooltips
fmt_t <- function(x) paste0(comma(round(x)), " t")
fmt_m <- function(x) paste0("$", comma(round(x)), "m")
fmt_pc <- function(x) paste0(ifelse(x >= 0, "+", ""), round(100 * x), "%")
fmt_pp <- function(x) paste0(round(100 * abs(x)), "%")
# All numbers in the story are calculated here, so text and charts always agree.
annual <- core %>%
group_by(fy, year_end) %>%
summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop") %>%
arrange(year_end) %>%
mutate(tonnes_idx = 100 * tonnes / first(tonnes),
value_idx = 100 * value / first(value))
by_mat_fy <- core %>%
group_by(material, fy) %>%
summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop") %>%
mutate(vpt = value * 1e6 / tonnes)
mat_chg <- by_mat_fy %>%
filter(fy %in% c(first_fy, last_fy)) %>%
mutate(period = ifelse(fy == first_fy, "first", "last")) %>%
select(-fy) %>%
pivot_wider(names_from = period, values_from = c(tonnes, value, vpt)) %>%
mutate(d_tonnes = tonnes_last - tonnes_first,
d_value = value_last - value_first)
get <- function(m, col) mat_chg[[col]][mat_chg$material == m]
tonnes_pc <- last(annual$tonnes) / first(annual$tonnes) - 1
value_pc <- last(annual$value) / first(annual$value) - 1
# Which product line in customs has the highest declared value in the latest year?
top_line <- core %>% filter(fy == last_fy) %>%
group_by(ahecc, ahecc_desc) %>%
summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop") %>%
slice_max(value, n = 1)
line_txt <- sub(" \\(excl\\..*$", "", top_line$ahecc_desc) # short description
line_txt <- trimws(line_txt)
line_txt <- paste0(tolower(substring(line_txt, 1, 1)), substring(line_txt, 2))
line_by_fy <- core %>% filter(ahecc == top_line$ahecc) %>%
group_by(fy) %>% summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop")
line_first <- line_by_fy$value[line_by_fy$fy == first_fy]
line_chg <- top_line$value - line_first
annual_ex <- core %>% filter(ahecc != top_line$ahecc) %>%
group_by(fy) %>% summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop") %>% arrange(fy)
value_pc_ex <- last(annual_ex$value) / first(annual_ex$value) - 1
tonnes_pc_ex <- last(annual_ex$tonnes) / first(annual_ex$tonnes) - 1
Our bins are emptied every week by trucks and they take off. Most people don’t question the next step in the journey of the material or if anyone profits from it.
The following is a tale of the export side of that trip. It uses Australia’s official records of waste and recovered materials leaving the country, from 2017-18 to 2025-26. Almost all of it is recovered material (RM) that is recycled or used for energy recovery (Department of Climate Change, Energy, the Environment and Water [DCCEEW], 2026). Your bin doesn’t have places for the data. It can reveal what the entire nation exports, what it imports and who imports it.
Both lines begin at 100 in 2017-18, allowing you to make direct comparisons in growth. Hover over a point for the actual figures.
# Reshape the two indexed measures so they can share one line chart
annual_long <- annual %>%
pivot_longer(cols = c(tonnes_idx, value_idx), names_to = "measure", values_to = "index") %>%
mutate(measure = recode(measure, tonnes_idx = "Tonnes exported", value_idx = "Declared value"),
actual = ifelse(measure == "Tonnes exported", tonnes, value),
tip = ifelse(measure == "Tonnes exported",
paste0(fy, "<br>Tonnes exported: ", comma(round(actual)), " t"),
paste0(fy, "<br>Declared value: $", comma(round(actual)), " million")),
tip = paste0(tip, "<br>Index: ", round(index)))
g1 <- ggplot(annual_long, aes(x = year_end, y = index, colour = measure, group = measure, text = tip)) +
geom_line(linewidth = 1.1) +
geom_point(size = 2.5) +
scale_colour_manual(values = c("Declared value" = "#1A1A1A", "Tonnes exported" = "#8C8C8C")) +
scale_x_continuous(breaks = seq(2018, 2026, 2), labels = annual$fy[annual$year_end %in% seq(2018, 2026, 2)]) +
scale_y_continuous(limits = c(80, 250), breaks = seq(100, 250, 50)) +
labs(x = "Financial year", y = "Index (2017-18 = 100)", colour = "") +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank())
ggplotly(g1, tooltip = "text") %>%
layout(title = left_title(paste0("Tonnes up ", fmt_pp(tonnes_pc), ", declared value up ", fmt_pp(value_pc))),
legend = list(orientation = "h", x = 0, y = -0.25),
margin = list(t = 70, b = 110, l = 70),
paper_bgcolor = "white", plot_bgcolor = "white") %>%
plotly::config(displaylogo = FALSE)
Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values.
Values are not profit, but nominal dollars (not inflation adjusted) as reported in the customs declaration. They never take into account collection, sorting and shipping expenses.
Each bar is the change between 2017-18 and 2025-26. Each bar is the change between 2017-18 and 2025-26. The bars to the right of the zero indicate rises and the bars to the left of the zero indicate falls. Panel A (top) shows the change in weight (tonnes). This represents the amount of money that changed (declared value in millions of dollars) in Panel B (bottom). Both panels are the same colour for each material.
# Put tonnes and dollars in one long table so they can be shown as two panels
mat_long <- mat_chg %>%
select(material, d_tonnes, d_value) %>%
pivot_longer(cols = c(d_tonnes, d_value), names_to = "measure", values_to = "change") %>%
mutate(label = ifelse(measure == "d_tonnes",
paste0(ifelse(change >= 0, "+", "-"), comma(round(abs(change))), " t"),
paste0(ifelse(change >= 0, "+$", "-$"), comma(round(abs(change))), "m")),
measure = recode(measure, d_tonnes = "A. WEIGHT: change in tonnes exported",
d_value = "B. MONEY: change in declared value (A$ million)"),
measure = factor(measure, levels = c("A. WEIGHT: change in tonnes exported",
"B. MONEY: change in declared value (A$ million)")),
material = factor(material, levels = mat_chg$material[order(mat_chg$d_tonnes)]),
tip = paste0("<b>", material, "</b><br>", measure, "<br>", label))
g2 <- ggplot(mat_long, aes(x = change, y = material, fill = material, text = tip)) +
geom_col() +
geom_vline(xintercept = 0, colour = "#333333", linewidth = 0.4) +
facet_wrap(~measure, ncol = 1, scales = "free_x") +
scale_fill_manual(values = pal) +
scale_x_continuous(labels = label_comma()) +
labs(x = "", y = "") +
theme_minimal(base_size = 12) +
theme(legend.position = "none", panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold", size = 13, hjust = 0),
panel.spacing.y = unit(4, "lines"))
ggplotly(g2, tooltip = "text") %>%
layout(title = left_title("Tyres surged, plastics fell, and metals drove the value growth"),
margin = list(t = 80, b = 60, l = 150),
paper_bgcolor = "white", plot_bgcolor = "white") %>%
plotly::config(displaylogo = FALSE)
Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values.
Each panel is one destination, and every panel uses the same vertical scale so the heights can be compared. Hover over a point for the tonnes.
# Top six destinations by plastic tonnes over the whole period; everything else is grouped.
top_pl <- core %>% filter(material == "Plastics") %>%
group_by(dest) %>% summarise(t = sum(tonnes)) %>% arrange(desc(t)) %>% slice_head(n = 6) %>% pull(dest)
pl <- core %>% filter(material == "Plastics") %>%
mutate(panel = ifelse(dest %in% top_pl, dest, "All other destinations")) %>%
group_by(panel, fy, year_end) %>% summarise(tonnes = sum(tonnes), .groups = "drop") %>%
complete(panel, nesting(fy, year_end), fill = list(tonnes = 0)) %>%
mutate(panel = factor(panel, levels = c(top_pl, "All other destinations")),
tip = paste0("<b>", panel, "</b><br>", fy, "<br>", fmt_t(tonnes)))
pl_tot <- core %>% filter(material == "Plastics") %>% group_by(fy) %>% summarise(t = sum(tonnes))
pl_peak <- pl_tot %>% slice_max(t, n = 1)
pl_last <- pl_tot %>% filter(fy == last_fy)
g3 <- ggplot(pl, aes(x = year_end, y = tonnes, group = panel)) +
geom_line(colour = pal["Plastics"], linewidth = 1.1) +
geom_point(aes(text = tip), colour = pal["Plastics"], size = 2) +
facet_wrap(~panel, ncol = 4) +
scale_x_continuous(breaks = c(2018, 2022, 2026)) +
scale_y_continuous(labels = label_comma(), limits = c(0, NA)) +
labs(x = "Financial year ending", y = "Tonnes exported") +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "#E8E8E8"),
strip.text = element_text(face = "bold", hjust = 0),
panel.spacing = unit(1.1, "lines"))
p3 <- ggplotly(g3, tooltip = "text") %>%
layout(
title = left_title(paste0("Plastic exports fell ", round(100 * (1 - pl_last$t / pl_peak$t)),
"% from their ", pl_peak$fy, " peak")),
margin = list(t = 80, b = 70, l = 80),
paper_bgcolor = "white", plot_bgcolor = "white"
)
p3
Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values.
Press play or drag the slider. Further right means more tonnes. Higher up means more declared dollars per tonne. Bigger bubbles mean more total declared value. Both axes are logarithmic, so each gridline is ten times the last.
# Same approach as the Gapminder animation in the course notes:
# x, y, colour and size, with the financial year as the animation frame
bub <- by_mat_fy %>%
arrange(fy) %>%
mutate(tip = paste0("<b>", material, "</b> (", fy, ")",
"<br>Tonnes: ", comma(round(tonnes)),
"<br>Declared value: ", fmt_m(value),
"<br>Value per tonne: $", comma(round(vpt))))
plot_ly(bub, x = ~tonnes, y = ~vpt, size = ~value, color = ~material, colors = pal,
frame = ~fy, text = ~tip, hoverinfo = "text",
type = "scatter", mode = "markers", sizes = c(15, 70)) %>%
layout(title = left_title("The heaviest exports are not the most valuable per tonne"),
xaxis = list(type = "log", title = "Tonnes exported (log scale)",
tickvals = c(100, 1000, 10000, 100000, 1000000),
ticktext = c("100", "1,000", "10,000", "100,000", "1 million"),
range = c(2.2, 6.8)),
yaxis = list(type = "log", title = "Declared value per tonne (A$, log scale)",
tickvals = c(10, 100, 1000, 10000),
ticktext = c("$10", "$100", "$1,000", "$10,000"),
range = c(1.5, 4.7)),
legend = list(title = list(text = "Material")),
margin = list(t = 80, b = 160, l = 90),
paper_bgcolor = "white", plot_bgcolor = "white") %>%
animation_opts(frame = 1000, transition = 600) %>%
animation_slider(currentvalue = list(prefix = "Financial year: "), pad = list(t = 70)) %>%
plotly::config(displaylogo = FALSE)
Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values. Bubble area shows total declared value.
Same layout as the plastics chart: one panel per destination on a shared scale. Hover over a bar for tonnes and declared value.
top_ty <- core %>% filter(material == "Tyres", fy == last_fy) %>%
group_by(dest) %>% summarise(t = sum(tonnes)) %>% arrange(desc(t)) %>% slice_head(n = 5) %>% pull(dest)
ty <- core %>% filter(material == "Tyres") %>%
mutate(panel = ifelse(dest %in% top_ty, dest, "All other destinations")) %>%
group_by(panel, fy, year_end) %>%
summarise(tonnes = sum(tonnes), value = sum(value) / 1e6, .groups = "drop") %>%
complete(panel, nesting(fy, year_end), fill = list(tonnes = 0, value = 0)) %>%
mutate(panel = factor(panel, levels = c(top_ty, "All other destinations")),
tip = paste0("<b>", panel, "</b><br>", fy, "<br>", fmt_t(tonnes),
ifelse(tonnes > 0, paste0("<br>Declared value: ", fmt_m(value),
"<br>About $", comma(round(value * 1e6 / tonnes)), " a tonne"), "")))
ty_tot <- core %>% filter(material == "Tyres") %>% group_by(fy) %>%
summarise(t = sum(tonnes), v = sum(value) / 1e6)
ty_first <- ty_tot %>% filter(fy == first_fy)
ty_last <- ty_tot %>% filter(fy == last_fy)
ty_chg <- ty_last$t / ty_first$t - 1
ty_india <- ty %>% filter(panel == "India", fy == last_fy) %>% pull(tonnes)
g5 <- ggplot(ty, aes(x = year_end, y = tonnes)) +
geom_col(aes(text = tip), fill = pal["Tyres"], width = 0.75) +
facet_wrap(~panel, ncol = 3) +
scale_x_continuous(breaks = c(2018, 2022, 2026)) +
scale_y_continuous(labels = label_comma()) +
labs(x = "Financial year ending", y = "Tonnes exported") +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(colour = "#E8E8E8"),
strip.text = element_text(face = "bold", hjust = 0),
panel.spacing = unit(1.1, "lines"))
p5 <- ggplotly(g5, tooltip = "text") %>%
layout(
title = left_title(paste0("Tyre and rubber-scrap exports rose ", round(100 * ty_chg), "%, led by India")),
margin = list(t = 80, b = 70, l = 80),
paper_bgcolor = "white", plot_bgcolor = "white"
)
p5
Source: DCCEEW (2026), Waste Exports data collation (ABS customs data). Core waste + ash, nominal values. Tyres material group. The customs classification codes changed over the period; see Methodology.
The yellow bin is the visible tip of a much bigger export system.
So when the next truck arrives ask, where does it go and who is paid to take it? Councils, recyclers and governments can answer that. The export data only covers the outward trip.
scope_tbl <- waste %>%
mutate(scope = ifelse(is.na(scope), "No scope recorded", scope)) %>%
group_by(scope) %>%
summarise(records = n(), tonnes_total = sum(tonnes), .groups = "drop") %>%
mutate(tonnes_total = comma(round(tonnes_total)), records = comma(records))
knitr::kable(scope_tbl, col.names = c("Data scope in workbook", "Records", "Total tonnes (all years)"),
align = "lrr")
| Data scope in workbook | Records | Total tonnes (all years) |
|---|---|---|
| Core waste + ash | 72,618 | 37,529,045 |
| No scope recorded | 25 | 667 |
| Non-core waste | 3,142 | 1,611,057 |
• I used ChatGPT to offer feedback on spelling and grammar, to suggest references and online sources and to help with troubleshooting and understanding R code. I read through the comments and made changes as necessary and presented all final analysis, interpretation, decisions, code, and work submitted are my own.
• Country: I acknowledge the WurundjeriWoi-wurrung people of the Kulin Nation as the Traditional Custodians of the land on which RMIT University is located, and pay my respects to Elders past and present.
• University: I acknowledge RMIT University for providing the academic resources, learning environment and support required to complete this work.
• Datasets: I acknowledge the creators and maintainers of the publicly available datasets used in this analysis for making the data accessible for research and educational purposes.
Department of Climate Change, Energy, the Environment and Water. (2026, September). Waste exports data collation [Data set]. Commonwealth of Australia. https://www.dcceew.gov.au/environment/protection/waste/exports-data
Evergreen, S., & Emery, A. K. (2016). Updated data visualization checklist. https://stephanieevergreen.com/updated-data-visualization-checklist/
Okabe, M., & Ito, K. (2008). Color universal design (CUD): How to make figures and presentations that are friendly to colorblind people. https://jfly.uni-koeln.de/color/
R Core Team. (2025). R: A language and environment for statistical computing [Computer software]. R Foundation for Statistical Computing. https://www.R-project.org/
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
Wickham, H. (2016). ggplot2: Elegant graphics for data analysis. Springer. https://ggplot2.tidyverse.org/
Wickham, H., & Bryan, J. (2023). readxl: Read Excel files (R package version 1.4.3) [Computer software]. https://readxl.tidyverse.org/
Wickham, H., François, R., Henry, L., & Müller, K. (2023). dplyr: A grammar of data manipulation (R package version 1.1.4) [Computer software]. https://dplyr.tidyverse.org/
Wickham, H., Vaughan, D., & Girlich, M. (2024). tidyr: Tidy messy data (R package version 1.3.1) [Computer software]. https://tidyr.tidyverse.org/
Xie, Y., Allaire, J. J., & Grolemund, G. (2018). R Markdown: The definitive guide. Chapman and Hall/CRC. https://bookdown.org/yihui/rmarkdown/