Skip to contents

Extract data

Load OISST data:


library(terra)
library(tidyverse)
library(dhw)
library(sf)
library(tmap)
library(rnaturalearth)
library(patchwork)

# get Ningaloo extent via WA government SLIP services
url_geojson <- paste0(
    "https://public-services.slip.wa.gov.au/public/rest/services/",
    "SLIP_Public_Services/Property_and_Planning/MapServer/98/query?",
    "where=1%3D1&outFields=*&f=geojson"
)

ningaloo_shp <- st_read(url_geojson, quiet = TRUE)

# process OISST
process_OISST(input = "/Volumes/Extreme_SSD/dhw/OISST/",
              polygon = ningaloo_shp, crs = "EPSG:7844",
              combinedfilename = "/Users/rof011/Desktop/Ningaloo_OISST.rds",
              crop=TRUE, mask=TRUE, downsample=FALSE, mc.cores=1)

# read in raster, write for `dhw`
# ningaloo_OISST <- rast("/Users/rof011/Ningaloo_OISST.rds")
# ningaloo_OISST |> writeRaster("ningaloo_OISST.tif", overwrite=TRUE)

# read raster from package
ningaloo_OISST <- system.file("extdata", "ningaloo_OISST.tif", package = "dhw", mustWork = TRUE) |> rast()

Extract OISST timeseries for two sites: Ningaloo forereef and Exmouth Gulf (Bundegi):


# set sites
ningaloo_pt <- st_point(c(113.926, -21.92)) |>
  st_sfc(crs = 4326) |>
  st_transform(terra::crs(ningaloo_OISST)) |>
  st_buffer(0.01)

exmouth_pt <- st_point(c(114.179, -21.87)) |>
  st_sfc(crs = 4326) |>
  st_transform(terra::crs(ningaloo_OISST)) |>
  st_buffer(0.01)

# extract OISST for each site
ningaloo_slope_OISST <- crop(ningaloo_OISST, vect(ningaloo_pt))
exmouth_OISST <- crop(ningaloo_OISST, vect(exmouth_pt))

# get rNaturalEarth data
aus <- rnaturalearth::ne_countries(country="Australia", scale=10) |> st_transform(crs(ningaloo_shp))
ningaloo_park <- st_difference(ningaloo_shp, aus)

# map sites
tm_basemap("Esri.WorldImagery") +
tm_shape(ningaloo_park,bbox=st_bbox(ext(ningaloo_OISST))) +
  tm_polygons(fill_alpha=0, col="red", lwd=1.5) +
tm_shape(aus, bbox=st_bbox(ext(ningaloo_OISST))) +
  tm_polygons(fill="cornsilk", col="black") +
tm_facets(nrow=1) + tm_layout() +
tm_shape(ningaloo_pt) + 
  tm_dots(size=0.6, shape=21, fill="red") +
tm_shape(ningaloo_pt) +
  tm_text("Ningaloo", col="white", ymod=0.6, xmod=-1.9) +
tm_shape(exmouth_pt) + 
  tm_dots(size=0.6, shape=21, fill="aquamarine2") +
tm_shape(exmouth_pt) + 
  tm_text("Exmouth", col="white", ymod=0.8, xmod=1.4)

Maximum DHW per year

For the reef slope site at Ningaloo (red circle in the map above) and Exmouth gulf site (Bundegi, blue circle), calculate the maximum DHW per year (1982-2025):

# calculate Ningaloo climatology
all_climatology <- create_climatology(ningaloo_OISST, baa = TRUE, quiet=TRUE)

# calculate Ningaloo climatology
ningaloo_climatology <- create_climatology(ningaloo_slope_OISST, baa = TRUE, quiet=TRUE)
ningaloo_climatology_df <- as_climatology_df(ningaloo_climatology) |> mutate(site="Ningaloo")

