P-values from Tests on ggplots in R (rstatix)

Compute a test with rstatix, position the brackets, draw them on any ggpubr plot — basic, grouped, faceted & horizontal

Data Visualization

Add p-values from real statistical tests to a ggplot in R with rstatix and ggpubr. The workflow is always the same: compute a test with t_test() / wilcox_test() / anova_test(), adjust for multiplicity with adjust_pvalue(), auto-place the brackets with add_xy_position(), then draw them with stat_pvalue_manual() — on basic, grouped, faceted, free-scale and horizontal plots. Every example is rendered for you and runs live.

Published

June 22, 2026

Modified

July 12, 2026

TipKey takeaways
  • The workflow is three steps, always the same: compute the test with rstatix (t_test(), wilcox_test(), anova_test()), position the brackets with add_xy_position(), then draw them with stat_pvalue_manual().
  • Get adjusted p-values with adjust_pvalue() + add_significance() — then label with "p.adj" or the stars "p.adj.signif".
  • Grouped plots: group_by() the test, then add_xy_position(dodge = 0.8) so brackets line up with the dodged boxes.
  • Faceted plots: group_by() the facet variable; for panels with different y-axes add scales = "free" to both add_xy_position() and the facet.
  • Horizontal plots: pass coord.flip = TRUE to stat_pvalue_manual() and add coord_flip().
  • Every plot below is rendered for you — then tweak any example live in the sandbox.
Get the book — R Graphics Essentials (PDF)

Introduction

The companion lesson Add p-values to ggplots uses one call, stat_compare_means(), that computes and draws the test together — perfect for a quick annotation. But the moment you need adjusted p-values on the brackets, a specific test (anova_test(), tukey_hsd()), or p-values on a grouped / faceted / horizontal plot, you want the test as a data frame you control, then drawn separately.

That is the rstatix → ggpubr workflow, and it never changes:

  1. Compute the test with rstatixt_test(), wilcox_test(), anova_test() (see the biostatistics guides for when to use each) — and optionally adjust_pvalue() + add_significance() for multiplicity-corrected p-values and stars.
  2. Position the brackets automatically with add_xy_position() — it reads the plot type (box, bar, dodged, faceted) and computes a sensible y.position for every comparison.
  3. Draw them on the plot with stat_pvalue_manual() [ggpubr] — point its label at any column of your test ("p", "p.adj", "p.adj.signif").

This lesson walks that workflow on the built-in ToothGrowth data, through every layout people actually search for: basic comparisons, grouped (dodged) plots, faceted panels, facets with free / different scales, and horizontal plots. Each figure is rendered right here, and you can re-run or edit any of them live.

If you only need a quick global or pairwise p-value with no adjustment, start with stat_compare_means() — it’s the simpler entry point. For the modern one-layer shortcut that computes and positions in a single call, see Auto p-values with geom_pwc().

The canonical workflow: compute → position → draw

Here is the image most people come for: a box plot of tooth length by dose with a bracket and an adjusted significance star over each pair of doses. Read it top to bottom — the three steps are labelled in the code.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# 1. COMPUTE: pairwise t-tests, p-values adjusted for multiplicity
stat.test <- df %>%
  t_test(len ~ dose) %>%
  adjust_pvalue(method = "holm") %>%
  add_significance("p.adj")

# 2. POSITION: auto-place the brackets for this box plot
stat.test <- stat.test %>% add_xy_position(x = "dose")

# 3. DRAW: put the adjusted stars on the plot
ggboxplot(df, x = "dose", y = "len", fill = "dose", palette = "jco") +
  stat_pvalue_manual(stat.test, label = "p.adj.signif", tip.length = 0.01)

A ggpubr box plot of tooth length by dose with three pairwise comparison brackets (0.5 vs 1, 1 vs 2, 0.5 vs 2) carrying adjusted significance stars computed with rstatix t_test.

Everything else in this lesson is a variation on those three steps. Let’s build them up.

Basic comparisons: two groups and a clean label

Start with the simplest case — do the two supplements (OJ vs VC) differ? Compute a t_test() (use wilcox_test() for the non-parametric version — same call), add the position, and draw the raw p-value. add_significance() appends the p.signif stars column for later.

library(ggpubr)
library(rstatix)
df <- ToothGrowth

# Compute: t-test of len by supplement
stat.test <- df %>%
  t_test(len ~ supp) %>%
  add_significance()
