Skip to contents

Design

ggposter builds a poster declaratively: a theme controls colours, fonts and spacing; a title band spans the full width; and the rest of the poster is a set of columns, each a vertical stack of cards. A card is a rounded box with an accent-coloured header tab and one piece of content – text, a table, a ggplot figure, or a strip of photos. Every card is built as a plain [grid] grob, and the whole poster is assembled as one [gtable::gtable], so the same building blocks work whether the content is a bullet list or a ggplot.

Content and layout are described either as an R list, or as YAML read by [read_poster_yaml()]. Figures, tables, and photos referenced from the YAML are passed in separately as R objects (objects = list(...)), so the YAML stays purely declarative while data analysis stays in R.

An example poster

The same mpg fuel-economy example used in the package README: a full-width title band and a two-column body of rounded, tab-headed cards for text, tables, figures, and a photo strip. It also shows height = "auto" (a card sized to fit its own content, instead of a fixed proportion of the column) and notes (a bullet-list description placed beside a table or figure):

tbl_class <- aggregate(cbind(hwy, cty) ~ class, data = mpg, FUN = function(x) round(mean(x), 1))
names(tbl_class) <- c("Class", "Mean hwy", "Mean cty")
class_best  <- tbl_class$Class[which.max(tbl_class$`Mean hwy`)]
class_worst <- tbl_class$Class[which.min(tbl_class$`Mean hwy`)]

tbl_drv <- aggregate(cbind(hwy, cty) ~ drv, data = mpg, FUN = function(x) round(mean(x), 1))
names(tbl_drv) <- c("Drivetrain", "Mean hwy", "Mean cty")
drv_best  <- tbl_drv$Drivetrain[which.max(tbl_drv$`Mean hwy`)]
drv_worst <- tbl_drv$Drivetrain[which.min(tbl_drv$`Mean hwy`)]

fig_facet <- ggplot(mpg, aes(displ, hwy, colour = class)) +
  geom_point() +
  labs(caption = paste(
    "• Highway mileage falls as engine displacement rises.",
    "• Compact and subcompact classes reach the highest mileage.",
    sep = "\n")) +
  theme_bw() +
  theme(legend.position = "inside", legend.position.inside = c(0.85, 0.72),
        legend.background = element_rect(fill = scales::alpha("white", 0.7), colour = NA),
        legend.key.size = unit(0.9, "lines"),
        plot.caption = element_text(hjust = 0, size = rel(1.3), colour = "black"))

fig_scatter <- ggplot(mpg, aes(cty, hwy, colour = drv)) +
  geom_point(alpha = 0.7) +
  theme_bw() +
  theme(legend.position = "inside", legend.position.inside = c(0.75, 0.32),
        legend.background = element_rect(fill = scales::alpha("white", 0.7), colour = NA))

fig_box <- ggplot(mpg, aes(drv, hwy, fill = drv)) +
  geom_boxplot(show.legend = FALSE) +
  labs(x = "Drivetrain", y = "Highway mpg") +
  theme_bw()
drv_med <- aggregate(hwy ~ drv, data = mpg, FUN = median)
drv_box_best <- as.character(drv_med$drv[which.max(drv_med$hwy)])

mpg_heat <- aggregate(hwy ~ class + drv, data = mpg, FUN = function(x) round(mean(x), 1))
fig_heat <- ggplot(mpg_heat, aes(drv, class, fill = hwy)) +
  geom_tile(colour = "white") +
  geom_text(aes(label = hwy), size = 2.6) +
  scale_fill_gradient(low = "#E8F5E9", high = "#2E7D32") +
  labs(x = "Drivetrain", y = "Class", fill = "Mean hwy") +
  theme_minimal() +
  theme(legend.position = "bottom")

img_dir <- system.file("extdata", package = "ggposter")
stock_photos <- c("small.JPG", "tall.jpg", "wide.jpg", "large.JPG")
stock_labels <- c("Photo A", "Photo B", "Photo C", "Photo D")

