teunbrand / ggh4x

ggplot extension: options for tailored facets, multiple colourscales and miscellaneous
https://teunbrand.github.io/ggh4x/
Other
542 stars 33 forks source link

hsl or hsv? #29

Closed r2evans closed 3 years ago

r2evans commented 3 years ago

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 allowing ggplot2 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:

Between the two, I think I prefer HSL since we might introduce aesthetics lightness= and saturation= without likelihood of name-collision. (HSV's use of value= is a bit vague in that regard.) The colorspace package provides HLS (not sure why it's not HSL, but hey) and HSV, 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,

# using up through:
# g <- g + force_panelsizes(rows = 1, cols = c(1, phi, phi^2), respect = TRUE)
g +
  geom_point(aes(seto = Petal.Length),  
             ggsubset(Species == "setosa")) +
  geom_point(aes(vers  = Petal.Length), 
             ggsubset(Species == "versicolor")) +
  geom_point(aes(virg = Petal.Length),
             ggsubset(Species == "virginica")) +
  scale_colour_multi(
    aesthetics = c("seto", "vers", "virg"),
    colours = list(c("white", "green"),
                   c("white", "red"),
                   c("white", "blue")),
    guide = guide_colourbar(barheight = unit(50, "pt"))
  )

image

(Legend names need to be updated.) This might be reduced to something like

g +
  geom_point(aes(color = Species, lightness = Petal.Length)) +
  scale_colour_hsl()

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-as colour=.

teunbrand commented 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.

teunbrand commented 3 years ago

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)

teunbrand commented 3 years ago

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)))

image

r2evans commented 3 years ago

That ... is ... INCREDIBLE!

So many questions ... I'll start with three:-)

(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.

teunbrand commented 3 years ago

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.

  1. No, not yet. I have barely figured out how I could hold 3 continuous values in a single scale and work with it. I haven't gotten to making 2 continuous + 1 discrete yet, but that would definitely be nice.
  2. No, not yet. Though the code is setup that once I get everything to work as I want, it would be very easy to make whatever colourspace you want (as long as the farver package supports it). Any new colourspace would just require 2 constructors, 4 boilerplate vctrs methods and 1 palette to get it to work, and doesn't need readjustments in the scale ggproto methods itself.
  3. Making the skeleton of the cube is not that hard except that I had to pay attention what I'm placing in what plane and in what order. I'm still not sure why it works, but it works so I'm a bit reluctant to touch it now :') The hard thing is the alignment with labels and titles and such. As you can see, I haven't gotten to titles yet and the rightmost labels are clipped incorrectly. I'm simply not sure how I can get the absolute dimensions in centimetres of those labels, if at the point where I'm making the grobs for the cube I don't know yet what kind of fonts the user has given it.

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.

r2evans commented 3 years ago

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:

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")).

teunbrand commented 3 years ago

I'm closing this in favour of the issue over at https://github.com/teunbrand/ggchromatic/issues/7