stat.test
# A tibble: 1 × 9
  .y.   group1 group2    n1    n2 statistic    df      p p.signif
  <chr> <chr>  <chr>  <int> <int>     <dbl> <dbl>  <dbl> <chr>   
1 len   OJ     VC        30    30      1.92  55.3 0.0606 ns      
# Position + draw
stat.test <- stat.test %>% add_xy_position(x = "supp")
ggboxplot(df, x = "supp", y = "len", fill = "#3a86d4") +
  stat_pvalue_manual(stat.test, label = "p") +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.1)))

A ggpubr box plot of tooth length by supplement (OJ vs VC) with a single bracket carrying the t-test p-value.

The columns you’ll label with are worth knowing: p (raw p-value), p.adj (adjusted), p.signif / p.adj.signif (the stars), and y.position (added by add_xy_position()).

Labels are partially hidden by the top border? Add headroom with scale_y_continuous(expand = expansion(mult = c(0.05, 0.1))) — 5% padding at the bottom, 10% at the top, so the brackets aren’t clipped.

Choosing and formatting the label

Point label at whichever column you want — or build a custom one. A glue template like "T-test, p = {p}" interpolates any column into the label; you can also format the numbers yourself.

library(ggpubr)
library(rstatix)
df <- ToothGrowth

stat.test <- df %>% t_test(len ~ supp)
stat.test <- stat.test %>% add_xy_position(x = "supp")

ggboxplot(df, x = "supp", y = "len", fill = "#3a86d4") +
  stat_pvalue_manual(
    stat.test, label = "T-test, p = {p}",
    vjust = -1, bracket.nudge.y = 1
    ) +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.15)))

A ggpubr box plot of tooth length by supplement with a glue-formatted label reading T-test, p = the p-value.

A few label recipes you’ll reuse:

  • Formatted p-value with controlled accuracy — p_format(stat.test$p, accuracy = 0.01, leading.zero = FALSE) into a new column, then label = "p.format" (renders <.01).
  • Scientific notationformat(stat.test$p, scientific = TRUE) into a column, then label that.
  • Show p if significant else “ns” — build a custom column with ifelse(stat.test$p <= 0.05, stat.test$p, "ns") and label it.

Paired samples

If the same subjects were measured under both conditions, pass paired = TRUE to the test so it accounts for the pairing. ggpaired() draws the matched observations with connecting lines.

library(ggpubr)
library(rstatix)
df <- ToothGrowth

stat.test <- df %>%
  t_test(len ~ supp, paired = TRUE) %>%
  add_significance()

stat.test <- stat.test %>% add_xy_position(x = "supp")
ggpaired(df, x = "supp", y = "len", fill = "#E7B800",
         line.color = "gray", line.size = 0.4) +
  stat_pvalue_manual(stat.test, label = "{p}{p.signif}")

A ggpubr paired plot of tooth length by supplement with gray lines connecting matched observations and a paired t-test star annotated above.

Pairwise across three groups + a reference group

For three or more groups, t_test(len ~ dose) runs all pairwise comparisons and adjusts the p-values automatically. Often you instead want each group against one reference (a control) — set ref.group. The tip.length argument shortens the little vertical bracket tips.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

bxp <- ggboxplot(df, x = "dose", y = "len", fill = "dose", palette = "jco")

# All pairwise comparisons
all.pairs <- df %>% t_test(len ~ dose) %>% add_xy_position(x = "dose")
p1 <- bxp + stat_pvalue_manual(all.pairs, label = "p.adj.signif", tip.length = 0.01)

# Each dose vs the reference 0.5
ref <- df %>% t_test(len ~ dose, ref.group = "0.5") %>% add_xy_position(x = "dose")
p2 <- bxp + stat_pvalue_manual(ref, label = "p.adj.signif", tip.length = 0.01)

ggarrange(p1, p2, ncol = 2)

Two ggpubr box plots side by side: left shows all pairwise dose comparisons with stars, right compares every dose to the reference 0.5.

Two reference shortcuts worth knowing:

  • ref.group = "all" — compares each group to all the data pooled (the grand mean / base-mean), a clean way to flag which groups stand out.
  • One-sample testt_test(len ~ 1) (optionally group_by()’d) compares each group’s mean to a hypothetical mu (default 0).

