Adapting ggplot2 legend shapes when using the dark contour trick

data-visualisation
ggplot2
r-how-to
If you’ve given this trick a go, you may end up with a legend that doesn’t show the colours. We can fix this either with guides() or with a carefully chosen default shape in our custom ggplot2 theme!
Author
Affiliation

Building Stories with Data

Published

July 14, 2026

The problem

Say you have two different shapes to indicate whether a penguin is male or female, and the fill colour is tied to their species. What you end up with using all the normal tricks is this plot.

Code
library(ggplot2)

demo_plot <- penguins |>
  dplyr::filter(!is.na(sex)) |>
  ggplot(aes(x = flipper_len, y = bill_len, shape = sex, fill = species)) +
  geom_point(size = 5, colour = "#2c3d4f") +
  labs(
    title = "Perfectly proportional penguins",
    x = "Flipper length (mm)",
    y = "Bill length (mm)"
  ) +
  scale_shape_manual(values = c("female" = 21, "male" = 24)) +
  scale_fill_manual(
    values = c(
      "Chinstrap" = "#ffccff",
      "Gentoo" = "#76ad9d",
      "Adelie" = "#4c6dc6"
    )
  ) +
  theme_minimal() +
  theme(
    text = element_text(family = "Noah", face = "bold"),
    plot.title = element_text(size = rel(1.5)),
    plot.background = element_rect(fill = "#f1f1f1", colour = "#f1f1f1"),
    plot.margin = margin_auto(11),
    panel.grid = element_line(colour = "white")
  )
demo_plot

A scatterplot, showing three clusters of dots, each with their own colour (one per species). The dots are also circles for female and triangles for male. The legend shows the shapes correctly, but for the colour, we just have three black circles instead of circles that are the right colour for each species.

Note that the legend doesn’t do what we need it to do. We can’t see the fill colours!

The fix

Thankfully, there’s an easy fix to this one too: modifying the aesthetic of the guide for fill:

demo_plot +
  guides(fill = guide_legend(override.aes = list(shape = 22)))

This is the same graph as above, but the legend this time is three squares filled with the correct colour for each species.

Problem solved! 🥳

The more reusable fix

The alternative fix is to specify a default geom in your custom theme which is one with a contour. Just remember it’s pointshape not just shape.

demo_plot +
  theme(geom = element_geom(pointshape = 22))

This code produces exacty the same graph as above. Different solution to get to the same outcome.

Because our geom_point is specifiying a shape per species, and our scale_shape_manual determines which point gets assigned to which species, we actually don’t see any squares (our default pointshape) within the plot - the default shape is overridden by the shapes we assign elsewhere in the plot code, but it stays there for the legend. I personally like the square for the colour legend, and other shapes within the graph, so I’ll be adding that trick to future dataviz design system packages by default!

By the same token, we can apply a fill colour within our legend by default, without it affecting the fill colour of our points within the graph.

demo_plot +
  theme(geom = element_geom(pointshape = 22, fill = "#2c3d4f"))

In this one, the shapes in the legend have a dark fill, where previously they were just a shape outline.

Happy days! The key is making sure the default shape matches the way we’re using colour/fill in the rest of the graph!

Reuse

Citation

For attribution, please cite this work as:
Thompson, Cara. 2026. “Adapting Ggplot2 Legend Shapes When Using the Dark Contour Trick.” July 14, 2026. https://www.cararthompson.com/posts/2026-07-14-shapes-in-ggplot-legends/.