# calculate Exmouth climatology
exmouth_climatology <- create_climatology(exmouth_OISST, baa = TRUE, quiet=TRUE)
exmouth_climatology_df <- as_climatology_df(exmouth_climatology)|> mutate(site="Exmouth")

# combine df for climatologies:
climatology_df <- rbind(ningaloo_climatology_df, exmouth_climatology_df)

# plot max dhw timeseries
max_dhw_ningaloo <- plot_max_dhw(ningaloo_climatology$dhw) + ggtitle("Ningaloo")
max_dhw_exmouth <- plot_max_dhw(exmouth_climatology$dhw) + ggtitle("Exmouth") + ylab("")
max_dhw_ningaloo+max_dhw_exmouth

Spatial patterns of SST

For the top 5 warmest years (1989, 1999, 2011, 1998, 2013, 2015) & map the max DHW days within each year (2024 included as a reference cool year)

  
# get the max DHW for the top five warmest years
top_5 <- ningaloo_climatology_df |>
  filter(year %in% c(1988, 1999, 2011, 2013, 2024, 2025)) |>
  group_by(year) |>
  slice_max(order_by = dhw, n = 1, with_ties = FALSE) |>
  ungroup()

sel_names <- format(as.Date(top_5$time), "%Y-%m-%d")  
idx <- match(sel_names, names(all_climatology$dhw))
ningaloo_subset <- all_climatology$dhw[[idx[!is.na(idx)]]]

tm_shape(ningaloo_subset) +
  tm_raster(col_alpha = 0.5,
            col.scale = tm_scale_continuous(limits=c(0,26), values="-brewer.spectral"), 
            col.free=FALSE,
            col.legend = tm_legend(title="DHW", orientation = "landscape")) +
tm_shape(aus, bbox=st_bbox(ext(ningaloo_OISST))) +
  tm_polygons(fill="cornsilk") +
tm_shape(ningaloo_park) +
  tm_polygons(fill_alpha=0, lwd=1.5) +
tm_facets(nrow=1) + tm_layout() +
tm_shape(ningaloo_pt) + 
  tm_dots(size=0.6, shape=21, fill="red") +
tm_shape(ningaloo_pt) +
  tm_text("Ningaloo", ymod=0.6, xmod=-1.9) +
tm_shape(exmouth_pt) + 
  tm_dots(size=0.6, shape=21, fill="aquamarine2") +
tm_shape(exmouth_pt) + 
  tm_text("Exmouth", ymod=-0.6, xmod=1.6)

DHW timeseries

For each site plot annual timeseries for the top 10 warmest years (black line = mean DHW over the timeseries, grey = 95% CI).

Increases in DHW intensity through time, but also increase in DHW window with more recent events (2011, 2013, 2025) starting early in the year (Jan) than earlier events (1988, 1998).


# plot DHW timeseries
a <- plot_annual_dhw(ningaloo_climatology$dhw) + ggtitle("Ningaloo")
b <- plot_annual_dhw(exmouth_climatology$dhw) + ggtitle("Exmouth")

a+b

SST timeseries

For the top five warmest years on record: In earlier warm years (1988, 1999) SST warming increased in Feb-March, whereas in later events resulting in bleaching (2011, 2013, 2025) SST increased in late Dec - early Jan, resulting in prolonged elevated SST (grey line = monthly mean climatology for the period 1985 to 2012).


ref_sst_years <- c(1985, 1998, 2011, 2013, 2025)