You can also override the auto-placement: pass y.position = c(35, 40, 35) and bracket.shorten = 0.05 straight to stat_pvalue_manual() to hand-tune crowded brackets.

The omnibus test: ANOVA / Kruskal-Wallis

Before the pairwise brackets, you often want the global (“is anything different?”) test. rstatix gives it to you as a tidy frame too: anova_test() for parametric, kruskal_test() for non-parametric. Annotate it as a subtitle with get_test_label() while the brackets carry the pairwise results.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# Global one-way ANOVA
aov.test <- df %>% anova_test(len ~ dose)

# Pairwise comparisons for the brackets
pwc <- df %>%
  t_test(len ~ dose) %>%
  add_xy_position(x = "dose")

ggboxplot(df, x = "dose", y = "len", fill = "dose", palette = "jco") +
  stat_pvalue_manual(pwc, label = "p.adj.signif", tip.length = 0.01) +
  labs(subtitle = get_test_label(aov.test, detailed = TRUE))

A ggpubr box plot of tooth length by dose with three pairwise brackets and a global ANOVA p-value printed in the subtitle.

Swap anova_test() for kruskal_test() (and t_test() for wilcox_test()) to get the non-parametric pair — the rest of the call is identical.

Grouped (dodged) plots

When colour splits each x-position into several dodged boxes, the test must be grouped and the brackets must follow the dodge. Group the data by dose, compare the supp levels within each, then tell add_xy_position() the same dodge you used on the plot.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# Compute within each dose: OJ vs VC
stat.test <- df %>%
  group_by(dose) %>%
  t_test(len ~ supp) %>%
  adjust_pvalue(method = "bonferroni") %>%
  add_significance("p.adj")

# Position with the SAME dodge as the plot
stat.test <- stat.test %>% add_xy_position(x = "dose", dodge = 0.8)

ggboxplot(df, x = "dose", y = "len",
          color = "supp", palette = c("#00AFBB", "#E7B800")) +
  stat_pvalue_manual(stat.test, label = "p.adj", tip.length = 0) +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.1)))

A grouped ggpubr box plot of tooth length by dose, dodged by supplement, with an adjusted p-value bracket within each dose.

Useful options on the grouped layout:

  • remove.bracket = TRUE — drop the bracket and print just the label (handy when boxes are narrow).
  • hide.ns = TRUE — hide non-significant annotations; combine with a combined label "{p.adj}{p.adj.signif}".
  • Bar / line plots — for ggbarplot(..., add = "mean_sd") or ggline(..., add = "mean_sd"), pass the matching fun = "mean_sd" to add_xy_position() so the bracket starts above the error bars.

Pairwise brackets grouped by colour

To draw multiple pairwise brackets per group, group the test by the x variable, then stack the brackets within each group with step.group.by + step.increase.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# Within each supplement, compare the doses
pwc <- df %>%
  group_by(supp) %>%
  t_test(len ~ dose, p.adjust.method = "bonferroni")

pwc <- pwc %>% add_xy_position(x = "supp", group = "dose", dodge = 0.8)

ggboxplot(df, x = "supp", y = "len", color = "dose", palette = "npg") +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.25))) +
  stat_pvalue_manual(
    pwc, step.group.by = "supp",
    tip.length = 0, step.increase = 0.1
    )

A grouped ggpubr box plot of tooth length by supplement, coloured by dose, with pairwise dose brackets coloured and stacked per supplement group.

Faceted plots

For a multipanel plot, group the test by the facet variable so each panel gets its own comparison, then position and draw exactly as before — stat_pvalue_manual() matches each test row to its panel.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

# One test per facet (dose), comparing OJ vs VC
stat.test <- df %>%
  group_by(dose) %>%
  t_test(len ~ supp) %>%
  adjust_pvalue(method = "bonferroni") %>%
  add_significance()

stat.test <- stat.test %>% add_xy_position(x = "supp")

ggboxplot(df, x = "supp", y = "len", fill = "#00AFBB", facet.by = "dose") +
  stat_pvalue_manual(stat.test, hide.ns = TRUE, label = "{p.adj}{p.adj.signif}") +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.1)))

A ggpubr box plot of tooth length by supplement, faceted by dose, with an adjusted p-value bracket in each panel.

