mjskay / ggblend

Support for blend modes in ggplot2

Home Page:https://mjskay.github.io/ggblend/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Make sure ggblend works with ggrastr

mjskay opened this issue · comments

Will probably have to investigate ggrastr internals for this. These two plots should be different but aren't:

library(ggplot2)
library(ggblend)
library(ggrastr)

diamonds |>
  ggplot() + 
  geom_point(aes(carat, price, colour = cut)) |> rasterize(dpi = 30)

image

diamonds |>
  ggplot() + 
  geom_point(aes(carat, price, colour = cut)) |> blend("multiply") |> rasterize(dpi = 30)

image

Second one should look like this:

diamonds |>
  ggplot() + 
  geom_point(aes(carat, price, colour = cut)) |> blend("multiply")

image

Looking at ggrastr, it looks like it rasterizes at the geom level, not the layer level, by overriding Geom$draw_panel():

https://github.com/VPetukhov/ggrastr/blob/7aed9af2b9cffabda86e6d2af2fa10d4e60cc63d/R/rasterise.R#L46C6-L65

However, ggblend applies its operations at the layer level via Layer$draw_geom():

ggblend/R/transform.R

Lines 48 to 61 in 54bd316

transformed_layer = ggproto("TransformedLayer", geom_blank(inherit.aes = FALSE),
draw_geom = function(self, data, layout) {
check()
groblists = lapply(layers_to_transform, function(l) {
groblist = l$ggblend__draw_geom_(layout)
# do not transform within layers
lapply(groblist, groupGrob)
})
transform_groblists(groblists, grob_transform)
}
)
c(layers, list(transformed_layer))
}

My guess is that ggrastr should do the same?

Hi Matthew, I contributed the rasterise() code to {ggrastr} so I have a passing familiarity with it, but not so much with {ggblend}. IIRC the rasterisation takes place at draw-time, so that the number of pixels scale appropriately to the device/window size. Is ggblend pre-rendering the layers during the ggplot2::ggplot_gtable() step?

ggblend works by replacing each layer being blended with a "HiddenLayer", which when Layer$draw_geom() is called renders itself and stores the rendered grobs, but displays nothing. It adds a "TransformedLayer" to the layer list, which when Layer$draw_geom() is called takes the HiddenLayers' grobs and does the appropriate transformation then renders them.

So say you have something like

list(
  <LayerInstance>,
  <LayerInstance>
) |> blend("multiply")

This will return something like:

list(
  <HiddenLayer>,
  <HiddenLayer>,
  <TransformedLayer>
)

which can be added to a ggplot object. I think this would be compatible with {ggrastr} if it, instead of wrapping Geom$draw_panel() to rasterize a layer, it wrapped Layer$draw_geom() to rasterize the layer (which seemed the natural wrapping point to me since it is highest up in the call chain). However, if ggblend should be wrapping at a different point (like Geom$draw_panel()), I could try to change it.

Looking at a few different packages with this type of functionality, I see:

  • {relayer} wraps Geom$draw_panel() here
  • {ggfx} wraps Geom$draw_layer() here
  • {ggrastr} wraps Geom$draw_panel() here

I feel like there's not a big difference between these. Basically, each package has to intervene somewhere in the stack of Layer$draw_geom() -> Geom$draw_layer() -> Geom$draw_panel(). My logic was that wrapping further down the stack has greater potential for creating incompatibilities between packages, in case a geom author decides to override any of the preceding functions to do something different (including, say, not calling down to the lower functions at all). Thus, I overrode at Layer$draw_geom(). I don't see a big difference between that and overriding at Geom$draw_layer() (as Layer$draw_geom() basically just does a check and then calls Geom$draw_layer()), so I could probably switch to that. However, I'm not sure overriding at Geom$draw_panel() would be the best approach if it can be avoided, just in case a geom implements a weird version of Geom$draw_layer()...

But, I'm happy to change how ggblend is doing this if I've misinterpreted the ggplot internals in some way here.

I don't think you're misinterpreting the internals, this sounds about right. In any case, yeah if we move the ggrastr adjustments up a level, it should work fine:

library(ggplot2)
library(ggblend)
library(ggrastr)

# Manually override the layer method
rasterise.Layer <- function(input, dpi=NULL, dev="cairo", scale=1, ...) {
  dev <- match.arg(dev, c("cairo", "ragg", "ragg_png"))
  if (is.null(dpi)) {
    dpi <- getOption("ggrastr.default.dpi")
  }
  
  # Take geom from input layer
  old.geom <- input$geom
  # Reconstruct input layer
  ggproto(
    NULL, input,
    geom = ggproto(
      NULL, old.geom,
      draw_layer = function(...) {
        grobs <- old.geom$draw_layer(...)
        lapply(grobs, function(grob) {
          grob$dpi <- dpi
          grob$dev <- dev
          grob$scale <- scale
          return(grob)
        })
      }
    )
  )
}

file <- tempfile(fileext = ".png")
png(file, type = "cairo")

diamonds |>
  ggplot() + 
  geom_point(aes(carat, price, colour = cut)) |>  blend("multiply") |> rasterize(dpi = 30)

dev.off()
#> png 
#>   2
knitr::include_graphics(file)

Created on 2023-05-22 with reprex v2.0.2

Closing this as it should be resolved by VPetukhov/ggrastr#34 thanks to the speedy work of @teunbrand :)