climatology_df %>%
  ungroup() %>%
  mutate(
    year  = lubridate::year(time),
    month = lubridate::month(time),
    day   = lubridate::day(time),
    season_anchor = dplyr::if_else(month >= 10, year + 1L, year),
    reference_date = lubridate::make_date(
      dplyr::if_else(month >= 10, 1999L, 2000L),
      month, day
    )
  ) |> 
  filter(season_anchor %in% ref_sst_years) %>%
   ggplot() +
  theme_bw() +
  facet_wrap(~site, ncol=1) +
  geom_line(aes(x = reference_date, y = sst, color = as.factor(season_anchor)), linewidth = 1) +
  geom_line(aes(x = reference_date, y = climatology), color="black", linewidth = 2, alpha=0.2) +
  scale_x_date(
    date_breaks = "1 month",
    date_labels = "%b",
    limits = c(as.Date("1999-09-19"), as.Date("2000-08-31"))
  ) +
  labs(x = "Month", y = "SST") +
  scale_color_manual(
    name = "Season",
    values = c("2025" = "#67001F", "2013" = "#CA4741", "2011" = "#F7B698", "1998" = "#A7CFE4", "1985" = "#053061")
  ) +
  theme(
    axis.text.x = element_text(angle = 0),
    panel.grid.minor = element_blank(),
    legend.position = c(0.99, 0.99),
    legend.justification = c("right", "top"),
    legend.background = element_rect(fill = "white", color = NA),
    legend.box.background = element_blank(),
    legend.title = element_blank()
  ) +
  geom_vline(xintercept = as.Date("2000-01-01"), color = "grey", linetype = "dashed") 

Comparing 1998 with 2025 (note the early initiation in 2025 vs 1998)




year_1998 <- plot_sst_timeseries(ningaloo_climatology, 1998) + ggtitle("1998")
year_2025 <- plot_sst_timeseries(ningaloo_climatology, 2025) + ggtitle("2025")

(year_1998 / year_2025)

Hotspot profiles

Comparing the HotSpot profiles for Ningaloo, 2025 has a similar window duration for thermal stress with HS > 1 (Jan-early May), but higer intensity thermal stress (HotSpot values 2.5-3) compared to 2011 (HotSpot values 1.5 - 2).

The HotSpot profiles for 2011 indicate sustained values ≥ 1 in Jan and Feb in 2011, with brief minor anomalies in April and May. In contrast, 2025 indicates prolonged HotSpots spanning two periods: Jan to mid-Feb, and late-Feb to mid-April.


ref_hs_years <- c(2011, 2025)


# summarise min/max per season
rects <-  
climatology_df %>%
  ungroup() %>%
  mutate(
    year  = lubridate::year(time),
    month = lubridate::month(time),
    day   = lubridate::day(time),
    season_anchor = dplyr::if_else(month >= 10, year + 1L, year),
    reference_date = lubridate::make_date(
      dplyr::if_else(month >= 10, 1999L, 2000L),
      month, day
    )
  ) |> 
  filter(season_anchor %in% ref_hs_years) |> 
  filter(site == "ningaloo") |> 
   mutate(hs_fill = if_else(hotspots >= 1, hotspots, 1))  %>%
  filter(hotspots > 0) %>%
  group_by(season_anchor) %>%
  summarise(
    xmin = as.Date(min(reference_date, na.rm = TRUE)),
    xmax = as.Date(max(reference_date, na.rm = TRUE)),
    ymin = min(hotspots, na.rm = TRUE),
    ymax = max(hotspots, na.rm = TRUE),
    .groups = "drop"
  ) |> 
  mutate(ymin=0)