spec <- list(
  title = list(
    title = "Example Poster: Fuel Economy Patterns in the mpg Dataset",
    authors = "*Jane Doe (Example University), John Smith (Example Institute)",
    funding = "This is a demonstration poster for the ggposter package; it does not describe real research."
  ),
  layout = list(
    left  = c("objectives", "background", "methods", "summary_table", "fig_box", "fig_heat"),
    right = c("conclusions", "results_table", "fig_facet", "fig_scatter", "photos_2")
  ),
  sections = list(
    objectives = list(header = "OBJECTIVES", height = "auto", body = list(type = "text", md = c(
      "- Demonstrate the ggposter package.",
      "- Use the mpg fuel-economy dataset as example content.",
      "- Combine text, tables, figures, and photos in one poster."
    ))),
    background = list(header = "BACKGROUND", height = "auto", body = list(type = "text", md = c(
      "- Conference posters often mix text, tables, and figures.",
      "- ggposter arranges these as rounded, tab-headed cards.",
      "- Layout and content can be declared as an R list or a YAML file."
    ))),
    methods = list(header = "METHODS", height = "auto", body = list(type = "text", md = c(
      "- Data: the mpg dataset (234 vehicles, model years 1999-2008).",
      "- Figures: highway/city mileage by class and drivetrain.",
      "- Photos: generic stock images bundled with ggposter."
    ))),
    summary_table = list(header = "SUMMARY by class", height = "auto", body = list(
      type = "table", object = "tbl_class", title = "Mean mileage by vehicle class",
      notes = c(
        sprintf("- **%s** has the best highway mileage of all vehicle classes in this dataset.", class_best),
        sprintf("- **%s** has the worst, largely due to its greater size and weight.", class_worst),
        "- Compact and subcompact classes have nearly identical mean highway mileage.",
        "- Midsize vehicles average close to the compact/subcompact classes.",
        "- Pickup and SUV classes have the two lowest highway mileages, both under 19 mpg.",
        "- City mileage tracks highway mileage closely across all seven classes."
      )
    )),
    fig_box = list(header = "Mileage spread by drivetrain", height = "auto", body = list(
      type = "figure", object = "fig_box", notes_width = 0.4, height = 117,
      notes = c(
        "- Boxes show the full spread of highway mileage, not just the mean.",
        sprintf("- **%s**-wheel drive has the highest median highway mileage.", drv_box_best)
      )
    )),
    fig_heat = list(header = "Mean mileage: class x drivetrain", height = "auto", body = list(
      type = "figure", object = "fig_heat", notes_width = 0.4, height = 137,
      notes = c(
        "- Colour shows mean highway mpg for each class/drivetrain combination.",
        "- Blank cells are combinations that don't occur in the data."
      )
    )),
    conclusions = list(header = "CONCLUSIONS", height = "auto", body = list(type = "text", md = c(
      "- Compact and subcompact cars get the best highway mileage.",
      "- SUVs and pickups get the lowest.",
      "- ggposter can lay out this kind of summary automatically."
    ))),
    results_table = list(header = "SUMMARY by drivetrain", height = "auto", body = list(
      type = "table", object = "tbl_drv", title = "Mean mileage by drivetrain",
      notes = c(
        sprintf("- **%s**-wheel drive has the best highway mileage among the three drivetrain types.", drv_best),
        sprintf("- **%s**-wheel drive has the worst, mainly due to the added weight of the drivetrain.", drv_worst),
        "- The gap between front- and four-wheel drive is nearly 9 mpg highway."
      )
    )),
    fig_facet = list(header = "Mileage by class", height = "auto",
      body = list(type = "figure", object = "fig_facet", height = 280)),
    fig_scatter = list(header = "Highway vs. city mileage", height = "auto", body = list(
      type = "figure", object = "fig_scatter", notes_width = 0.45, height = 102,
      notes = c(
        "- Highway and city mileage are closely correlated.",
        "- 4-wheel drive vehicles cluster at the low-mileage end.",
        "- Front-wheel drive vehicles cluster at the high-mileage end."
      )
    )),
    photos_2 = list(header = "More sample photos", height = "auto", body = list(
      type = "image", files = stock_photos, labels = stock_labels,
      width = 230
    ))
  )
)

