How to stack (overlap) legend items in ggplot2

Viewed 68

I was wondering if it is possible to "stack" the items of the legend on a ggplot2 map (or any package that allows to produce the same result).

For spatial (sf, etc.) objects there is at least one package, mapsf, that produces the desired output, but I would like to produce this type of legend also for non-spatial objects (dataframes/tibble, etc.).

See here a reprex:

# A regular plot with ggplot2

library(ggplot2)

# A plot
ggplot(mtcars, aes(wt, mpg)) +
  geom_point(aes(size = drat))


enter image description here

# A map with the circles of the legend "stacked" (overlapping points)


# Need to conver to sf
mtcars_sf <- sf::st_as_sf(mtcars, coords = c("wt", "mpg"))

library(mapsf)

# See the legend on the top-right corner (blue arrow)
mf_map(x = mtcars_sf)
mf_map(x = mtcars_sf, var = "drat", type = "prop",
       inches = .2)


enter image description here

1 Answers

As Allan said, there is (to my knowledge) no general way to do this. But you can hack this together for your custom graph. Thomas Pedersen's awesome packages ggforce and patchwork are your friend.

I am essentially faking a legend. The challenge is to get the right dimensions between main plot and legend, and to stitch the legend in a reasonable way to the main plot. This is achieved by using carefully chosen radii on geom_ellipse, setting the coordinate ratio accordingly, and adjusting the coordinate limits from both plots. More comments in the code.

library(ggplot2)
library(ggforce)
library(patchwork)

## we cannot just use geom_point,
## because the size of the circles need to correspond to the legend later
## you will need to play around with this constant
r_constant <- 25
mtcars$r <- mtcars$drat / r_constant
## this is to make the points round  - it will have an effect on the panel dimension
ellipse_fac <- .1
p <-
  ggplot(mtcars) +
  geom_ellipse(aes(x0 = wt, y0 = mpg, a = r, b = r / ellipse_fac, angle = 0), fill = "darkred") +
  theme(legend.position = "none") +
  coord_equal(ellipse_fac)

## for the legend, chose rounded values from the radius range
unique_r <- unique(plyr::round_any(mtcars$r, .1))
y_circles <- floor(min(mtcars$mpg))
## I'm sorting the radii decreasingly, so that the circles overlap correctly
circles <- data.frame(x = 0, r = sort(unique_r, decreasing = TRUE))
## the segment / label poistion is also arbitrary
x_lab <- max(circles$r) + .1
y_seg <- y_circles + 2 * circles$r / ellipse_fac

p_leg <-
  ggplot(circles) +
  geom_ellipse(aes(x0 = x, y0 = y_circles + r / ellipse_fac, a = r, b = r / ellipse_fac, angle = 0), fill = "darkred", alpha = .5) +
  geom_segment(aes(x = x, xend = x_lab, y = y_seg, yend = y_seg)) +
  geom_text(aes(x = x_lab, y = y_seg, label = r), hjust = 0) +
  ## you need to set the ylimits similar to the main plot for control of legend position
  coord_equal(ratio = ellipse_fac, ylim = range(mtcars$mpg), clip = "off") +
  theme_void() +
  ## also need to set a margin
  theme(plot.margin = margin(r = .2, unit = "in"))

p + p_leg

I've added an alpha just for aesthetic reasons

Related