Skip to contents

Overview

This vignette demonstrates two gt features that zztable1 does not currently support: hierarchical column spanners and summary rows with subtotals and grand totals.

Column Spanners

Column spanners group columns under shared headers. gt supports multi-level spanners – spanners that contain other spanners – producing hierarchical column headers.

Single-level spanners

trial_summary <- data.frame(
  variable = c("Age", "BMI", "SBP"),
  placebo_mean = c(63.2, 26.8, 132.4),
  placebo_sd = c(11.5, 4.2, 18.1),
  treatment_mean = c(61.8, 27.1, 128.9),
  treatment_sd = c(12.3, 4.8, 16.7),
  p_value = c(0.403, 0.538, 0.042)
)

trial_summary |>
  gt(rowname_col = "variable") |>
  tab_spanner(
    label = "Placebo (N=53)",
    columns = c(placebo_mean, placebo_sd)
  ) |>
  tab_spanner(
    label = "Treatment (N=67)",
    columns = c(treatment_mean, treatment_sd)
  ) |>
  cols_label(
    placebo_mean = "Mean",
    placebo_sd = "SD",
    treatment_mean = "Mean",
    treatment_sd = "SD",
    p_value = "P-value"
  ) |>
  fmt_number(decimals = 1) |>
  fmt_number(columns = p_value, decimals = 3) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_body(
      columns = p_value,
      rows = p_value < 0.05
    )
  ) |>
  tab_header(
    title = "Baseline Characteristics",
    subtitle = "Single-level column spanners"
  )
Baseline Characteristics
Single-level column spanners
Placebo (N=53)
Treatment (N=67)
P-value
Mean SD Mean SD
Age 63.2 11.5 61.8 12.3 0.403
BMI 26.8 4.2 27.1 4.8 0.538
SBP 132.4 18.1 128.9 16.7 0.042

Multi-level (nested) spanners

Spanners can contain other spanners. Here the top-level spanner ‘Vital Signs’ groups two sub-spanners, each of which groups its own columns.

vitals <- data.frame(
  visit = c("Baseline", "Week 4", "Week 8", "Week 12"),
  sbp_placebo = c(132.4, 131.8, 130.2, 129.5),
  dbp_placebo = c(84.1, 83.6, 82.9, 82.4),
  sbp_treatment = c(128.9, 124.3, 120.1, 118.6),
  dbp_treatment = c(82.7, 80.1, 78.4, 76.9),
  hr_placebo = c(72.3, 71.8, 72.0, 71.5),
  hr_treatment = c(73.1, 72.4, 71.8, 71.2)
)

vitals |>
  gt(rowname_col = "visit") |>
  tab_spanner(
    label = "Placebo",
    columns = c(sbp_placebo, dbp_placebo),
    id = "bp_placebo"
  ) |>
  tab_spanner(
    label = "Treatment",
    columns = c(sbp_treatment, dbp_treatment),
    id = "bp_treatment"
  ) |>
  tab_spanner(
    label = "Blood Pressure",
    spanners = c("bp_placebo", "bp_treatment"),
    id = "bp_top"
  ) |>
  tab_spanner(
    label = "Heart Rate",
    columns = c(hr_placebo, hr_treatment)
  ) |>
  cols_label(
    sbp_placebo = "SBP",
    dbp_placebo = "DBP",
    sbp_treatment = "SBP",
    dbp_treatment = "DBP",
    hr_placebo = "Placebo",
    hr_treatment = "Treatment"
  ) |>
  fmt_number(decimals = 1) |>
  tab_stubhead(label = "Visit") |>
  tab_header(
    title = "Vital Signs by Visit",
    subtitle = "Three-level column hierarchy: "
  ) |>
  tab_source_note(
    "Blood Pressure > Heart Rate > Arm"
  )
Vital Signs by Visit
Three-level column hierarchy:
Blood Pressure
Visit
Placebo
Treatment
Heart Rate
SBP DBP SBP DBP Placebo Treatment
Baseline 132.4 84.1 128.9 82.7 72.3 73.1
Week 4 131.8 83.6 124.3 80.1 71.8 72.4
Week 8 130.2 82.9 120.1 78.4 72.0 71.8
Week 12 129.5 82.4 118.6 76.9 71.5 71.2
Blood Pressure > Heart Rate > Arm

Delimiter-based spanners

When column names follow a delimiter convention, gt can auto-generate the spanner structure.

labs <- data.frame(
  patient = paste("Patient", 1:5),
  hematology.wbc = c(6.2, 7.1, 5.8, 8.4, 6.9),
  hematology.rbc = c(4.5, 4.8, 4.2, 5.1, 4.6),
  hematology.platelets = c(245, 310, 198, 276, 255),
  chemistry.glucose = c(98, 112, 95, 140, 103),
  chemistry.creatinine = c(0.9, 1.1, 0.8, 1.4, 1.0),
  chemistry.alt = c(22, 35, 18, 48, 26)
)

labs |>
  gt(rowname_col = "patient") |>
  tab_spanner_delim(delim = ".") |>
  fmt_number(
    columns = c(
      hematology.wbc, hematology.rbc,
      chemistry.creatinine
    ),
    decimals = 1
  ) |>
  fmt_number(
    columns = c(
      hematology.platelets,
      chemistry.glucose, chemistry.alt
    ),
    decimals = 0
  ) |>
  tab_stubhead(label = "Patient") |>
  tab_header(
    title = "Laboratory Results",
    subtitle = "Spanners generated from column name delimiters"
  )
