Can I add a coord ggproto object directly to a ggplot2?

Hello ggplot2 experts

Can I add a CoordCartesian ggplot2 layer as a ggproto subclass to a plot??.

In the example below, I know that I could use coord_cartesian(), but shouldn't CoordCartesian work?

library(tidyverse)
library(palmerpenguins)
penguins |>
  ggplot() +
  geom_point(aes(x = flipper_length_mm, y = body_mass_g)) +
  coord_cartesian()
#> Warning: Removed 2 rows containing missing values or values outside the scale range
#> (`geom_point()`).

  
penguins |>
  ggplot() +
  geom_point(aes(x = flipper_length_mm, y = body_mass_g)) +
  CoordCartesian
#> Error in !expand: invalid argument type

Created on 2024-01-18 with reprex v2.1.0

Not an expert, I can't really explain the "why" (though see the end), but I think I have the "how". Let's look at the source code of coord_cartesian(), which is very simple:

function (xlim = NULL, ylim = NULL, expand = TRUE, default = FALSE, 
  clip = "on") 
{
  ggproto(NULL, CoordCartesian, limits = list(x = xlim, y = ylim), 
    expand = expand, default = default, clip = clip)
}

So, we take create a new ggproto object, that inherits from the existing CoordCartesian ggroto object, adding a few properties.

Now let's look at the error message:

penguins |>
  ggplot() +
  geom_point(aes(x = flipper_length_mm, y = body_mass_g)) +
  CoordCartesian
#> Error in !expand: invalid argument type

So here the problem is quite specific: it's the lack of the expand property (that's one one the properties added by coord_cartesian()). This can also be seen in the traceback():

13: default_expansion(scale, expand = expand)
12: view_scales_from_scale(scale_x, self$limits$x, self$expand)
11: setup_panel_params(..., self = self)
...

The problem in step 12 is to call self$expand that doesn't exist.

So, we can try to correct that:

library(ggplot2)
library(palmerpenguins)


penguins |>
  ggplot() +
  geom_point(aes(x = flipper_length_mm, y = body_mass_g)) +
  CoordCartesian
#> Error in !expand: invalid argument type

CoordCartesian$expand <- TRUE

penguins |>
  ggplot() +
  geom_point(aes(x = flipper_length_mm, y = body_mass_g)) +
  CoordCartesian
#> Warning: Removed 2 rows containing missing values (`geom_point()`).

Created on 2024-01-18 with reprex v2.0.2

One important note, and potentially the explanation of the "why": if you try to copy CoordCartesian and modify the copy, that modifies the original too!

library(ggplot2)

CoordCartesian$expand
#> NULL

coord_proto <- CoordCartesian
coord_proto$expand <- TRUE

CoordCartesian$expand
#> [1] TRUE

So I guess ggproto objects are mutable and copied by reference; if you use CoordCartesian directly in your ggplot you risk inadvertently modifying it (and thus affecting every subsequent plot). coord_cartesian() simply makes a temporary copy that is safe to modify.

The part I don't really know about is why specifically the properties limits, expand, default and clip are the ones that need to be set during the copy.

1 Like

Awesome answer, thanks @AlexisW

Oh and thinking two more minutes about it, there is a much more obvious explanation to the "why": it allows the end-user to override this parameter when calling coord_cartesian(expand = FALSE).

The slightly surprising aspect in that case in that CoordCartesian$clip is pre-defined, whereas CoordCartesian$expand is not. There might be a logic in terms of what other ggproto typically inherit from CoordCartesian.

Seems weird that the defaults don't allow you to add it directly, given that's how the other Geom, Stat and Position ggproto objects work.

I raised a issue on ggplot github - will see what the developers there say

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.