20.3 New geoms
While many things can be achieved by creating new stats, there are situations where creating a new geom is necessary. Some of these are
- It is not meaningful to return data from the stat in a form that is understandable by any current geoms.
- The layer need to combine the output of multiple geoms.
- The geom needs to return grobs not currently available from existing geoms.
Creating new geoms can feel slightly more daunting than creating new stats as the end result is a collection of grobs rather than a modified data.frame and this is something outside of the comfort zone of many developers. Still, Apart from the last point above, it is possible to get by without having to think too much about grid and grobs.
The main functionality of geoms is, like for stats, a tiered succession of calls: draw_layer()
, draw_panel()
, and draw_group()
, and it follows much the same logic as for stats. It is usually easier to implement a draw_group()
method, but if the layer is expected to handle a large amount of distinct groups you should consider if it is possible to use draw_panel()
instead as grid performance will take a hit when dealing with many separate grobs (e.g. 10,000 pointGrobs with a single point each vs. a single pointGrob with 10,000 points). In line with stats, geoms also have a setup_params()
+setup_data()
pair that function in much the same way. One note, though, is that setup_data()
is called before any position adjustment is done as part of the build step.
If you want a new geom that is a version of an existing geom, but with different input expectations, it can usually be handled by overwriting the setup_data()
method of the existing geom. This approach can be seen with geom_spoke()
which is a simple reparameterisation of geom_segment()
:
print(GeomSpoke$setup_data)
#> <ggproto method>
#> <Wrapper function>
#> function (...)
#> f(...)
#>
#> <Inner function (f)>
#> function (data, params)
#> {
#> data$radius <- data$radius %||% params$radius
#> data$angle <- data$angle %||% params$angle
#> transform(data, xend = x + cos(angle) * radius, yend = y +
#> sin(angle) * radius)
#> }
If you want to combine the functionality of multiple geoms it can usually be achieved by preparing the data for each of the geoms inside the draw_*()
call
and send it off to the different geoms, collecting the output in a gList
(a list of grobs) if the call is draw_group()
or a gTree
(a grob containing multiple children grobs) if the call is draw_panel()
. An example of this can be seen in geom_smooth()
which combines geom_line()
and geom_ribbon()
:
print(GeomSmooth$draw_group)
#> <ggproto method>
#> <Wrapper function>
#> function (...)
#> f(...)
#>
#> <Inner function (f)>
#> function (data, panel_params, coord, se = FALSE, flipped_aes = FALSE)
#> {
#> ribbon <- transform(data, colour = NA)
#> path <- transform(data, alpha = NA)
#> ymin = flipped_names(flipped_aes)$ymin
#> ymax = flipped_names(flipped_aes)$ymax
#> has_ribbon <- se && !is.null(data[[ymax]]) && !is.null(data[[ymin]])
#> gList(if (has_ribbon)
#> GeomRibbon$draw_group(ribbon, panel_params, coord, flipped_aes = flipped_aes),
#> GeomLine$draw_panel(path, panel_params, coord))
#> }
If you cannot leverage any existing geom implementation for creating the grobs, you’d have to implement the full draw_*()
method from scratch. Later chapters will have examples of this.