Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradient fills with R 4.1 #17

Open
jimjam-slam opened this issue Mar 26, 2021 · 11 comments
Open

Gradient fills with R 4.1 #17

jimjam-slam opened this issue Mar 26, 2021 · 11 comments

Comments

@jimjam-slam
Copy link

Hi Claus,

I'm thinking of having a play with R-devel to see what the new grid gradient support is like:

The ‘grid’ package now allows ‘gpar(fill)’ to be a ‘linearGradient()’, a ‘radialGradient()’, or a ‘pattern()’.

My naive attempt to see if this integrates with gridtext and ggtext is to just straight-up supply a grid::linearGradient in lieu of a colour string for fill arguments. Do you think it's likely to be that simple, or will it require some pass-through?

I understand dev is frozen on this package for now. Depending on the complexity, I might be able to submit a PR to enable this with a little guidance (or maybe I get lucky and it doesn't need any changes at all).

@clauswilke
Copy link
Collaborator

It's quite possibly that simple. Worth it to try out.

@jimjam-slam
Copy link
Author

Unfortunately not!

library(tidyverse)
library(ggtext)

{
  ggplot(mtcars) +
    aes(mpg, hp) +
    geom_point() +
    theme(
      plot.title = element_textbox(
        colour = "white",
        fill = grid::linearGradient(
          colours = c("#020024", "#102b81", "#833ab4"),
          stops = c(0, 0.4, 1)))) +
    labs(title = "Hello gradients!")
} %>% ggsave('test.pdf', .)
Saving 7 x 7 in image
# Error in bl_render(x$vbox_outer, x_pt, y_pt) :
#   Not compatible with STRSXP: [type=list].

I'm not so familiar with how STRSXP works, but I'll keep tinkering!

@clauswilke
Copy link
Collaborator

Ok. It means that somewhere in the code base we assume that fill is a string. May be difficult to track down and change.

@jimjam-slam
Copy link
Author

jimjam-slam commented Mar 27, 2021

I should note that this example, modelled on the one in the technical report for the gradients feature, does work:

library(tidyverse)
library(ggtext)
library(grid)

cairo_pdf('test.pdf')

p1 <-  ggplot(mtcars) +
  aes(mpg, hp) +
  geom_point() +
  theme_grey(base_size = 16) +
  theme(
    plot.title = element_textbox(
      colour = "white",
      fill = 'blue')) +
  labs(title = "Hello gradients!")
p1

grid.force()

# use grid.ls() to identify the title background
# here, it's "gridtext.ect.7"
grid.edit("gridtext.rect.7", grep = TRUE,
  gp = gpar(fill = linearGradient(
    colours = c("#020024", "#102b81", "#833ab4"),
    stops = c(0, 0.4, 1))))
dev.off()

image

@jimjam-slam
Copy link
Author

jimjam-slam commented Mar 27, 2021

So I can see one place where this might be explicitly assumed, in grid-renderer.h at line 21:

RObject gpar_lookup(List gp, const char* element) {
  if (!gp.containsElementNamed(element)) {
    return R_NilValue;
  } else {
    return gp[element];
  }
}

(It looks like all gpar elements are assumed to be strings, which squares with the existing docs.) Nope, my bad; those are the names of gp elements.

This is called for fill further down, when rect() is deciding whether to draw anything (grid-renderer.h at line 74):

RObject fill_obj = gpar_lookup(gp, "fill");

if (!fill_obj.isNULL()) {
  CharacterVector fill(fill_obj);
  if (fill.size() > 0 && !CharacterVector::is_na(fill[0])) {
    have_fill_col = true;
  }
}

As far as I can tell, fill_obj and fill don't go any further; this appears just to be about flagging have_fill_col in order to decide whether to draw a grob (although gp does get passed wholesale to either rect_grob() or roundrect_grob()).

I'm not entirely sure how to modify these sections to accommodate fill potentially being a list, though. Can we declare element as an RObject instead? Will that still work if it is indeed a string? Will gp.containsElementNamed() and fill.size() work?

Maybe CharacterVector fill(fill_obj); is the critical line here?

@jimjam-slam
Copy link
Author

jimjam-slam commented Mar 27, 2021

I tried running with this. No errors, but specifying a gradient silently produced a corrupt PDF. (I broke regular fills but got 'em going again, haha.)

RObject fill_obj = gpar_lookup(gp, "fill");

  if (!fill_obj.isNULL()) {

    // check fill object presence depending on type
    if (fill_obj.inherits("GridPattern")) {
      // gradient or pattern
      have_fill_col = true;
    } else {
      // a string (neither a gradient nor a pattern)
      CharacterVector fill(fill_obj);
      if (fill.size() > 0 && !CharacterVector::is_na(fill[0])) {
        have_fill_col = true;
      }
    }

I wonder if maybe text_grob, rect_grob and roundrect_grob in grid.cpp need to do some translation to make sure the GridPattern lists make it back out the other side :/

@jimjam-slam
Copy link
Author

Ah! But this modification does work for PNGs!

library(tidyverse)
library(ggtext)
{
  ggplot(mtcars) +
  aes(mpg, hp) +
  geom_point() +
  theme_grey(base_size = 20) +
  theme(
    plot.title = element_textbox(
      colour = "white",
      fill = grid::linearGradient(
    colours = c("#020024", "#102b81", "#833ab4"),
    stops = c(0, 0.4, 1)))) +
  labs(title = "Hello gradients!", subtitle = "Another go")
} %>% ggsave('test_grad_title.png', ., device = png(type = "cairo"))

test_grad_title

Maybe there's something going wrong with the PDF device when I do it this way... it definitely worked with the PDF device when I did it using grid.edit().

More work to do for geom_textbox too (which makes sense to me, since there're lots of ways data columns could work as aesthetics inside a gradient):

{
  ggplot(mtcars %>% rownames_to_column()) +
  aes(mpg, hp) +
  # geom_point() +
  geom_textbox(
    aes(label = rowname),
    colour = "white",
    fill = linearGradient(
      colours = c("#020024", "#102b81", "#833ab4"),
      stops = c(0, 0.4, 1))
  ) +
  theme_grey(base_size = 20) +
  theme(
    plot.title = element_textbox(
      colour = "white",
      fill = linearGradient(
    colours = c("#020024", "#102b81", "#833ab4"),
    stops = c(0, 0.4, 1)))) +
  labs(title = "Hello gradients!", subtitle = "Another go")
} %>% ggsave('test_grad_title2.png', ., device = png(type = "cairo"))
# Saving 6.67 x 6.67 in image
# Error: Aesthetics must be either length 1 or the same as the data (32): fill

@clauswilke
Copy link
Collaborator

Just FYI, you're welcome to try things out and see where you get, but please don't expect much assistance from me going forward. I'd much rather spend my limited time on the next iteration of the code base.

@jimjam-slam
Copy link
Author

Very reasonable 🙂 Thanks for the heads up!

@jimjam-slam
Copy link
Author

jimjam-slam commented Mar 29, 2021

(Also adding a note to myself that this issue has been discussed in tidyverse/ggplot2#3997 for the more general scope of ggplot2... might want to watch to see if that moves before I try to bite anything wild off with geom_textbox).

@jimjam-slam
Copy link
Author

jimjam-slam commented Apr 4, 2021

False alarm! I was misspecifying the PDF device. This example works for both PDF and PNG:

myplot <-
  ggplot(mtcars) +
  aes(mpg, hp) +
  geom_point() +
  theme_grey(base_size = 20) +
  theme(
    plot.title = element_textbox(
      colour = "red",
      fill = grid::linearGradient(
        colours = c("#020024", "#102b81", "#833ab4"),
        stops = c(0, 0.4, 1)))) +
  labs(title = "Hello gradients!", subtitle = "Another go")

ggsave('test_grad_title.png', myplot, device = png(type = "cairo-png"))
ggsave('test_grad_title.pdf', myplot, device = cairo_pdf)

Tested on both Linux (using the rocker/tidyverse:r-devel image) and Windows :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants