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

68% of the city’s 82,064 trees have been planted since 2004.

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.

1. A forest planted in twenty years

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.

Show the numbers behind this chart
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.

2. The old guard is running out of time

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.

Show the numbers behind this chart
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.

3. Where the old trees still stand

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 = "&copy; 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.

Show the numbers behind this chart
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

4. What replaced them

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.

Show the numbers behind this chart
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.

5. Is the new forest following the rule?

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.

Show the numbers behind this chart
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.

What this means

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?

About the data

References

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