Creating hierarchical, nested axis labels in ggplot2 is one of the best ways to present grouped experimental data. However, if you are using modern guide extension packages like legendry or ggh4x alongside newer versions of ggplot2 (3.5.0+), you may run into a confusing error:

Error:
! object 'decor' not found

In this guide, we will unpack why this error occurs and walk through multiple robust solutions to achieve clean, nested axis brackets for your categorical data.


Why Does the 'decor' Not Found Error Occur?

This error typically stems from one of three issues interacting at once:

  1. Syntax Mismatch between legendry and ggh4x: Arguments like levels_text and levels_brackets belong to ggh4x::guide_axis_nested(). In legendry (a newer package developed by the author of ggh4x to align with ggplot2 >= 3.5.0's revamped guide system), styling is handled via primitives (such as bracket = primitive_bracket()). Passing ggh4x parameters to legendry triggers internal rendering failures where decor cannot resolve.
  2. Conflict Between facet_wrap() and Nested Guides: You are faceting by sample while also trying to nest by sample along the x-axis. Axis guides are calculated per panel. When combined with scales = "free_x", the guide layout builder tries to evaluate brackets across panels where categories do not span multiple positions, breaking the decoration calculation.
  3. Interaction Factor Ordering: To display nested axis levels correctly, the innermost level (the subcategory, e.g., experiment) must come first, followed by the outer group (e.g., sample). Using interaction(sample, experiment) inverts the hierarchy.

Solution 1: Use ggh4x::guide_axis_nested() (Single Panel)

If your goal is to have grouped brackets along a unified x-axis, remove facet_wrap() and use ggh4x::guide_axis_nested(). Note the order inside interaction(experiment, sample):

library(ggplot2)
library(dplyr)
library(ggpattern)
library(ggh4x)

# Filter subsets
df_none <- uf_vcf_m |> filter(filters == "none")
df_qual <- uf_vcf_m |> filter(filters != "none")

ggplot(df_none, aes(x = interaction(experiment, sample), y = count)) +
  geom_col(
    aes(fill = sample, alpha = experiment),
    color = "black",
    position = "dodge2"
  ) +
  geom_col_pattern(
    data = df_qual,
    aes(x = interaction(experiment, sample), y = count, pattern = "stripe"),
    fill = NA,
    color = "black",
    pattern_angle = 30,
    pattern_size = 0.075,
    pattern_spacing = 0.0075,
    position = "dodge2"
  ) +
  scale_alpha_manual(values = c(0.2, 0.4, 0.6, 0.8, 1)) +
  scale_pattern_manual(name = "filters", values = "stripe", labels = "QUAL > 0") +
  guides(
    x = ggh4x::guide_axis_nested(
      levels_text = list(
        element_text(size = 9),
        element_text(size = 11, face = "bold")
      ),
      levels_brackets = list(
        element_line(linewidth = 0.375),
        element_line(linewidth = 0.125)
      )
    )
  ) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  theme_classic() +
  theme(
    ggh4x.axis.nestline = element_line(color = "black"),
    legend.position = "top",
    axis.title = element_blank()
  )

Solution 2: Use Modern legendry Syntax (ggplot2 >= 3.5.0)

If you prefer to stay on modern legendry, construct nested guides using guide_axis_nested() with its updated primitives. In legendry, brackets are configured using primitive_bracket() rather than levels_brackets:

library(ggplot2)
library(dplyr)
library(ggpattern)
library(legendry)

ggplot(df_none, aes(x = interaction(experiment, sample), y = count)) +
  geom_col(
    aes(fill = sample, alpha = experiment),
    color = "black",
    position = "dodge2"
  ) +
  geom_col_pattern(
    data = df_qual,
    aes(x = interaction(experiment, sample), y = count),
    pattern = "stripe",
    fill = NA,
    color = "black",
    position = "dodge2"
  ) +
  scale_alpha_manual(values = c(0.2, 0.4, 0.6, 0.8, 1)) +
  guides(
    x = legendry::guide_axis_nested(
      delim = ".",
      bracket = primitive_bracket(colour = "black", linewidth = 0.5)
    )
  ) +
  theme_classic() +
  theme(
    legend.position = "top",
    axis.title = element_blank()
  )

Solution 3: Native Facet Bottom Strips (No Guide Extensions Needed)

In many scientific workflows, faceting is preferred over nesting on a continuous axis because it visually isolates independent clones. You can create a visually nested appearance natively in ggplot2 by placing the facet strip on the bottom:

ggplot(df_none, aes(x = experiment, y = count)) +
  geom_col(
    aes(fill = sample, alpha = experiment),
    color = "black",
    position = "dodge2"
  ) +
  geom_col_pattern(
    data = df_qual,
    aes(x = experiment, y = count),
    pattern = "stripe",
    fill = NA,
    color = "black",
    position = "dodge2"
  ) +
  facet_wrap(~sample, nrow = 1, strip.position = "bottom", scales = "free_x") +
  theme_classic() +
  theme(
    strip.placement = "outside",
    strip.background = element_rect(fill = "grey90", color = "black"),
    strip.text = element_text(face = "bold", size = 11),
    panel.spacing = unit(0, "lines"), # Glues panels together like nested labels
    axis.title.x = element_blank()
  )

Summary of Best Practices

  • Check package versions: If you use ggh4x functions, import from ggh4x; do not mix parameters with legendry.
  • Order your factor interactions: Always place the lower-level variable first: interaction(inner_var, outer_var).
  • Avoid combining scales = "free_x" with nested guide axes: Either drop faceting completely to allow brackets to render, or let facet strips represent your higher-level hierarchy.