dkysh
dkysh

Reputation: 61

Is it possible to create a geom_segment with a gradient between two colors, but the start/end colors change depending of a variable?

I'm building a plot with a discrete Y-axis with multiple pairwise comparisons. However, instead of using text for the labels, I created a secondary plot with color points indicating the 2 groups involved in the comparison, with a segment connecting the 2 dots for clarity. See figure, left side:

figure_with_colored_dots

The code I used to generate the left-side plot is:

p_left <- ggplot(tb_deg) +
  geom_segment(aes(y=comparison, x=var1, xend=var2), size=2, color="slategrey", show.legend = F) +
  geom_point(aes(y=comparison, x=var1, fill=var1), shape=21, size=5, show.legend = F) +
  geom_point(aes(y=comparison, x=var2, fill=var2), shape=21, size=5, show.legend = F) +
  scale_fill_manual(name="Group",
                    values=c('ControlD1' = "limegreen", 
                             'HSP90iD1' = "firebrick1", 
                             'PRRTD1' = "deepskyblue", 
                             'CombinationD1' = "orchid1", 
                             'HSP90iD3' = "firebrick4", 
                             'PRRTD3' = "deepskyblue3", 
                             'CombinationD3' = "purple3")
                    ) +
  scale_x_discrete(limits=rev(levels(md$group)) ) +
  theme_void() +
  labs(x=NULL, y=NULL, title = NULL) +
  theme(legend.position = "none",
        aspect.ratio = 2,
        axis.text.x = element_text(angle=90, hjust=1),
        axis.text.y = element_blank())

Right now I am making the connecting segment grey, but I was wondering if I can make the segment into a gradient between the colors of the 2 connected dots. Yes, it will probably be ugly AF and I won't use it in the final figure, but I wanted to try.

I've been searching how to do this, but all the solutions I've found are using a fixed (gradient) color scale that is the same for all the plotted segments (mainly using geom_link). I couldn't find any example where the color of the tips of the segment depends on variables. Ideally, I'd be looking for something like

    ... + geom_segment(aes(y=comparison, x=var1, xend=var2, 
***color_start=var1, color_end=var2***) ) + ...

Is there any way to this without manually coding a gradient with a different scale for each of the 15 comparisons?

Thanks,

Edit: The data:

> dput(unique(tb_deg[,c("comparison","var1","var2")]))
structure(list(comparison = structure(c(3L, 7L, 8L, 15L, 6L, 
10L, 11L, 1L, 4L, 13L, 2L, 9L, 5L, 12L, 14L), levels = c("without001 - HSP90iD1_vs_ControlD1", 
"without001 - PRRTD1_vs_ControlD1", "without001 - CombinationD1_vs_ControlD1", 
"without001 - HSP90iD3_vs_ControlD1", "without001 - PRRTD3_vs_ControlD1", 
"without001 - CombinationD3_vs_ControlD1", "without001 - CombinationD1_vs_HSP90iD1", 
"without001 - CombinationD1_vs_PRRTD1", "without001 - PRRTD1_vs_HSP90iD1", 
"without001 - CombinationD3_vs_HSP90iD3", "without001 - CombinationD3_vs_PRRTD3", 
"without001 - PRRTD3_vs_HSP90iD3", "without001 - HSP90iD3_vs_HSP90iD1", 
"without001 - PRRTD3_vs_PRRTD1", "without001 - CombinationD3_vs_CombinationD1"
), class = "factor"), var1 = structure(c(4L, 4L, 4L, 7L, 7L, 
7L, 7L, 2L, 5L, 5L, 3L, 3L, 6L, 6L, 6L), levels = c("ControlD1", 
"HSP90iD1", "PRRTD1", "CombinationD1", "HSP90iD3", "PRRTD3", 
"CombinationD3"), class = "factor"), var2 = structure(c(1L, 2L, 
3L, 4L, 1L, 5L, 6L, 1L, 1L, 2L, 1L, 2L, 1L, 5L, 3L), levels = c("ControlD1", 
"HSP90iD1", "PRRTD1", "CombinationD1", "HSP90iD3", "PRRTD3", 
"CombinationD3"), class = "factor")), class = c("grouped_df", 
"tbl_df", "tbl", "data.frame"), row.names = c(NA, -15L), groups = structure(list(
    comparison = structure(1:15, levels = c("without001 - HSP90iD1_vs_ControlD1", 
    "without001 - PRRTD1_vs_ControlD1", "without001 - CombinationD1_vs_ControlD1", 
    "without001 - HSP90iD3_vs_ControlD1", "without001 - PRRTD3_vs_ControlD1", 
    "without001 - CombinationD3_vs_ControlD1", "without001 - CombinationD1_vs_HSP90iD1", 
    "without001 - CombinationD1_vs_PRRTD1", "without001 - PRRTD1_vs_HSP90iD1", 
    "without001 - CombinationD3_vs_HSP90iD3", "without001 - CombinationD3_vs_PRRTD3", 
    "without001 - PRRTD3_vs_HSP90iD3", "without001 - HSP90iD3_vs_HSP90iD1", 
    "without001 - PRRTD3_vs_PRRTD1", "without001 - CombinationD3_vs_CombinationD1"
    ), class = "factor"), var1 = structure(c(2L, 3L, 4L, 5L, 
    6L, 7L, 4L, 4L, 3L, 7L, 7L, 6L, 5L, 6L, 7L), levels = c("ControlD1", 
    "HSP90iD1", "PRRTD1", "CombinationD1", "HSP90iD3", "PRRTD3", 
    "CombinationD3"), class = "factor"), .rows = structure(list(
        8L, 11L, 1L, 9L, 13L, 5L, 2L, 3L, 12L, 6L, 7L, 14L, 10L, 
        15L, 4L), ptype = integer(0), class = c("vctrs_list_of", 
    "vctrs_vctr", "list"))), class = c("tbl_df", "tbl", "data.frame"
), row.names = c(NA, -15L), .drop = TRUE))