climatology_df %>%
  ungroup() %>%
  mutate(
    year  = lubridate::year(time),
    month = lubridate::month(time),
    day   = lubridate::day(time),
    season_anchor = dplyr::if_else(month >= 10, year + 1L, year),
    reference_date = lubridate::make_date(
      dplyr::if_else(month >= 10, 1999L, 2000L),
      month, day
    )
  ) |> 
  dplyr::filter(season_anchor %in% ref_hs_years) |> 
  mutate(hs_fill = if_else(hotspots >= 1, hotspots, 1)) %>%   # cap lower bound at 1
 ggplot() +
  theme_bw() +
  facet_wrap(~season_anchor, ncol = 2) +
  geom_rect(
    data = rects,
    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
    inherit.aes = FALSE,
    fill = NA, color = "darkred", linewidth = 0.4, alpha=0.2, linetype = "dotted"
  ) +
  geom_ribbon(
    aes(x = reference_date, ymin = 1, ymax = hs_fill, fill = as.factor(season_anchor)),
    alpha = 0.8, show.legend=FALSE
  ) +

  geom_line(
    aes(x = reference_date, y = hotspots), color="black",
    linewidth = 0.5, show.legend=FALSE
  ) +

  scale_x_date(
    date_breaks = "1 month",
    date_labels = "%b",
    limits = c(as.Date("1999-11-19"), as.Date("2000-06-19"))
  ) +
  labs(x = "Month", y = "Hotspots") +
  scale_color_manual(
    name = "Season",
    values = c("2025" = "#D53E4F", "2011" = "yellow3")
  ) +
  scale_fill_manual(
    name = "Season",
    values = c("2025" = "#D53E4F", "2011" = "yellow3")
  ) +
  theme(
    axis.text.x = element_text(angle = 0),
    panel.grid.minor = element_blank(),
    legend.position = c(0.99, 0.99),
    legend.justification = c("right", "top"),
    legend.background = element_rect(fill = "white", color = NA),
    legend.box.background = element_blank(),
    legend.title = element_blank()
  ) +
  ylim(0, 4) +
  geom_vline(xintercept = as.Date("2000-01-01"), color = "grey", linetype = "dashed") +
  geom_hline(yintercept = 1, color = "darkgrey", linetype = "dashed") +
  theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    axis.text.x = element_text(angle = 0),
    legend.position = c(0.99, 0.99),
    legend.justification = c("right", "top"),
    legend.background = element_rect(fill = "white", color = NA),
    legend.box.background = element_blank(),
    legend.title = element_blank()
  )

DHW profiles

The sustained HotSpot values values ≥ 1 °C when a rolling 12-week window results in substantial accumulation of DHW over the 2025 period, reaching a x2.4 higher DHW than 2011 (19.9 DHW vs 8.32 DHW), despite broadly similar intensity in maximum SST anomaly values between the two years:



climatology_df %>%
  ungroup() %>%
  mutate(
    year  = lubridate::year(time),
    month = lubridate::month(time),
    day   = lubridate::day(time),
    season_anchor = dplyr::if_else(month >= 10, year + 1L, year),
    reference_date = lubridate::make_date(
      dplyr::if_else(month >= 10, 1999L, 2000L),
      month, day
    )
  ) |> 
  dplyr::filter(season_anchor %in% ref_hs_years) |> 
  dplyr::filter(site == "Ningaloo") |> 
    # mutate(hs_fill = if_else(hs >= 1, hs, 1)) %>%   # cap lower bound at 1
 ggplot() +
  theme_bw() +
  facet_wrap(~season_anchor, ncol = 2) +
  geom_ribbon(
    aes(x = reference_date, ymin = 0, ymax = dhw, fill = as.factor(season_anchor)),
    alpha = 0.8, show.legend=FALSE
  ) +

  geom_line(
    aes(x = reference_date, y = dhw), color="black",
    linewidth = 0.5, show.legend=FALSE
  ) +

  scale_x_date(
    date_breaks = "1 month",
    date_labels = "%b",
    limits = c(as.Date("1999-11-19"), as.Date("2000-06-19"))
  ) +
  labs(x = "Month", y = "Hotspots") +
  scale_color_manual(
    name = "Season",
    values = c("2025" = "#D53E4F", "2011" = "yellow3")
  ) +
  scale_fill_manual(
    name = "Season",
    values = c("2025" = "#D53E4F", "2011" = "yellow3")
  ) +
  theme(
    axis.text.x = element_text(angle = 0),
    panel.grid.minor = element_blank(),
    legend.position = c(0.99, 0.99),
    legend.justification = c("right", "top"),
    legend.background = element_rect(fill = "white", color = NA),
    legend.box.background = element_blank(),
    legend.title = element_blank()
  ) +
  ylim(0, 20) +
  geom_vline(xintercept = as.Date("2000-01-01"), color = "grey", linetype = "dashed") +
  geom_hline(yintercept = 1, color = "darkgrey", linetype = "dashed") +
  theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    axis.text.x = element_text(angle = 0),
    legend.position = c(0.99, 0.99),
    legend.justification = c("right", "top"),
    legend.background = element_rect(fill = "white", color = NA),
    legend.box.background = element_blank(),
    legend.title = element_blank()
  )

