jrnold / ggthemes

Additional themes, scales, and geoms for ggplot2
http://jrnold.github.io/ggthemes/
1.33k stars 226 forks source link

Set default point color for solarized theme #63

Closed flying-sheep closed 6 years ago

flying-sheep commented 8 years ago
d <- data.frame(x = 1:10, y = (1:10)^2, s = as.character(1:10 < 6))
ggplot(d, aes(x, y, shape = s)) + geom_point() + theme_solarized(light = FALSE)

solarized

jrnold commented 8 years ago

The way that ggplot works, the default colors are set by Geoms, not by themes. So there's no way to do what you request. However, this is one reason why ggthemes makes the colors used in the themes available as data in the object ggthemes_data. The colors used in the solarized themes and scales are in ggthemes_data$solarized. For example,

d <- data.frame(x = 1:10, y = (1:10)^2, s = as.character(1:10 < 6))
ggplot(d, aes(x, y, shape = s)) + geom_point(colour = ggthemes_data$solarized$base['base1']) + theme_solarized(light = FALSE) 

rplot

flying-sheep commented 8 years ago

hmm, so if i do ggplot(d, aes(x, y, shape = s, colour = c)) + geom_point(), even if the geom color isn’t used at all, i still need to set it to get a sensible color in the legend?

that is, unless i use update_geom_defaults for a global change

jrnold commented 8 years ago

If you do ggplot(d, aes(x, y, shape = s, colour = c)) then color is an aesthetic mapped to the variable c and the colors themselves are determined by a scale. You use a color argument to a geom in order to directly set its value to a

You could also use update_geom_defaults, but since that applies globally and acts at a different time, it needs to be handled separately from the theme. I don't like changing the state anways, since that often leads to harder to debug bugs.

flying-sheep commented 8 years ago

yes, i know. my point is that in the shape legend, the point color will still be black instead of being taken from the theme or the color scale.

i’d need to do this if i wanted to have something pretty, which is non-obvious and goes against the idea of the “grammar of graphics”:

ggplot(d, aes(x, y, shape = s, colour = c)) +
    # this color is just for the legend:
    geom_point(colour = ggthemes_data$solarized$base[['base1']]) 
jrnold commented 8 years ago

The problem is that in general you can't get assured that you will get reasonable values for geom colors from themes. Some themes may set all background values to be blank. For example, theme_void.

flying-sheep commented 8 years ago

who’s talking about backgrounds?

> sapply(theme_classic()[c('line', 'rect', 'text')], function(e) e$colour)
   line    rect    text 
"black" "black" "black"
flying-sheep commented 7 years ago

but in any case: then we can still fall back to some value.

basically:

  1. use color from aes if set
  2. else use color from theme if set
  3. else use global fallback

ATM this is my (incomplete) workaround:

apply_theme <- function(gg, t) {
    for (l in seq_along(gg$layers)) {
        gg$layers[[l]]$geom$default_aes$colour <- t$text$colour
    }
    gg + t
}
jrnold commented 7 years ago

That's interesting. How about using update_geom_defaults instead? Personally, I like to avoid changing the state, but it seems an okay idea (at least one that ggplot2 has a built-in way to do). If you implement it, make a pull request and I'll incorporate it.

My own approach would use something like purrr::partial to return a new geom function from an old geom function with new defaults set.

flying-sheep commented 7 years ago

we could do something withr-style, like:

with_theme <- function(theme, expr) {
  old_col <- GeomPoint$default_aes$colour
  tryCatch({
    update_geom_defaults('point', list(colour = get_primary_colour('point', theme)))
    expr + theme
  }, finally = {
    update_geom_defaults('point', list(colour = old_col))
  })
}

where get_primary_colour gets a color for a theme and geom or falls back to the default (Geom*$default_aes$colour)

flying-sheep commented 7 years ago

My own approach would use something like purrr::partial to return a new geom function from an old geom function with new defaults set.

ultimately this issue is about working around a ggplot2 shortcoming. I think partial would be the way to go if we’d deal with optimal design (like most other parts of ggplot2), but as we’re doing a workaround, i think practicality beats purity and we should just wrap the whole expression instead of each individual geom call.

flying-sheep commented 7 years ago

hmm, doesn’t work though. if i define with_theme as above and create a dummy get_primary_colour <- function(geom, theme) theme$text$colour

then GeomPoint$default_aes$colour is correct at the time of the evaluation of expr but it doesn’t end up in the layer.

In[1]: gg <- with_theme(theme_solarized_2(light = FALSE), {
  ...:   cat(GeomPoint$default_aes$colour)
  ...:   qplot(1:10, (1:10)^2)
  ...: })
"#586e75"
In[2]: cat(gg$layers[[1]]$default_aes$colour)
"black"

any ideas?

flying-sheep commented 7 years ago

an alternative to my apply_theme above:

`%+theme%` <- function(gg, theme) {
  gg <- gg + theme
  for (l in seq_along(gg$layers)) {
    gg$layers[[l]]$geom$default_aes$colour <- theme$text$colour
  }
  gg
}

qplot(1:10, (1:10)^2) %+theme% theme_solarized_2(light = FALSE)
jrnold commented 7 years ago

%+theme looks nice! Should it only change colour or should it also change fill? And how about two functions. One that changes update_geom_defaults for those that want it, and the %+theme for temporary changes?

Make a PR and incorporate it.

flying-sheep commented 7 years ago

i guess we should make it dependent on if the defaults are set. are there geoms with a default fill?

jrnold commented 7 years ago

All the geoms have all their default aesthetics set in their ggproto object, e.g. geom_ribbon

flying-sheep commented 7 years ago

hmm, that makes it a bit harder.

so we have to do something like this (or even more fine-grained)

for (aesthetic in c('fill', 'colour')) {
  default <- gg$layers[[l]]$geom$default_aes[[aesthetic]]
  gg$layers[[l]]$geom$default_aes[[aesthetic]] <-
    switch(default, grey20 = get_soft_colour(theme), black = get_primary_colour(theme))
}

now we only need get_soft_colour and get_primary_colour.

my idea would be to try for a few colors that can be set in a theme. e.g. for the soft color, try all grid colours that could be set. any suggestions for an order for primary and soft colors?

question to myself for later: are there more than grey20 and black defined as default?

jrnold commented 7 years ago

I think it was that sort of issue that kept me from implementing something like this in the first place. That it is one of those problems that seems like it should be easy and obvious, but there are a bunch of annoying edge cases. Which is why my initial "solution" was to expose the colors for the themes through ggthemes_data to allow allow for the manual use of it.

I think you can get something that works 80% of the time, which is big time saver. But I would guess that it'll be annoying for those last percentage points.

Going back to the idea of a closure, how about a function that applies specific values from the theme

themed_geom_point() <- apply_theme(geom_point, theme_solarized(), colour = c("text", "colour"))

(Sorry: I didn't have time to figure out the body of the function). Then this can be extended with smart defaults, or to apply to multiple geoms, or also work through update_geom_defaults.

Regarding the default aes: off the top of my head geom_smooth uses blue for the line.

flying-sheep commented 6 years ago

hi! did you implement anything to make this easier or why did you close it?