p <- poster(
  spec,
  objects = list(tbl_class = tbl_class, tbl_drv = tbl_drv,
                 fig_facet = fig_facet, fig_scatter = fig_scatter,
                 fig_box = fig_box, fig_heat = fig_heat),
  theme = theme_green(base_size = 24),
  base_dir = img_dir
)

p is a ggposter object – a thin wrapper around the assembled gtable. Font sizes and spacing are absolute physical measurements sized for the poster’s true paper size, so previewing at an arbitrary plot size (as this vignette must) needs a scaled-down render rather than printing p directly:

render_poster(p, "ggposter-minimal-preview.png", scale = 0.3, dpi = 150)
knitr::include_graphics("ggposter-minimal-preview.png")

An example poster built from the mpg dataset, with a placeholder title and authors, showing a full-width title band and a two-column body of tab-headed cards for text, tables, figures, and photos.

Save it at true size with [render_poster()]:

render_poster(p, "poster.pdf")          # true A1 size, fonts embedded
render_poster(p, "preview.png", scale = 0.25, dpi = 150)  # A4-ish preview

Content types

Every section has a header (or NULL for no header) and a body with a type:

  • "text" – a character vector of markdown-ish lines, rendered via [card_text()]. Lines starting with "- " become bulleted.
  • "table" – a data.frame/tibble named in objects, rendered via [card_table()] as a booktab-style table with an optional title/caption. The table’s font size is shrunk automatically to fit the column.
  • "figure" – a ggplot object named in objects, rendered via [card_figure()], re-themed to the poster’s base font size/family. To show several plots as one figure, combine them first with patchwork’s wrap_plots() or cowplot’s plot_grid() – both return a ggplot object, so the combined result can be passed as a single "figure" just like any other plot.
  • "image" – one or more photo files, rendered via [card_image()] as a labelled row (e.g. a “dominant species” photo band).

Irregular layouts with grid:

layout: places every section into one of a fixed set of equal-width columns, each stacked top to bottom – a good fit for most posters, but it can’t put a card beside another card of a different height, or make one card span several columns. grid: is an alternative to layout: for that case: instead of naming columns, give the total column count and place each section by x/y grid coordinates (0-based) and, optionally, a w/h span (both default to 1):

grid_spec <- list(
  title = list(title = "Irregular layout with grid:", authors = "*A. Author"),
  grid = list(
    columns = 3,
    boxes = list(
      list(name = "intro", x = 0, y = 0, w = 2),  # 2 columns wide
      list(name = "tall",  x = 2, y = 0, h = 3),  # 3 rows tall
      list(name = "a",     x = 0, y = 1),
      list(name = "b",     x = 1, y = 1),
      list(name = "wide",  x = 0, y = 2, w = 2)   # 2 columns wide
    )
  ),
  sections = list(
    intro = list(header = "INTRO", height = "auto", body = list(type = "text", md = c(
      "- `intro` spans both left columns (`w = 2`) on row 0.",
      "- A column-spanning box is placed like any other box, just with",
      "  `w` greater than `1`; its width is `w` times the grid column width.",
      "- Use it for anything that reads better at full measure: an",
      "  introduction, an abstract, or a transition between sections."
    ))),
    tall = list(header = "TALL", height = "auto", body = list(type = "text", md = c(
      "- `tall` spans all three rows (`h = 3`) in the right column.",
      "- A spanning box never stretches its neighbours: `a`/`b`/`wide`",
      "  keep whatever height their own content needs.",
      "- If `tall`'s content is shorter than the rows it spans, it just",
      "  leaves blank space below instead of growing them -- so a very",
      "  short `tall` box won't force `a`/`b`/`wide` to spread out to fill",
      "  the page.",
      "- This is the natural home for the one element that needs the most",
      "  room on the page: a large results table, an ordination plot, or",
      "  a long species list, sitting beside several shorter cards."
    ))),
    a = list(header = "A", height = "auto", body = list(type = "text", md = c(
      "- One of two side-by-side cards sharing row 1.",
      "- Both are plain `x`/`y` boxes with the default `w = 1`, `h = 1`.",
      "- Row 1's height is set by whichever of `a`/`b` needs more room."
    ))),
    b = list(header = "B", height = "auto", body = list(type = "text", md = c(
      "- The other side-by-side card in row 1.",
      "- `a` and `b` don't have to hold the same amount of text -- the",
      "  shorter one just leaves blank space below, top-anchored."
    ))),
    wide = list(header = "WIDE", height = "auto", body = list(type = "text", md = c(
      "- `wide` spans both left columns again, on row 2, closing out the",
      "  left side of the page under `a` and `b`.",
      "- A section can span columns on some rows and not others -- there's",
      "  no requirement that spans line up from row to row."
    )))
  )
)

p_grid <- poster(grid_spec, theme = theme_green(base_size = 24))
render_poster(p_grid, "ggposter-grid-preview.png", scale = 0.3, dpi = 150)
knitr::include_graphics("ggposter-grid-preview.png")

An irregular poster layout: a two-column-wide INTRO band and a two-column-wide WIDE band sandwich two side-by-side cards A and B on the left, while a single TALL card spans all three rows on the right.

A few rules to keep in mind:

  • Every section named in sections must appear in exactly one box, and every box must name a section – a mismatch, an out-of-bounds box (x + w past columns, or a negative x/y), or two boxes claiming the same cell all raise an error naming the offending box(es).
  • A row’s height comes from whichever non-spanning boxes (h == 1) sit in it, using the same height rule as layout: (a number shares out leftover space; "auto" measures the tallest box’s own content). A row covered only by spanning boxes falls back to an equal share of the leftover space.
  • If the resolved content is taller than the page, [poster()] warns (rather than erroring) so you can still inspect what overflowed.
  • If a spec sets both layout: and grid:, grid: wins and a warning is raised – keep only one of the two in a spec you intend to reuse.

Moving a poster between the sibling tools

ggposter has two siblings that build the same kind of poster by other routes: acposter (build-poster-pdf: Markdown -> pandoc -> headless Chrome) and qtposter (Quarto -> Typst). The body of a poster is written very differently in each – ggposter takes structured sections, the other two take prose under # headings – but two things are deliberately kept identical, so that moving a poster between them is a rewrite of the body only:

  • grid: is the shared interchange form for layout. ggposter and acposter accept exactly the same grid: block: columns plus boxes of name/x/y/w/h, 0-based, w/h defaulting to 1, with the same overlap/out-of-bounds/unplaced-section errors. layout: is not interchangeable – ggposter’s names columns (left:/right:), acposter’s is a row-by-row matrix – so prefer grid: for any layout you expect to carry across.

  • One header works in all three. acposter and qtposter write their metadata flat, at the top level; ggposter groups it into title/poster/theme. Both forms are accepted here, and the names the three tools use for the same thing (author/authors/poster-authors, institute/institutes/affiliation(s), note/footer/funding, paper, font-size, font) are folded into the right block and renamed. A top-level columns count stands in for layout when neither layout nor grid is given. So this is a complete spec:

    title: "A poster"
    author: ["A. One", "B. Two"]
    institute: "Somewhere Univ."
    note: "Funded by X"
    paper: A1
    columns: 2
    font-size: 20
    sections:
      intro: {header: "INTRO", body: {type: text, md: ["- Hello"]}}
      outro: {header: "OUTRO", body: {type: text, md: ["- Bye"]}}

    See [poster()] for the full mapping. Keys the other tools need and ggposter doesn’t (type, date, format) are simply ignored, so a header can carry them along.

