Skip to contents

ggpop gives you two ways to build a legend, and picking the right one keeps your code short:

  1. Native icon legends - for a legend keyed to your plot’s data. ggplot2 builds it for you; you only switch the keys to icons. This is what you want almost every time.
  2. Standalone composite legends - for a legend that is really a small annotated figure, decoupled from any plot (multiple grouped columns, mixed symbology, fixed pixel dimensions). ggplot2’s guide system cannot express these, so marker_legend() draws them for you.

The common case: native icon legends

Map an aesthetic, set legend_icons = TRUE, and let ggplot2 do the rest. The icon keys are drawn by ggpop’s custom key glyph and recoloured to match each group; scale_legend_icon() sizes them.

Show the code
df_modes <- data.frame(
  x     = c(1, 2, 3, 4, 5, 6, 7, 8),
  y     = c(1, 2, 1, 2, 1, 2, 1, 2),
  group = c("Car", "Bus", "Subway", "Bicycle", "Plane", "Ferry", "Truck", "Walking"),
  icon  = c("car", "bus", "train-subway", "bicycle", "plane", "ship", "truck", "person-walking"),
  stringsAsFactors = FALSE
)
df_modes$group <- factor(df_modes$group, levels = df_modes$group)

col_map <- c(
  Car = "#E41A1C", Bus = "#377EB8", Subway = "#4DAF4A", Bicycle = "#984EA3",
  Plane = "#FF7F00", Ferry = "#009E9E", Truck = "#A65628", Walking = "#666666"
)

ggplot(df_modes, aes(x = x, y = y, icon = icon, colour = group)) +
  geom_icon_point(size = 6, dpi = 120, legend_icons = TRUE) +
  scale_colour_manual(values = col_map) +
  coord_cartesian(ylim = c(0.5, 2.6), clip = "off") +
  scale_legend_icon(size = 6) +
  theme_minimal()
Figure 1: A native icon legend - ggplot2 builds the guide, ggpop draws the keys.

Tip

For any legend tied to your data, stop here. The native path stays in sync with your scales automatically and needs no manual layout. Reach for marker_legend() only when you need a standalone composite that ggplot2 guides cannot produce.

One ordering rule: scale_legend_icon() must come after any theme() call, because a later theme() resets the legend key size.

Markers beyond Font Awesome

The icon aesthetic accepts more than Font Awesome names. ggpop ships a set of bundled markers, and you can register a folder of your own .svg files. List what is available with ggpop_markers():

ggpop_markers()$bundled
 [1] "circle-cross"        "circle-hollow"       "circle-inset"
 [4] "circle-solid"        "diamond-cross"       "diamond-hollow"
 [7] "diamond-inset"       "diamond-solid"       "plus-bold"
[10] "plus-hollow"         "square-cross"        "square-hollow"
[13] "square-inset"        "square-solid"        "triangle-down"
[16] "triangle-down-inset"

These names work anywhere an icon is expected - in the geoms above and in the composite legends below. To use your own SVGs, pass a folder via icon_path (or set options(ggpop.icon_path = "path/to/svgs")) and reference each file by its bare name.

Standalone composite legends with marker_legend()

When a legend needs multiple grouped columns, mixed symbology, and a fixed size - the kind of figure often exported as a standalone image - ggplot2’s guide system falls short. marker_legend() takes a tidy data frame of icon + label (+ optional per-row colour and column) and lays it out for you.

A Font Awesome composite

No bundled markers required - any icon source works, including mixed sources in one legend.

Show the code
df_legend <- data.frame(
  column = c(1, 1, 1, 2, 2, 2),
  icon   = c("car", "bus", "bicycle", "plane", "ship", "truck"),
  label  = c("Car", "Bus", "Bicycle", "Plane", "Ferry", "Truck"),
  colour = c("#E41A1C", "#377EB8", "#984EA3", "#FF7F00", "#009E9E", "#A65628"),
  stringsAsFactors = FALSE
)

marker_legend(
  df_legend,
  title = "Transport modes",
  marker_size = 5, label_size = 4, col_spacing = 14, label_gap = 1.6
)
Figure 2: A two-column composite legend built entirely from Font Awesome icons.

Multi-column composite legend

This is the use case marker_legend() exists for: a multi-column legend that encodes two semantic dimensions simultaneously — here, region type (colour) and indicator domain (column) — using bundled markers to distinguish subcategories.

Show the code
blue <- "#1E88E5"
teal <- "#2A9D8F"

df_legend <- rbind(
  data.frame(
    column = 1, colour = blue,
    icon  = c("square-inset", "square-hollow", "square-cross", "square-solid"),
    label = c("Urban — Excellent", "Urban — Good",
              "Urban — Fair",     "Urban — Poor")
  ),
  data.frame(
    column = 2, colour = teal,
    icon  = c("circle-inset", "circle-hollow", "circle-cross", "circle-solid"),
    label = c("Rural — Excellent", "Rural — Good",
              "Rural — Fair",     "Rural — Poor")
  ),
  data.frame(
    column = 3, colour = teal,
    icon  = c("diamond-inset", "diamond-hollow", "diamond-cross", "diamond-solid"),
    label = c("Remote — Excellent", "Remote — Good",
              "Remote — Fair",      "Remote — Poor")
  ),
  stringsAsFactors = FALSE
)

marker_legend(
  df_legend,
  marker_size = 5, label_size = 4, dpi = 200,
  col_spacing = 3, row_spacing = 0.8, label_gap = 0.4
) +
  coord_cartesian(xlim = c(-0.6, 8.5), ylim = c(-3.2, 0.48), clip = "off")
Figure 3: A standalone composite legend encoding region type and health indicator domain.

Note

