0

I'm trying to make ggplot2 to take lists and make elements of the lists available to other custom geom functions.

I have a new ggplot function that accepts lists:

ggplot.list <- function(data = NULL,
                           mapping = ggplot2::aes(),
                           ...,
                           environment = parent.frame()) {

    p <- ggplot2::ggplot(data=data[[1]],mapping=mapping,..., environment=environment)

    p$data_ext <- data[[2]]
    p
}

I create my list, and I plot the first data.frame:

l <- list(tibble(x=1:10, y=1:10), tibble(x=1:10+100, y =1:10+200))

ggplot(l) + geom_point(aes(x=x,y=y))

Ideally I would like to create something like this (which doesn't work), another geom that by default takes the data_ext from the ggplot object

geom_point2 <- function (mapping = NULL, data_ext = NULL, stat = "identity", position = "identity", 
                         ..., na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) 
{
    layer(data_ext = data_ext, mapping = mapping, stat = stat, geom = GeomPoint, 
          position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
          params = list(na.rm = na.rm, ...))
}

ggplot(l) + geom_point(aes(x=x,y=y)) +  geom_point2(aes(x=x,y=y))

I see that that my second data.frame is inside the ggplot object, but I don't know how to access it; that is, ggplot(l)$data_ext works. I've tried playing with ggproto but I'm not proficient enough to understand what to do with it, and if it could help.

ADDED By the way, I can achieve what I want with the pipe, but I don't want to confuse potiential users of my functions:

pipe_point2 <-function (plot, mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                        ..., na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) 
{

  plot +  layer(data = plot$data_ext, mapping = mapping, stat = stat, geom = GeomPoint,
          position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
          params = list(na.rm = na.rm, ...))
}

{ggplot(l) + geom_point(aes(x=x,y=y))} %>%  pipe_point2(aes(x=x,y=y))
Bruno
  • 115
  • 1
  • 9

1 Answers1

2

Important pointers from when I started making my own ggproto objects came from the following link Extending ggplot2. TL;DR: It is going to become very difficult to write new geoms without ggproto objects.

I have tried smuggling a matrix into a custom geom_raster(), which would be analogous to smuggling a list into custom geoms. What I learned was that the scales are very difficult to train, i.e. the position scales didn't know what the limits of the matrix are and the fill scale couldn't know what the limits of the matrix were. I got to the point where everything looked correct, but the colour bar guide didn't show the proper numbers.

Now lists are probably going to be a bit easier because you can split them into meaningful elements more easily and you can smuggle the list inside a dataframe. That means that you can easily do stuff like this and feed it to ggplot:

mydf <- data.frame(x = 1:3, y = 1:3)
mydf$z <- list(c("A","B"), 1:5, function(x) x/10)
print(mydf)
  x y                   z
1 1 1                A, B
2 2 2       1, 2, 3, 4, 5
3 3 3 function (x) , x/10

And I think that will be your best bet for smuggling in a list to a geom. You just call it as an extra aesthetic to a function.

g <- ggplot(mydf) + geom_point(aes(x = x, y = y, z = z))
# Warning: Ignoring unknown aesthetics: z

And check that the layer has acces to it:

layer_data(g)
  x y                   z PANEL group shape colour size fill alpha stroke
1 1 1                A, B     1    -1    19  black  1.5   NA    NA    0.5
2 2 2       1, 2, 3, 4, 5     1    -1    19  black  1.5   NA    NA    0.5
3 3 3 function (x) , x/10     1    -1    19  black  1.5   NA    NA    0.5

And it does, now you only have to write your custom geom panel drawing function to portray this list in a plot, for which I recommend the link above.

The only thing the following does is to print z before drawing the points, and accept z as an optional aesthetic:

MyGeom <- ggproto(
  "MyGeom", GeomPoint,
  draw_panel = function(data, panel_params, coord, na.rm = FALSE) {
    print(data$z)
    GeomPoint$draw_panel(data, panel_params, coord, na.rm = FALSE)
  },
  optional_aes = "z"
)

Now you need a wrapper for your layer which points to MyGeom:

geom_mine <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                         ..., na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) 
{
  layer(mapping = mapping, stat = stat, geom = MyGeom, 
        position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
        params = list(na.rm = na.rm, ...))
}

You can now use this in a plot:

ggplot(mydf) + geom_mine(aes(x = x, y = y, z = z))
[[1]]
[1] "A" "B"

[[2]]
[1] 1 2 3 4 5

[[3]]
function (x) 
x/10

And voila, it makes the plot and prints z, just like we said it should.

Hope these pointers help!

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • The problem is that I have two data.frames that are quite different in the real scenario. I would need to put a list with a dataframe inside a dataframe, and that would mean that the dataframe would get repeated as many times as the first one has rows. Not a good idea in my case (with a lot of data). – Bruno Apr 30 '19 at 06:01