Closed r2evans closed 3 years ago
I like the idea to add more complex colourscales. I've considered making an RGB-scale wherein three values are mapped to the three channels and making a HSL-scale seems to have many of the same challenges.
When I thought about this on the implementation side, I was unsure how to user should interact with this. I agree that something like aes(hue = ..., lightness = ..., saturation = ...)
sounds most intuitive (or replace hue with colour/color), but this would lead to the same warnings as the current multiscales.
Then at the other end, if/when everything has been mapped as intended, there is also the legend that needs to be displayed. One option would be to display just three colorbars next to oneanother displaying the values mapped to the components. Another option is to use a 3D display like those images you showed from wikipedia, but this seems more challenging to set the correct breaks and display the values.
If I have some extra time soon, I'd like to explore this topic a bit deeper.
Here is a proof of concept draft of an hsl scale, if you're interested in progress or want to play around. It uses the hsv
function, so I probably have misnamed it. Also currently only works with geoms that can handle the aesthetics, and overrides the colour
aesthetic.
library(ggplot2)
scale_colour_hsl <- function(
..., aesthetics = c("hue", "saturation", "lightness")
) {
continuous_scale(aesthetics, "hsl", ...,
palette = NULL, guide = "none",
super = ScaleHSL)
}
RangeHSL <- ggproto(
"RangeHSL", ggplot2:::RangeContinuous,
train = function(self, x, aes) {
self$range[[aes]] <- scales::train_continuous(x, self$range[[aes]])
},
range = list(hue = NULL, saturation = NULL, lightness = NULL),
reset = function(self) {
self$range <- list(hue = NULL, saturation = NULL, lightness = NULL)
}
)
hsl_range <- function() {ggproto(NULL, RangeHSL)}
ScaleHSL <- ggproto(
"ScaleHSL", ScaleContinuous,
range = hsl_range(),
train = function(self, x, aes) {
if (length(x) == 0) {
return()
}
self$range$train(x, aes = aes)
},
train_df = function(self, df) {
if (ggplot2:::empty(df)) return()
aesthetics <- intersect(self$aesthetics, names(df))
for (aesthetic in aesthetics) {
self$train(df[[aesthetic]], aes = aesthetic)
}
invisible()
},
map_df = function(self, df, i = NULL) {
if (ggplot2:::empty(df)) return()
# browser()
aesthetics <- intersect(self$aesthetics, names(df))
names(aesthetics) <- aesthetics
if ("hue" %in% names(aesthetics)) {
hlim <- self$range$range$hue
x <- if (is.null(i)) df[["hue"]] else df[["hue"]][i]
h <- self$rescaler(df[["hue"]], to = c(0, 1), from = hlim)
}
if ("saturation" %in% names(aesthetics)) {
slim <- self$range$range$saturation
x <- if (is.null(i)) df[["saturation"]] else df[["saturation"]][i]
s <- self$rescaler(df[["saturation"]], to = c(0, 1), from = slim)
}
if ("lightness" %in% names(aesthetics)) {
llim <- self$range$range$lightness
x <- if (is.null(i)) df[["lightness"]] else df[["lightness"]][i]
l <- self$rescaler(df[["lightness"]], to = c(0, 1), from = llim)
}
hsl <- hsv(h = h, s = s, v = l)
list(colour = hsl)
},
clone = function(self) {
new <- ggproto(NULL, self)
new$range <- hsl_range()
new
}
)
geom_point2 <- function(
mapping = NULL, data = NULL,
stat = "identity", position = "identity",
...,
na.rm = FALSE, show.legend = FALSE, inherit.aes = TRUE
) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomPoint2,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
...
)
)
}
GeomPoint2 <- ggproto(
"GeomPoint2", GeomPoint,
default_aes = aes(
shape = 19, colour = "black", size = 1.5,
fill = NA, alpha = NA, stroke = 0.5,
hue = 0, saturation = 1, lightness = 1
)
)
ggplot(mtcars, aes(wt, mpg)) +
geom_point2(aes(hue = mpg, lightness = vs, saturation = 1)) +
scale_colour_hsl()
Created on 2020-12-10 by the reprex package (v0.3.0)
After giving this a more thorough attempt, I think this requires (too) much additional infrastructure without much overlap with the current code in ggh4x. To not waste effort, maybe this could be a separate package that specialises in rgb/hsv/hsl(/cmyk) scales. I'll post something here once I get something to a sort of useable state.
Got to the point where I could do this though:
ggplot(mtcars, aes(mpg, wt)) +
geom_point(aes(colour = rgb_spec(mpg, drat, wt)))
That ... is ... INCREDIBLE!
So many questions ... I'll start with three:-)
hsl_spec(...)
version? e.g., hsl_spec(H=factor(cyl), L=mpg)
?(I'm looking at your branches and cannot see rgb_spec
, so I can't look that stuff up on my own.)
It sounds like you are suggesting a new package altogether that may provide alternative color scales, is that right? I don't know that I can help much (I don't yet speak ggplot-internals very well), but I want to stay involved and help where I can.
I've pushed the changes to https://github.com/teunbrand/ggh4x/tree/channel_scales so you can browse some code and do the usual github stuff with it. I may have forgotten to @export
the colour cube, but feel free to fork and adapt.
To answer the questions.
Yes, I'm indeed suggesting a new package and I made a repo here but at the time of writing that is still completely empty. If you have awesome name suggestions, now would be a good time! I'll move over the stuff from the branch to that repo in the weekend, I think.
While I have nothing against trichromancer
, for directness I suggest something like ggcolorscales
(blech) or ggchroma
. (I'm making the assumption that the package intent is primarily color scales.)
Side note: the side-cube is very impressive, and I recognize the challenges with it. If not already, I would expect it to reduce dimensionality if one or more of the three properties (R, G, or B, in rgb_spec
; or H/S/L) is/are unassigned.
Having one or more discrete scales within that complicates it in two ways:
4.5
will only get significantly worse. Perhaps some offset labeling similar to ggrepel
without the stochastic optimization? (Deterministic, combining the concepts from https://ggrepel.slowkow.com/articles/examples.html#make-curved-line-segments-or-arrows-1 and https://ggrepel.slowkow.com/articles/examples.html#align-labels-on-the-left-or-right-edge-1.)Last thought: having a gradient is nice and very visually appealing. An option might be to bin the colors (similar to scale_color_gradientn(..., guide="legend")
).
I'm closing this in favour of the issue over at https://github.com/teunbrand/ggchromatic/issues/7
The current method for using different color scales (as you show in your
scale_colour_multi
example) is a good way of manually controlling gradients or such on different levels of a variable. As an alternative, what about adding an aesthetic for "lightness" and/or "saturation"? (This is not equivalent to your example, moreso a generalization to vary lightness/saturation dynamically while allowingggplot2
to handle the base color itself.)I'm not a color guru, I use those terms from HSL or HSV, not certain which is more prevalent or easy in R. From https://en.wikipedia.org/wiki/HSL_and_HSV, two helpful images are:
HSL:![image](https://user-images.githubusercontent.com/5815808/101258078-02155180-36ee-11eb-895d-958ac59628d9.png)
HSV:![image](https://user-images.githubusercontent.com/5815808/101258084-0e99aa00-36ee-11eb-90c4-3a8fafc43752.png)
Between the two, I think I prefer HSL since we might introduce aesthetics
lightness=
andsaturation=
without likelihood of name-collision. (HSV's use ofvalue=
is a bit vague in that regard.) Thecolorspace
package providesHLS
(not sure why it's not HSL, but hey) andHSV
, so those might be used as imported or their logic references/duplicated here.Modifying your readme example a little, notably to use only
Petal.Length
,(Legend names need to be updated.) This might be reduced to something like
This is different than
ggplot2::scale_colour_hue
, which does not allow separate (or even dynamic) lightness/saturation levels. If I am missing that this is already directly supported somewhere, please forgive me :-)For generality, there is no reason to not support an aesthetic of
hue=
as well, not sure if that's just "completeness" or actually different-enough-from and as-useful-ascolour=
.