The same recipe scales up:

  • Pairwise within a facet — when each panel holds 3+ groups, group_by(facet_var) then add_y_position() (the y-only helper) instead of add_xy_position().
  • facet_grid by two variablesgroup_by(var1, var2) the test and facet = c("var1", "var2") the plot; everything else is unchanged.
  • Bar plots in facetsggbarplot(..., add = "mean_sd") with add_xy_position(fun = "mean_sd") (use fun = "max" if you also add jitter, so brackets clear the points).

Faceted plots with free / different scales

When panels have very different y-ranges, fixed scales squash some panels. Setting scales = "free" fixes the plot — but the brackets must be repositioned for it too: pass scales = "free" to add_xy_position() as well, and use facet_wrap() so each panel frees both axes. Here we use tukey_hsd() (Tukey post-hoc) for the pairwise tests.

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)
df$group <- factor(rep(c("grp1", "grp2"), 30))
# Make the panels genuinely different in scale
df[c(1, 3, 5), "len"] <- c(500, 495, 505)

# Tukey post-hoc within each supp x group panel
stat.test <- df %>%
  group_by(supp, group) %>%
  tukey_hsd(len ~ dose)

# Position for FREE scales: pass scales = "free" here too
stat.test <- stat.test %>%
  add_xy_position(x = "dose", fun = "mean_se", scales = "free")

ggbarplot(df, x = "dose", y = "len", fill = "#00AFBB",
          add = "mean_se", facet.by = c("supp", "group")) +
  facet_wrap(vars(supp, group), scales = "free") +
  stat_pvalue_manual(stat.test, hide.ns = TRUE, tip.length = 0) +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.15)))

A ggpubr bar plot of tooth length by dose faceted by supplement and group with free y-scales, each panel carrying correctly positioned significance brackets.

facet_wrap vs facet_grid with free scales. facet_wrap() frees both axes per panel, so each plot is independent — best when panels are unrelated. facet_grid(scales = "free") only frees within a row/column, so brackets may still need nudging: set step.increase = 0 in add_xy_position() and supply it (per-panel) in stat_pvalue_manual() instead, or hand-tune with a vector bracket.nudge.y.

Multiple response variables

Sometimes you measure several outcomes on the same subjects — four flower measurements, a panel of biomarkers — and you want the same group comparison on each, with all the p-values on one figure. The move is to test them together: reshape the data so every outcome stacks into one value column keyed by a variables column, then group_by(variables) and run one test per outcome.

Reshape wide → long with base R’s stack() (no extra packages), group by the outcome, and compute a t_test() of value ~ Species inside each group. adjust_pvalue() corrects across all the tests at once, and add_xy_position(scales = "free") gives each panel its own bracket height — essential here, since petal and sepal measurements live on very different scales.

library(ggpubr)
library(rstatix)

# Reshape WIDE -> LONG: stack the four numeric columns into value + variables
iris.long <- stack(iris[, 1:4])              # columns: values, ind
iris.long$Species <- rep(iris$Species, times = 4)
names(iris.long) <- c("value", "variables", "Species")

# One test per outcome: compare the three species within each variable
stat.test <- iris.long %>%
  group_by(variables) %>%
  t_test(value ~ Species) %>%
  adjust_pvalue(method = "holm") %>%
  add_significance("p.adj")

# Position for FREE scales, then facet by the outcome
stat.test <- stat.test %>% add_xy_position(x = "Species", scales = "free")

ggboxplot(iris.long, x = "Species", y = "value", fill = "Species", palette = "npg") +
  facet_wrap(~variables, scales = "free") +
  stat_pvalue_manual(stat.test, label = "p.adj.signif", tip.length = 0.01) +
  scale_y_continuous(expand = expansion(mult = c(0.05, 0.1)))

A ggpubr box plot of the four iris measurements in four free-scale panels — Sepal.Length, Sepal.Width, Petal.Length, Petal.Width — each comparing setosa, versicolor and virginica with pairwise adjusted-significance brackets.

Each panel is one outcome, tested on its own: the species differ on every measurement (mostly ****, with the closer versicolor-vs-virginica sepal width landing at **), and every bracket sits at the right height because scales = "free" was passed to both add_xy_position() and facet_wrap() — the same rule as the free-scale facets above. One stat.test frame, one figure, every outcome covered.

Name the key column variables, not variable. rstatix uses variable internally, so a column literally called variable collides with add_xy_position() (names are duplicated). Any other name — variables, outcome — works.

Horizontal plots

