6  Custom plotting

6.1 Schematic of experiment design

library(tidyplots)

df <- "data/experimental-design.csv" |> 
    readr::read_csv(show_col_types = FALSE)

# View df (the "63" of "days" column is just a placeholder for arrow head)
# type "?tidyplots::add_data_points()" in R consule for available shape values
df
# A tibble: 12 × 6
    days manipulation  groups shape manipulation_1 manipulation_2
   <dbl> <chr>          <dbl> <dbl> <chr>          <chr>         
 1     1 candidate 1st      2    19 candidate 1st  <NA>          
 2     7 candidate 2nd      2    19 candidate 2nd  <NA>          
 3    14 candidate 3rd      2    19 candidate 3rd  <NA>          
 4    28 challenge          2    17 <NA>           challenge     
 5    58 endpoint           2    13 <NA>           endpoint      
 6    63 <NA>               2    NA <NA>           <NA>          
 7     1 saline 1st         1    19 saline 1st     <NA>          
 8     7 saline 2nd         1    19 saline 2nd     <NA>          
 9    14 saline 3rd         1    19 saline 3rd     <NA>          
10    28 challenge          1    17 <NA>           <NA>          
11    58 endpoint           1    13 <NA>           <NA>          
12    63 <NA>               1    NA <NA>           <NA>          
# Plot
p1 <- df |> 
  tidyplot(x = days, y = groups, color = groups) |> 
  add_reference_lines(x = df$days[1:5], color = "#dddddd") |>   
  add_line(linewidth = 0.5, arrow = grid::arrow(length = grid::unit(0.15, "cm"))) |> 
  add_data_points(shape = df$shape, color = "#000000", na.rm = TRUE) |> 
  add_title(title = "p1") |> 
  adjust_size(height = 35) |> # adjust width and height as needed
  adjust_colors(new_colors = c("#6699cc", "#994455")) |> 
  remove_legend()

p2 <- p1 |> 
  adjust_title(title = "p2: padding and axis breaks") |>
  adjust_padding(left = 0.3, bottom = 0.8, top = 0.5) |> 
  adjust_x_axis(breaks = df$days[1:5]) |> 
  adjust_y_axis(breaks = df$groups)

p3 <- p2 |> 
  adjust_title(title = "p3: data labels") |> 
  add_data_labels(label = manipulation, label_position = "left", 
    angle = 45, color = "#000000", na.rm = TRUE, fontsize = 6)

p4 <- p2 |> 
  adjust_title(title = "p4: move some labels") |> 
  add_data_labels(label = manipulation_1, label_position = "left",
    angle = 45, color = "#000000", na.rm = TRUE, fontsize = 6) |> 
  add_data_labels(label = manipulation_2, label_position = "above",
    color = "#000000", na.rm = TRUE)

p5 <- p2 |> 
  adjust_title(title = "p5: another way of adding labels") |> 
  add_annotation_text(text = df$manipulation_1, x = df$days, y = df$groups, 
    angle = 45, hjust = 1.1, vjust = 1.5, na.rm = TRUE, fontsize = 6) |> 
  add_annotation_text(text = df$manipulation_2, x = df$days,
    y = df$groups, hjust = 0.5, vjust = -1, na.rm = TRUE, fontsize = 6)

p6 <- p5 |> 
  adjust_title(title = "p6") |> 
  remove_x_axis_line() |> 
  remove_y_axis_line() |> 
  remove_y_axis_ticks()

Schematic of experiment design.

6.2 Inset image to a figure

library(tidyplots)

df <- "data/experimental-design.csv" |> 
    readr::read_csv(show_col_types = FALSE)

# View df (the "63" of "days" column is just a placeholder for arrow head)
# type "?tidyplots::add_data_points()" in R consule for available shape values
df
# A tibble: 12 × 6
    days manipulation  groups shape manipulation_1 manipulation_2
   <dbl> <chr>          <dbl> <dbl> <chr>          <chr>         
 1     1 candidate 1st      2    19 candidate 1st  <NA>          
 2     7 candidate 2nd      2    19 candidate 2nd  <NA>          
 3    14 candidate 3rd      2    19 candidate 3rd  <NA>          
 4    28 challenge          2    17 <NA>           challenge     
 5    58 endpoint           2    13 <NA>           endpoint      
 6    63 <NA>               2    NA <NA>           <NA>          
 7     1 saline 1st         1    19 saline 1st     <NA>          
 8     7 saline 2nd         1    19 saline 2nd     <NA>          
 9    14 saline 3rd         1    19 saline 3rd     <NA>          
10    28 challenge          1    17 <NA>           <NA>          
11    58 endpoint           1    13 <NA>           <NA>          
12    63 <NA>               1    NA <NA>           <NA>          
# See the last section (i.e. Schemetic of experiment design) for the detail of p1
# Plot
p1 <- df |> 
  tidyplot(x = days, y = groups, color = groups) |> 
  add_reference_lines(x = df$days[1:5], color = "#dddddd") |>   
  add_line(linewidth = 0.5, arrow = grid::arrow(length = grid::unit(0.15, "cm"))) |> 
  add_data_points(shape = df$shape, color = "#000000", na.rm = TRUE) |> 
  add_title(title = "p1: experiment design") |> 
  adjust_size(height = 35) |> # adjust width and height as needed
  adjust_colors(new_colors = c("#6699cc", "#994455")) |> 
  remove_legend() |> 
  adjust_padding(left = 0.3, bottom = 0.8, top = 0.5) |> 
  adjust_x_axis(breaks = df$days[1:5]) |> 
  adjust_y_axis(breaks = df$groups) |> 
  add_annotation_text(text = df$manipulation_1, x = df$days, y = df$groups, 
    angle = 45, hjust = 1.1, vjust = 1.5, na.rm = TRUE, fontsize = 6) |> 
  add_annotation_text(text = df$manipulation_2, x = df$days,
    y = df$groups, hjust = 0.5, vjust = -1, na.rm = TRUE, fontsize = 6)

# Read mouse image (from https://bioart.niaid.nih.gov/bioart/20)
mouse <- "images/ApodemusSilhouette0001-grey.svg" |> magick::image_read()

mouse_pattern <- mouse |> grid::rasterGrob(x = 0.7, y = 0.5,
  width = grid::unit(0.4, "snpc"), height = grid::unit(0.4, "snpc")) |> 
  grid::pattern() # snpc: Square Normalised Parent Coordinates

p2 <- p1 |> 
  adjust_title(title = "p2: via 'adjust_theme_details()'") |> 
  adjust_theme_details(panel.background = ggplot2::element_rect(fill = mouse_pattern))

mouse_grob <- mouse |> grid::rasterGrob()

p3 <- p1 |> 
  adjust_title(title = "p3: via 'add()'") |> 
  add(
    ggplot2::annotation_custom(mouse_grob,
    xmin = 30, xmax = 55, ymin = 1.1, ymax = 1.9)) # tailored

p4 <- p1 |> 
  adjust_title(title = "p4: via 'rphylopic' package") + 
  rphylopic::add_phylopic(
    uuid = "36dc0476-ae7d-49ed-85c4-220139930bfc",
    x = 44, y = 1.5, height = 0.4, alpha = 0.4)
# See https://rphylopic.palaeoverse.org/index.html for detail

