How to implement a 2-d (hue x luminance) color sca

2020-02-14 09:46发布

问题:

Here's a toy data.frame that illustrates the problem (the most basic version thereof, that is; there will be an additional wrinkle later on):

df <- read.table(textConnection(
"toxin  dose    x   y
A   1   0.851   0.312
A   10  0.268   0.443
A   100 0.272   0.648
B   1   0.981   0.015
B   10  0.304   0.658
B   100 0.704   0.821
C   1   0.330   0.265
C   10  0.803   0.167
C   100 0.433   0.003
D   1   0.154   0.611
D   10  0.769   0.616
D   100 0.643   0.541
"), header = TRUE)

I want to make a scatterplot of these data in which the toxin is indicated by the hue of the points, and the dose is indicated by their luminance (to a first approximation, a low dose should correspond to a high luminance).

The particularly challenging aspect of this visualization problem is that the legend would have to be a 2-dimensional color grid (rather than a 1-dimensional color bar), with the rows corresponding to the toxin variable and the columns corresponding to dose (or a transform thereof).

The extra wrinkle I alluded to above is that the data actually includes one control observation, where the dose is different from all the other ones (note the row with toxin = "Z", below):

df <- read.table(textConnection(
"toxin  dose    x   y
A   1   0.851   0.312
A   10  0.268   0.443
A   100 0.272   0.648
B   1   0.981   0.015
B   10  0.304   0.658
B   100 0.704   0.821
C   1   0.330   0.265
C   10  0.803   0.167
C   100 0.433   0.003
D   1   0.154   0.611
D   10  0.769   0.616
D   100 0.643   0.541
Z   0.001   0.309   0.183
"), header = TRUE)

The point for the control ("Z") toxin should be a single gray dot. (It's OK if the 2-d color grid legend does not include the control value, but in this case there should be at least one legend that identifies its point appropriately.)

In summary, the problem has three parts:

  1. Represent toxin and dose by hue and luminance, respectively.
  2. Make a 2-d color grid legend.
  3. Legends should identify the control point.

Below is what I've managed so far.

The only way I can think of to solve the first aspect of the problem would be to devote a different layer to each toxin, and use a color gradient based on the dose.

Unfortunately, there does not seem to be a way to specify a different gradient scale for each layer.

More specifically, I first define the following:

library(ggplot2)

hues <- RColorBrewer::brewer.pal(4, "Set1")

gradient <- function (hue_index) {
  scale_color_gradient(high = hues[hue_index],
                       low = "white",
                       trans = "log",
                       limits = c(0.1, 100),
                       breaks = c(1, 10, 100))
}

baseplot <- ggplot(mapping = aes(x = x, y = y, color = dose))

The first layer, by itself, looks promising:

(
 baseplot
          + geom_point(data = subset(df, toxin == "A"), size = 4)
          + gradient(1)
)

But when I add the second layer...

(
 baseplot
          + geom_point(data = subset(df, toxin == "A"), size = 4)
          + gradient(1)
          + geom_point(data = subset(df, toxin == "B"), size = 4)
          + gradient(2)
)

...I get the following warning:

Scale for 'colour' is already present. Adding another scale for 'colour', which will replace the existing scale.

And, sure enough, this is the plot I get:

I have not been able to find a way to define different layers each with its own color scale.

回答1:

Does it have to be a grid for the legend? If you are willing to have one legend for toxin (color) and a second legend for dose (alpha), you could use this (and set your colors/fills to what makes sense for your data)

df$dose <- factor(df$dose)

ggplot(
  df
  , aes(x = x, y = y
        , col = toxin
        , alpha = dose)
) +
  geom_point(size = 4)

If it really must be a matrix for the legend, you could do make the matrix yourself, then combine them on the plot. You will lose some flexibility, and will need to carefully set things, but this should work in general (note that I am using the minimal theme as it seems best for the legend -- obviously personal preference):

theme_set(theme_minimal())

mainPlot <-
  ggplot(
    df
    , aes(x = x, y = y
          , col = toxin
          , alpha = dose)
  ) +
  geom_point(size = 4)

mainPlot


allLevels <-
  expand.grid(toxin = levels(df$toxin)
              , dose = levels(df$dose))

legendPlot <-
  ggplot(
    allLevels
    , aes(x = toxin, y = dose
          , col = toxin
          , alpha = dose)
  ) +
  geom_point(size = 4)

legendPlot



library(gridExtra)

grid.arrange(
  mainPlot +
    theme(legend.position = "none")
  , legendPlot +
    theme(legend.position = "none") +
    ggtitle("Legend")
  , layout_matrix =
    matrix(c(1,1,1,NA,2,NA)
           , ncol = 2)
  , widths=c(2,1)
  , heights = c(1,2,1)
  )



回答2:

This solution is an adaptation of the one given in this answer. It does not really do what the question asks for (much of the solution's heavy lifting is not done by ggplot2, and the legend is not as clear as it could be), but this may be the best one can do with ggplot2 for this problem.

baseplot <- ggplot(data = df, mapping = aes(x = x, y = y))

palette <- function (name, indices = c(3, 5, 7)) {
  RColorBrewer::brewer.pal(9, name)[indices]
}

colors <- c(as.vector(sapply(c("Reds", "Blues", "Greens", "Purples"), palette)),
            "white")

labels <- mapply(function(toxin, dose) {
                     paste(toxin, as.character(dose), sep = " @ ")
                 },
                 df$toxin, df$dose)

(
  baseplot + geom_point(mapping = aes(color = interaction(dose, toxin)),
                        size = 4)
           + scale_color_manual(name = "toxin @ dose",
                                values = colors,
                                labels = labels)
           + guides(color = guide_legend(nrow = 5, byrow = TRUE))
)

And here's what the output looks like:



标签: r ggplot2