Skip to contents
library(legendry)
#> Loading required package: ggplot2

The expert’s eye may have noticed that not all exported functionality are full blown guides or key constructors. Besides aforementioned important components, let’s talk a bit about the innards of guides.

Stacking

When we cut open a guide and examine its innards spilling out, our eyes first meets the skeleton that holds the guide together. This skeleton is the composition guide: a guide whose task it is to render and compose other guides. The composition guide may not be entirely foreign to a ggplot2-user. This is because vanilla ggplot2 also contains a composition guide, namely guide_axis_stack(). It accepts as it input some guide specifications and stacks the guides on top of one another.

standard <- ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  labs(
    x = "Engine displacement (litres)",
    y = "Highway Miles per Gallon"
  ) +
  theme(axis.line = element_line())

standard + guides(x = guide_axis_stack("axis", "axis", "axis"))

In legendry, we have the same composition operation: compose_stack(). For your regular axis guides for the x and y aesthetics, it works the same way as guide_axis_stack(). However, if the guides it stacks supports other aesthetics, like colour, it can also be used there.

staxis <- compose_stack("axis_base", "axis_base", "axis_base")

standard + 
  aes(colour = cty) +
  guides(
    x = staxis,
    colour = guide_colbar(first_guide = staxis, second_guide = "none")
  ) +
  theme(legend.axis.line = element_line())

Primitives

At this point, it might be wise to take a closer look at exactly what guide_axis_base() is doing. Once we pull on the threads on what its class is, the mystery starts to unravel:

class(guide_axis_base())
#> [1] "ComposeStack" "Compose"      "Guide"        "ggproto"      "gg"

Indeed, by using compose_stack("axis_base", "axis_base", ...) we have been stacking a stack! So when we peel back the layers and see what is inside guide_axis_base(), will we find more stacks? No: all good things must come to an end and so too must guides have their building blocks.

If the composition is the skeleton then these building blocks are the flesh providing function. These building blocks are called ‘primitives’ in legendry. For example, the guide_axis_base() function is a stack of three primitives. They respectively build the axis line, the ticks and the labels. In the complete guide, they don’t have any spacing between them, but let’s exaggerate the spacing for clarity.

standard +
  guides(x = compose_stack(
    primitive_line(),
    primitive_ticks(),
    primitive_labels(),
    theme = theme_guide(spacing = unit(5, "pt"))
  ))

Introducing the rest of the family

We’ve already met the more ‘famous’ primitives (lines, ticks, labels), so let’s round out our acquaintances. There is a spacer primitive that you can use to increase the spacing between the different pancakes in the stack, as it were. Then there is also the ‘ranged’ triplet: brackets, boxes and fences. These require a more niche range key and power the display of nested axes.

range_key <- key_range_manual(
  start = c(2, 4), end = c(5, 6), 
  name = c("First", "Second")
)

standard + guides(x = compose_stack(
  "axis_base",
  primitive_bracket(range_key, "curvy"),
  primitive_spacer(unit(0.5, "cm")),
  primitive_box(range_key),
  primitive_spacer(unit(0.5, "cm")),
  primitive_fence(range_key, rail = "outer")
))

The last member of the family is primitive_title(). Aside from just displaying text, it does three things:

  1. If we have >1 title, it will distribute the titles along the range. This makes it more convenient to label the lower and higher ends of a scale.
  2. It ‘absorbs’ the regular axis title by turning it off. Because the title primitive is a guide and not the actual title, this makes it slightly easier to prevent unwanted double titles.
standard + scale_x_continuous(guide = compose_stack(
  primitive_title(c(
    "Sneaky little displacementses", 
    "We needs it. Must have the displament")
  ),
  primitive_title("Gollum explains engine displacement to you")
)) + labs(x = "A normal person explains engine displacement")

  1. As it is implemented as a guide, it cannot take the place of regular axis titles. This also means that the titles will repeat over facets.
last_plot() +
  facet_grid(~ year)

Don’t use primitives directly

By giving them the primitive_-prefix, we have made it impossible to use them by name inside function that don’t expect them. This acts as a little bit of discouragement from using primitives as complete guides.

standard + 
  guides(x.sec = "ticks")
#> Error in `validate_guide()`:
#> ! Unknown guide: ticks

However, you can use primitives by name inside guide composition functions, or use their full constructor function.

standard +
  guides(x.sec = compose_stack("ticks"))

# Looks identical to the above
standard + 
  guides(x.sec = primitive_ticks())

Sandwiching

Aside from making a stack of primitives, we can also build a sandwich of primitives. Analogous to two pieces of bread on the outside with the good bits on the inside, a sandwich composition of guides often sees a centrepiece flanked by two other guides.

Similar to how we naively believed guide_axis_stack() to be its own little snowflake of a guide, so too is guide_colbar() in fact a composition:

class(guide_colbar())
#> [1] "ComposeSandwich" "Compose"         "Guide"           "ggproto"        
#> [5] "gg"

Focussing on the ‘bread’ part first, there are two guides we can use for flanking the innards. One is called ‘text’ and the other one is called ‘opposite’.

standard +
  aes(colour = cty) +
  scale_colour_viridis_c(
    guide = compose_sandwich(
      text     = primitive_labels(), 
      opposite = primitive_ticks()
    )
  )

