All charts are interactive: hover for details, click legend items to hide or show them, and drag to zoom (double-click to reset). The Show buttons on the right reveal the R code. The first four load, clean and style the data.
data_file <- "trees-with-species-and-dimensions-urban-forest.csv"
data_url <- paste0("https://data.melbourne.vic.gov.au/api/explore/v2.1/catalog/",
"datasets/trees-with-species-and-dimensions-urban-forest/exports/csv")
if (!file.exists(data_file)) {
download.file(data_url, data_file, mode = "wb")
}
# Read every column as text first, then convert the ones we need.
trees_raw <- read_delim(data_file, delim = ";",
col_types = cols(.default = col_character()))
new_years <- as.integer(trees_raw$year_planted)
first_year <- min(new_years[new_years > 1900], na.rm = TRUE)
last_year <- max(new_years, na.rm = TRUE)
old_label <- "Older trees (planted before records)"
new_label <- paste0("Planted ", first_year, "-", last_year)
# Order of the "years left" groups, from most urgent to least.
years_left_levels <- c("Under 10 years", "11-20 years", "21-30 years",
"31-40 years", "41+ years", "Not assessed")
trees <- trees_raw %>%
transmute(
common_name = str_to_title(common_name),
scientific_name,
genus,
family,
located_in, # "Street" or "Park"
precinct,
year_planted = as.integer(year_planted),
latitude = as.numeric(latitude),
longitude = as.numeric(longitude),
cohort = if_else(year_planted == 1900, old_label, new_label),
years_left = case_when(
useful_life_expectency == "< 10 years" ~ years_left_levels[1],
useful_life_expectency == "11 - 20 years" ~ years_left_levels[2],
useful_life_expectency == "21 - 30 years" ~ years_left_levels[3],
useful_life_expectency == "31 - 40 years" ~ years_left_levels[4],
useful_life_expectency == "> 41 years" ~ years_left_levels[5],
TRUE ~ years_left_levels[6]
),
years_left = factor(years_left, levels = years_left_levels)
) %>%
filter(!is.na(latitude), !is.na(longitude))
# Groups that count as "30 years or less left"
at_risk_levels <- years_left_levels[1:3]
# Friendly names for the tree groups (genus) used in the charts.
# This is only a label lookup, not data.
genus_names <- c(
Ulmus = "Elm", Platanus = "Plane", Eucalyptus = "Gum (Eucalyptus)",
Corymbia = "Spotted gum (Corymbia)", Acacia = "Wattle", Quercus = "Oak",
Melaleuca = "Paperbark", Allocasuarina = "She-oak (Allocasuarina)",
Casuarina = "River she-oak (Casuarina)", Lophostemon = "Brush box",
Callistemon = "Bottlebrush", Ficus = "Fig", Acer = "Maple",
Bursaria = "Sweet bursaria", Angophora = "Angophora", Banksia = "Banksia",
Phoenix = "Palm (Phoenix)", Fraxinus = "Ash", Pyrus = "Pear",
Brachychiton = "Kurrajong", Zelkova = "Zelkova", Melia = "White cedar"
)
family_names <- c(
Myrtaceae = "Myrtle family (gums, paperbarks)", Fabaceae = "Pea family (wattles)",
Casuarinaceae = "She-oak family", Ulmaceae = "Elm family",
Platanaceae = "Plane family", Fagaceae = "Oak family",
Sapindaceae = "Soapberry family (maples)", Proteaceae = "Protea family (banksias)",
Pittosporaceae = "Pittosporum family", Meliaceae = "Mahogany family"
)
nice_genus <- function(x) ifelse(x %in% names(genus_names), genus_names[x], x)
nice_family <- function(x) ifelse(x %in% names(family_names), family_names[x], x)
# ---------------------------------------------------------------
# 3. ONE SHARED STYLE FOR EVERY CHART
# Colours were checked for colour-blind safety. Text stays dark
# grey, and colour is only used for the data.
# ---------------------------------------------------------------
col_new <- "#2a78d6" # blue = trees planted since records began
col_old <- "#eb6834" # orange = older trees
col_park <- "#1baf7a" # green = park trees
col_over <- "#d03b3b" # red = over the city's diversity limit
col_under <- "#a8a7a0" # grey = within the limit
col_grid <- "#e1e0d9"
col_axis <- "#c3c2b7"
col_muted <- "#52514e"
# One-hue (orange) scale for "years left": darker = less time left
years_left_colours <- setNames(
c("#5c1a05", "#8f300f", "#c04a1b", "#e5743f", "#f0996c", "#d6d5cf"),
years_left_levels
)
chart_font <- list(family = "Avenir, 'Avenir Next', 'Nunito Sans', 'Segoe UI', sans-serif",
size = 13, color = "#0b0b0b")
axis_style <- list(gridcolor = col_grid, linecolor = col_axis, zeroline = FALSE,
tickfont = list(color = col_muted), title = list(font = list(color = col_muted)))
style_chart <- function(p) {
p %>%
layout(font = chart_font,
paper_bgcolor = "#fcfcfb", plot_bgcolor = "#fcfcfb",
hoverlabel = list(font = list(family = chart_font$family))) %>%
config(displaylogo = FALSE,
modeBarButtonsToRemove = c("lasso2d", "select2d", "autoScale2d"))
}
# ---------------------------------------------------------------
# 4. KEY NUMBERS USED IN THE TEXT
# Every number in the story is calculated here from the data,
# so the text updates automatically if the data changes.
# ---------------------------------------------------------------
fmt <- function(x) formatC(round(x), format = "d", big.mark = ",")
pct <- function(x) paste0(round(x), "%")
n_total <- nrow(trees)
n_old <- sum(trees$cohort == old_label)
n_new <- sum(trees$cohort == new_label)
share_new <- 100 * n_new / n_total
# Largest single-day planting record
biggest_day <- trees_raw %>%
filter(year_planted != "1900") %>%
count(date_planted, sort = TRUE) %>% slice(1)
biggest_day_park <- trees_raw %>%
filter(date_planted == biggest_day$date_planted) %>%
summarise(p = 100 * mean(located_in == "Park"),
top_precinct = names(sort(table(precinct), decreasing = TRUE))[1])
# How much time the older elms and planes have left
old_at_risk <- trees %>%
filter(cohort == old_label) %>%
group_by(genus) %>%
summarise(n = n(), at_risk = 100 * mean(years_left %in% at_risk_levels))
elm_risk <- old_at_risk$at_risk[old_at_risk$genus == "Ulmus"]
plane_risk <- old_at_risk$at_risk[old_at_risk$genus == "Platanus"]
# Share of the older street trees that are elms or planes
old_street <- trees %>% filter(cohort == old_label, located_in == "Street")
elm_plane_street_old <- 100 * mean(old_street$genus %in% c("Ulmus", "Platanus"))
new_street <- trees %>% filter(cohort == new_label, located_in == "Street")
elm_plane_street_new <- 100 * mean(new_street$genus %in% c("Ulmus", "Platanus"))
The short version
One of the issues Melbourne faced in 2012 was its ageing urban forest (Melbourneurbanforestvisual, 2020). The Urban Forest Strategy revealed that 23 percent of the existing trees would reach the end of their useful life within 10 years, and 39 percent within 20 years.
To make sure these problems are not experienced again, the council has set biodiversity objectives for future planting: no single species should exceed 5% of trees, no genus 10%, and no family 20%.
This was followed by making sure the council held all the necessary information on the trees in its care: 82,064 trees from streets and parks, with their expected useful lives and other details.
Trees present in Melbourne, classified by the year they were planted. There are 26,521 trees that have no planting date at all (Melbourne, 2023).
# ---------------------------------------------------------------
# CHART 1: Trees planted each year, split by street and park
# Interactivity: hover for exact counts, click the legend to hide
# a group, drag to zoom (double-click to reset).
# ---------------------------------------------------------------
chart1_data <- trees %>%
filter(cohort == new_label) %>%
count(year_planted, located_in)
chart1 <- plot_ly(chart1_data, x = ~year_planted, y = ~n,
color = ~located_in, colors = c(Park = col_park, Street = col_new),
type = "bar",
marker = list(line = list(color = "#fcfcfb", width = 1)),
hovertemplate = "<b>%{x}</b><br>%{fullData.name} trees: %{y:,}<extra></extra>") %>%
layout(barmode = "stack", bargap = 0.25,
title = list(text = paste0("<b>Two out of every three living trees have been planted since ",
first_year, "</b><br><span style='font-size:13px;color:#52514e'>",
"Trees still standing today, by the year they were planted</span>"),
x = 0, xanchor = "left", y = 0.95),
xaxis = c(axis_style, list(title = "Year planted", dtick = 2)),
yaxis = c(axis_style, list(title = "Trees planted", tickformat = ",")),
margin = list(t = 110),
legend = list(orientation = "h", x = 0, y = 1.02),
annotations = list(list(
x = as.integer(substr(biggest_day$date_planted, 1, 4)),
y = sum(chart1_data$n[chart1_data$year_planted ==
as.integer(substr(biggest_day$date_planted, 1, 4))]),
text = paste0(fmt(biggest_day$n), " trees recorded on one day<br>(",
format(as.Date(biggest_day$date_planted), "%d %b %Y"),
"), mostly parkland in ", biggest_day_park$top_precinct),
showarrow = TRUE, arrowcolor = col_muted, ax = 90, ay = -10,
xanchor = "left", align = "left", font = list(size = 12, color = col_muted)))) %>%
style_chart()
chart1
Figure 1: Annual tree planting in streets and parks, 2004-2025. Implications of this figure: the number of trees planted each year from 2004 to 2025 is depicted by each vertical bar. The blue segments show street trees, whereas the green ones show park trees.
chart1_data %>%
pivot_wider(names_from = located_in, values_from = n, values_fill = 0) %>%
mutate(Total = Park + Street) %>%
rename(`Year planted` = year_planted) %>%
kable(format.args = list(big.mark = ","))
| Year planted | Street | Park | Total |
|---|---|---|---|
| 2,004 | 1 | 0 | 1 |
| 2,005 | 92 | 133 | 225 |
| 2,006 | 1,409 | 1,340 | 2,749 |
| 2,007 | 707 | 431 | 1,138 |
| 2,008 | 1,733 | 862 | 2,595 |
| 2,009 | 1,234 | 950 | 2,184 |
| 2,010 | 803 | 1,151 | 1,954 |
| 2,011 | 769 | 1,142 | 1,911 |
| 2,012 | 1,586 | 6,750 | 8,336 |
| 2,013 | 1,010 | 613 | 1,623 |
| 2,014 | 893 | 854 | 1,747 |
| 2,015 | 1,229 | 2,210 | 3,439 |
| 2,016 | 882 | 1,245 | 2,127 |
| 2,017 | 1,007 | 1,845 | 2,852 |
| 2,018 | 1,025 | 1,254 | 2,279 |
| 2,019 | 1,309 | 2,028 | 3,337 |
| 2,020 | 856 | 2,235 | 3,091 |
| 2,021 | 1,329 | 2,259 | 3,588 |
| 2,022 | 1,062 | 1,672 | 2,734 |
| 2,023 | 902 | 830 | 1,732 |
| 2,024 | 1,619 | 1,510 | 3,129 |
| 2,025 | 2,251 | 521 | 2,772 |
Melbourne did not wait for its old trees to die out; it started planting early. Since 2004, a total of 55,543 trees that are still alive today have been planted, accounting for about 68% of the urban forest. The other 26,521 trees were planted before the recording of planting began.
top_old_genera <- trees %>%
filter(cohort == old_label) %>%
count(genus, sort = TRUE) %>%
slice_head(n = 8) %>%
pull(genus)
chart2_data <- trees %>%
filter(cohort == old_label, genus %in% top_old_genera) %>%
count(genus, years_left) %>%
complete(genus, years_left, fill = list(n = 0)) %>%
group_by(genus) %>%
mutate(share = 100 * n / sum(n),
at_risk = sum(share[years_left %in% at_risk_levels]),
total = sum(n)) %>%
ungroup() %>%
mutate(label = paste0(nice_genus(genus), " (", fmt(total), ")"))
# Order bars: most at-risk group at the top
bar_order <- chart2_data %>% distinct(label, at_risk) %>% arrange(at_risk) %>% pull(label)
chart2 <- plot_ly(chart2_data, y = ~factor(label, levels = bar_order), x = ~share,
color = ~years_left, colors = years_left_colours,
type = "bar", orientation = "h",
marker = list(line = list(color = "#fcfcfb", width = 1)),
customdata = ~n,
hovertemplate = "<b>%{y}</b><br>%{fullData.name} left: %{x:.0f}% (%{customdata:,} trees)<extra></extra>") %>%
layout(barmode = "stack",
title = list(text = paste0("<b>", pct(elm_risk), " of the old elms have 30 years or less left</b>",
"<br><span style='font-size:13px;color:#52514e'>",
"Older trees (planted before records), by how many years each is expected to live</span>"),
x = 0, xanchor = "left", y = 0.96),
xaxis = c(axis_style, list(title = "Share of trees in the group", ticksuffix = "%", range = c(0, 100))),
yaxis = c(axis_style, list(title = "")),
legend = list(orientation = "h", x = 0, y = -0.18, traceorder = "normal"),
margin = list(t = 80, r = 150, b = 40),
annotations = lapply(bar_order, function(b) {
v <- chart2_data$at_risk[chart2_data$label == b][1]
list(x = 1.01, xref = "paper", y = b, yref = "y", xanchor = "left",
text = paste0("<b>", pct(v), "</b> have 30 yrs or less"), showarrow = FALSE,
font = list(size = 12, color = "#0b0b0b"))
})) %>%
style_chart()
chart2
Figure 2: Remaining lifespans across eight tree groups. Older trees, which were planted before systematic recording began, are categorised by their estimated remaining useful life. The three darkest segments represent trees expected to live 30 years or less, and the percentage beside each bar adds these segments together. Elms are more endangered than plane trees, at 81% and 60% respectively.
chart2_data %>%
select(`Tree group` = label, `Years left` = years_left, Trees = n) %>%
pivot_wider(names_from = `Years left`, values_from = Trees) %>%
kable(format.args = list(big.mark = ","))
| Tree group | Under 10 years | 11-20 years | 21-30 years | 31-40 years | 41+ years | Not assessed |
|---|---|---|---|---|---|---|
| Wattle (1,068) | 8 | 48 | 76 | 180 | 416 | 340 |
| She-oak (Allocasuarina) (765) | 10 | 30 | 112 | 199 | 311 | 103 |
| Spotted gum (Corymbia) (1,961) | 7 | 86 | 995 | 546 | 175 | 152 |
| Gum (Eucalyptus) (6,305) | 155 | 546 | 1,259 | 1,042 | 1,813 | 1,490 |
| Paperbark (771) | 1 | 71 | 297 | 80 | 28 | 294 |
| Plane (4,163) | 21 | 221 | 2,246 | 1,203 | 286 | 186 |
| Oak (843) | 0 | 48 | 480 | 166 | 97 | 52 |
| Elm (4,372) | 8 | 648 | 2,901 | 481 | 139 | 195 |
The rest of Melbourne’s mature forest is mainly made up of elms, plane trees and gums (Unimelb, 2026). Of these, elms pose a special problem. They were planted at around the same time, so they are growing old together and will need to be replaced at the same time.
Older trees are mostly found in Melbourne’s older suburbs, while Docklands is home to younger trees (Melbourne, 2026).
map_old_risk <- "Older tree, 30 years or less left"
map_old_ok <- "Older tree, more time left (or not assessed)"
map_data <- trees %>%
mutate(layer = case_when(
cohort == new_label ~ new_label,
years_left %in% at_risk_levels ~ map_old_risk,
TRUE ~ map_old_ok),
layer = factor(layer, levels = c(new_label, map_old_ok, map_old_risk)),
latitude = round(latitude, 5), longitude = round(longitude, 5))
map_colours <- setNames(c(col_new, col_park, col_old), c(new_label, map_old_ok, map_old_risk))
chart3 <- plot_ly(map_data, type = "scattermapbox", mode = "markers",
lat = ~latitude, lon = ~longitude,
split = ~layer, color = ~layer, colors = map_colours,
marker = list(size = 4, opacity = 0.7),
text = ~common_name,
hovertemplate = "<b>%{text}</b><br>%{fullData.name}<extra></extra>") %>%
# Background map: free OpenStreetMap tiles (no API key needed).
# "white-bg" + a faded tile layer keeps the coloured dots easy to see.
layout(mapbox = list(style = "white-bg", zoom = 12.2,
layers = list(list(
below = "traces", sourcetype = "raster", opacity = 0.45,
source = list("https://tile.openstreetmap.org/{z}/{x}/{y}.png"),
sourceattribution = "© OpenStreetMap contributors")),
center = list(lat = -37.803, lon = 144.947)),
title = list(text = paste0("<b>Old trees hold on in the older suburbs; Docklands is almost all new; OpenStreetMap contributors (2026).</b>"),
x = 0, xanchor = "left", y = 0.985, font = list(size = 16)),
margin = list(t = 80, l = 0, r = 0, b = 0),
legend = list(orientation = "h", x = 0, y = 1.06, bgcolor = "rgba(252,252,251,0.9)")) %>%
style_chart()
chart3
Figure 3: Spatial distribution of older and newer trees. What this indicates: each point on the map is an individual tree managed by the council. Orange dots indicate older trees expected to live 30 years or less. Green dots denote older trees with a longer predicted life, or no prediction, whereas blue dots mark trees planted since 2004. Zoom into Carlton, East Melbourne or Parkville to see the old elm and plane avenues.
map_data %>%
count(Precinct = precinct, layer) %>%
pivot_wider(names_from = layer, values_from = n, values_fill = 0) %>%
arrange(desc(.data[[new_label]])) %>%
kable(format.args = list(big.mark = ","))
| Precinct | Planted 2004-2025 | Older tree, more time left (or not assessed) | Older tree, 30 years or less left |
|---|---|---|---|
| Parkville | 18,071 | 7,166 | 3,330 |
| Kensington | 6,327 | 322 | 906 |
| West Melbourne | 5,830 | 108 | 818 |
| Docklands | 5,637 | 55 | 124 |
| Melbourne | 4,795 | 1,467 | 2,801 |
| North Melbourne | 3,153 | 279 | 1,283 |
| Carlton | 2,636 | 557 | 1,175 |
| East Melbourne | 2,283 | 864 | 1,359 |
| Port Melbourne | 2,201 | 274 | 148 |
| Southbank | 1,477 | 589 | 272 |
| South Yarra | 1,217 | 372 | 1,042 |
| Carlton North | 1,172 | 404 | 747 |
| Flemington | 451 | 0 | 59 |
| North And West Melbourne | 113 | 0 | 0 |
| Princes Hill | 65 | 0 | 0 |
| Fishermans Bend | 61 | 0 | 0 |
| Central City | 26 | 0 | 0 |
| Brunswick West | 24 | 0 | 0 |
| Brunswick | 2 | 0 | 0 |
| Richmond | 2 | 0 | 0 |
The streets of Melbourne have moved from relying on elms and planes to a wider mix of tree groups. The chart compares each group’s share of the older trees with its share of the trees planted from 2004 to 2025.
views <- c("All trees", "Street trees", "Park trees")
trees_by_view <- bind_rows(
trees %>% mutate(view = "All trees"),
trees %>% filter(located_in == "Street") %>% mutate(view = "Street trees"),
trees %>% filter(located_in == "Park") %>% mutate(view = "Park trees")
)
genus_share <- trees_by_view %>%
count(view, cohort, genus) %>%
group_by(view, cohort) %>%
mutate(share = 100 * n / sum(n)) %>%
ungroup()
# For each view keep the top 6 groups among old trees plus the top 6 among new trees
dumbbell_data <- genus_share %>%
group_by(view, cohort) %>%
slice_max(share, n = 6) %>%
ungroup() %>%
distinct(view, genus) %>%
left_join(genus_share %>% select(view, cohort, genus, share), by = c("view", "genus")) %>%
pivot_wider(names_from = cohort, values_from = share, values_fill = 0) %>%
rename(old = all_of(old_label), new = all_of(new_label)) %>%
mutate(label = nice_genus(genus))
chart4 <- plot_ly()
y_orders <- list()
for (i in seq_along(views)) {
d <- dumbbell_data %>% filter(view == views[i]) %>% arrange(old)
y_orders[[i]] <- d$label
show <- i == 1 # only the first view is visible at the start
chart4 <- chart4 %>%
add_segments(data = d, x = ~old, xend = ~new, y = ~label, yend = ~label,
line = list(color = col_axis, width = 2), hoverinfo = "none",
showlegend = FALSE, visible = show) %>%
add_markers(data = d, x = ~old, y = ~label, name = old_label,
marker = list(color = col_old, size = 11, line = list(color = "#fcfcfb", width = 2)),
hovertemplate = "<b>%{y}</b><br>Older trees: %{x:.1f}%<extra></extra>",
legendgroup = "old", showlegend = show, visible = show) %>%
add_markers(data = d, x = ~new, y = ~label, name = new_label,
marker = list(color = col_new, size = 11, line = list(color = "#fcfcfb", width = 2)),
hovertemplate = "<b>%{y}</b><br>New trees: %{x:.1f}%<extra></extra>",
legendgroup = "new", showlegend = show, visible = show)
}
# One button per view: show its 3 traces, hide the rest, reorder the y axis
view_buttons <- lapply(seq_along(views), function(i) {
list(method = "update", label = views[i],
args = list(list(visible = rep(seq_along(views) == i, each = 3),
showlegend = rep(c(FALSE, TRUE, TRUE), length(views)) &
rep(seq_along(views) == i, each = 3)),
list(yaxis.categoryarray = y_orders[[i]])))
})
chart4 <- chart4 %>%
layout(title = list(text = paste0("<b>The streets swapped elms and planes for variety</b>",
"<br><span style='font-size:13px;color:#52514e'>",
"Share of each tree group among older trees vs trees planted since ",
first_year, "</span>"),
x = 0, xanchor = "left", y = 0.96),
xaxis = c(axis_style, list(title = "Share of trees in that age group", ticksuffix = "%", rangemode = "tozero")),
yaxis = c(axis_style, list(title = "", categoryorder = "array", categoryarray = y_orders[[1]])),
shapes = list(list(type = "line", x0 = 10, x1 = 10, y0 = 0, y1 = 1, yref = "paper",
line = list(color = col_over, width = 1.5))),
annotations = list(list(x = 10, y = 1, yref = "paper", xanchor = "left", yanchor = "bottom",
text = " City's 10% limit per genus", showarrow = FALSE,
font = list(size = 12, color = col_over))),
updatemenus = list(list(type = "buttons", direction = "right", buttons = view_buttons,
x = 0, xanchor = "left", y = 1.17, yanchor = "top",
bgcolor = "#f0efec", bordercolor = col_axis, font = list(size = 12))),
margin = list(t = 140, b = 40),
legend = list(orientation = "h", x = 0, y = -0.15)) %>%
style_chart()
chart4
Figure 4: Comparative dot plot with the All trees, Street trees and Park trees buttons and the 10% genus threshold. Interpretation: the orange points show each tree group’s share among trees planted before systematic recording. The blue points show the share of the same groups among trees planted from 2004 to 2025. The red vertical line is the city’s 10% cap on any one genus. The longer the line between the two points, the bigger the change.
dumbbell_data %>%
arrange(view, desc(old)) %>%
transmute(View = view, `Tree group` = label,
`Older trees (%)` = round(old, 1), `New trees (%)` = round(new, 1)) %>%
kable()
| View | Tree group | Older trees (%) | New trees (%) |
|---|---|---|---|
| All trees | Gum (Eucalyptus) | 23.8 | 20.5 |
| All trees | Elm | 16.5 | 3.1 |
| All trees | Plane | 15.7 | 2.7 |
| All trees | Spotted gum (Corymbia) | 7.4 | 4.9 |
| All trees | Wattle | 4.0 | 11.9 |
| All trees | Oak | 3.2 | 3.1 |
| All trees | She-oak (Allocasuarina) | 2.9 | 5.8 |
| All trees | River she-oak (Casuarina) | 1.0 | 4.5 |
| Park trees | Gum (Eucalyptus) | 36.2 | 28.1 |
| Park trees | Elm | 13.6 | 1.9 |
| Park trees | Wattle | 6.5 | 17.1 |
| Park trees | Spotted gum (Corymbia) | 6.1 | 3.9 |
| Park trees | She-oak (Allocasuarina) | 4.2 | 8.2 |
| Park trees | Bottlebrush | 3.5 | 1.3 |
| Park trees | River she-oak (Casuarina) | 1.4 | 7.1 |
| Park trees | Sweet bursaria | 0.0 | 3.4 |
| Street trees | Plane | 37.8 | 5.6 |
| Street trees | Elm | 21.2 | 4.8 |
| Street trees | Spotted gum (Corymbia) | 9.5 | 6.3 |
| Street trees | Brush box | 5.0 | 0.9 |
| Street trees | Oak | 3.6 | 4.8 |
| Street trees | Gum (Eucalyptus) | 3.5 | 10.3 |
| Street trees | Maple | 0.1 | 5.0 |
| Street trees | Wattle | 0.1 | 4.8 |
Choose Street trees to see a clear trend. Elms and planes together make up 59% of older street trees, roughly three out of five. Among street trees planted since 2004, their share dropped to only 10%. This is a significant shift towards greater diversity on Melbourne’s streets.
However, choosing Park trees shows a different trend. Gums (Eucalyptus) were already the biggest group in the parks. Their share fell from 36% of older park trees to 28% of new ones, which is still well above the 10% limit. Wattles (Acacia) went the other way, growing from 6% to 17%, and are now over the limit too.
Melbourne’s recent forest relies heavily on eucalyptus trees, and only the street trees come close to meeting the diversity criteria. The chart shows the five most common species, genera and families planted since 2004, compared with the city’s 5-10-20 guidelines.
rule_limits <- c(Species = 5, Genus = 10, Family = 20)
# A common name for each species (the most frequent one in the data)
species_names <- trees %>%
count(scientific_name, common_name) %>%
group_by(scientific_name) %>%
slice_max(n, n = 1, with_ties = FALSE) %>%
select(scientific_name, species_label = common_name)
new_by_view <- trees_by_view %>%
filter(cohort == new_label) %>%
left_join(species_names, by = "scientific_name")
top_shares <- function(level_col, level_name, label_fun) {
new_by_view %>%
mutate(item = .data[[level_col]]) %>%
count(view, item) %>%
group_by(view) %>%
mutate(share = 100 * n / sum(n)) %>%
slice_max(share, n = 5, with_ties = FALSE) %>%
ungroup() %>%
mutate(level = level_name, label = label_fun(item))
}
rule_check <- bind_rows(
top_shares("species_label", "Species", identity),
top_shares("genus", "Genus", nice_genus),
top_shares("family", "Family", nice_family)
) %>%
mutate(limit = rule_limits[level],
over = share > limit,
bar_text = paste0(round(share, 1), "%", if_else(over, " over limit", "")))
# Helper for the text: the biggest share at one level in one view
biggest <- function(v, lvl) rule_check %>% filter(view == v, level == lvl) %>% slice_max(share, n = 1)
levels_order <- c("Species", "Genus", "Family")
y_orders5 <- list() # y-axis order for each (view, level)
panels <- lapply(seq_along(levels_order), function(j) {
lvl <- levels_order[j]
p <- plot_ly()
for (i in seq_along(views)) {
d <- rule_check %>% filter(view == views[i], level == lvl) %>% arrange(share)
y_orders5[[paste(i, j)]] <<- d$label
p <- p %>% add_bars(data = d, x = ~share, y = ~label, orientation = "h",
marker = list(color = if_else(d$over, col_over, col_under)),
text = ~bar_text, textposition = "outside", cliponaxis = FALSE,
textfont = list(size = 12, color = "#0b0b0b"),
hovertemplate = paste0("<b>%{y}</b><br>%{x:.1f}% of new trees<br>",
"Limit for one ", tolower(lvl), ": ",
rule_limits[lvl], "%<extra></extra>"),
showlegend = FALSE, visible = i == 1)
}
p %>% layout(
shapes = list(list(type = "line", x0 = rule_limits[lvl], x1 = rule_limits[lvl],
y0 = -0.5, y1 = 4.5, line = list(color = col_over, width = 1.5))),
yaxis = c(axis_style, list(categoryorder = "array", categoryarray = y_orders5[[paste(1, j)]]))
)
})
chart5 <- subplot(panels, nrows = 3, shareX = TRUE, margin = 0.06) %>%
layout(xaxis = c(axis_style, list(title = "Share of trees planted since records began",
ticksuffix = "%", range = c(0, 52))))
# Panel titles (one per small chart)
panel_titles <- lapply(seq_along(levels_order), function(j) {
lvl <- levels_order[j]
list(text = paste0("<b>One ", tolower(lvl), "</b> <span style='color:", col_over,
"'>limit ", rule_limits[lvl], "%</span>"),
x = 0, xref = "paper", xanchor = "left",
y = c(1.0, 0.64, 0.28)[j], yref = "paper", yanchor = "bottom",
showarrow = FALSE, font = list(size = 13))
})
# Buttons: each panel holds 3 traces (one per view)
view_buttons5 <- lapply(seq_along(views), function(i) {
axis_updates <- list()
for (j in seq_along(levels_order)) {
axis_name <- if (j == 1) "yaxis.categoryarray" else paste0("yaxis", j, ".categoryarray")
axis_updates[[axis_name]] <- y_orders5[[paste(i, j)]]
}
list(method = "update", label = views[i],
args = list(list(visible = rep(seq_along(views) == i, times = length(levels_order))),
axis_updates))
})
chart5 <- chart5 %>%
layout(title = list(text = paste0("<b>The new forest leans on gums. Only the street species pass the rule</b>",
"<br><span style='font-size:13px;color:#52514e'>",
"Top 5 species, genera and families among trees planted since ",
first_year, ", vs the city's 5-10-20 rule</span>"),
x = 0, xanchor = "left", y = 0.98, yanchor = "top"),
annotations = panel_titles,
updatemenus = list(list(type = "buttons", direction = "right", buttons = view_buttons5,
x = 0, xanchor = "left", y = 1.15, yanchor = "top",
bgcolor = "#f0efec", bordercolor = col_axis, font = list(size = 12))),
margin = list(t = 170, r = 30)) %>%
style_chart()
chart5
Figure 5: Species, genus and family shares, with the red threshold lines and the All trees, Street trees and Park trees buttons. What this demonstrates: the three horizontal bar charts show how much of the forest planted since 2004 is made up of the dominant species, genera and families. Each bar is one name’s share of those trees, and the red line is the council’s limit for that level.
rule_check %>%
transmute(View = view, Level = level, Name = label,
`Share of new trees (%)` = round(share, 1), `Limit (%)` = limit,
`Over limit?` = if_else(over, "Yes", "No")) %>%
kable()
| View | Level | Name | Share of new trees (%) | Limit (%) | Over limit? |
|---|---|---|---|---|---|
| All trees | Species | River Red Gum | 8.5 | 5 | Yes |
| All trees | Species | Drooping Sheoak | 4.9 | 5 | No |
| All trees | Species | Black Wattle | 4.6 | 5 | No |
| All trees | Species | Yellow Box | 4.0 | 5 | No |
| All trees | Species | Sweet Bursaria | 2.8 | 5 | No |
| Park trees | Species | River Red Gum | 13.0 | 5 | Yes |
| Park trees | Species | Drooping Sheoak | 7.0 | 5 | Yes |
| Park trees | Species | Black Wattle | 6.9 | 5 | Yes |
| Park trees | Species | Yellow Box | 5.4 | 5 | Yes |
| Park trees | Species | River Sheoak | 4.1 | 5 | No |
| Street trees | Species | London Plane | 4.6 | 5 | No |
| Street trees | Species | Spotted Gum | 4.0 | 5 | No |
| Street trees | Species | Smooth-Barked Apple | 4.0 | 5 | No |
| Street trees | Species | Chinaberry Tree | 3.4 | 5 | No |
| Street trees | Species | River Red Gum | 2.6 | 5 | No |
| All trees | Genus | Gum (Eucalyptus) | 20.5 | 10 | Yes |
| All trees | Genus | Wattle | 11.9 | 10 | Yes |
| All trees | Genus | She-oak (Allocasuarina) | 5.8 | 10 | No |
| All trees | Genus | Spotted gum (Corymbia) | 4.9 | 10 | No |
| All trees | Genus | River she-oak (Casuarina) | 4.5 | 10 | No |
| Park trees | Genus | Gum (Eucalyptus) | 28.1 | 10 | Yes |
| Park trees | Genus | Wattle | 17.1 | 10 | Yes |
| Park trees | Genus | She-oak (Allocasuarina) | 8.2 | 10 | No |
| Park trees | Genus | River she-oak (Casuarina) | 7.1 | 10 | No |
| Park trees | Genus | Spotted gum (Corymbia) | 3.9 | 10 | No |
| Street trees | Genus | Gum (Eucalyptus) | 10.3 | 10 | Yes |
| Street trees | Genus | Spotted gum (Corymbia) | 6.3 | 10 | No |
| Street trees | Genus | Plane | 5.6 | 10 | No |
| Street trees | Genus | Maple | 5.0 | 10 | No |
| Street trees | Genus | Oak | 4.8 | 10 | No |
| All trees | Family | Myrtle family (gums, paperbarks) | 33.6 | 20 | Yes |
| All trees | Family | Pea family (wattles) | 12.6 | 20 | No |
| All trees | Family | She-oak family | 10.4 | 20 | No |
| All trees | Family | Elm family | 4.2 | 20 | No |
| All trees | Family | Protea family (banksias) | 4.2 | 20 | No |
| Park trees | Family | Myrtle family (gums, paperbarks) | 37.5 | 20 | Yes |
| Park trees | Family | Pea family (wattles) | 17.5 | 20 | No |
| Park trees | Family | She-oak family | 15.2 | 20 | No |
| Park trees | Family | Protea family (banksias) | 3.8 | 20 | No |
| Park trees | Family | Pittosporum family | 3.8 | 20 | No |
| Street trees | Family | Myrtle family (gums, paperbarks) | 28.3 | 20 | Yes |
| Street trees | Family | Elm family | 7.2 | 20 | No |
| Street trees | Family | Pea family (wattles) | 6.0 | 20 | No |
| Street trees | Family | Soapberry family (maples) | 5.8 | 20 | No |
| Street trees | Family | Plane family | 5.6 | 20 | No |
On the streets, compliance is relatively higher. The most common new street species, London Plane, makes up 4.6% of new street trees, which is below the 5% guideline. The biggest genus, Gum (Eucalyptus), is 10.3%, just over the 10% line. The myrtle family is still 28.3%, above the 20% limit.
In the parks, the rule is broken at every level. River Red Gum alone makes up 13% of new park trees, against a 5% limit. Gum (Eucalyptus) is 28.1% against a 10% limit, and the myrtle family (gums, paperbarks) is 37.5% against a 20% limit.
Melbourne acted instead of staying stuck in the planning stage: about two-thirds of its trees were planted within the last 20 years. Rather than relying on two kinds of tree, the street forest is now strengthening itself by diversifying its tree groups.
The parks are in a harder spot. The newer parkland is largely planted with native Australian trees such as river red gums, wattles and she-oaks, in order to bring back native wildlife and ecosystems. But this is exactly the “one family everywhere” planting that the city is trying to prevent. If a pest or disease arrives, it could wipe out a whole tree family very quickly.
The question for the next Urban Forest Strategy is simple: should the 5-10-20 rule apply to the parks too?
year_planted field. In this analysis such trees are called
“older trees”, and 1900 is not treated as a real planting year.Melbourne. (2023). Tree planting zone schedules, with years (Urban Forest). Retrieved from https://data.melbourne.vic.gov.au/explore/dataset/tree-planting-zone-schedules-with-years-urban-forest/ October 24, 2026
Melbourne. (2026). If these trees could talk. Retrieved from https://www.melbourne.vic.gov.au/news/if-these-trees-could-talk October 24, 2026
Melbourneurbanforestvisual. (2020). Explore Melbourne’s urban forest. Retrieved from http://melbourneurbanforestvisual.com.au/ October 24, 2026,
Unimelb. (2026). Have you ever wondered what your Melbourne suburb looked like before all the streets and houses? Retrieved from https://pursuit.unimelb.edu.au/articles/weve-reconstructed-melbournes-200-year-old-forests October 24, 2026