El toto
El toto

Reputation: 43

ggplot: Plot point data as RGB colors and add legend

I want to plot the share of three variables of each geographical point.

I thought this would be easily done with RGB values and I managed to do it simply like this:

data <- data.frame(
  Variable = c("Point1", "Point2", "Point3"),
  R = c(0.4, 0.6, 0.3),
  G = c(0.3, 0.2, 0.4),
  B = c(0.3, 0.2, 0.3),
  x = c(1, 2, 4),
  y = c(5, 6, 7)
)
color_plot <- rgb(data$R, data$G, data$B)

ggplot() +
  geom_point(data = data, aes(x = x, y = y, color = color_plot))  

The problem is that the color values are discrete, making the legend that ggplot produces useless. Is there an elegant way to add a meaningful (continuous) color scale to this plot? Ideally, the names of R, G and B would be on the scale.

Upvotes: 4

Views: 148

Answers (3)

knitz3
knitz3

Reputation: 936

A continuous color scale is typically showing you the value of one variable, for instance low to high you might see blue -> white -> red in a continuous gradient.

You seem to describe plotting x/y points but coloring by varying degrees of 3 variables, R, G, and B.

This might be a completely different direction but I wanted to try it out. Your R, G, B values you've provided all add to 1, so it seems like your plotting some sort of proportions.

Here's your data, with points colored by their RGB values (with the strategy Jon provided in the comments). I added some longer labels in the legend and labeled the points.

library(ggplot2)

data <- data.frame(
  Variable = c("Point1", "Point2", "Point3"),
  R = c(0.4, 0.6, 0.3),
  G = c(0.3, 0.2, 0.4),
  B = c(0.3, 0.2, 0.3),
  x = c(1, 2, 4),
  y = c(5, 6, 7)
)
data$hex <- with(data, rgb(R, G, B))
data$hex <- factor(data$hex, levels = unique(data$hex))
data$label <- 1:3
data$label.long <- with(data,
  sprintf("%s. R: %.1f, G: %.1f, B: %.1f", label, R, G, B))

p1 <- ggplot(data, aes(x, y)) +
  geom_point(aes(color = hex), size = 3) +
  geom_label(aes(label = label), size = 5, nudge_y = 0.15) +
  scale_color_identity(guide = guide_legend(), label = data$label.long) +
  theme_gray(16) +
  theme(aspect.ratio = 1)

p1

Ternary plots are meant to show relative proportions / ratios between 3 different variables. We can plot out your 3 RGB values on the ternary plot to see which color they land on. This plot will serve as a continuous scale of sorts.

Generate a grid of colors for the background of the ternary plot, because you only plot points on a ternary plot that all have the same sum (thinking in ratios), we filter to only have rows adding to the same value. I divide by 100 below to make the scales coherent with plot #1 and put everything on the 0 to 1 range.

all.colors <- expand.grid(R = 0:100, G = 0:100, B = 0:100)
all.colors <- all.colors[rowSums(all.colors) == 100, ]
all.colors <- all.colors / 100
all.colors$hex <- with(all.colors, rgb(R, G, B))
head(all.colors)
#>        R    G B     hex
#> 101 1.00 0.00 0 #FF0000
#> 201 0.99 0.01 0 #FC0300
#> 301 0.98 0.02 0 #FA0500
#> 401 0.97 0.03 0 #F70800
#> 501 0.96 0.04 0 #F50A00
#> 601 0.95 0.05 0 #F20D00

Using the ggtern package, we put in the RGB variables as our 3 aesthetics. We also label the points from our previous dataframe. You can ignore the warnings about unknown aesthetics z, this happens often with these ggplot extensions.

library(ggtern)
p2 <- ggtern(all.colors, aes(x = B, y = R, z = G)) +
  geom_point(aes(color = hex), size = 3, shape = 17) +
  geom_label(data = data, inherit.aes = FALSE,
    mapping = aes(x = B, y = R, z = G, label = label),
    size = 4, alpha = 0.5) +
  scale_color_identity() +
  scale_L_continuous(breaks = seq(0, 1, 0.1), labels = seq(0, 1, 0.1)) +
  scale_T_continuous(breaks = seq(0, 1, 0.1), labels = seq(0, 1, 0.1)) +
  scale_R_continuous(breaks = seq(0, 1, 0.1), labels = seq(0, 1, 0.1)) +
  theme_rgbw(14)
p2

Now putting them together with patchwork. The axis text of the 3 sides seems to disappear this way unfortunately, and it adds a couple of other labels that I remove with labs below. But you get the idea.

library(patchwork)
wrap_elements(p2 + labs(x = NULL, y = NULL)) +
  p1 +
  plot_layout(nrow = 2, heights = c(1, 0.8))