Laboratory Results
Spanners generated from column name delimiters
Patient
hematology
chemistry
wbc rbc platelets glucose creatinine alt
Patient 1 6.2 4.5 245 98 0.9 22
Patient 2 7.1 4.8 310 112 1.1 35
Patient 3 5.8 4.2 198 95 0.8 18
Patient 4 8.4 5.1 276 140 1.4 48
Patient 5 6.9 4.6 255 103 1.0 26

Summary Rows

gt computes per-group subtotals and table-wide grand totals using arbitrary aggregation functions. These summary rows are lazy-evaluated at render time.

Per-group summary rows

enrollment <- data.frame(
  site = c(
    rep("Site A", 3), rep("Site B", 3), rep("Site C", 3)
  ),
  month = rep(c("Jan", "Feb", "Mar"), 3),
  screened = c(45, 52, 38, 30, 28, 35, 22, 25, 31),
  enrolled = c(32, 41, 28, 22, 20, 27, 15, 18, 24),
  excluded = c(13, 11, 10, 8, 8, 8, 7, 7, 7)
)

enrollment |>
  gt(
    rowname_col = "month",
    groupname_col = "site"
  ) |>
  summary_rows(
    fns = list(
      Total = ~ sum(.),
      Mean = ~ round(mean(.), 1)
    ),
    side = "bottom"
  ) |>
  tab_stubhead(label = "Month") |>
  tab_header(
    title = "Enrollment by Site",
    subtitle = "Per-group totals and means"
  ) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_summary()
  )
Enrollment by Site
Per-group totals and means
Month screened enrolled excluded
Site A
Jan 45 32 13
Feb 52 41 11
Mar 38 28 10
Total 135 101.0 34.0
Mean 45 33.7 11.3
Site B
Jan 30 22 8
Feb 28 20 8
Mar 35 27 8
Total 93 69.0 24.0
Mean 31 23.0 8.0
Site C
Jan 22 15 7
Feb 25 18 7
Mar 31 24 7
Total 78 57.0 21.0
Mean 26 19.0 7.0

Grand summary rows

enrollment |>
  gt(
    rowname_col = "month",
    groupname_col = "site"
  ) |>
  summary_rows(
    fns = list(`Site Total` = ~ sum(.)),
    side = "bottom"
  ) |>
  grand_summary_rows(
    fns = list(`Grand Total` = ~ sum(.)),
    side = "bottom"
  ) |>
  tab_header(
    title = "Enrollment by Site",
    subtitle = "Subtotals and grand total"
  ) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_summary()
  ) |>
  tab_style(
    style = list(
      cell_text(weight = "bold"),
      cell_fill(color = "#e8e8e8")
    ),
    locations = cells_grand_summary()
  )
Enrollment by Site
Subtotals and grand total
screened enrolled excluded
Site A
Jan 45 32 13
Feb 52 41 11
Mar 38 28 10
Site Total 135 101 34
Site B
Jan 30 22 8
Feb 28 20 8
Mar 35 27 8
Site Total 93 69 24
Site C
Jan 22 15 7
Feb 25 18 7
Mar 31 24 7
Site Total 78 57 21
Grand Total 306 227 79

Custom aggregation functions

Summary rows accept any function. Here we compute the median alongside the sum.

ae_data <- data.frame(
  system = c(
    rep("Gastrointestinal", 4),
    rep("Neurological", 3),
    rep("Dermatological", 3)
  ),
  event = c(
    "Nausea", "Vomiting", "Diarrhea", "Abdominal pain",
    "Headache", "Dizziness", "Insomnia",
    "Rash", "Pruritus", "Alopecia"
  ),
  placebo_n = c(12, 5, 8, 3, 15, 7, 4, 6, 3, 1),
  treatment_n = c(18, 9, 14, 6, 12, 5, 3, 11, 7, 2)
)

ae_data |>
  gt(
    rowname_col = "event",
    groupname_col = "system"
  ) |>
  cols_label(
    placebo_n = "Placebo",
    treatment_n = "Treatment"
  ) |>
  summary_rows(
    fns = list(
      Total = ~ sum(.),
      Median = ~ median(.)
    ),
    side = "bottom"
  ) |>
  grand_summary_rows(
    fns = list(`All Events` = ~ sum(.)),
    side = "bottom"
  ) |>
  tab_spanner(
    label = "Number of Events",
    columns = c(placebo_n, treatment_n)
  ) |>
  tab_header(
    title = "Adverse Events by System Organ Class",
    subtitle = "Per-group summaries with custom aggregation"
  ) |>
  tab_style(
    style = list(
      cell_text(weight = "bold"),
      cell_fill(color = "#d4e8d4")
    ),
    locations = cells_grand_summary()
  )
Adverse Events by System Organ Class
Per-group summaries with custom aggregation
Number of Events
Placebo Treatment
Gastrointestinal
Nausea 12 18
Vomiting 5 9
Diarrhea 8 14
Abdominal pain 3 6
Total 28.0 47.0
Median 6.5 11.5
Neurological
Headache 15 12
Dizziness 7 5
Insomnia 4 3
Total 26.0 20.0
Median 7.0 5.0
Dermatological
Rash 6 11
Pruritus 3 7
Alopecia 1 2
Total 10.0 20.0
Median 3.0 7.0
All Events 64 87

Implications for zztable1

These examples illustrate two structural capabilities that zztable1’s flat blueprint does not currently support:

  • Column spanners require a tree structure for column headers rather than a flat character vector. Implementing this would require changes to blueprint$col_names and the header rendering logic in all three output formats.

  • Summary rows require the ability to insert computed rows at group boundaries, with aggregation functions that operate on the group’s data. This would extend the current row model beyond variable-header and level rows.

Both features could be added to zztable1’s blueprint architecture. Alternatively, a bridge function that converts an evaluated zztable1 blueprint to a gt-compatible data frame would give users access to gt’s full rendering capabilities without reimplementing them.