2.6 Percentage cover of coral trait groups

Last updated

July 6, 2026

2.6 Percentage cover of coral trait groups

On this page

This page assigns each surveyed coral taxon to one of four life-history trait groups from Darling et al. (2012) (competitive, weedy, generalist, stress-tolerant), then sums percent cover within each group and averages it by site and year across the three monitoring programs (TCRMP, VINPS, CSUN). The classification follows Darling et al. (2012) with one update from more recent literature: Millepora joins the competitive life-history category, because it shares ecological traits with Acropora, the other emblematic competitive coral taxon (Cramer et al. 2021). The page produces the site-faceted trait-group cover time series in Figure 1 and the downloadable long-format table s2pt6_benthicCoverCoralTraitGroup, which the resilience and publication pages read downstream.

Data sources

This page reads the reformatted, cross-referenced benthic-cover workspace written upstream by 01_reformat_data.qmd and 02_xref_benthic_codes.qmd, which combine the three monitoring programs: TCRMP, VINPS, and CSUN. The workspace holds the per-program reformatted benthic-cover data and the shared benthic-code cross-reference that carries the trait classifications, all held to the site embargo (data through 2023). This page’s own derived output is available in the Downloads section below.

Figure 1 from Darling et al. (2012)

Load workspace and define some variables

What happens here: the page loads the reformatted, cross-referenced benthic-cover workspace from parts 1 and 2, then sets the grouping variable that drives every step below.

Show code
load("benthicCoverXrefBenthicCodes.RData")

groupingvar names the column of benthicCodes used to group taxa on this page (coralTraits), and section sets the output-file prefix (s2pt6).

Show code
# subfoldername <- "benthicCoverGrouped"
groupingvar <- "coralTraits"
section <- "s2pt6" # for naming

Reorganize benthicDat into subgroupings of interest

This step filters benthicCodes down to the coral taxa that carry a trait classification. It keeps coral rows, drops rows with no assigned trait, and unsets the CSUN Millepora trait so it is handled consistently with the other programs.

Show code
benthicCodes$tcrmp_trait[which(benthicCodes$csun_random_code == "Millepora")] <-
  NA
benthicCodes <-
  benthicCodes[which(benthicCodes$tcrmp_Category == "Coral"), ]
benthicCodes <-
  benthicCodes[-which(is.na(benthicCodes$tcrmp_trait)), ]

getBenthicDatColInds()

this is one of two functions that connect benthicDat to benthicCodes for all three programs.

getBenthicDatColInds() makes _columnInds that stores column indices of benthic codes corresponding to each item of coralTraits

arguments:

  • benthicDat is benthic dataset of interest

  • codecolumn is the name of the column of benthic code containing the code for benthicdat

  • groupingcolumn is the name of the column of benthic codes where the grouping variable of interest is stored.

Show code
getBenthicDatColInds <-
  function(benthicDat, codecolumn, groupingcolumn) {
    codecolind <-
      which(colnames(benthicCodes) == codecolumn)
    groupingcolind <-
      which(colnames(benthicCodes) == groupingcolumn)
    genusdf <-
      data.frame(group =
                   unique(benthicCodes[, groupingcolind]),
                 colinds =
                   rep(0, length(unique(benthicCodes[, groupingcolind]))))
    for (i in 1:nrow(genusdf)) {
      codei <-
        benthicCodes[which(benthicCodes[, groupingcolind] == genusdf$group[i]), ]
      genusdf$colinds[i] <-
        list(which(colnames(benthicDat) %in% codei[, codecolind]))
    }
    # UAGA-guard: warn on benthicDat data columns whose code is absent from benthicCodes and is
    # therefore silently dropped (the footgun that lost VINPS Agaricia agaricites, code UAGA).
    .known <- benthicCodes[[codecolind]]; .known <- .known[nchar(.known) > 0]
    .meta_cols <- c("program","date","site","period","replicate","replicatetype","nopts",
                    "Year","Date","SiteFullName","year","month","percentCover_allCoral",
                    "PC","Check","Notes","Transect")
    .dropped <- setdiff(colnames(benthicDat), c(.known, .meta_cols))
    if (length(.dropped) > 0)
      warning("getBenthicDatColInds(", codecolumn, "): ", length(.dropped),
              " data column(s) have codes absent from benthicCodes and are DROPPED: ",
              paste(.dropped, collapse = ", "), " -- add them to benthicCodes if they are taxa.")
    return(genusdf)
  }

The page applies getBenthicDatColInds() to each program’s benthic data, mapping every trait group to the data columns that belong to it.

Show code
tcrmp_columnInds <-
  getBenthicDatColInds(tcrmp_benthicDat, "tcrmp_Code", "tcrmp_trait")