Upvotes: 1

Views: 51

Answers (2)

stefan
stefan

Reputation: 125398

Just as a reference: Here is a slightly modified version of the approach by @dkysh which - as long as no legend is desired - does not require ggnewscale but uses scales::pal_seq_gradient to create the list of color palettes. Additionally, instead of a for loop it uses purrr::imap to create a list of geom_link layers.

Note: The code below requires ggplot2 >= 3.5.0 as I make use of the new functionality to render colors inside aes() by wrapping in I().

library(ggforce)
library(ggplot2)

packageVersion("ggplot2")
#> [1] '3.5.1'

palettes <- purrr::map2(
  color_list[tb_deg$var1],
  color_list[tb_deg$var2],
  scales::pal_seq_gradient
)
names(palettes) <- tb_deg$comparison

p_left_segments <- tb_deg |>
  split(~comparison) |>
  purrr::imap(\(x, y) {
    pal <- palettes[[y]]
    geom_link(
      data = x,
      aes(
        y = comparison, yend = comparison,
        x = var1, xend = var2,
        color = I(after_stat(pal(index)))
      ), size = 2
    )
  })

p_left +
  p_left_segments

enter image description here

Upvotes: 1

dkysh
dkysh

Reputation: 61

Using both ggforce's geom_link and ggnewscale, it is possible to make a loop, element by element, creating a new colorscale for each of them:

library(ggforce)
library(ggnewscale)

## Index with colors
color_list <- c('ControlD1' = "limegreen", 
                             'HSP90iD1' = "firebrick1", 
                             'PRRTD1' = "deepskyblue", 
                             'CombinationD1' = "orchid1", 
                             'HSP90iD3' = "firebrick4", 
                             'PRRTD3' = "deepskyblue3", 
                             'CombinationD3' = "purple3")

## old-ish code
p_left <- ggplot(tb_deg) +
  geom_segment(aes(y=comparison, x=var1, xend=var2), size=2, color="slategrey", show.legend = F) +
  geom_point(aes(y=comparison, x=var1, fill=var1), shape=21, size=5, show.legend = F) +
  geom_point(aes(y=comparison, x=var2, fill=var2), shape=21, size=5, show.legend = F) +
  scale_fill_manual(name="Group",
                    values=color_list ) +
  scale_x_discrete(limits=rev(levels(md$group)) ) +
  theme_minimal() +
  labs(x=NULL, y=NULL, title = NULL) +
  theme(legend.position = "none",
        aspect.ratio = 2,
        axis.text.x = element_text(angle=90, hjust=1),
        axis.text.y = element_blank(),
        panel.grid.major.y = element_blank())



p_left

### Loop through each element of the Y-axis, creating a geom_link + a new color scale
p2<-p_left

for (comp in unique(tb_deg$comparison)) {

  v1 <-   unique(tb_deg[tb_deg$comparison==comp, c("comparison","var1","var2")])$var1
  v2 <-   unique(tb_deg[tb_deg$comparison==comp, c("comparison","var1","var2")])$var2

p2 <- p2 +
  new_scale_color() +
  geom_link(data=unique(tb_deg[tb_deg$comparison==comp, c("comparison","var1","var2")]),
            aes(y=comparison, yend=comparison, x=var1, xend=var2, color=stat(index)), size=2) +
  scale_color_gradient(high=color_list[v2],low=color_list[v1])
  
}

p2

geom_link_manually_in_loop

Still, I wonder if there is a more straightforward way to do this.

Upvotes: 2

Related Questions