-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Allow default geom aesthetics to be set from theme #2749
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
Conversation
I think this is going to be a great addition to ggplot2. One comment that dovetails with something @baptiste mentioned at some point in a related post: It's unlikely that there's going to be one choice of color that is going to work for all geoms. Usually, the way this is solved in design is via a small palette of choices. E.g., there could be a foreground color, a background/fill color, and a few accent colors. The geom defaults would then pick the appropriate choices from this palette. As an example, themes in PowerPoint have 10 colors, background 1, background 2, text 1, text 2, and accent 1 through 6. For ggplot, where we often need to fill things, I'd propose to use something like background 1 & 2, text 1 & 2, accent 1 - 3, and fill 1 - 3. And if you wanted to make it even simpler, it could be just background, text, accent, and fill. |
Thanks @clauswilke - the palette idea would solve the variability issue I mentioned above. I just need to think critically about how to choose and document these palettes. |
Nice, thanks for doing this. For reference, there could be some interesting ideas to pick from Gadfly (notably: a number of foreground + background tints, but also the sort of automatic colour pairing that I mentioned in the issue (e.g deriving a nice palette by muting/complementing/... a base colour). |
I think we'll need at least colour.accent and fill.accent. I'm not sure how we'd define multiple accents (e.g. 1-3) to make it clear where they are used. The googlesheet should be helpful for pulling these together. We might want to consider naming them |
Alternatively, maybe vectors? |
Making it a vector suggests that there could be arbitrarily many of them. I don't know if that's any better. |
True – not entirely sure either if it's better. Mostly, I find names like colour1...colour2 quite non-descriptive and rather unhelpful (what if someone wants to generate such themes programmatically – e.g. with an app/add-in –, or extend their number, etc. -> immediately leads to things like assign()/get()/paste()). |
I don't think that's an axis of generalisation that will be helpful. This is about enabling built-in geoms to be themeable, so there is a fixed and known set of possible values. |
Not sure what's limiting this to built-in geoms – couldn't the ggunicorn package want to write
? |
What I'm trying to say is that while we could make it more flexible, I don't think it is a good idea because it makes the implementation more complex for relatively little gain (since it requires both a custom theme and a custom geom, and shatters the independence of themes and geoms). |
Hmm, I guess we're not seeing it the same way – I'll leave it at what I wrote above, should others want to weigh in. Edit: the only interaction I could see being a problem is if a user mixes a built-in theme that defines 3 colours with a geom that requires 4 in its default aes settings. But that doesn't seem technically unsurmountable – maybe set a hard limit of 5 and recycle shorter vectors. |
geom_sf is now the only geom that does not allow aesthetic setting from themes due to a unique way of setting default aesthetics. Presumably should be adapted in a future commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basic approach looks good.
Let's iterate on how the default aesthetics look, and how they are evaluated.
R/geom-.r
Outdated
@@ -107,11 +107,18 @@ Geom <- ggproto("Geom", | |||
setup_data = function(data, params) data, | |||
|
|||
# Combine data with defaults and set aesthetics from parameters | |||
use_defaults = function(self, data, params = list()) { | |||
use_defaults = function(self, data, params = list(), theme) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the code related to evaluating aesthetics in the special theme environment should be pulled out into its own function so that we can think about it in isolation, and then test it.
R/geom-defaults.r
Outdated
@@ -13,7 +13,11 @@ | |||
#' @rdname update_defaults | |||
update_geom_defaults <- function(geom, new) { | |||
g <- check_subclass(geom, "Geom", env = parent.frame()) | |||
old <- g$default_aes | |||
|
|||
env <- new.env() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If geoms are themeable, I think we can deprecate this function, so we should just leave as is and in the documentation mark it as internal, and point people towards the new theme system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we deprecate this, we will presumably need to make all aesthetics themeable so as to not lose the functionality?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah. In that case we should pull this out into a function that can be shared with eval_defaults()
R/layer.r
Outdated
if (empty(data)) return(data) | ||
|
||
self$geom$use_defaults(data, self$aes_params) | ||
# Combine aesthetics, defaults, & params | ||
self$geom$use_defaults(data, self$aes_params, plot$theme) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you rewrite these calls with many unnamed arguments, can you please rewrite to add names? (i.e. here use theme = plot$theme
)
R/sf.R
Outdated
@@ -178,11 +178,11 @@ GeomSf <- ggproto("GeomSf", Geom, | |||
|
|||
default_aesthetics <- function(type) { | |||
if (type == "point") { | |||
GeomPoint$default_aes | |||
aes(shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can we no longer inherit from point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because with the expr()
wrapper the default_aes
no longer played nice with defaults()
function or utils::modifyList()
which is used by sf
. I expect this can/will be reverted with this round of changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually now that I'm looking at this again, geom_sf can't currently inherit any "themeable" aesthetics because this function (and thus these aesthetics) get evaluated in draw_geom
step of layer.r
(rather than earlier in the get_defaults
step as do all other geoms), so it currently passes an unevaluated expression to grDevices::col2rgb()
which predictably gives an error . So this needs some refactoring to be "themeable" - either by evaluating aesthetics at the proper step (rather than passing NULL values as geom_sf does now) or by pulling theme into draw_geom
so that the sf aesthetics can be evaluated in the context of the plots theme. It would seem to me that the former is a preferable solution but I expect that it was written this way for a reason that will become obvious as I dig in. I'm fixing your other concerns now and will work on sf in a future commit.
R/theme-elements.r
Outdated
#' @param colour.accent2,color.accent2 accent colour 2, | ||
#' typically a bright colour used for geom_smooth et al. | ||
#' @param fill.accent accent fill colour, typically a darker version of fill | ||
#' @param alpha colour/fill transparency, between 0 & 1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need alpha as a default? People could always set individually with alpha()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably not. Currently alpha
has to be NA by default to respect RGBA colour specification
R/theme-elements.r
Outdated
#' @rdname element | ||
element_geom <- function(fill = NULL, fill.accent = NULL, | ||
colour = NULL, color = NULL, | ||
colour.accent1 = NULL, color.accent1 = NULL, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we used fill
and fill_1
?
And what if we ditched colour
+ color
in favo(u)r of col
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the aesthetics renaming code that I wrote, we should be able to get away with defining just one spelling of colour
(probably the British one). The intent was to internally rename everything to that spelling. It probably already works as is, but worst-case scenario a few more renaming calls would have to be added in some places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e., take the argument names of element_geom()
and run through this function:
Line 155 in 4d2ca99
standardise_aes_names <- function(x) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, this would require a ...
argument instead of listing everything explicitly, so maybe not the right approach. In any case, I’d argue against col
just because in the rest of ggplot2 col
is not used. And I think we also should talk about whether it’s colour.accent1
or colour_accent1
. We’re not entirely consistent with points vs underscores, and it would be good to pick a scheme and at least be consistent going forward.
Merge remote-tracking branch 'upstream/master' into theme_geom # Conflicts: # R/theme.r # man/element.Rd # man/theme.Rd
So I've implemented most of your suggested changes at least enough so that we can iterate on them. A couple notes:
|
How about implementing element_geom <- function(fill = NULL,
fill_1 = NULL,
colour = NULL,
colour_1 = NULL,
colour_2 = NULL,
...,
inherit.blank = FALSE) {
extra_aes <- ggplot2:::rename_aes(list(...))
aes_list <- modifyList(
list(
fill = fill, fill_1 = fill_1, colour = colour,
colour_1 = colour_1, colour_2 = colour_2,
inherit.blank = inherit.blank
),
extra_aes,
keep.null = TRUE
)
structure(
aes_list,
class = c("element_geom", "element")
)
}
element_geom()
#> List of 6
#> $ fill : NULL
#> $ fill_1 : NULL
#> $ colour : NULL
#> $ colour_1 : NULL
#> $ colour_2 : NULL
#> $ inherit.blank: logi FALSE
#> - attr(*, "class")= chr [1:2] "element_geom" "element"
element_geom(fill = "red", colour = "blue")
#> List of 6
#> $ fill : chr "red"
#> $ fill_1 : NULL
#> $ colour : chr "blue"
#> $ colour_1 : NULL
#> $ colour_2 : NULL
#> $ inherit.blank: logi FALSE
#> - attr(*, "class")= chr [1:2] "element_geom" "element"
element_geom(fill = "red", color = "blue", color_1 = "black")
#> List of 6
#> $ fill : chr "red"
#> $ fill_1 : NULL
#> $ colour : chr "blue"
#> $ colour_1 : chr "black"
#> $ colour_2 : NULL
#> $ inherit.blank: logi FALSE
#> - attr(*, "class")= chr [1:2] "element_geom" "element" Created on 2018-08-09 by the reprex package (v0.2.0). |
R/geom-.r
Outdated
eval_defaults = function(self, theme) { | ||
if (length(theme) == 0) theme <- theme_grey() | ||
|
||
lapply(self$default_aes, rlang::eval_tidy, data = list(theme = theme)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like the right approach to me, but I think I'd embed I do:
from_theme <- function(name) {
# plus error handling
theme$geom[[name]]
}
Then data = list(from_theme = from_theme)
Sorry for the long delay on my end, getting back to this now. I have just embedded and implemented |
Hey team, I'm so glad there is still interest in this feature! I continue to think this would be really cool and am sorry I let it lie fallow for so long. I'm working on reviving this zombie PR now. Currently, it's breaking a lot of seemingly unrelated tests which I will work through shortly, but I would appreciate a fresh review of the general approach and scope since so much has changed internally since @hadley and I first began implementing this. CC: @clauswilke @thomasp85 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked it over and added some comments.
fill_1 = NULL, | ||
colour = NULL, | ||
colour_1 = NULL, | ||
colour_2 = NULL, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add arguments color
, color_1
, color_2
and use those if the colour
versions are NULL
(or the other way round, doesn't really matter).
R/theme-elements.r
Outdated
@@ -491,7 +523,8 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { | |||
plot.tag.position = el_def("character"), # Need to also accept numbers | |||
plot.margin = el_def("margin"), | |||
|
|||
aspect.ratio = el_def("character") | |||
aspect.ratio = el_def("character"), | |||
geom = el_def("element_geom", "geom") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the "geom"
argument in el_def()
correct? Doesn't this imply that geom
inherits from geom
?
- pull theme through draw_geom into LayerSf and GeomSf
…. Update test files
read Rd for from_theme
Hi all, As of these most recent commits, all geoms now respect default aesthetics set by themes 🎉. Currently I’ve only done this for color, fill, and some text aesthetics however, after reviewing and agreeing on the implementation, we should discuss the scope of aesthetics we want to make “themeable”. Some notes on current implementation:In order to get this to work for geom_sf (which dynamically chooses defaults not during I originally attempted (viewable in a separate Some work still to be doneI still need to review/repair GeomSf More granular tests should absolutely be written and as I mentioned above, before this PR is merged there needs to be a larger discussion about scope and I suspect some work to implement further themeable defaults. |
I'm not sure I like the special casing of |
I believe that is still held up on a decision about layer-specific scales. I don't understand layer-specific scales, so I don't know where it stands. If there are no layer-specific scales, then then I think the Would it be worth making the theme available everywhere during the plot build/render? Or is that too confusing? It might allow more flexibility customizing other things. The mechanics of it aren't difficult (see https://github.com/paleolimbot/ggr6/blob/master/R/theme.R#L28-L44 ). |
My thinking was simply: We now have two very different cases that suggest However, I don't remember the discussion about layer-specific scales. Would handing |
I think there are two things being conflated with "layer specific scales" (at least in my head). The discussion I remember entered around allowing the geom access to the scales used for the different aesthetics... The other thing, which I have thought about a bit recently, is an API for setting scales on a per-layer base, such that two layer could use e.g. two different colour scales |
Let's move the discussion about the function signature of |
Please use #3854 for the discussion of |
#' typically a lighter version of colour | ||
#' @param colour_2 geom accent colour 2, | ||
#' typically a bright colour used for geom_smooth et al. | ||
#' @param fill_1 geom accent fill colour, typically a darker version of fill |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have y'all considered that there are a handful of different levels of (grayscale) fill in ggplot2 alone (likely for a reason)?
> geoms <- mget(ls(asNamespace("ggplot2"), pattern = "^Geom[A-Z]"), asNamespace("ggplot2"))
> unique(unlist(lapply(geoms, function(g) g$default_aes$fill)))
[1] "grey20" "grey35" "white" NA "black" "grey50" "grey60"
With that in mind, a more robust (and backwards compatible) interface would be something like theme(geom = element_geom(bg = "white", fg = "black", accent = "#3366FF", ...))
and hold the geom responsible for mixing the background and foreground colors. For example, GeomRect$default_aes$fill
currently defaults to "gray35"
(i.e., interpolate 35% of the way from fg
to bg
), so the default could be:
GeomRect$default_aes$fill = scales::colour_ramp(c(from_theme('bg'), from_theme('fg')), alpha = TRUE)(0.35)
By the way, I've been thinking a lot about this sort of stuff recently for the new wip auto-theming feature coming to shiny, and I'm thinking of abstracting out some of that functionality into a self-contained package so it could be used in any context. Towards that end, having this PR, as well as #3828 and #3833, would be hugely helpful in avoiding terrible hacks to reach that goal. Also, by using the interface I propose above, it'd be more consistent with the approach I'll be taking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In one of my earlier attempts at suggesting this geom-defaults-in-themes feature I floated the idea of extending rel()
beyond sizes; one could think of defining a few main colours — a bit like a css-variables model — and have functions to calculate derivative shades (e.g desaturated, complement, lighter/darker, etc.)
(The css analogy isn't entirely unmotivated as it seems likely more and more use-cases will want to style ggplots consistently with a surrounding webpage (cf Joe's comment on Shiny, for instance))
Edit: found one of these earlier discussions: #2173 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I implemented something like this in my just-for-fun ggr6 package...you can pass a function as a theme element that transforms the parent!)
Closing in favour of #5833 |
Motivation
The goal of this PR is to allow certain default aesthetics to be set by the plot theme. This can be used to achieve better default behaviours (e.g. plotting white points by default, instead of black, when using
theme_dark()
) and to allow user created themes to more easily control default aesthetics. This PR addresses #2239.Basic Approach
The basic approach is to alter each Geom's
default_aes
from something like:to something more like:
and then evaluate the
default_aes
mappings in an environment conscious ofplot$theme
. Sinceaes()
quotes all arguments, I currently I do this by wrappingdefault_aes
inexpr()
and evaluating it insidegeom$use_defaults
. This in turn gets called to build each layer and the guides. Some minor adjustments were also made to handle incomplete themes (I'm confident there is a better way, consider this a temporary patch) and to maintainupdate_geom_defaults()
functionality.@hadley, I think this is still slightly different from the algorithm we originally discussed. You originally indicated that we should evaluate the mappings within the plot environment and then call something like
on.exit(set_geom_theme(NULL))
, to reset to the default expression. I so far have been unable to make this work, and I think it may require our evaluation to happen earlier in the code. If this is still what you have in mind, I can reconsider and refactor the code. I agree it could be more streamline to evaluate these defaults upfront rather than at multiple points in the build, I simply have yet to make that work.Next Steps
This PR is a work in progress. Currently I have only added theme elements
colour
andfill
and only updated thedefault_aes
forGeomPoint
andGeomRect
, eventually all geoms will need to be updated. This will mean making some decisions about how to handle variability across geom defaults. For example, looking just atcolour
: the default forgeom_smooth()
,geom_contour()
,geom_density2d()
, andgeom_quantile()
is all#3366FF
and the rest of the geoms default tocolour = "black"
(orgrey20
for boxplots and violins but this can and probably should beblack
also). It would make sense to set the default toblack
intheme_grey()
, but that either means thatgeom_smooth()
et al. should not be coded to inherit fromcolour
fromtheme()
ever, or these geoms will also be forced to default to black. I lean towards leaving these special defaults hardcoded in these cases as to not change expected behaviour. This may also influence exactly which elements should be set in themes (eg.size
has more meaningful variability across geoms that we may want to preserve). A full list of geom defaults is here. From my analysis, I suspect we will want to allowtheme()
to set at leastcolour
,fill
, andalpha
, potentially alsoshape
,size
andlinetype
. Fontfamily
andsize
should also be considered but likely should inherit from other text elements set bytheme()
.I have not yet updated documentation, added a NEWS bullet, or additional tests as all of this will be done further down the line. This branch does successfully build and pass tests. Running through existing package examples was very useful for finding side effects.
Visualizing the changes:
Created on 2018-07-10 by the reprex package (v0.2.0).