vinps_columnInds <-
  getBenthicDatColInds(vinps_benthicDat, "vinps_TaxonCode", "tcrmp_trait")
csunrandom_columnInds <-
  getBenthicDatColInds(csun_random_benthicDat, "csun_random_code", "tcrmp_trait")
NoteValidation note

getBenthicDatColInds() resolves the trait groups competitive, generalist, stresstolerant, weedy for TCRMP. The UAGA guard inside the function warns if any data column carries a taxon code that is absent from benthicCodes (the footgun that once dropped VINPS Agaricia agaricites); watch the render log for that warning when new data or codes arrive.

makeGroupedBenthicDat()

this is the second function that connects benthicDat to benthicCodes for all three programs.

makeGroupedBenthicDat() extracts the column indices (colinds) from each row of *_columnInds.

If only one index in colinds , assigns the corresponding column from benthicdat to the i-th column of a data frame _groupedBenthicDat.

if more than one index in colinds, stores row sum of benthicDat[,colinds] in the i-th column of _groupedBenthicDat.

arguments:

  • benthicDat is benthic dataset of interest

  • columnInds is the output of getBenthicDatColInds() above that contains column indices of benthic codes for each benthic subgroup of interest

Show code
makeGroupedBenthicDat <- function(benthicDat, columnInds) {
  groupeddat <- data.frame(matrix(nrow = nrow(benthicDat),
                                  ncol = nrow(columnInds)))
  colnames(groupeddat) <- columnInds$group
  benthicDat <- benthicDat %>% dplyr::mutate(dplyr::across(dplyr::where(is.numeric), ~replace(.x, is.na(.x), 0)))
  for (i in 1:nrow(columnInds)) {
    geni <- columnInds[i,]
    datcoli <- unlist(geni$colinds)
    if (length(datcoli) == 1) {
      groupeddat[, i] <- benthicDat[, datcoli]
    } else {
      groupeddat[, i] <- rowSums(benthicDat[, datcoli])
    }
  }
  groupeddat <- cbind(benthicDat[, 1:6], groupeddat)
  return(groupeddat)
}

The page applies makeGroupedBenthicDat() to each program, producing one trait-group cover value per survey record.

Show code
tcrmp_groupedBenthicDat <-
  makeGroupedBenthicDat(tcrmp_benthicDat, tcrmp_columnInds)
vinps_groupedBenthicDat <-
  makeGroupedBenthicDat(vinps_benthicDat, vinps_columnInds)
csun_groupedBenthicDat <-
  makeGroupedBenthicDat(csun_random_benthicDat, csunrandom_columnInds)

The grouped TCRMP table now holds 4,412 survey records with the trait-group cover columns competitive, generalist, stresstolerant, weedy alongside the six survey-identifier columns.

Merge the benthicDat from three programs

now have three _groupedBenthicDat, need to merge them, start by comparing the column names, because some are missing from csun.

make sure each _groupedBenthicDat has the same column names

  • get unique column names across all data frames
Show code
all_columns <-
  unique(c(
    colnames(tcrmp_groupedBenthicDat),
    colnames(vinps_groupedBenthicDat),
    colnames(csun_groupedBenthicDat)
  ))
  • identify missing columns in each data frame
Show code
missing_columns_tcrmp <-
  setdiff(all_columns, colnames(tcrmp_groupedBenthicDat))
missing_columns_vinps <-
  setdiff(all_columns, colnames(vinps_groupedBenthicDat))
missing_columns_csung <-
  setdiff(all_columns, colnames(csun_groupedBenthicDat))
  • add missing column with NAs.
Show code
for (col in missing_columns_tcrmp) {
  tcrmp_groupedBenthicDat[[col]] <- NA
}
for (col in missing_columns_vinps) {
  vinps_groupedBenthicDat[[col]] <- NA
}
for (col in missing_columns_csung) {
  csun_groupedBenthicDat[[col]] <- NA
}
  • reorder columns to match unique column order
Show code
tcrmp_groupedBenthicDat <- tcrmp_groupedBenthicDat[, all_columns]
vinps_groupedBenthicDat <- vinps_groupedBenthicDat[, all_columns]
csun_groupedBenthicDat <- csun_groupedBenthicDat[, all_columns]
  • combine the modified data frames using rbind to make groupedBenthicDat.
Show code
groupedBenthicDat <-
  rbind(tcrmp_groupedBenthicDat,
        vinps_groupedBenthicDat,
        csun_groupedBenthicDat)
  • add column year
Show code
groupedBenthicDat <-
  cbind(year = lubridate::year(groupedBenthicDat$date),
        groupedBenthicDat)
  • melt groupedBenthicDat into long format.