Inset an image. See reference (Gearty and Jones 2023) or the rphylopic documentation (https://rphylopic.palaeoverse.org/index.html) for details on p4.
Tip

Stephen D. Turner has compiled and reviewed a collection of resources under the title “Free and open-source images, icons, and tools for creating scientific illustrationshttps://blog.stephenturner.us/p/free-open-source-images-tools-scientific-illustrations (Turner 2026)

6.3 Annotate gel image

library(tidyplots)
img <- "images/20230515-10_cropped.tif" |> magick::image_read()

# View image information
img_info <- img |> magick::image_info()
img_width <- img_info$width; img_height <- img_info$height
img_info |> print()
# A tibble: 1 × 7
  format width height colorspace matte filesize density
  <chr>  <int>  <int> <chr>      <lgl>    <int> <chr>  
1 TIFF     180    237 Gray       FALSE    42970 600x600
# Change to graphical object
img_grob <- img |> grid::rasterGrob()
# Create a data frame relevant to img_grob
df <- tibble::tibble(x = seq(0, img_width, length.out = 100),
  y = seq(0, img_height, length.out = 100))
# View 1st row of df
df |> dplyr::slice_head(n = 1)
# A tibble: 1 × 2
      x     y
  <dbl> <dbl>
1     0     0
# The intended width of image in plot
img_intend_width = 50 # unit: mm
# Tailor relative extra spaces of plot (0 (0%) - 1 (100%))
top_extra = 0.12; right_extra = 0; bottom_extra = 0; left_extra = 0.16
# The width and height of plot
plot_width = img_intend_width * (1 + left_extra + right_extra) # unit: mm
plot_height = img_intend_width * (img_height/img_width) * (1 + top_extra + bottom_extra)

# Plot
p1 <- df |> tidyplot(x = x, y = y) |> 
  add_data_points(alpha = 0) |> 
  add(ggplot2::annotation_custom(img_grob, xmin = 0, xmax = img_width, 
    ymin = 0, ymax = img_height)) |> 
  add_title(title = "p1: add image") |> 
  adjust_size(width = plot_width, height = plot_height) |> 
  adjust_x_axis(limits = c(-img_width*left_extra, img_width*(1 + right_extra))) |> 
  adjust_y_axis(limits = c(-img_height * bottom_extra, img_height * (1 + top_extra)))
p2 <- p1 |> 
  adjust_title(title = "p2: annotate marker bands") |> 
  add_annotation_line(x = 0, xend = -5, 
    y = img_height - c(90, 113, 122, 134, 153, 170), # numbers are obtained via ImageJ
    yend = img_height - c(90, 113, 122, 134, 153, 170)) |> 
  add_annotation_text(text = c("2000", "1000", "750", "500", "250", "100", "bps"),
    x = -7, y = img_height - c(90, 113, 122, 134, 153, 170, 190), hjust = 1)    
p3 <- p2 |> 
  adjust_title(title = "p3: annotate lanes") |> 
  add_annotation_line(x = c(33, 108), xend = c(100, 175),
    y = img_height + 15, yend = img_height + 15) |> 
  add_annotation_text(text = c("A", "B"), x = c(65.3, 140.8), y = img_height + 25) |> 
  add_annotation_text(text = c("M", "-", "+", "+", "-", "+", "+"),
    x = seq(15, 166, length.out = 7), y = img_height + 7)
p4 <- p3 |>
  adjust_title(title = "p4: remove axis and margins") |> 
  remove_x_axis() |> remove_y_axis() |> 
  adjust_theme_details(plot.margin = ggplot2::margin(0, 0, 0, 0))

Annotate gel image.
Tip

Open the image in ImageJ/Fiji and hover over it; the pixel coordinates will be displayed in real time under the toolbar.

Note that the origin differs between systems: in ImageJ/Fiji, the top-left corner is (0, 0); whereas in tidyplots, the bottom-left corner is (0, 0) (also see Section 7.3).

6.4 Combining multiple images together

library(tidyplots)

# Get list of image names (four); All images are nearly identical in size here
img_names <- fs::dir_ls(path = "images", regexp = "^images/OS-2_.*")

# Read images
imgs <- purrr::set_names(
  purrr::map(img_names, magick::image_read),
  paste0("img", 1:length(img_names)))

# Get width and height of the 1st image
img_width <- magick::image_info(imgs[[1]])$width
img_height <- magick::image_info(imgs[[1]])$height

# Change images to graphic objectives
imgs_grob <- purrr::set_names(
  purrr::map(imgs, grid::rasterGrob),
  paste0("img", 1:length(imgs)))

# The intended width of combined image
img_intend_width = 50 # unit: mm
# Tailor relative extra spaces of plot (0 (0%) - 1 (100%))
top_extra = 0.1; right_extra = 0; bottom_extra = 0; left_extra = 0.1
# The width and height of plot
plot_width = img_intend_width * (1 + left_extra + right_extra) # unit: mm
plot_height = img_intend_width * (img_height/img_width) * (1 + top_extra + bottom_extra)

# Set a data frame for plotting
df <- tibble::tibble(x = 1:100, y = 1:100)

# Set the range of each img covered
image_cover_width <- 45 # note that df is 1:100 in x, and 1:100 in y
image_cover_height <- image_cover_width * (img_height/img_width) * (plot_width/plot_height)

# Set an intuitive function to add image to plot this time
add_image <- function(
  x, grob, top_left_x, top_left_y, 
  img_cover_width, img_cover_height) {
  x |> add(ggplot2::annotation_custom(
    grob = grob,
    xmin = top_left_x, xmax = top_left_x + img_cover_width,
    ymin = top_left_y - img_cover_height, ymax = top_left_y))}

# Plot
p1 <- df |> 
  tidyplot(x = x, y = y) |> 
  add_data_points(alpha = 0) |> 
  add_title(title = "p1: add 1st image") |> 
  adjust_size(width = plot_width, height = plot_height) |> 
  adjust_x_axis(limits = c(-100 * left_extra, 100 * (1 + right_extra))) |> 
  adjust_y_axis(limits = c(-100 * bottom_extra, 100 * (1 + top_extra))) |> 
  add_image(
    grob = imgs_grob$img1, top_left_x = 0, top_left_y = 100,
    img_cover_width = image_cover_width, img_cover_height = image_cover_height)

p2 <- p1 |> 
  adjust_title(title = "p2: add 2nd image") |> 
  add_image(
    grob = imgs_grob$img2, top_left_x = 50, top_left_y = 100,
    img_cover_width = image_cover_width, img_cover_height = image_cover_height)

p3 <- p2 |> 
  adjust_title(title = "p3: add 3rd and 4th images") |> 
  add_image(
    grob = imgs_grob$img3, top_left_x = 0, top_left_y = 50,
    img_cover_width = image_cover_width, img_cover_height = image_cover_height) |> 
  add_image(
    grob = imgs_grob$img4, top_left_x = 50, top_left_y = 50,
    img_cover_width = image_cover_width, img_cover_height = image_cover_height)

p4 <- p3 |> 
  adjust_title(title = "p4: add texts and remove axis") |> 
  add_annotation_text(
    text = paste("text", 1:4),
    x = c(22, 72, -5, -5), y = c(105, 105, 27, 77), angle = c(0, 0, 90, 90)) |> 
  remove_x_axis() |> remove_y_axis()

Combining multiple images together. The images are cropped from https://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/OS-2.ndpi using the QuPath software (Bankhead et al. 2017).

6.5 Display condition combinations in axis labels

library(tidyplots)

# View top 10 rows of the columns used
study |> dplyr::select(treatment, score) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 2
   treatment score
   <chr>     <dbl>
 1 A             2
 2 A             4
 3 A             5
 4 A             4
 5 A             6
 6 B             9
 7 B             8
 8 B            12
 9 B            15
10 B            16
# Plot
p1 <- study |> 
  tidyplot(x = treatment, y = score, color = treatment) |> 
  add_violin(trim = FALSE) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_title(title = "p1") |> 
  remove_legend()

p2 <- p1 |> 
  adjust_title(title = "p2: + or - combination") |> 
  rename_x_axis_levels(new_names = c(
    "A" = "+\n+\n+", "B" = "+\n+\n-",
    "C" = "+\n-\n+", "D" = "-\n+\n+"))

p3 <- p2 |> 
  adjust_title(title = "p3: specifiy conditions") |> 
  add_annotation_text( # space could be used to offset the length differences
    text = "condition x\ncondition y\ncondition z", 
    x = 1, y = 0, # location of panel lowerleft
    vjust = 1.5, # should be tailored
    hjust = 1.5, # should be tailored
    fontsize = 5.8) |> # should be tailored
  add(ggplot2::coord_cartesian(clip = "off")) # ensure text could be visualized outside panel.

# Another way to p3
axis_labels <- c("+\n+\n+", "+\n+\n-", "+\n-\n+", "-\n+\n+")

p4 <- p1 |> 
  adjust_title(title = "p4: another way to p3") |> 
  adjust_x_axis(labels = axis_labels) |> 
  add_annotation_text(
    text = "condition x\ncondition y\ncondition z",
    x = 1, y = 0,
    vjust = 1.5, hjust = 1.5, fontsize = 5.8) |> 
  add(ggplot2::coord_cartesian(clip = "off")) 

Displaying condition combinations in axis labels.
Note

So far so good, then perhaps minor adjustment might be needed via inkscape, adobe illustrator, or some other softwares for vector images.

6.6 Display condition combinations in axis labels (using add())

library(tidyplots)

# Add a column indicating combinations
study_comb <- study |> 
  dplyr::mutate(comb = c(
    rep("a,b,c", 5),
    rep("a,b", 5),
    rep("a,c", 5),
    rep("b,c", 5)))

# View top 10 rows of the columns used
study_comb |> 
  dplyr::select(comb, score) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 2
   comb  score
   <chr> <dbl>
 1 a,b,c     2
 2 a,b,c     4
 3 a,b,c     5
 4 a,b,c     4
 5 a,b,c     6
 6 a,b       9
 7 a,b       8
 8 a,b      12
 9 a,b      15
10 a,b      16
# Plot
p1 <- study_comb |> 
  tidyplot(x = comb, y = score, color = comb) |> 
  add_violin(trim = FALSE) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_title(title = "p1")

p2 <- p1 |> 
  adjust_title(title = "p2: reorder x levels") |> 
  reorder_x_axis_levels("a,b,c", "a,b", "a,c", "b,c")

p3 <- p2 |> 
  adjust_title(title = "p3: via 'add()' 1") |> 
  add(ggplot2::guides(x = legendry::guide_axis_upset(
    legendry::key_upset(sep = ","), 
    override.aes = list(size = 2),
    connect = NULL)))

p4 <- p2 |> 
  adjust_title(title = "p4: via 'add()' 2") |> 
  add(ggplot2::guides(x = legendry::guide_axis_upset(
    legendry::key_upset(sep = ","),
    override.aes = list(shape = c("+", "-", NA), size = 4),
    connect = NULL))) |> 
  theme_minimal_xy()

Displaying condition combinations in axis labels using add().

6.7 Change point shape/color and annotate axis

library(tidyplots)

# Add two columns to specify point shapes and colors
study_shape <- study |> 
  dplyr::mutate(
    shape = rep(c(21, 25, 22, 24, 23), 4),
    color = rep(c("#eecc66", "#994455"), each = 5, length.out = 20))

# View top 10 rows of the columns used
study_shape |> dplyr::select(treatment, score, shape, color) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 4
   treatment score shape color  
   <chr>     <dbl> <dbl> <chr>  
 1 A             2    21 #eecc66
 2 A             4    25 #eecc66
 3 A             5    22 #eecc66
 4 A             4    24 #eecc66
 5 A             6    23 #eecc66
 6 B             9    21 #994455
 7 B             8    25 #994455
 8 B            12    22 #994455
 9 B            15    24 #994455
10 B            16    23 #994455
# Plot
p1 <- study_shape |> 
  tidyplot(x = treatment, y = score) |> 
  add_data_points_beeswarm(shape = study_shape$shape, size = 2.5, 
    color = study_shape$color, fill = study_shape$color, alpha = 0.5) |> 
  add_mean_dash(color = "#000000") |> 
  add_test_pvalue(hide_info = TRUE, comparisons = list(c(1, 2), c(3, 4))) |> 
  add_reference_lines(x = 2.5, linetype = "solid", color = "#dddddd") |> 
  add_annotation_line(x = c(0.5, 2.5), xend = c(2.5, 4.5), 
    y = c(10, 30), yend = c(10, 30), linetype = "dotted", color = "#bbbbbb") |> 
  add_title(title = "p1: points, statistics, ref/cutoff lines")

p2 <- p1 |> 
  adjust_title(title = "p2: axis levels") |> 
  rename_x_axis_levels(new_names = c(
    "A" = "Cont IgG", "B" = "rmIFNγR1\n-IgG",
    "C" = "Cont IgG\n", "D" = "rmIFNγR1\n-IgG\n")) |> 
  adjust_x_axis(rotate_labels = TRUE, title = "") |> 
  adjust_y_axis(title = "$Log[10]*CFU$")

p3 <- p2 |> 
  adjust_title(title = "p3: lines below x axis levels") |> 
  add_annotation_line(x = c(0.6, 2.6), xend = c(2.4, 4.4), y = c(-26, -26),
    yend = c(-26, -26), linewidth = 0.3) |> 
  add(ggplot2::coord_cartesian(clip = "off", ylim = c(0, NA)))

p4 <- p3 |> 
  adjust_title(title = "p4: texts below x axis levels") |> 
  add_annotation_text(text = c("Granuloma", "Pulmonary LN"), 
    x = c(1.5, 3.5), y = c(0, 0), vjust = 9, fontsize = 8, fontface = "bold") |> 
  add_caption("mimic fig 1b of\nNat Commun. 2026. PMID: 42034649")

Change point shape/color and annotate axis.

6.8 Point annotation

library(tidyplots)

# Set a df for point annotation
df <- tibble::tibble(
  x = rep(1:5, length.out = 10),
  y = rep(c("a", "b"), each = 5),
  label = c("RC7", "RCK", "RJH", "RCD", "RFL", "RB9", "RML", "RED", "RHG", "RD6"),
  shape = rep(c(21, 25, 22, 24, 23), 2),
  color = rep(c("#eecc66", "#994455"), each = 5))

# View df
df
# A tibble: 10 × 5
       x y     label shape color  
   <int> <chr> <chr> <dbl> <chr>  
 1     1 a     RC7      21 #eecc66
 2     2 a     RCK      25 #eecc66
 3     3 a     RJH      22 #eecc66
 4     4 a     RCD      24 #eecc66
 5     5 a     RFL      23 #eecc66
 6     1 b     RB9      21 #994455
 7     2 b     RML      25 #994455
 8     3 b     RED      22 #994455
 9     4 b     RHG      24 #994455
10     5 b     RD6      23 #994455
# Plot
p5 <- df |>   
  tidyplot(x = x, y = y) |> 
  add_data_points(shape = df$shape, color = df$color, 
    fill = df$color, size = 2.5, alpha = 0.5) |> 
  remove_legend() |> 
  add_title(title = "p5: points") |> 
  adjust_size(width = 90, height = 10) |> 
  adjust_x_axis(limits = c(-1.6, 5.7))

p6 <- p5 |> 
  adjust_title(title = "p6: texts") |> 
  add_annotation_text(text = df$label, x = df$x, y = df$y, hjust = -0.5) |> 
  add_annotation_text(text = c("Control IgG:", "rmIFNγR1-IgG:"), x = c(0.85, 0.85),
    y = c(1, 2), hjust = 1) |> 
  add_annotation_text(text = "Animal ID", x = -0.6, y = 1.5, 
    hjust = 1, fontface = "bold")

p7 <- p6 |> 
  adjust_title(title = "p7: rectangle and remove axis") |> 
  add_annotation_line(x = -1.55, xend = -0.6, y = 1.15, yend = 1.15) |>
  add_annotation_rectangle(xmin = -1.6, xmax = 5.7, 
    ymin = 0.3, ymax = 2.8, color = "#000000", alpha = 0) |> 
  remove_x_axis() |> remove_y_axis() |> 
  adjust_theme_details(plot.margin = ggplot2::margin(0, 0, 0, 0)) |> 
  add_caption("mimic fig 1b of\nNat Commun. 2026. PMID: 42034649")

Point annotation. p4 is from the last section.
Note

Then perhaps adjustment might be needed via inkscape, adobe illustrator, or some other softwares for vector images.

6.9 Add sum values for negative bars

library(tidyplots)

df <- tibble::tibble(
  x = letters[1:10],
  y = seq(-5, 5, length.out = 10))

df <- df |> 
  dplyr::mutate(
    vjust_custom = c(ifelse(y >= 0, -1, 2)))

# View df
df
# A tibble: 10 × 3
   x          y vjust_custom
   <chr>  <dbl>        <dbl>
 1 a     -5                2
 2 b     -3.89             2
 3 c     -2.78             2
 4 d     -1.67             2
 5 e     -0.556            2
 6 f      0.556           -1
 7 g      1.67            -1
 8 h      2.78            -1
 9 i      3.89            -1
10 j      5               -1
# Plot
p1 <- df |> 
  tidyplot(
    x = x, 
    y = y, 
    color = y) |> 
  add_sum_bar(alpha = 0.5) |> 
  add_title(title = "p1") |> 
  remove_legend()

p2 <- p1 |> 
  adjust_title(title = "p2: default value position") |> 
  add_sum_value(color = "#000000")

p3 <- p1 |> 
  adjust_title(title = "p3: vertical adjust = 0") |> 
  add_sum_value(color = "#000000", vjust = 0)

p4 <- p1 |> 
  adjust_title(title = "p4: custom vertical adjust") |> 
  adjust_y_axis(
    limits = c(min(df$y) - 2, max(df$y) + 2)) |> 
  add_sum_value(
    color = "#000000", 
    vjust = vjust_custom, 
    extra_padding = 0) # no extra spacing above panel

Add sum values for negative bars.

6.10 Sequential issues

library(tidyplots)

df <- tibble::tibble(x = seq(1, 6), group = rep(1, 6))

# View df
df
# A tibble: 6 × 2
      x group
  <int> <dbl>
1     1     1
2     2     1
3     3     1
4     4     1
5     5     1
6     6     1
# Plot
p1 <- df |> 
  tidyplot(x = x, y = group) |> 
  add_line(
    arrow = grid::arrow(length = grid::unit(0.15, "cm")),
    color = "#3d4f6a", linewidth = 0.5) |> 
  add_title(title = "p1: line with arrowhead") |> remove_legend()

# Set several fixed locations/constants for efficient adjustment
text_up_dn <- 0.6 # vertical distance (tailored) to line
text_x <- df$x[1:5] # x axis locations (center) of texts
text_y <- df$group[1:5] + ifelse(seq_along(text_x) %% 2, text_up_dn, -text_up_dn)
rectangle_half_width <- 0.7 # half length (tailored) of rectangle behind text
rectangle_half_height <- 0.4
line_yend <- text_y + ifelse(seq_along(text_y) %% 2, -rectangle_half_height, rectangle_half_height) # yend locations of vertical lines

p2 <- p1 |> 
  adjust_title(title = "p2: rectangle") |> 
  add_annotation_rectangle(
    xmin = text_x - rectangle_half_width, xmax = text_x + rectangle_half_width,
    ymin = text_y - rectangle_half_height, ymax = text_y + rectangle_half_height,
    fill = "#b9e6f1", alpha = 1)

labels <- c(
  "Embrace\ndiversity as a\nkey aspect of\nscientific\nrigour",
  "Evolve ethical\nframeworks",
  "Democratise\ndata access\nand benefit\nsharing",
  "Invest in\ncapacity\nstrengthening",
  "Eevelop\ntrans-ethnic\nanalytical\nmethods")

p3 <- p2 |> 
  adjust_title(title = "p3: text") |> 
  add_annotation_text(text = labels, x = text_x, y = text_y)

p4 <- p3 |> 
  adjust_title(title = "p4: vertical lines") |> 
  add_annotation_line(x = text_x, xend = text_x,
    y = df$group[1:5], yend = line_yend, color = "#3d4f6a")

p5 <- p4 |> 
  adjust_title(title = "p5: add points and polish") |> 
  add_data_points(
    data = filter_rows(x <= 5), color = "#3d4f6a", 
    white_border = TRUE, size = 2) |> 
  adjust_size(width = 100) |> 
  remove_x_axis() |> remove_y_axis() |> 
  add_caption("redraw part of fig 1b of \nNat Rev Nephrol. 2026. PMID: 42014456")

Sequential issues.

6.11 Sequential issues (using add())

library(tidyplots)

# Set a data frame
df <- tibble::tibble(
  phase = c("Phase 1", "Phase 2", "Phase 3", 
    "Candidate\na1", "Candidate\nb1", "Candidate\nc1", # \n means line feed
    "Candidate\nc2", "Candidate\nb2", "Candidate\na2"),
  x_start = c(rep(1:3, times = 2), 3:1),
  x_end = c(x_start[1:6] + 0.9, x_start[7:9] + 1.2),
  y = rep(3:1, each = 3))

# View
df
# A tibble: 9 × 4
  phase           x_start x_end     y
  <chr>             <int> <dbl> <int>
1 "Phase 1"             1   1.9     3
2 "Phase 2"             2   2.9     3
3 "Phase 3"             3   3.9     3
4 "Candidate\na1"       1   1.9     2
5 "Candidate\nb1"       2   2.9     2
6 "Candidate\nc1"       3   3.9     2
7 "Candidate\nc2"       3   4.2     1
8 "Candidate\nb2"       2   3.2     1
9 "Candidate\na2"       1   2.2     1
# Plot
p1 <- df |> 
  tidyplot(y = y, color = x_start) |> 
  add(gggenes::geom_gene_arrow(
    mapping = ggplot2::aes(xmin = x_start, xmax = x_end),
    data = filter_rows(y == 3), 
    arrowhead_height = grid::unit(6, "mm"),
    color = "#000000", fill = "#ffffff")) |> 
  add_title(title = "p1") |> 
  adjust_size(width = 70, height = 30) |> 
  adjust_y_axis(limits = c(0.5, 3.5)) |> 
  remove_legend()

p2 <- p1 |> 
  adjust_title(title = "p2") |> 
  add(gggenes::geom_gene_label(
    mapping = ggplot2::aes(xmin = x_start, xmax = x_end, label = phase),
    data = dplyr::filter(df, y == 3),
    color = "#000000"))

p3 <- p2 |> 
  adjust_title(title = "p3") |> 
  add(gggenes::geom_gene_arrow(
    mapping = ggplot2::aes(xmin = x_start, xmax = x_end),
    data = filter_rows(y == 2), 
    arrowhead_width = grid::unit(0, "mm"),
    arrowhead_height = grid::unit(8, "mm"), 
    arrow_body_height = grid::unit(8, "mm"), 
    color = "#000000")) |> 
  adjust_colors(new_colors = c("#eecc66", "#ee99aa", "#6699cc"))

p4 <- p3 |> 
  adjust_title(title = "p4") |> 
  add_annotation_text(
    text = df$phase[4:6], 
    x = (df$x_start[4:6] + df$x_end[4:6])/2, 
    y = df$y[4:6]) |> 
  adjust_y_axis(limits = c(0, 3.5))

p5 <- p4 |> 
  adjust_title(title = "p5") |> 
  add(gggenes::geom_gene_arrow(
    mapping = ggplot2::aes(xmin = x_start, xmax = x_end, y = y),
    data = filter_rows(y == 1), 
    arrowhead_width = grid::unit(6, "mm"),
    arrowhead_height = grid::unit(5, "mm"),
    arrow_body_height = grid::unit(8, "mm"), 
    color = "#000000"))

p6 <- p5 |> 
  adjust_title(title = "p6") |> 
  add_annotation_text(
    text = df$phase[7:9], 
    x = (df$x_start[7:9] + df$x_end[7:9])/2, 
    y = df$y[7:9]) |> 
  remove_x_axis() |> remove_y_axis()

Sequential issues (using add()).

6.12 Change Chinese date axis labels into English ones

library(tidyplots)

# View top 1 rows of the columns used
energy_week |> 
  dplyr::select(date, power, energy_source) |> 
  dplyr::slice_head(n = 1)
# A tibble: 1 × 3
  date                power energy_source
  <dttm>              <dbl> <fct>        
1 2023-09-03 22:00:00     0 Nuclear      
# Plot
p1 <- energy_week |> 
  tidyplot(x = date, y = power, color = energy_source) |> 
  add_title(title = "p1: x axis labels in Chinese") |>  
  add_areastack_relative()
# Notice the Chinese character "月" in the x axis labels.

p2 <- p1 |> 
  adjust_title(title = "p2: x axis labels in English") |> 
  adjust_legend_position(position = "left") |> 
  adjust_x_axis(
    labels = scales::label_date(
      format = "%b %d",
      locale = "en"))
# Change date column with <dttm> type to a new column with <chr> type
energy_week_chr <- energy_week |> 
  dplyr::mutate(date_chr = as.character(date))

# View top 1 row of the columns used
energy_week_chr |> 
  dplyr::select(date, date_chr, power, energy_source) |> 
  dplyr::slice_head(n = 1)
# A tibble: 1 × 4
  date                date_chr            power energy_source
  <dttm>              <chr>               <dbl> <fct>        
1 2023-09-03 22:00:00 2023-09-03 22:00:00     0 Nuclear      
# Also view the locale setting of my computer
Sys.getlocale("LC_TIME")
[1] "Chinese (Simplified)_China.utf8"
p3 <- energy_week_chr |> 
  tidyplot(x = date_chr, y = power, color = energy_source) |> 
  add_title(title = "p3: x axis labels in English\n2nd (tedious) way") |> 
  add_areastack_relative() |> 
  adjust_x_axis(
    breaks = c("2023-09-05", "2023-09-07", "2023-09-09"),
    labels = c("Sep 05", "Sep 07", "Sep 09"))

Change Chinese date axis labels into English ones.
Note

What about the same issue in non-English operating systems other than Chinese?

6.13 Plot with a broken axis

library(tidyplots)
library(patchwork)

# Enlarge score values of treatments "C" and "D" by adding 900
study_modi <- study |> 
  dplyr::mutate(score_modi = ifelse(treatment %in% c("C", "D"), score + 900, score))

# View rows 6 to 15 of the columns involved
study_modi |> dplyr::select(treatment, score, score_modi) |> 
  dplyr::slice(6:15)
# A tibble: 10 × 3
   treatment score score_modi
   <chr>     <dbl>      <dbl>
 1 B             9          9
 2 B             8          8
 3 B            12         12
 4 B            15         15
 5 B            16         16
 6 C            32        932
 7 C            35        935
 8 C            24        924
 9 C            45        945
10 C            56        956
# Plot
p1 <- study_modi |> 
  tidyplot(x = treatment, y = score_modi, color = treatment) |> 
  add_mean_bar(alpha = 0.3) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar() |> 
  add_test_asterisks(hide_info = TRUE) |> 
  add_title(title = "p1: treatments 'A' and 'B' are invisible")

p2 <- p1 |> 
  adjust_title(title = "p2: transform y axis via 'log10'") |> 
  adjust_y_axis(transform = "log10")

study_AB <- study_modi |> 
  dplyr::filter(treatment %in% c("A", "B"))

p3_lower_segment <- p1 |> 
  adjust_y_axis(limits = c(0, max(study_AB$score_modi) + 5)) |> 
  adjust_size(height = 15) |> 
  remove_y_axis_title() |> 
  remove_title()

study_CD <- study_modi |> 
  dplyr::filter(treatment %in% c("C", "D"))

p3_upper_segment <- p1 |> 
  adjust_title(title = "p3: broken-axis plot") |> 
  adjust_y_axis(
    limits = c(min(study_CD$score_modi) - 100, NA)) |> 
  adjust_y_axis_title(hjust = -1) |> 
  adjust_size(height = 30) |> 
  adjust_padding(top = 0.1) |> 
  remove_x_axis()

# Combine plots into the same graphic
design <- "AB
           C#
           D#"

p1 + p2 + p3_upper_segment + p3_lower_segment + plot_layout(design = design)

Plot with a broken y axis.
Note

Inkscape or adobe illustrator can be used to adjust, modify, remove plot elements of p3 (e.g. axis labels and legends).

P3 is actually composed of two plots: p3_upper_segment and p3_lower_segment.

By the way, it seems that the ggbreak package is incompatible with ggplot or tidyplot that use absolute dimentions (Mu et al. 2026).

6.14 Color each point within each group

library(tidyplots)

y <- c(2.3, 4.5, 6.3, 3.4, 7.8, 3.05, 5.30, 7.05, 4.20, 8.55)
colors <- c("#eecc66", "#ee99aa", "#6699cc", "#997700", "#994455")
df <- tibble::tibble(
  y = y, group = paste0("g", rep(c(1, 2), each = 5)), batch = paste0("b", c(1:5, 1:5)), 
  batch_color_1 = rep(colors, times = 2), batch_color_2 = rep(colors, each = 2))

# View df
df
# A tibble: 10 × 5
       y group batch batch_color_1 batch_color_2
   <dbl> <chr> <chr> <chr>         <chr>        
 1  2.3  g1    b1    #eecc66       #eecc66      
 2  4.5  g1    b2    #ee99aa       #eecc66      
 3  6.3  g1    b3    #6699cc       #ee99aa      
 4  3.4  g1    b4    #997700       #ee99aa      
 5  7.8  g1    b5    #994455       #6699cc      
 6  3.05 g2    b1    #eecc66       #6699cc      
 7  5.3  g2    b2    #ee99aa       #997700      
 8  7.05 g2    b3    #6699cc       #997700      
 9  4.2  g2    b4    #997700       #994455      
10  8.55 g2    b5    #994455       #994455      
# Plot
p1 <- df |> 
  tidyplot(x = group, y = y, color = group) |> 
  add_boxplot() |> add_data_points(white_border = TRUE, size = 1.5) |> 
  add_title(title = "p1") |> add_test_pvalue(paired_by = batch, hide_info = TRUE)

p2 <- df |> 
  tidyplot(x = group, y = y, color = group) |> 
  add_boxplot(fill = "#000000", alpha = 0.2) |> add_title(title = "p2: no points")

p3 <- p2 |> 
  adjust_title(title = "p3: points with labels") |> 
  add_data_points(white_border = TRUE, size = 1.5) |> 
  add_data_labels_repel(label = batch) |> 
  add_line(group = batch, color = "#000000", linetype = "dotted") |> 
  add_test_pvalue(paired_by = batch, hide_info = TRUE, bracket.nudge.y = 0.2)

p4 <- p2 |> 
  adjust_title(title = "p4: different point colors (method 1)") |> 
  add_data_points(white_border = TRUE, size = 1.5, color = df$batch_color_2) |> 
  add_line(group = batch, color = "#000000", linetype = "dotted") |> 
  add_test_pvalue(paired_by = batch, hide_info = TRUE)

p5 <- p2 |> 
  adjust_title(title = "p5: different point colors (method 2)") |> 
  add_data_points(data = filter_rows(group == "g1"), white_border = TRUE, size = 1.5, color = df$batch_color_1[1:5]) |> 
  add_data_points(data = filter_rows(group == "g2"), white_border = TRUE, size = 1.5, color = df$batch_color_1[6:10]) |> 
  add_line(group = batch, color = "#000000", linetype = "dotted") |> 
  add_test_pvalue(paired_by = batch, hide_info = TRUE)

p6 <- p2 |> 
  adjust_title(title = "p6: different point colors (method 3)") |> 
  add(ggplot2::geom_point(
    ggplot2::aes(fill = I(batch_color_1)), shape = 21, 
    size = 2.25, color = "#ffffff", show.legend = FALSE)) |> 
  add_line(group = batch, color = "#000000", linetype = "dotted") |> 
  add_test_pvalue(paired_by = batch, hide_info = TRUE)

Color each point within each group.

6.15 Forest plot

library(tidyplots)

# Create a data frame (from Global Spine J. 2021. PMID: 33939533)
df <- tibble::tibble(
  studies = c(
    "Blumenthal et al, 2005", "Geisler et al, 2009", 
    "Gornet et al, 2011", "total"),
  weight = c(0.208, 0.279, 0.513, 1),
  rr = c(1.38, 1.15, 1.14, 1.19),
  ci95_lower = c(1.13, 0.97, 1.04, 1.07),
  ci95_upper = c(1.68, 1.35, 1.25, 1.32))

# View
df
# A tibble: 4 × 5
  studies                weight    rr ci95_lower ci95_upper
  <chr>                   <dbl> <dbl>      <dbl>      <dbl>
1 Blumenthal et al, 2005  0.208  1.38       1.13       1.68
2 Geisler et al, 2009     0.279  1.15       0.97       1.35
3 Gornet et al, 2011      0.513  1.14       1.04       1.25
4 total                   1      1.19       1.07       1.32
# Plot
p1 <- df |> 
  tidyplot(
    x = rr, 
    y = studies) |> 
  add(ggplot2::geom_linerange(
    ggplot2::aes(xmin = ci95_lower, xmax = ci95_upper), 
    color = "#000000")) |> 
  add_data_points(
    size = 8 * df$weight, 
    shape = c(15, 15, 15, 18), 
    color = "#000000") |> 
  add_title(title = "p1: involving `add()`") |> 
  reverse_y_axis_levels() |> 
  add_reference_lines(x = 1) # line of no effect

p2 <- df |> 
  tidyplot(
    x = rr, 
    y = studies) |> 
  add_annotation_line(
    x = df$ci95_lower, 
    xend = df$ci95_upper, 
    y = df$studies, 
    yend = df$studies) |> 
  add_data_points(
    size = 8 * df$weight, 
    shape = c(15, 15, 15, 18), 
    color = "#000000") |> 
  add_title(title = "p2: without involving `add()`") |> 
  reverse_y_axis_levels() |> 
  add_reference_lines(x = 1) # line of no effect

Forest plot. The data involved are from (Dettori et al. 2021).

6.16 Relative to control

library(tidyplots)

# A putative df
relative <- study |> 
  dplyr::mutate(
    `rel. CFU (D3/D0)` = score/20)

# View top 10 rows of the columns used
relative |> 
  dplyr::select(group, `rel. CFU (D3/D0)`, dose) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 3
   group   `rel. CFU (D3/D0)` dose 
   <chr>                <dbl> <chr>
 1 placebo               0.1  high 
 2 placebo               0.2  high 
 3 placebo               0.25 high 
 4 placebo               0.2  high 
 5 placebo               0.3  high 
 6 placebo               0.45 low  
 7 placebo               0.4  low  
 8 placebo               0.6  low  
 9 placebo               0.75 low  
10 placebo               0.8  low  
# Plot
p1 <- relative |> 
  tidyplot(x = group, y = `rel. CFU (D3/D0)`, color = dose) |> 
  add_mean_bar(alpha = 0.4) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar() |> 
  add_title(title = "p1")

p2 <- p1 |> 
  add_test_asterisks(hide_info = TRUE) |> 
  adjust_title(title = "p2: comparisons")

p3 <- p2 |> 
  adjust_padding(bottom = 0.1) |> 
  adjust_title(title = "p3: log2-transformed y") |> 
  adjust_y_axis(
    transform = "log2", 
    breaks = 2^(-3:2))

p4 <- p3 |> 
  add_reference_lines(y = 1, linetype = "solid") |> 
  add_caption(caption = "mimic fig 3b of\nPLoS Pathog. 2026. PMID: 42461973") |> 
  adjust_title(title = "p4: reference line at y = 1")

Relative to control.

6.17 Tick for subgroup

library(tidyplots)

# A putative df
studyx <- study |> 
  dplyr::mutate(treatmentx = rep(c(1, 2, 4, 5), each = 5))

# View top 10 rows of the columns used
studyx |> 
  dplyr::select(treatmentx, score, treatment) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 3
   treatmentx score treatment
        <dbl> <dbl> <chr>    
 1          1     2 A        
 2          1     4 A        
 3          1     5 A        
 4          1     4 A        
 5          1     6 A        
 6          2     9 B        
 7          2     8 B        
 8          2    12 B        
 9          2    15 B        
10          2    16 B        
# Plot
p1 <- studyx |> 
  tidyplot(x = treatmentx, y = score, color = treatment) |> 
  add_boxplot() |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_title(title = "p1") |> 
  adjust_x_axis(breaks = c(1, 2, 4, 5), labels = c("A", "B", "A", "B"))

stat_df <- tibble::tibble(
  group1 = c(1), group2 = c(2, 4, 5),
  p = c(0.005, 0.003, 0.0001),
  y.position = c(25, 55, 55))

p2 <- p1 |> 
  adjust_title(title = "p2: add comparisons manually") |> 
  add_test_pvalue_manual(data = stat_df, label = "p")

p3 <- p2 |> 
  adjust_title(title = "p3: adjust colors") |> 
  adjust_colors(new_colors = c("#ddaa33", "#004488", "#ddaa33", "#004488"))

score_max <- max(studyx$score)
y_axis_lower_limit <- 0 - score_max * 0.03 #tailored

p4 <- p3 |> 
  adjust_title(title = "p4: fix y axis limits") |> 
  adjust_y_axis(limits = c(y_axis_lower_limit, score_max * 1.45)) #tailored

p5 <- p4 |> 
  adjust_title(title = "p5: annotate group info") |>
  remove_x_axis_title() |> 
  remove_clipping() |> 
  add_annotation_text(
    text = c("group 1", "group 2"),
    x = c(1.5, 4.5), y = -10) # tailored

p6 <- p5 |> 
  adjust_title(title = "p6: remove legend") |> 
  remove_legend() |> 
  adjust_caption(
    caption = "\nmimic fig 6 of\nMicrobiol Spectr. 2026. PMID: 42474179")

Tick for subgroup.

6.18 Segmented x axis line

library(tidyplots)

# View top 10 rows of the columns used
study |> 
  dplyr::select(treatment, score) |> 
  dplyr::slice_head(n = 10)
# A tibble: 10 × 2
   treatment score
   <chr>     <dbl>
 1 A             2
 2 A             4
 3 A             5
 4 A             4
 5 A             6
 6 B             9
 7 B             8
 8 B            12
 9 B            15
10 B            16
# Plot
p1 <- study |> 
  tidyplot(x = treatment, y = score) |> 
  add_mean_bar(alpha = 0.4) |>   
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar() |> 
  add_test_asterisks(hide_info = TRUE, ref.group = "A", y.position = c(22, 60, 65)) |> 
  add_title(title = "p1")

p2 <- p1 |> 
  adjust_title(title = "p2: remove x axis line") |> 
  remove_x_axis_line()

score_max <- max(study$score)
y_axis_lower_limit <- 0 - score_max * 0.03

p3 <- p2 |> 
  adjust_title(title = "p3: two segments of x axis line") |> 
  add_annotation_line(
    x = 1, xend = 2, 
    y = y_axis_lower_limit, yend = y_axis_lower_limit, 
    linewidth = 0.25) |> 
  add_annotation_line(
    x = 3, xend = 4, 
    y = y_axis_lower_limit, yend = y_axis_lower_limit, 
    linewidth = 0.25) |> 
  adjust_y_axis(limits = c(y_axis_lower_limit, NA)) |> 
  adjust_padding(top = 0.1)

p4 <- p3 |> 
  adjust_title(title = "p4: rename x axis labels and titles") |> 
  adjust_x_axis(labels = c("A", "B", "A", "B")) |> 
  remove_x_axis_title() |> 
  remove_clipping() |> 
  add_annotation_text(
    text = c("Uninfected", "Infected"), 
    x = c(1.5, 3.5), y = -10) |> # -10 is a tailored number
  add_caption(caption = "mimic fig 2a of\nMicrobiol Spectr. 2026. PMID: 42474179")

Segmented x axis line.

6.19 Nested bar plot

library(tidyplots)

# View top 10 rows of the columns used
study |>
  dplyr::select(group, score, dose) |>
  dplyr::slice_head(n = 10)
# A tibble: 10 × 3
   group   score dose 
   <chr>   <dbl> <chr>
 1 placebo     2 high 
 2 placebo     4 high 
 3 placebo     5 high 
 4 placebo     4 high 
 5 placebo     6 high 
 6 placebo     9 low  
 7 placebo     8 low  
 8 placebo    12 low  
 9 placebo    15 low  
10 placebo    16 low  
# Plot
p1 <- study |> 
  tidyplot(x = group, y = score, color = dose) |> 
  add_mean_bar(alpha = 0.5) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar(color = "#000000") |> 
  add_title(title = "p1")

p2 <- study |> 
  tidyplot(x = group, y = score, color = dose, dodge_width = 0) |> 
  add_mean_bar(alpha = 0.5) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar(color = "#000000") |> 
  add_title(title = "p2: overlapped group bars")

p3 <- study |> 
  tidyplot(x = group, y = score, color = dose, dodge_width = 0) |> 
  add_mean_bar(alpha = 0.5, width = c(0.6, 0.4, 0.6, 0.4)) |> 
  add_data_points_beeswarm(white_border = TRUE) |> 
  add_sem_errorbar(color = "#000000") |> 
  add_title(title = "p3: narrower top bars")

p4 <- study |> 
  tidyplot(x = group, y = score, color = dose, dodge_width = 0) |> 
  add_mean_bar(alpha = 0.5, width = c(0.6, 0.4, 0.6, 0.4)) |> 
  add_data_points_beeswarm(white_border = TRUE, dodge_width = 0.6) |> 
  add_sem_errorbar(color = "#000000") |> 
  add_title(title = "p4: group points with dodging")

p5 <- p4 |> 
  adjust_title(title = "p5: test asterisks") |> 
  add_test_asterisks(hide_info = TRUE)

p6 <- p4 |> 
  adjust_title(title = "p6: brackets removed") |> 
  add_test_asterisks(hide_info = TRUE, remove.bracket = TRUE)

Nested bar plot.

6.20 Stacked bar plot with error bar

library(tidyplots)

# Calculation
study_c <- study |> 
  dplyr::summarise(
    mean = mean(score), sem = sd(score) / sqrt(length(score)),
    .by = c(group, dose)) |> 
  dplyr::mutate(
    cmean = cumsum(mean),
    lower = cmean - sem, upper = cmean + sem,
    gpos = ifelse(group == "placebo", 1, 2),
    .by = group) |> 
  dplyr::mutate(
    group = forcats::fct_inorder(group),
    dose = forcats::fct_rev(dose))

# View
study_c
# A tibble: 4 × 8
  group     dose   mean   sem cmean lower upper  gpos
  <fct>     <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 placebo   high    4.2 0.663   4.2  3.54  4.86     1
2 placebo   low    12   1.58   16.2 14.6  17.8      1
3 treatment high   38.4 5.54   38.4 32.9  43.9      2
4 treatment low    22.8 0.663  61.2 60.5  61.9      2
# Plot
p1 <- study_c |> 
  tidyplot(x = group, y = mean, color = dose) |> 
  add_barstack_absolute(alpha = 0.5) |> 
  add_title(title = "p1: absolute barstack")

p2 <- p1 |> 
  adjust_title(title = "p2: vertical lines") |> 
  add_annotation_line(
    x = study_c$gpos, xend = study_c$gpos,
    y = study_c$lower, yend = study_c$upper)

p3 <- p2 |> 
  adjust_title(title = "p3: upper lines") |> 
  add_annotation_line(
    x = study_c$gpos - 0.1, xend = study_c$gpos + 0.1,
    y = study_c$upper, yend = study_c$upper)

p4 <- p3 |> 
  adjust_title(title = "p4: lower lines") |> 
  add_annotation_line(
    x = study_c$gpos - 0.1, xend = study_c$gpos + 0.1,
    y = study_c$lower, yend = study_c$lower)

p5 <- p2 |> 
  adjust_title(title = "p5: via 'ggplot2'") |> 
  add(ggplot2::geom_errorbar(
    ggplot2::aes(ymin = lower, ymax = upper), 
    width = 0.2, show.legend = FALSE, color = "#000000"))

Stacked bar plot with error bar.

6.21 Stacked bar plot with data labels

library(tidyplots)

# Calculation
study_c1 <- study |> 
  dplyr::summarise(
    mean = mean(score), mean_half = mean / 2,
    .by = c(group, dose)) |> 
  dplyr::mutate(
    cmean = cumsum(mean), sum = sum(mean),    
    gpos = ifelse(group == "placebo", 1, 2),
    .by = group) |> 
  dplyr::mutate(
    mean_rel = mean / sum,
    mean_half_rel = mean_half / sum,
    cmean_rel = cmean / sum) |> 
  dplyr::mutate(
    group = forcats::fct_inorder(group),
    dose = forcats::fct_rev(dose))

# View
study_c1
# A tibble: 4 × 10
  group dose   mean mean_half cmean   sum  gpos mean_rel mean_half_rel cmean_rel
  <fct> <fct> <dbl>     <dbl> <dbl> <dbl> <dbl>    <dbl>         <dbl>     <dbl>
1 plac… high    4.2       2.1   4.2  16.2     1    0.259         0.130     0.259
2 plac… low    12         6    16.2  16.2     1    0.741         0.370     1    
3 trea… high   38.4      19.2  38.4  61.2     2    0.627         0.314     0.627
4 trea… low    22.8      11.4  61.2  61.2     2    0.373         0.186     1    
# Plot
p1 <- study_c1 |> 
  tidyplot(x = group, y = mean, color = dose) |> 
  add_barstack_absolute(alpha = 0.5) |> 
  add_title(title = "p1: absolute stacked bar")

p2 <- p1 |> 
  adjust_title(title = "p2: data labels") |> 
  add_annotation_text(
    text = study_c1$mean, 
    x = study_c1$gpos, 
    y = study_c1$cmean - study_c1$mean_half)

p3 <- study_c1 |> 
  tidyplot(x = group, y = mean_rel, color = dose) |> 
  add_barstack_absolute(alpha = 0.5) |> 
  add_title(title = "p3: relative stacked bar")

p4 <- p3 |> 
  adjust_title(title = "p4: data labels") |> 
  add_annotation_text(
    text = paste0(
      study_c1$mean, "\n", 
      "(", round(study_c1$mean_rel, 2), ")"),
    x = study_c1$gpos,
    y = study_c1$cmean_rel - study_c1$mean_half_rel)

Stacked bar plot with data labels.

6.22 Pie and donut plots with data labels

library(tidyplots)

# Calculation
energy_c <- energy |> 
  dplyr::summarise(
    energy_sum = sum(energy),
    .by = energy_type) |> 
  dplyr::arrange(factor(energy_type)) |> # sort rows in consistent with factor levels
  dplyr::mutate(
    energy_ratio = energy_sum / sum(energy_sum),
    energy_ratio_half = energy_ratio / 2,
    energy_ratio_cum = cumsum(energy_ratio))

# View
energy_c
# A tibble: 4 × 5
  energy_type energy_sum energy_ratio energy_ratio_half energy_ratio_cum
  <fct>            <dbl>        <dbl>             <dbl>            <dbl>
1 Fossil           6078.       0.510             0.255             0.510
2 Nuclear          2247.       0.189             0.0943            0.699
3 Other             272.       0.0228            0.0114            0.722
4 Renewable        3312.       0.278             0.139             1    
# Plot
p1 <- energy_c |> 
  tidyplot(y = energy_sum, color = energy_type) |> 
  add_pie(reverse = TRUE) |> # in clockwise
  add_title(title = "p1: pie")

p2 <- p1 |> 
  adjust_title(title = "p2: pie with labels") |> 
  add_annotation_text(
    text = paste0(round(energy_c$energy_ratio, digits = 2) * 100, "%"),
    x = 1.1, # tailored (e.g., try 1 - 2)
    y = energy_c$energy_ratio_cum - energy_c$energy_ratio_half,
    fontface = "bold",
    color = "#ffffff")

p3 <- energy_c |> 
  tidyplot(y = energy_sum, color = energy_type) |> 
  add_donut(reverse = TRUE) |> # in clockwise
  add_title(title = "p3: donut")

p4 <- p3 |> 
  adjust_title(title = "p4: donut with labels") |> 
  add_annotation_text(
    text = paste0(round(energy_c$energy_ratio, digits = 2) * 100, "%"),
    x = 3, # tailored (e.g., try 2 - 4)
    y = energy_c$energy_ratio_cum - energy_c$energy_ratio_half,
    fontface = "bold",
    color = "#ffffff")

Pie and donut plots with labels.
Bankhead, Peter, Maurice B. Loughrey, José A. Fernández, et al. 2017. “QuPath: Open Source Software for Digital Pathology Image Analysis.” Scientific Reports 7 (1). https://doi.org/10.1038/s41598-017-17204-5.
Dettori, Joseph R., Daniel C. Norvell, and Jens R. Chapman. 2021. “Seeing the Forest by Looking at the Trees: How to Interpret a Meta-Analysis Forest Plot.” Global Spine Journal 11 (4): 614–16. https://doi.org/10.1177/21925682211003889.
Gearty, William, and Lewis A. Jones. 2023. “Rphylopic: An r Package for Fetching, Transforming, and Visualising PhyloPic Silhouettes.” Methods in Ecology and Evolution 14 (11): 2700–2708. https://doi.org/10.1111/2041-210X.14221.
Mu, Zepeng, Pratik Rath, and Jan Broder Engler. 2026. “Use Ggbreak.” February. https://github.com/jbengler/tidyplots/discussions/158.
Turner, Stephen D. 2026. “Free and Open-Source Images, Icons, and Tools for Creating Scientific Illustrations.” May 2. https://blog.stephenturner.us/p/free-open-source-images-tools-scientific-illustrations.