Justin Landis
Justin Landis

Reputation: 2071

ggplot2: How to get a Geom Class to affect ScaleContinuous Class

I have begun to extend ggplot2 and I'm still getting a feel for how the package calls all of its internal functions.

I have a new ggproto class that extends one of the current Geom environments. The current model of the class will plot something along the discrete x axis, ideally touching the x axis ticks. This model works well when the y axis is already on a discrete scale, because the default expansion values only adds .6 padding. However on a continuous y scale, the default padding can make these new plotted objects seem far. In summary, how can I make my Geom class override the default expansion without just adding either scale_y_continuous(expand = c(0,0,.05,0) or scale_y_discrete(expand = c(0, 0, .6,0) to my layer function...

Consider the following reproducible example

library(dplyr)
library(tidyr)
library(ggplot2)
library(stringr)
mtcars0 <- as_tibble(mtcars, rownames = "CarNames") %>%
  gather(key = "qualities", value = "value", -CarNames) %>%
  group_by(qualities) %>%
  mutate(scaledValue = scale(value)) %>%
  ungroup() %>%
  mutate(carCase = case_when(str_detect(CarNames, "^[A-M]") ~ "A-M",
                             TRUE ~ "N-Z"))
"%||%" <- function(a, b) {
  if (!is.null(a)) a else b
}

MyText <- ggproto("GeomMyText",
                  GeomText,
                  extra_params = c("na.rm","padDist"),
                  setup_data = function(data, params){
                    #find bottom of plot with sufficent space
                    minpadding <- params$padDist %||% diff(range(data$y))*.05
                    data$y <- min(data$y) - minpadding
                    data 
                  })

geom_mytext <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                         ..., parse = FALSE, nudge_x = 0, nudge_y = 0, check_overlap = FALSE, 
                         na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, padDist = NULL) 
{ 
  if (!missing(nudge_x) || !missing(nudge_y)) {
    if (!missing(position)) {
      abort("You must specify either `position` or `nudge_x`/`nudge_y`.")
    }
    position <- position_nudge(nudge_x, nudge_y)
  }
  layer(data = data, mapping = mapping, stat = stat, geom = MyText, 
        position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
        params = list(parse = parse, check_overlap = check_overlap, 
                      na.rm = na.rm, padDist = padDist, ...))
}


result <- ggplot(mtcars0, aes(x = CarNames, value)) +
  geom_point() +
  geom_mytext(aes(label = carCase)) +
  theme(axis.text.x = element_text(angle=90))
#Default
result 
#Desired Result without having to call scale_y_continuous
result + scale_y_continuous(expand = c(0,0,0.05,0))

I'm assuming I need to extend the ScaleContinuous environment but I have no idea how to connect the MyText environment with it.

Any suggestions?

---- EDIT ----

Thanks for the quick replies! A few things -

  1. I am aware there is over plotting and clipping of labels, this isn't my actual Geom environment, just something I could put together that demonstrates my question.
  2. As I was afraid, it seems that everyone so far is providing the solution that was raised for this question. While supplying my own scale is less than ideal - because I have to put logic in to discern if they associated y axis is discrete/continuous, when ggplot2 already knows this, I figured that there might be a trick I was missing. For now I will continue development with the suggestions given. Thanks!

---- EDIT 2 ----

I took another look at the solution given here. The exact parameters I need to modify is

panel_params$y$continuous_range[1] <- panel_params$y$limits[1]

And I need to do this somewhere in draw_panel. It seems like the associated scales are contained there and the coord$transform(data, panel_params) is responsible for including the padding on the rescaled axis depending on what is set for panel_params$y$limits and panel_params$y$continuous_range.

Thanks again to everyone who contributed!

Upvotes: 3

Views: 209

Answers (2)

teunbrand
teunbrand

Reputation: 38063

I wouldn't generally recommend the route you are taking to achieve text at the bottom. The reason is that within the grammar of graphics paradigm, all different graphical elements (themes, coords, stats, facets, geoms) should all operate independently from oneanother. When I add a geom to a plot I expect the data to affect the scales but not the geom affecting the scales.

That said, here is the easiest way to automatically set the y-expansion with a geom, and that is to simply return a list of the geom and the scale from the constructor. This is similar to how geom_sf() automatically sets the coord_sf(). I know in your question you mention this is not how you like it, but there is just no natural infrastructure for geoms layers to communicate to the scales other than with data.

geom_mytext <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                         ..., parse = FALSE, nudge_x = 0, nudge_y = 0, check_overlap = FALSE, 
                         na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, padDist = NULL) 
{ 
  if (!missing(nudge_x) || !missing(nudge_y)) {
    if (!missing(position)) {
      abort("You must specify either `position` or `nudge_x`/`nudge_y`.")
    }
    position <- position_nudge(nudge_x, nudge_y)
  }
  layer <- layer(data = data, mapping = mapping, stat = stat, geom = MyText, 
        position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
        params = list(parse = parse, check_overlap = check_overlap, 
                      na.rm = na.rm, padDist = padDist, ...))
  list(layer, scale_y_continuous(expand = c(0,0,0.05,0)))
}

What I'm recommending instead is to simply set data$y <- -Inf, which will be ignored by the scale training, leaving the default expansion factor intact but placing your data at the x-axis anyway.

MyText <- ggproto("GeomMyText",
                  GeomText,
                  extra_params = c("na.rm","padDist"),
                  setup_data = function(data, params){
                    data$y <- -Inf
                    data 
                  })

Which gives me this plot:

enter image description here

For comparison this is what your reprex plots for me:

enter image description here

As an aside, there seem to be a lot of duplicated labels, which you might want to adress in your final geom.

Upvotes: 2

Allan Cameron
Allan Cameron

Reputation: 174506

Nice question - thanks for posting.

This is easier than you'd think. You simply bundle the desired scale object with the layer object that is returned from your geom_mytext function by concatenating them with c. In this example I have also bundled a coord_cartesian object so that I could turn clipping off to show the text properly. I have also changed the default check.overlap to TRUE because your labels are being overplotted.

Note I haven't changed your ggplot call at all

geom_mytext <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
                         ..., parse = FALSE, nudge_x = 0, nudge_y = 0, check_overlap = FALSE, 
                         na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, padDist = NULL) 
{ 
  if (!missing(nudge_x) || !missing(nudge_y)) {
    if (!missing(position)) {
      abort("You must specify either `position` or `nudge_x`/`nudge_y`.")
    }
    position <- position_nudge(nudge_x, nudge_y)
  }
  c(layer(data = data, mapping = mapping, stat = stat, geom = MyText, 
        position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
        params = list(parse = parse, check_overlap = check_overlap, 
                      na.rm = na.rm, padDist = padDist, ...)), 
    scale_y_continuous(expand = c(0,0,0.05,0)),
    coord_cartesian(clip = "off"))
}


result <- ggplot(mtcars0, aes(x = CarNames, value)) +
  geom_point() +
  geom_mytext(label = "test") +
  theme(axis.text.x = element_text(angle=90))

result 

enter image description here

Now come the caveats. Because you are supplying your own scale_y_continuous object, users won't like that ggplot complains when they try to add their own y scale. You will also need some logic to choose between adding a continuous or discrete y scale. I don't think these are insurmountable problems though.

Upvotes: 2

Related Questions