Extending DHW windows

The standard DHW metric is calculated using the following equation with a window hotspot accumulation across a 12 week period and an anomly of 1C

\[ \text{DHW}_i = \sum_{n = i-83}^{i} \left( \frac{\text{HS}_n}{7} \right), \quad \text{where } \text{HS}_n \geq 1 \]

using library(dhw) we can adjust the window length used to calculate DHW from 12 weeks to a broader range (6, 8, 10, 12, 16, 20 weeks) as follows:




ningaloo_climatology$sst_dhw_6 <- calculate_dhw(ningaloo_climatology$hotspots, window = 6*7) 
ningaloo_climatology$sst_dhw_8 <- calculate_dhw(ningaloo_climatology$hotspots, window=8*7) 
ningaloo_climatology$sst_dhw_10 <- calculate_dhw(ningaloo_climatology$hotspots, window=10*7) 
ningaloo_climatology$sst_dhw_12 <- calculate_dhw(ningaloo_climatology$hotspots, window=12*7) 
ningaloo_climatology$sst_dhw_14 <- calculate_dhw(ningaloo_climatology$hotspots, window=16*7) 
ningaloo_climatology$sst_dhw_16 <- calculate_dhw(ningaloo_climatology$hotspots, window=20*7) 


ningaloo_OISST_dhw_lag <- rbind(
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_6)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_6))) |> mutate(windowlength="6 weeks"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_8)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_8))) |> mutate(windowlength="8 weeks"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_10)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_10))) |> mutate(windowlength="10 weeks"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_12)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_12))) |> mutate(windowlength="12 weeks"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_14)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_14))) |> mutate(windowlength="14 weeks"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_dhw_16)), dhw=as.vector(values(ningaloo_climatology$sst_dhw_16))) |> mutate(windowlength="16 weeks")) |> 
  mutate(year = year(time), month = month(time), day=day(time)) |> 
  mutate(windowlength=factor(windowlength, levels=c("6 weeks", "8 weeks", "10 weeks", "12 weeks", "14 weeks", "16 weeks"))) 

library(patchwork)

  
  
a <-  
ggplot() + theme_bw() + ggtitle("2011 event") +
  geom_line(data = ningaloo_OISST_dhw_lag |> dplyr::filter((year == 2010 & month %in% 10:12) | (year == 2011 & month %in% 1:6)),
            aes(time, dhw, color=windowlength), show.legend=FALSE) + 
  scale_color_manual(values = c("#2166AC", "#67A9CF", "#D1E5F0", "#000000", "#EF8A62", "#B2182B")) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b") +
  scale_y_continuous(limits=c(0,26), breaks=seq(0,26,4)) 


b <-
ggplot() + theme_bw() + ggtitle("2025 event") +
  geom_line(data = ningaloo_OISST_dhw_lag |> dplyr::filter((year == 2024 & month %in% 10:12) | (year == 2025 & month %in% 1:6)),
            aes(time, dhw, color=windowlength), show.legend=TRUE) + 
  scale_color_manual(values = c("#2166AC", "#67A9CF", "#D1E5F0", "#000000", "#EF8A62", "#B2182B")) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b") +
  scale_y_continuous(limits=c(0,26), breaks=seq(0,26,4)) + ylab("")

a+b

In prolonged heating events like 2025, adjusting the windowlength for cumulative HotSpots results in amplified DHW. During less intense events like 2011, increasing windowlength results in prolonged DHW values with less impact on maximum DHW.

Extending DHW anomalies

