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

Adding new theme elements to ggplot2 #5521

Closed
eliocamp opened this issue Nov 13, 2023 · 3 comments
Closed

Adding new theme elements to ggplot2 #5521

eliocamp opened this issue Nov 13, 2023 · 3 comments

Comments

@eliocamp
Copy link
Contributor

I'm wondering what's the best way of adding new theme elements to ggplot2 plots.
As motivation, I wanted to make this plot, which includes a yellow line at the top of the plot:

image

I think this is not possible with vainilla ggplot2 as there is no element in the plot layout for a line in that place. My solution was to subclass the plot and create a new ggplot_gtable method that adds the line to the regular gtable.

Reprex here:

library(ggplot2)

theme_ba <- function() {
  list(
    # Normal theme stuff
    theme_minimal(base_size = 14),
    # Placeholder structure with special class "ba"
    structure(.Data = list(), class = "ba")  
  )
}

# The "ba" object adds a new class to the plot when added to it
ggplot_add.ba <- function(object, plot, object_name) {
  class(plot) <- c("ba_plot", class(plot))
  return(plot)
}


# The "ba_plot" object has special ggplot_build and ggplot_gtable methods. 

# The first method does nothing except to add a new class 
# to the built plot. This then uses the custom ggplot_gtable method
ggplot_build.ba_plot <- function(plot) {
  gb <- NextMethod("ggplot_build")
  class(gb) <- c("ba_plot", class(gb))
  return(gb)
}

# The ggplot_gtable method adds a line above the title panel. 
ggplot_gtable.ba_plot <- function(data) {
  # First, do all the normal ggplot_gtable stuff
  gt <- NextMethod("ggplot_gtable")  
  
  # Now, add the line
  line <- grid::linesGrob(x = c(0, 1), 
                          y = c(1, 1), 
                          gp = grid::gpar(col = "#fdd306",
                                          lwd = 10))
  
  panels <- gt$layout$name == "title"
  this_panel <- gt$layout[panels, ]
  
  gt <- gtable::gtable_add_grob(gt, line, t = this_panel$t, l = this_panel$l) 
    
  return(gt)
}


ggplot(mtcars, aes(cyl, disp)) +
  geom_point() +
  labs(title = "Title") +
  theme_ba()

This is a similar to what I did with in my tagger package, which adds a new panel tag element.

Now I'm wondering if this can be generalised. Create plots that have custom theme elements similar to title, subtitle, strip, etc. This would be useful, for example, for adding a "logo" element.

So a few questions. Is this the best way to do this? Could it be possible to extend ggplot2 extendability to also include adding custom elements like this natively?

@teunbrand
Copy link
Collaborator

I suppose it all depends on how customised you want your plot to be. Here are some thoughts to consider though.

  1. If you can, I would try to avoid implementing ggplot_build() and ggplot_gtable(), but I can also see that may be unavoidable in some cases (like in patchwork, gganimate, ggside, plotly).
  2. If you change the gtable layout, in particular the number of rows/columns and in which cells some parts appear, {patchwork} might not be able to figure out how your plot should be combined with others. You may recall filing Error when combining facet_nested() with patchwork teunbrand/ggh4x#4 where this issue surfaced.
  3. If you're not tied to the theme system, for logos and the like, you might want to have a look at Custom guide #5496 that let's you draw grobs in the places where you might find legends. Combined with Arbitrary positions for guides #5488, you wouldn't even need to do concessions for your regular legends. Granted, you cannot place these anywhere, for example in between a panel and an axis, or above a title, but it should cover some needs.
  4. Whenever possible, try to repurpose existing theme settings to your needs. For example, the top yellow line could be implemented as an element_rect() subclass that only draws the top line of the rectangle, and then you could use that as the plot.background theme element. That particular example is akin to what ggh4x::element_part_rect() does, so apologies for using examples from my own stuff. If you want the line above the title only, you can subclass your own element_text() variant. It is much less pain than working in the gtable directly.
library(ggplot2)

ggplot(mtcars, aes(cyl, disp)) +
  geom_point() +
  labs(title = "Title") +
  theme(
    plot.background = ggh4x::element_part_rect(
      side = "t", colour = "#fdd306", linewidth = 5
    ),
    plot.title = element_text(margin = margin(t = 1.9, unit = "mm"))
  )

Here is also a quick sketch of a custom element if you want to draw the line in the title cell only:

library(ggplot2)

my_title <- function(..., linecolour = "#fdd306") {
  elem <- element_text(...)
  elem$linecolour <- linecolour
  class(elem) <- c("my_title", class(elem))
  elem
}

element_grob.my_title <- function(element, ...) {
  grob <- NextMethod()
  
  gp <- grid::gpar(col = element$linecolour, lwd = 5)
  height <- grid::convertHeight(grid::grobHeight(grob), "cm")
  grid::gTree(
    children = grid::gList(
      grob,
      grid::linesGrob(y = height, gp = gp)
    ),
    width = unit(1, "npc"), height = height,
    cl = "absoluteGrob"
  )
}

ggplot(mtcars, aes(cyl, disp)) +
  geom_point() +
  labs(title = "Title") +
  theme(
    plot.title = my_title(margin = margin(t = 5))
  )

@teunbrand
Copy link
Collaborator

Oh and perhaps I should have said it first, but there is already a mechanism for declaring new theme elements, see ?register_theme_elements(). The hard part is to have these be drawn somewhere though.

@eliocamp
Copy link
Contributor Author

Ah, those custom elements are super useful. I didn't know you could make them! Thank you for the examples, they are very useful.

I also wasn't aware that the plot layout was so crucial for compatibility with other packages. Which I guess goes against the main idea behind this issue, which is to add flexibility to that layout.

I guess it's not a very good idea, then. Feel free to close the issue :)

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