poster_sample_flat.yml in inst/extdata/ is a poster written that way, next to poster_sample.yml in the nested style:

flat <- read_poster_yaml(
  system.file("extdata", "poster_sample_flat.yml", package = "ggposter")
)
names(flat)
#>  [1] "title"       "author"      "institute"   "note"        "type"       
#>  [6] "paper"       "orientation" "columns"     "font-size"   "sections"
p_flat <- poster(flat)

Theming

[poster_theme()] controls the accent colour, fonts, corner radius, and spacing; [theme_green()] is a ready-made preset. Latin and CJK (Japanese) fonts are set independently, and [poster_font()] picks an installed family automatically so PDF export embeds real glyphs rather than falling back to a substitute:

th <- theme_green(base_size = 24, base_family = poster_font())
th
#> 
#> ── poster_theme ────────────────────────────────────────────────────────────────
#> accent: #2E7D32
#> box_fill: #FFFFFF
#> box_border: #2E7D32
#> base_family: DejaVu Sans
#> cjk_family: DejaVu Sans
#> base_size: 24 pt
# `cjk_family` already defaults to an installed CJK-capable family, and each
# piece of text -- title row, card header, body line, table, photo caption --
# is drawn in it when it actually contains CJK characters, and in
# `base_family` otherwise. Name one explicitly only to override that choice:
th_jp <- poster_theme(base_family = "Noto Sans", cjk_family = "Noto Sans JP")

A mixed poster therefore needs no special handling: a Latin heading and a Japanese paragraph in the same card each get the family that can draw them (grid has no per-script fallback within a single gpar(), so the choice is made per grob).

Reproducing a real poster

The following reproduces the layout and text of a real conference poster (poster.pdf in this package’s development repository): a full-width title band and a two-column body of rounded, tab-headed cards for the introduction/background/methods on the left and conclusions/results on the right.

The title, author list, funding note, and all body text and tables are transcribed verbatim from the original poster. The cutting-regime scatterplot and the community ordination plot are illustrative, generated from the reported per-community means and standard deviations (not the original point-level field data, which is not published in the poster). The photos are generic stock images bundled with the package, standing in for the original species photographs.

tbl_courses <- tibble::tribble(
  ~`Golf course`, ~`Established year`, ~`Area (ha)`, ~`Total grassland spp.`,
  ~`Grassland spp. (mean +/- SD /m2)`, ~`All spp. (mean +/- SD /m2)`,
  "A", 1903, 20, 79, "6.5 +/- 2.8", "11.2 +/- 4.6",
  "B", 1926, 70, 55, "2.9 +/- 2.0", "5.2 +/- 4.6",
  "C", 1930, 55, 78, "4.4 +/- 2.8", "8.1 +/- 5.1",
  "D", 1956, 60, 55, "2.1 +/- 1.5", "4.0 +/- 3.3"
)