similar to window length, using library(dhw) we can adjust the anomalies used to calculate DHW from 12 weeks to a broader range (0, 0.5, 1, 1.5, 2C) as follows.

Reducing the anomaly threshold gives a more proportionate response between the two events than windowlength, with the exception of 2C anomalies that are absent in the 2011 event:


ningaloo_climatology$sst_anom_0.0 <- calculate_hotspots(ningaloo_climatology$mmm, ningaloo_climatology$sst, anomaly=0) |> 
  calculate_dhw(anomaly=0)
ningaloo_climatology$sst_anom_0.5 <- calculate_hotspots(ningaloo_climatology$mmm, ningaloo_climatology$sst, anomaly=0.5) |> calculate_dhw(anomaly=0.5)
ningaloo_climatology$sst_anom_1.0 <- calculate_hotspots(ningaloo_climatology$mmm, ningaloo_climatology$sst, anomaly=1) |> 
  calculate_dhw(anomaly=1)
ningaloo_climatology$sst_anom_1.5 <- calculate_hotspots(ningaloo_climatology$mmm, ningaloo_climatology$sst, anomaly=1.5) |>
  calculate_dhw(anomaly=1.5)
ningaloo_climatology$sst_anom_2.0 <- calculate_hotspots(ningaloo_climatology$mmm, ningaloo_climatology$sst, anomaly=2) |> 
  calculate_dhw(anomaly=2)


ningaloo_OISST_dhw_anomalies <- rbind(
  data.frame(time=as.Date(time(ningaloo_climatology$sst_anom_0.0)), 
             dhw=as.vector(values(ningaloo_climatology$sst_anom_0.0))) |> mutate(anomaly="0 anom"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_anom_0.5)), 
             dhw=as.vector(values(ningaloo_climatology$sst_anom_0.5))) |> mutate(anomaly="0.5 anom"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_anom_1.0)), 
             dhw=as.vector(values(ningaloo_climatology$sst_anom_1.0))) |> mutate(anomaly="1.0 anom"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_anom_1.5)), 
             dhw=as.vector(values(ningaloo_climatology$sst_anom_1.5))) |> mutate(anomaly="1.5 anom"),
  data.frame(time=as.Date(time(ningaloo_climatology$sst_anom_2.0)), 
             dhw=as.vector(values(ningaloo_climatology$sst_anom_2.0))) |> mutate(anomaly="2.0 anom")) |> 
  mutate(year = year(time), month = month(time), day=day(time)) |> 
  mutate(anomaly=factor(anomaly, levels=c("0 anom", "0.5 anom", "1.0 anom", "1.5 anom", "2.0 anom"))) 

library(patchwork)


ref_years <- c(2011, 2025)

ningaloo_OISST_dhw_anomalies  %>%
  ungroup() %>%
  mutate(
    season_anchor = if_else(month >= 8, year + 1L, year),            # Oct–Dec belong to next year’s season
    reference_date = make_date(if_else(month >= 8, 1999L, 2000L),    # map Oct–Dec to 1999, Jan–Jun to 2000
                               month, day)
  ) %>%
  filter(season_anchor %in% ref_years) |> 


ggplot() + theme_bw() + ggtitle("2011 event") +
  facet_wrap(~season_anchor, ncol=2) +
  geom_line(aes(reference_date, dhw, group=anomaly, color=anomaly), show.legend=TRUE) + 
  scale_color_manual(values = c("0 anom" = "#2166AC", "0.5 anom" =  "#67A9CF", "1.0 anom" = "#000000", "1.5 anom" = "#EF8A62", "2.0 anom" = "#B2182B")) +
#  scale_x_date(date_breaks = "1 month", date_labels = "%b") #+
#  scale_y_continuous(limits=c(0,26), breaks=seq(0,26,4)) 
 scale_x_date(
    date_breaks = "1 month",
    date_labels = "%b",
    limits = c(as.Date("1999-11-19"), as.Date("2000-06-19"))
  )