They are called ‘text’ and ‘opposite’ because the ‘text’-guide will take on the position indicated by the legend.text.position theme element. Perhaps unsurprisingly; the guide at the other end is called the ‘opposite’ guide. You can expose this by swapping the theme element.

last_plot() + theme(legend.text.position = "left")

It should also be noted that if the guide has a label-supression mechanism, the ‘opposite’ guide will be drawn with surpressed labels. The mechanism is in place in guides like guide_axis()/guide_axis_base() for display in for example facet_grid(axis.labels = "margins").

standard +
  aes(colour = cty) +
  scale_colour_viridis_c(
    guide = compose_sandwich(
      text     = "axis_base", 
      opposite = "axis_base"
    )
  )

# The same plot
standard +
  aes(colour = cty) +
  scale_colour_viridis_c(
    guide = guide_colbar(vanilla = FALSE)
  )

The only thing that guide_colbar() does, that compose_sandwich() does not, is to adjust the theme settings to match ggplot2::guide_colourbar() more closely.

Alright, fine, I get the ‘bread’ part of the sandwich! What about the inside? I’m glad you asked.

Gizmos

The insides of the guide_colbar() is a so called ‘gizmo’. A gizmo differs from a primitive in that a gizmo is not a general building block, but rather a specialised display of a particular aesthetic. Also, it is not a complete guide as it does not allow you to complete the crucial task of translating the graphic display into an interpretation of data. It is called a ‘gizmo’ because it is sort of a gadget that I don’t have a more appropriate name for. Hence: gizmo.

My first gizmo

We can display only the gizmo from guide_colbar() —the sandwich spread, if you will— as gizmo_barcap(). It is also immediately clear why we need the bread in this case.

standard +
  aes(colour = cty) +
  scale_colour_viridis_c(
    guide = gizmo_barcap()
  )

Similarly guide_colsteps() has gizmo_stepcap(), which might be the second gizmo you may already have encountered.

Exotic gizmos

There are also gizmos that may be less familiar. A reasonably simple one is gizmo_grob(), which just allows you to place a grob in a guide. For example, we can stack it with an axis, or use it as the non-bread part of a sandwich. Please note that this is not a good representation of the plot’s colour aesthetic. I hope this wouldn’t have been necessary but: don’t make your plot like this!

my_grob <- grid::grobTree(
  grid::rectGrob(),
  grid::textGrob("Grob\nGizmo")
)
my_gizmo <- gizmo_grob(my_grob, width = unit(2, "cm"), height = unit(2, "cm"))

standard + 
  aes(colour = cty) +
  guides(
    x = compose_stack("axis", my_gizmo),
    colour = compose_sandwich(middle = my_gizmo, text = "axis_base")
  )

Perhaps some gizmo’s that are inherently more useful are gizmo_density() and gizmo_histogram(). By default, these wrap the density() and hist() functions that are then called on the layer’s data. You can pass arguments to these functions with the density.args and hist.args respectively. But I’ll repeat again: these are gizmos, not full guides. Also: please ignore that we’re guiding both colour and fill while only colour is visible.

standard +
  aes(colour = cty, fill = cty) +
  scale_colour_viridis_c(
    option = "C",
    guide = gizmo_density(density.args = list(adjust = 0.7))
  ) +
  scale_fill_viridis_c(
    option = "D",
    guide = gizmo_histogram(hist.args = list(breaks = 10))
  )

However, you can easily promote these gizmos to full guide by adding an axis. The compose_sandwich() can do a reasonable job for this.

standard +
  aes(colour = cty, fill = cty) +
  scale_colour_viridis_c(
    option = "C",
    guide = compose_sandwich(
      middle = gizmo_density(just = 1), 
      text = "axis_base"
    )
  ) +
  scale_fill_viridis_c(
    option = "D",
    guide = compose_sandwich(
      middle = gizmo_histogram(just = 0.5),
      text = "axis_base"
    )
  )

Other compositions

The following compositions are more situational and aren’t part of any ‘proper’ guide. The compose_ontop() guide lets you render one guide over the other. We can see in the plots below that it allows us to touch the panel when composing on top of one another, but not when stacking.

top <- guide_axis_base(
  key = key_manual(32, label = "Here is 32"),
  theme = theme_guide(
    ticks.length = unit(1.5, "cm"),
    ticks = element_line(arrow = arrow()), 
    line = element_blank()
  )
)

standard + guides(y.sec = compose_ontop("axis", top)) +
  labs(title = "compose_ontop")

standard + guides(y.sec = compose_stack("axis", top)) +
  labs(title = "compose_stack")

There is also compose_crux() (or ‘cross’) that is the parent composition of compose_sandwich(). Instead only allowing bread on two sides, it can also put guides on the top and bottom. It isn’t currently in use by any complete guide though.

ball <- gizmo_grob(grid::circleGrob(
  r = unit(0.5, "cm"), gp = grid::gpar(fill = "tomato")
))
block <- gizmo_grob(grid::rectGrob(
  width = unit(1, "cm"), height = unit(1, "cm"), 
  gp = grid::gpar(fill = "dodgerblue")
))

standard +
  aes(colour = cty) +
  scale_colour_viridis_c(
    guide = compose_crux(
      centre = gizmo_barcap(), 
      left = "axis_base", right = "axis_base",
      top = ball, bottom = block
    )
  )