Edit

Now putting them together with a custom legend grob as suggested by @allan-cameron. (credit for the height calculation too). C̶o̶n̶v̶e̶r̶t̶i̶n̶g̶ t̶o̶ g̶r̶o̶b̶ l̶o̶s̶e̶s̶ a̶ f̶e̶w̶ e̶l̶e̶m̶e̶n̶t̶s̶ o̶f̶ t̶h̶e̶ g̶g̶t̶e̶r̶n̶ p̶l̶o̶t̶, b̶u̶t̶ i̶t̶ s̶u̶f̶f̶i̶c̶e̶s̶. Much more elegant this way than using patchwork to slap them together. The labels in the ternary plot become small here so I removed them.

Edit 2: ggplot2::ggplotGrob() is much better to convert ggplot2 objects to grobs reliably, changed here.

leg <- ggtern(all.colors, aes(x = B, y = R, z = G)) +
  geom_point(aes(color = hex), size = 3, shape = 17) +
  scale_color_identity() +
  theme_rgbw(14) +
  scale_L_continuous(breaks = NULL) +
  scale_T_continuous(breaks = NULL) +
  scale_R_continuous(breaks = NULL) +
  theme(
    tern.panel.expand = 0.4,
    tern.axis.arrow.text.R = element_text(vjust = 1),
    tern.axis.arrow.text.L = element_text(vjust = 0),
    tern.axis.arrow.text.T = element_text(vjust = 0)
  )


p3 <- ggplot(data, aes(x, y)) +
  geom_point(aes(color = hex), size = 3) +
  geom_label(aes(label = label), size = 5, nudge_y = 0.15) +
  scale_color_identity() +
  guides(custom = guide_custom(
    title = "Relative Share", grob = ggplotGrob(leg),
    width = grid::unit(2, "inches"),
    height = grid::unit(2, "inches"),
  )) +
  theme_gray(16) +
  theme(
    aspect.ratio = 1,
    legend.title = element_text(hjust = 0.5, vjust = -3),
    legend.margin = margin(0.1, 0.1, 0.1, 0.1, unit = "in")
  )
p3

Upvotes: 6

Allan Cameron
Allan Cameron

Reputation: 174378

Since the three variable's values all add to one, you can use a ternary RGB as your legend. Whilst this technically works, it is very difficult for the viewer to cross reference the colors, and in reality I would probably use a different method, depending on how many points you have on the plot.

However, if you have a ternary RGB as a grob called my_legend, you can add it as a custom legend to your plot like this:

ggplot(data) +
  geom_point(aes(x = x, y = y, color = color_plot), size = 4) +
  scale_color_identity() +
  guides(custom = guide_custom(title = "Relative share", grob = my_legend,
                               width = unit(5, "cm"), 
                               height = unit(5 * sin(pi/3), "cm"))) +
  theme_bw(16) +
  theme(legend.title = element_text(hjust = 0.5))

enter image description here

The difficult part is creating my_legend. You could do this using ggtern or create one from scratch, as I have done here:

cols <- do.call("rbind", lapply(seq(0, 1, 0.01), function(r) {
  do.call("rbind", lapply(seq(0, 1 - r, 0.01), function(g) {
        data.frame(red = r, green = g, blue = 1 - r - g)
    }))
}))

cols$rgb <- rgb(cols)
cols$x <- cols$red * 0.5 - cols$green * 0.5 + 0.5
cols$y <- sin(pi/3) / 2 * (1 + cols$blue - (cols$red + cols$green))

my_legend <- (ggplot(cols, aes(x, y, colour = rgb)) +
  geom_point(size = 1) +
  annotate("text", x = c(-0.2, 1.2, 0.5), y = c(-0.1, -0.1, sin(pi/3) + 0.2),
           label = c("G", "R", "B"), size = 5) +
  annotate("segment", 
           x = c(0, 1, 0.5) + c(0, 0.1, -0.1) * sin(pi/3), 
           xend = c(1, 0.5, 0) + c(0, 0.1, -0.1) * sin(pi/3),
           y = c(-0.1, 0.05, sin(pi/3) + 0.05), 
           yend = c(-0.1, sin(pi/3) + 0.05, 0.05),
           arrow = arrow(length = unit(2, "mm"), type = "closed")) +
  scale_color_identity() +
  theme_void()) |>
  ggplotGrob()

Upvotes: 5

NoobR
NoobR

Reputation: 321

I may be misunderstanding your question, but I think this is all you need:

ggplot(data = data, aes(x = x, y = y, color = Variable)) + geom_point() + scale_color_manual(values = color_plot)

Upvotes: 1

Related Questions