tbl_community <- tibble::tribble(
  ~Community, ~`No. of plots`, ~`Grassland spp. (/m2)`, ~`All spp. (/m2)`,
  ~`Cutting height (cm)`, ~`Cutting freq. (/yr)`, ~`Main dominant species`,
  "com1", 57, "6.1 +/- 2.9", "10.7 +/- 4.7", "12.9 +/- 15.3", "4.2 +/- 6.1",
    "P. chino var. viridis, A. hirta, M. sinensis",
  "com2", 38, "5.3 +/- 2.7", "10.6 +/- 5.2", "22.7 +/- 19.2", "4.4 +/- 4.4",
    "P. chino var. viridis, I. cylindrica var. koenigii",
  "com3", 37, "3.9 +/- 1.3", "6.9 +/- 3.1",  "5.1 +/- 4.8",   "10.1 +/- 5.3",
    "Z. aponica, I. cylindrica var. koenigii",
  "com4", 68, "1.4 +/- 0.7", "2.4 +/- 1.6",  "5.2 +/- 5.0",   "14.6 +/- 5.5",
    "Z. aponica"
)
set.seed(42)
community_params <- tibble::tribble(
  ~Community, ~n, ~height_mean, ~height_sd, ~freq_mean, ~freq_sd,
  "com1", 57, 12.9, 15.3, 4.2, 6.1,
  "com2", 38, 22.7, 19.2, 4.4, 4.4,
  "com3", 37, 5.1,  4.8,  10.1, 5.3,
  "com4", 68, 5.2,  5.0,  14.6, 5.5
)

# illustrative points matching the reported mean +/- SD, not raw field data
df_cutting <- purrr::list_rbind(purrr::pmap(community_params, function(Community, n,
                                                          height_mean, height_sd,
                                                          freq_mean, freq_sd) {
  tibble::tibble(
    Community = Community,
    cutting_height = pmax(0, rnorm(n, height_mean, height_sd)),
    cutting_freq   = pmax(1, rnorm(n, freq_mean, freq_sd))
  )
}))
fig_cutting <- ggplot(df_cutting, aes(cutting_freq, cutting_height)) +
  geom_point(alpha = 0.6, colour = "#2E7D32") +
  facet_wrap(~Community, nrow = 1) +
  labs(x = "Cutting frequency (/yr)", y = "Cutting height (cm)") +
  theme_bw()

df_ord <- purrr::list_rbind(purrr::pmap(community_params, function(Community, n, ...) {
  centre <- switch(Community,
    com1 = c(1, 1), com2 = c(1, -1), com3 = c(-1, 0.5), com4 = c(-1.2, -1))
  tibble::tibble(Community = Community,
                 NMDS1 = rnorm(n, centre[1], 0.4),
                 NMDS2 = rnorm(n, centre[2], 0.4))
}))
fig_ordination <- ggplot(df_ord, aes(NMDS1, NMDS2, colour = Community)) +
  geom_point(alpha = 0.7) +
  stat_ellipse() +
  theme_bw() +
  theme(legend.position = "bottom")

The layout, theme, and section text live in a YAML file bundled with the package:

yml_path <- system.file("extdata", "poster_sample.yml", package = "ggposter")
cat(readLines(yml_path)[1:20], sep = "\n")
poster:
  size: A1
  orientation: portrait

theme:
  accent: "#2E7D32"
  base_size: 24

title:
  title: "The relationship between grassland species richness and the management in long-established golf courses in Japan"
  authors: "*Toshikazu Matsumura (Konan Women's Univ.), Kenji Yamada (Example Museum of Nature and Culture), Aya Suzuki (Example Landscape Institute)"
  funding: "This study was supported by a research grant. We wish to thank the golf courses for field investigations."

layout:
  left:  [objectives, backgrounds, methods, course_summary, dominant_species]
  right: [conclusions, community_table, community_figure, ordination, other_species]

sections:
  objectives:
    header: "OBJECTIVES"

Assembling and rendering:

sample_poster <- poster(
  yml_path,
  objects = list(
    tbl_courses = tbl_courses, tbl_community = tbl_community,
    fig_cutting = fig_cutting, fig_ordination = fig_ordination
  ),
  theme = theme_green(base_size = 24),
  base_dir = dirname(yml_path)
)
render_poster(sample_poster, "ggposter-sample-preview.png", scale = 0.3, dpi = 150)
knitr::include_graphics("ggposter-sample-preview.png")

A reproduction of a real conference poster on grassland species richness in golf courses, showing a full-width title band and a two-column body of tab-headed cards for the introduction, methods, results tables, figures, and photos.

render_poster(sample_poster, "sample_poster.pdf")