Show code
groupedBenthicDat <- groupedBenthicDat |>
  tidyr::gather(!!groupingvar, "perccover", 8:ncol(groupedBenthicDat))

groupedBenthicDat <- groupedBenthicDat |>
  filter(!is.na(perccover))
  • make column pres to indicate whether coralTraits was present or absent
Show code
groupedBenthicDat$pres <- rep(0, nrow(groupedBenthicDat))
groupedBenthicDat$pres[groupedBenthicDat$perccover > 0] <- 1

Summarize and plot

For TCRMP and VINPS, this page averages transect-level percent cover to an annual site value and carries the standard error across transects. CSUN has no transect replicate at the site level, so its cover is summed as cumulative cover per site rather than transect-averaged, and its points carry no error term.

WarningSampling differs by program

The three programs do not share a sampling structure. TCRMP and VINPS survey replicate transects per site, so their site-year values are transect means with a standard error. CSUN has no transect replicate, so its cover is cumulative per site and shows no error bar. The programs also span different windows (TCRMP and VINPS begin in the 1990s, CSUN records reach back to 1987). Read across-program comparisons in Figure 1 with these differences in mind. The error bars are within-program only, never pooled across programs.

Annual percentage coral cover

The first summary collapses trait-group cover to total coral cover per transect, then averages transects to a site-year mean with a standard error. These are the black total-cover points in Figure 1.

Show code
totaldatCovSum <- groupedBenthicDat |>
  dplyr::group_by(year, date, program, site, period, replicate) |>
  dplyr::summarise(perccover = sum(perccover))

totaldatCovSum <- totaldatCovSum |>
  dplyr::group_by(year, date, program, site) |>
  dplyr::summarise(
    meancov = mean(perccover),
    sdcov = sd(perccover),
    secov = sd(perccover) / (sqrt(length(perccover))),
    n = length(perccover)
  )

totaldatCovSum <- totaldatCovSum %>% dplyr::mutate(dplyr::across(dplyr::where(is.numeric), ~replace(.x, is.na(.x), 0)))

Annual percentage cover of coral traits

The second summary keeps the trait groups separate, giving the stacked areas in Figure 1: mean cover per trait group per site and year.

Show code
groupedBenthicDatCovSum <- groupedBenthicDat |>
  dplyr::group_by(year, date, program, site, period, coralTraits) |>
  dplyr::summarise(
    meancov = mean(perccover),
    sdcov = sd(perccover),
    secov = sd(perccover) / (sqrt(length(perccover))),
    n = length(perccover)
  )
groupedBenthicDatCovSum <- groupedBenthicDatCovSum %>% dplyr::mutate(dplyr::across(dplyr::where(is.numeric), ~replace(.x, is.na(.x), 0)))

Plot

Figure 1 shows how coral cover splits among the four life-history trait groups at each site through time. Reading a site’s stacked area shows whether its coral is dominated by competitive, weedy, generalist, or stress-tolerant taxa, and whether that mix shifts after disturbance. Sites are ordered shallow to deep, and the dashed line marks the 2005 bleaching event.

Show code
#add site info so can plot according to increasing depth
groupedBenthicDatCovSum <-
  merge(groupedBenthicDatCovSum, sitedat, by = "site")
groupedBenthicDatCovSum <-
  groupedBenthicDatCovSum[order(groupedBenthicDatCovSum$depth), ]
groupedBenthicDatCovSum$site <- factor(groupedBenthicDatCovSum$site,
                                       levels = unique(groupedBenthicDatCovSum$site))

#add site info so can plot according to increasing depth
totaldatCovSum <-
  merge(totaldatCovSum, sitedat, by = "site")
totaldatCovSum <-
  totaldatCovSum[order(totaldatCovSum$depth), ]
totaldatCovSum$site <- factor(totaldatCovSum$site,
                              levels = unique(totaldatCovSum$site))