marker_legend() returns a plain ggplot. Add ggplot2::annotate() layers for extra symbols or labels, then export at exact pixel dimensions with ggplot2::ggsave(width = W / 300, height = H / 300, dpi = 300).

Composite legends built from a data frame: legend_canvas()

marker_legend() above lays out one flat list of icon + label rows. Some legends need more structure than that - an icon grid crossed by two dimensions, a block of colour tiles for a grouping variable, and a small key for extra symbols (a trend line, a shaded band, a flagged point) - all in one figure. legend_canvas() builds exactly that from a single tidy data frame, df_legend, where a type column tells it what to draw:

type Renders as Lives in
icon An icon marker at row/col grid_section
swatch A filled rectangle group_section or symbol_section
line A short line segment symbol_section
point A bold glyph (default "*") symbol_section

icon-typed rows always render as icons no matter what type says - the value is a bookkeeping label there, not a switch. swatch/line/point are the only values key_legend() actually dispatches on, and that dispatch is the only place a new type could be added.

Deriving the icon grid from data with icon_grid()

If your icon-grid combinations already exist in a data frame - say, one row per package-size / distance-zone combination - icon_grid() derives the unique row/col positions for you instead of typing them by hand. Factor columns keep a deliberate row/col order; unordered columns sort automatically.

df_plans <- data.frame(
  size     = factor(c("Small", "Small", "Large", "Large"), levels = c("Small", "Large")),
  distance = factor(c("Local", "National", "Local", "National"), levels = c("Local", "National")),
  icon     = c("square-hollow", "square-solid", "circle-hollow", "circle-solid"),
  stringsAsFactors = FALSE
)
df_plans$cell_label <- paste0(
  substr(df_plans$size, 1, 1), "-",
  c(Local = "Loc", National = "Natl")[as.character(df_plans$distance)]
)

df_grid <- icon_grid(
  df_plans, icon = "icon", label = "cell_label",
  row = "size", col = "distance"
)
df_grid
  section type  label color          icon row col
1    grid icon  S-Loc  <NA> square-hollow   1   1
2    grid icon S-Natl  <NA>  square-solid   1   2
3    grid icon  L-Loc  <NA> circle-hollow   2   1
4    grid icon L-Natl  <NA>  circle-solid   2   2

Assembling grid + group + symbol sections

Combine that grid with a colour-tile group (group_section) and a small symbol key (symbol_section) by rbind()-ing three data frames that share section/type/label/color columns:

Show the code
plan_colours <- c(Standard = "#8D99AE", Express = "#EF476F")

df_legend <- rbind(
  df_grid,
  data.frame(
    section = "plan", type = "swatch",
    label = names(plan_colours), color = unname(plan_colours),
    icon = NA, row = NA, col = NA
  ),
  data.frame(
    section = "value", type = c("line", "swatch", "point"),
    label   = c("Best-value frontier", "Acceptable range", "Best pick"),
    color   = c("black", "grey75", "black"),
    icon = NA, row = NA, col = NA
  )
)

legend_canvas(
  df_legend,
  grid_title       = "Package size × distance zone",
  group_section    = "plan", group_title = "Plan", group_width = 0.9, group_gap = 0.3,
  symbol_section   = "value", symbol_right_gap = 0.8, symbol_key_width = 0.5,
  col_spacing = 2.2, row_spacing = 1.2, label_gap = 0.15,
  marker_size = 5, label_size = 3.6, dpi = 150
)
Figure 4: A grid + group + symbol composite legend built from one df_legend data frame.

Tip

group_title lets the displayed heading differ from the section value used to match rows - keep section values plain and lowercase ("plan") while the on-plot title stays capitalised ("Plan"). grid_section and grid_title work the same way for the icon grid.

Column spacing has to leave room for your longest label in that column before the next section starts - if sections start overlapping, widen col_spacing, group_width, or symbol_right_gap rather than shortening labels first.

Stacking a plot and its legend with legend_strip()

The composite above is a self-contained ggplot. To pin it under a real plot in one exported figure - rather than a standalone panel - add it with legend_strip(strip_plot, height):

Show the code
df_points <- data.frame(
  cost = c(4, 6, 9, 14, 7, 11, 16, 22),
  days = c(6, 4, 2, 1, 5, 3, 1.5, 1),
  plan = c("Standard", "Standard", "Express", "Express",
           "Standard", "Standard", "Express", "Express")
)

p_main <- ggplot(df_points, aes(x = cost, y = days, colour = plan)) +
  geom_point(size = 3) +
  scale_colour_manual(values = plan_colours, guide = "none") +
  scale_x_continuous("Cost ($)") +
  scale_y_continuous("Delivery time (days)") +
  ggtitle("Cost vs. delivery time") +
  theme_classic(base_size = 13) +
  theme(axis.line.x = element_blank())

p_legend <- legend_canvas(
  df_legend,
  grid_title       = "Package size × distance zone",
  group_section    = "plan", group_title = "Plan", group_width = 0.9, group_gap = 0.3,
  symbol_section   = "value", symbol_right_gap = 0.8, symbol_key_width = 0.5,
  col_spacing = 2.2, row_spacing = 1.2, label_gap = 0.15,
  marker_size = 5, label_size = 3.6, dpi = 150
)

p_main + legend_strip(p_legend, height = 1.4)
Figure 5: A scatter plot with its legend_canvas() legend stacked directly below it.

Tip

height is a physical size in inches, independent of the main plot’s own aspect ratio - set it once and ggsave(width =, height =) on the combined figure exactly like any other ggplot. Dropping theme_classic()’s bottom axis.line (as above) avoids a redundant rule sitting right above the legend - keep the left axis line, drop only axis.line.x.