To flip a plot horizontal you add coord_flip() — but the brackets are drawn in the original coordinate system, so they must be told about the flip. Pass coord.flip = TRUE to stat_pvalue_manual() and add coord_flip().

library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

stat.test <- df %>% t_test(len ~ dose) %>% add_xy_position(x = "dose")

ggboxplot(df, x = "dose", y = "len", fill = "dose", palette = "jco") +
  stat_pvalue_manual(
    stat.test, label = "p.adj.signif", tip.length = 0.01,
    coord.flip = TRUE
    ) +
  coord_flip()

A horizontal ggpubr box plot of tooth length by dose with pairwise significance-star brackets correctly drawn for the flipped coordinates.

For a reference-group comparison, or to rotate the labels upright, the same arguments you’d use upright still apply — label = "p = {p.adj}", angle = 90, and hjust / vjust to nudge the text — alongside coord.flip = TRUE and coord_flip().

P-values computed entirely elsewhere

stat_pvalue_manual() doesn’t care how the p-values were made — if you have a data frame with group1, group2, a label column and a y.position, it will draw it. This is the escape hatch for results from any test, even outside R.

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

# A hand-built results frame (could come from anywhere)
stat.test <- data.frame(
  group1   = c("0.5", "0.5", "1"),
  group2   = c("1",   "2",   "2"),
  p.adj    = c(2.5e-07, 1.3e-13, 1.9e-05)
)

ggboxplot(df, x = "dose", y = "len") +
  stat_pvalue_manual(
    stat.test, y.position = 35, step.increase = 0.1,
    label = "p.adj"
    )

A ggpubr box plot of tooth length by dose with three brackets carrying p-values supplied directly as a hand-built data frame.

For full control over a single annotation — custom text, plotmath expressions, exactly placed brackets — reach for geom_bracket():

library(ggpubr)
df <- ToothGrowth
df$dose <- factor(df$dose)

ggboxplot(df, x = "dose", y = "len") +
  geom_bracket(
    xmin = c("0.5", "1"), xmax = c("1", "2"),
    y.position = c(30, 35), label = c("***", "**"),
    tip.length = 0.01
  )

A ggpubr box plot of tooth length by dose with two manually specified brackets carrying custom significance-star labels.

The recipe never changes: compute the test with rstatix, position with add_xy_position(), draw with stat_pvalue_manual() — and geom_bracket() is there for the one-off custom annotation.

Try it live

The plots above were rendered at build time. Want to experiment? Edit the code and press Run — it executes in your browser via webR (no server, no install). Try swapping t_test for wilcox_test, changing ref.group, or pointing label at "p.adj" instead of "p.adj.signif".

🟢 With an AI agent

Ask Prova “my stat_pvalue_manual() brackets are floating in the wrong place on my grouped plot — how do I fix add_xy_position()?” — it answers with code you can run on your own data frame and checks the dodge/fun/scales arguments against your plot, so the fix is reproducible. The runtime is the judge. Ask Prova →

Common issues

  • Brackets float in the wrong place on a grouped plot. add_xy_position() must know the dodge — pass the same dodge you gave the plot (e.g. add_xy_position(x = "dose", dodge = 0.8)). For bar/line plots add the matching fun (e.g. fun = "mean_sd") so the bracket clears the error bars.
  • add_xy_position() errors with “object ‘p.adj’ not found”. Compute the column before you label it: adjust_pvalue() creates p.adj, and add_significance("p.adj") creates p.adj.signif. Run those steps before stat_pvalue_manual(label = "p.adj.signif").
  • Brackets on a free-scale facet are mispositioned. scales = "free" must be set in both places — add_xy_position(..., scales = "free") and the facet (facet_wrap(..., scales = "free")). Setting it only on the plot leaves the y-positions computed for a fixed scale.
  • Horizontal brackets are drawn vertically (or off the plot). coord_flip() alone isn’t enough — also pass coord.flip = TRUE to stat_pvalue_manual() so it flips the bracket geometry.
  • Labels are clipped by the top border. Add headroom: scale_y_continuous(expand = expansion(mult = c(0.05, 0.1))).

Frequently asked questions

Use the rstatix → ggpubr workflow: compute the test as a data frame with df %>% t_test(len ~ dose) (or anova_test(), wilcox_test()), position the brackets with add_xy_position(x = "dose"), then draw them with ggboxplot(...) + stat_pvalue_manual(stat.test, label = "p.adj.signif"). The three steps never change.