create_stacked_area_plot <- function(data_stacked, data_total,
                                                x_var = "year", y_var = "meancov", 
                                                fill_var = "coralTraits", facet_var = "site",
                                                error_var = "secov",
                                                min_year = NULL, max_year = NULL,
                                                highlight_year = NULL,
                                                max_facets_per_page = 35) {
  
  # Determine x-axis limits if not provided
  if (is.null(min_year))
    min_year <- floor(min(data_stacked[[x_var]]))
  if (is.null(max_year))
    max_year <- ceiling(max(data_stacked[[x_var]]))

  # Calculate optimal number of rows and columns
  n_facets <- length(unique(data_stacked[[facet_var]]))
  n_facets <- min(n_facets, max_facets_per_page)  # Limit to max_facets_per_page
  n_col <- ceiling(sqrt(n_facets))
  n_row <- ceiling(n_facets / n_col)
  
  # Create base plot
  p <- ggplot() +
    geom_area(data = data_stacked,
              aes(x = .data[[x_var]], y = .data[[y_var]], fill = .data[[fill_var]]),
              position = 'stack') +
    geom_point(
      data = data_total,
      aes(x = .data[[x_var]], y = .data[[y_var]]),
      color = "black",
      size = 0.5
    ) +
    geom_linerange(
      data = data_total,
      aes(
        x = .data[[x_var]],
        ymin = .data[[y_var]] - .data[[error_var]],
        ymax = .data[[y_var]] + .data[[error_var]]
      ),
      color = "black",
      linewidth = 0.5
    ) +
    facet_wrap(vars(.data[[facet_var]]), nrow = n_row, ncol = n_col, scales = "free_x") +
    labs(y = "Percent cover", x = toupper(x_var)) +
    scale_y_continuous(expand = c(0, 0)) +
    scale_x_continuous(
      expand = expansion(mult = 0.04),
      breaks = seq(min_year, max_year, by = 1),
      labels = function(x)
        ifelse(x %% 5 == 0, paste0("'", substr(x, 3, 4)), "")
    ) +
    theme_bw() +
    theme(
      strip.text = element_text(size = 10),
      legend.position = "bottom",
      panel.grid = element_blank()
    )
  
  # Add highlight line if specified
  if (!is.null(highlight_year)) {
    p <- p + geom_vline(
      xintercept = highlight_year,
      color = "gray50",
      alpha = 0.5,
      linetype = "dashed"
    )
  }
  
  return(p)
}

# Usage:
stack <- create_stacked_area_plot(
  data_stacked = groupedBenthicDatCovSum,
  data_total = totaldatCovSum,
  min_year = minyear,
  max_year = maxyear,
  highlight_year = 2005,
  fill_var = "coralTraits",
  max_facets_per_page = length(unique(groupedBenthicDatCovSum$site))
)

# Display the plot
print(stack)
Sec 2.6 Figure 1: Percent cover of coral trait groups over time at each site, across TCRMP, VINPS, and CSUN. Stacked areas are the four life-history trait groups (competitive, generalist, stress-tolerant, weedy). Black points show mean total coral cover with SE for the transect-replicated programs (TCRMP, VINPS); CSUN shows cumulative cover with no error term. Sites are ordered shallow to deep. The dashed line marks the 2005 bleaching event.
TipKey result

Trait-group composition tracks each site’s disturbance history. Where a site’s stacked area thins after 2005 and refills with weedy or stress-tolerant cover rather than competitive cover, the coral community has shifted toward a lower-relief, disturbance-associated state. Figure 1 makes that shift visible site by site and program by program.

Downloads

This page writes the coral trait-group cover product s2pt6_benthicCoverCoralTraitGroup (one row per site, year, and trait group) plus its metadata. The download links below are generated from the same object the page computes, so the file and its provenance travel together.

Interactive-viz candidate

Figure 1 faces 49 sites in one static frame. A Shiny explorer would let a reader pick sites, programs, and a year window and read a small, legible facet set instead of the full wall.

  • Purpose: let a reader compare coral trait-group cover across selected sites without scrolling a 49-facet figure.
  • Data: s2pt6_benthicCoverCoralTraitGroup (this page’s output) joined to site depth and program.
  • Controls: program filter (TCRMP / VINPS / CSUN), site multi-select (defaulting to a depth band), year-range slider, and a stacked-vs-proportional toggle.
  • Main output: the stacked-area trait-group plot for the chosen sites, with the total-cover points and SE shown only for the transect-replicated programs.
  • Value: replaces the oversized static facet grid and makes the program sampling differences explicit at the point of comparison.
  • Priority: high (this figure is one of the site’s largest, and the output already feeds it directly).

version 1.0.0 • in-review • data ≤ 2023

References

Cramer, Katie L., Mary K. Donovan, Jeremy B. C. Jackson, Benjamin J. Greenstein, Chelsea A. Korpanty, Geoffrey M. Cook, and John M. Pandolfi. 2021. “The Transformation of Caribbean Coral Communities Since Humans.” Ecology and Evolution 11 (15): 10098–118. https://doi.org/10.1002/ece3.7808.
Darling, Emily S., Lorenzo Alvarez-Filip, Thomas A. Oliver, Timothy R. McClanahan, and Isabelle M. Côté. 2012. “Evaluating Life-History Strategies of Reef Corals from Species Traits.” Ecology Letters 15 (12): 13781386. https://doi.org/10.1111/j.1461-0248.2012.01861.x.