Reputation: 2071
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 -
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
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:
For comparison this is what your reprex plots for me:
As an aside, there seem to be a lot of duplicated labels, which you might want to adress in your final geom.
Upvotes: 2
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
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