stat_compare_means() computes and draws the test in one layer — fast for a quick global or pairwise p-value. stat_pvalue_manual() draws p-values you computed separately (with rstatix), which is what you need for adjusted p-values, specific tests, or grouped / faceted / horizontal layouts. See the companion lesson Add p-values to ggplots for stat_compare_means().

Add adjust_pvalue(method = "holm") (or "bonferroni", "BH", …) after the test, then add_significance("p.adj") for the stars. Label the plot with label = "p.adj" for the number or label = "p.adj.signif" for the significance symbol.

add_xy_position() needs to match the plot’s geometry. For grouped/dodged plots pass the same dodge (e.g. dodge = 0.8); for bar/line plots pass the matching fun (fun = "mean_sd"); for free-scale facets pass scales = "free" in both add_xy_position() and the facet. Mismatched arguments are the usual cause of floating brackets.

Add coord_flip() to make the plot horizontal and pass coord.flip = TRUE to stat_pvalue_manual() so the brackets flip with it. You can rotate the labels upright with angle = 90 and nudge them with hjust / vjust.

Reshape the data from wide to long so the outcomes stack into one value column keyed by a variables column (base R stack() works), then group_by(variables) %>% t_test(value ~ group), adjust_pvalue() and add_xy_position(scales = "free"). Draw a facet_wrap(~variables, scales = "free") plot with stat_pvalue_manual() — each panel shows one outcome’s comparison. See the Multiple response variables section above.

Test your understanding

Complete the code so the test is computed within each dose (OJ vs VC), the p-values are Bonferroni-adjusted, the brackets are positioned for a dodged plot, and the adjusted stars are drawn.

# 1. Adjust the p-values:        adjust_pvalue(method = "bonferroni")
# 2. Match the plot's dodge:     dodge = 0.8
# 3. Label with the adj. stars:  p.adj.signif
library(ggpubr) library(rstatix) df <- ToothGrowth df$dose <- factor(df$dose) stat.test <- df %>% group_by(dose) %>% t_test(len ~ supp) %>% adjust_pvalue(method = "bonferroni") %>% add_significance("p.adj") stat.test <- stat.test %>% add_xy_position(x = "dose", dodge = 0.8) ggboxplot(df, x = "dose", y = "len", color = "supp", palette = "jco") + stat_pvalue_manual(stat.test, label = "p.adj.signif")
library(ggpubr)
library(rstatix)
df <- ToothGrowth
df$dose <- factor(df$dose)

stat.test <- df %>%
  group_by(dose) %>%
  t_test(len ~ supp) %>%
  adjust_pvalue(method = "bonferroni") %>%
  add_significance("p.adj")

stat.test <- stat.test %>% add_xy_position(x = "dose", dodge = 0.8)

ggboxplot(df, x = "dose", y = "len", color = "supp", palette = "jco") +
  stat_pvalue_manual(stat.test, label = "p.adj.signif")

Quick check. You add scales = "free" to facet_wrap() but the brackets are still mispositioned. What did you forget? (Answer: pass scales = "free" to add_xy_position() too — the y-positions must be recomputed for free scales, not just the plot.)

Conclusion

You learned the rstatix → ggpubr workflow for putting p-values from real statistical tests onto any ggplot: compute with t_test() / wilcox_test() / anova_test() (+ adjust_pvalue() for multiplicity), position with add_xy_position(), and draw with stat_pvalue_manual() — across basic, grouped, faceted, free-scale and horizontal layouts, plus geom_bracket() for one-off custom annotations. The three steps never change; only the add_xy_position() arguments adapt to the plot.

For a quicker, single-layer annotation with no adjustment, start with stat_compare_means(). For the modern shortcut that computes and positions in one call, see Auto p-values with geom_pwc(). To choose and interpret the tests themselves (assumptions, effect size), see the upcoming statistics series.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {P-Values from {Tests} on Ggplots in {R} (Rstatix)},
  date = {2026-06-22},
  url = {https://www.datanovia.com/learn/data-visualization/ggpubr/p-values-from-tests},
  langid = {en}
}
For attribution, please cite this work as:
“P-Values from Tests on Ggplots in R (Rstatix).” 2026. June 22. https://www.datanovia.com/learn/data-visualization/ggpubr/p-values-from-tests.