henhesu
henhesu

Reputation: 851

Split overlapping tiles by facet in geom_tile

I have stacked a data frame which shows values per id across groups:

df <- tibble::tibble(id = c(LETTERS[1:6], LETTERS[1:5]),
                     value = c(paste0("V", 1:6), paste0("V", 1:5)),
                     group = c(rep("group_1", 6), rep("group_2", 5)))

df
#> # A tibble: 11 x 3
#>    id    value group  
#>    <chr> <chr> <chr>  
#>  1 A     V1    group_1
#>  2 B     V2    group_1
#>  3 C     V3    group_1
#>  4 D     V4    group_1
#>  5 E     V5    group_1
#>  6 F     V6    group_1
#>  7 A     V1    group_2
#>  8 B     V2    group_2
#>  9 C     V3    group_2
#> 10 D     V4    group_2
#> 11 E     V5    group_2

I want to create a heatmap showing the "availability" of each value (x) for each id (y) across groups (fill):

ggplot(df, aes(x = id, y = value, fill = group)) + 
  geom_tile()

enter image description here

The problem is that the fill overlaps: All I can see is that F/V6 is only in group_1 (and not in group_2). However, for IDs A to E, values V1 to V5 are available in both groups, and thus the color of group_2 is on top of group_1, making it look like they are only available in group_2.

If I use facet_wrap(), the availability is more obvious:

ggplot(df, aes(x = id, y = value, fill = group)) + 
  geom_tile() + 
  facet_wrap("group")

enter image description here

However, in my real setting, the heatmap is very large so it is difficult to compare which values are available in which group.

Is it possible to split each tile in half if the value is available in both groups and keep it full if it is only present in one group? So in the first plot above, the blue tiles would be split in half (showing both blue and red), and the red tile would remain as is.


UPDATE

Thanks for stefan's excellent hint on using position = "dodge". However, I noticed that my problem is actually a bit more complex than my reprex above: Each value may appear in multiple ids per group. When using position = "dodge", ggplot2 then "divides" each id "column" in as many parts as there are occurrences of each value within this id:


df <- tibble::tibble(id = c("A", "A",  "A", "B", "B", "C", "C", "C", "A", "A", "B", "B", "C", "C"),
                     value = c("V1", "V2", "V3", "V1", "V3", "V1", "V2", "V4", "V1", "V2", "V1", "V3", "V1", "V4"),
                     group = c(rep("group_1", 8), rep("group_2", 6)))

df
#> # A tibble: 14 x 3
#>    id    value group  
#>    <chr> <chr> <chr>  
#>  1 A     V1    group_1
#>  2 A     V2    group_1
#>  3 A     V3    group_1
#>  4 B     V1    group_1
#>  5 B     V3    group_1
#>  6 C     V1    group_1
#>  7 C     V2    group_1
#>  8 C     V4    group_1
#>  9 A     V1    group_2
#> 10 A     V2    group_2
#> 11 B     V1    group_2
#> 12 B     V3    group_2
#> 13 C     V1    group_2
#> 14 C     V4    group_2

ggplot(df, aes(x = id, y = value, fill = group)) + 
  geom_tile(position = "dodge")

You can see that in "column A" the three tiles are placed both above and next to each other, splitting the available space in three. What I want to achieve is plotting these three pairs of tiles in "column A" on top of each other so they are aligned, using the whole available space alloted to "column A" for each value.

enter image description here

Upvotes: 4

Views: 1345

Answers (4)

Yun
Yun

Reputation: 305

I just add geom_subtile() and geom_subrect() function in package ggalign to subdivide rectangles with shared borders into a grid.


df <- tibble::tibble(
    id = c(LETTERS[1:6], LETTERS[1:5]),
    value = c(paste0("V", 1:6), paste0("V", 1:5)),
    group = c(rep("group_1", 6), rep("group_2", 5))
)
library(ggalign)
ggplot(df, aes(x = id, y = value, fill = group)) +
    geom_subtile()

enter image description here

It can do more:

## arranges by row
ggplot(data.frame(value = letters[seq_len(5)])) +
    geom_subtile(aes(x = 1, y = 1, fill = value))

enter image description here

## arranges by column
ggplot(data.frame(value = letters[seq_len(9)])) +
    geom_subtile(aes(x = 1, y = 1, fill = value), byrow = FALSE)

enter image description here

## one-row
ggplot(data.frame(value = letters[seq_len(4)])) +
    geom_subtile(aes(x = 1, y = 1, fill = value), direction = "h")

enter image description here

## one-column
ggplot(data.frame(value = letters[seq_len(4)])) +
    geom_subtile(aes(x = 1, y = 1, fill = value), direction = "v")

enter image description here

Upvotes: 0

jan-glx
jan-glx

Reputation: 9536

I just implemented a new geom GeomSplitTile that allows to plot two related values in each field of a matrix plot. Apart from adding the required definitions, this is much easier to use and should allow for arbitrary combination with faceting and positions:

library(rlang)
library(ggplot2)


# Provides a diagonally split version of GeomTile
# by [jan-glx](https://github.com/jan-glx), based on ggplot2::GeomTile by the [ggplot2 authors](https://github.com/tidyverse/ggplot2/graphs/contributors)

draw_key_split_tile <- function(data, params, size) {
  
  data$width <- data$width %||% params$width %||% 1
  data$height <- data$height %||% params$height %||% 1
  data$width[is.na(data$width)] <- 1
  data$height[is.na(data$height)] <- 1
  if (isTRUE(data$split)) {
    x <- c(0, 1, 0)
    y <- c(0, 1, 1)
  } else {
    x <- c(0, 1, 1)
    y <- c(0, 1, 0)
  }
  x <- 0.5 + (x-0.5) * data$width
  y <- 0.5 + (y-0.5) * data$height
  
  grid::polygonGrob(
    x = x,
    y = y,
    default.units = "npc",
    gp = grid::gpar(
      col = data$colour,
      fill = alpha(data$fill, data$alpha)
    )
  )
}

geom_split_tile <- function(mapping = NULL, data = NULL,
                            stat = "identity", position = "identity",
                            ...,
                            linejoin = "mitre",
                            na.rm = FALSE,
                            show.legend = NA,
                            inherit.aes = TRUE) {
  layer(
    data = data,
    mapping = mapping,
    stat = stat,
    geom = GeomSplitTile,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = rlang:::list2(
      linejoin = linejoin,
      na.rm = na.rm,
      ...
    )
  )
}

GeomSplitTile <- ggproto(
  "GeomSplitTile", GeomPolygon,
  extra_params = c("na.rm"),
  
  setup_data = function(data, params) {
    data$width <- data$width %||% params$width %||% resolution(data$x, FALSE)
    data$height <- data$height %||% params$height %||% resolution(data$y, FALSE)
    data$split <- as.factor(data$split %||% params$split %||% FALSE)
    
    K = 3
    n <- nrow(data)
    new_data <- data.frame(
      x = rep(data$x, each=K) + rep(3-as.integer(data$split)*2, each=K) * rep(c(-1,  1, 1), n) * rep(data$width / 2, each=K),
      y = rep(data$y, each=K) + rep(3-as.integer(data$split)*2, each=K) * rep(c(-1, -1, 1), n) * rep(data$height / 3, each=K),
      group = rep(seq_len(n), each=K)
    )
    new_data <- cbind(new_data, data[rep(seq_len(n), each = K), setdiff(colnames(data), c("x", "y", "group")), drop = FALSE])
    new_data
  },
  
  default_aes = aes(fill = "grey20", colour = NA, linewidth = 0.1, linetype = 1,
                    alpha = NA, width = NA, height = NA),
  
  required_aes = c("x", "y", "split"),
  
  draw_key = draw_key_split_tile
)

scale_split <- function(..., scale_name="scale_direction", palette = function(n) if(n>2) error(paste0(scale_name, " can handle at most 2 levels")) else c(FALSE, TRUE)) discrete_scale(aesthetics = "split", scale_name=scale_name, palette = palette, ... )

Use with your axample:

df <- tibble::tibble(id = c(LETTERS[1:6], LETTERS[1:5]),
                     value = c(paste0("V", 1:6), paste0("V", 1:5)),
                     group = c(rep("group_1", 6), rep("group_2", 5)))

ggplot(df, aes(x = id, y = value, fill = group, split=group)) + 
  geom_split_tile() + scale_split()

Created on 2023-09-29 by the reprex package (v2.0.1)

Hope this helps, let me know if you would like to this this in a standalone / some other R package!

Upvotes: 1

stefan
stefan

Reputation: 125897

One option would be to use position="dodge":

library(ggplot2)

ggplot(df, aes(x = id, y = value, fill = group)) + 
  geom_tile(position = "dodge")


UPDATE

You could try by mapping group on the group aes:

ggplot(df, aes(x = id, y = value, fill = group, group = group)) + 
  geom_tile(position = "dodge", color = "black") # adding 'color' for borders

enter image description here

Upvotes: 3

Allan Cameron
Allan Cameron

Reputation: 174616

If you want triangles, think you'll probably need to do it manually using some wrangling and geom_polygon, something like:

library(ggplot2)

df <- tibble::tibble(x = c(LETTERS[1:6], LETTERS[1:5]),
                     y = c(paste0("V", 1:6), paste0("V", 1:5)),
                     group = c(rep("group_1", 6), rep("group_2", 5)))

df1    <- df[!duplicated(interaction(df$x, df$y)),]
df2    <- df[duplicated(interaction(df$x, df$y)),]
df2    <- df[rep(seq(nrow(df)), each = 3),]
df2$x1 <- as.numeric(as.factor(df2$x))
df2$y1 <- as.numeric(as.factor(df2$y))
df2$x1 <- df2$x1 + c(-0.5, 0.5, 0.5)
df2$y1 <- df2$y1 + c(-0.5, -0.5, 0.5)
df2$z  <- rep(seq(nrow(df2)/3), each = 3)

ggplot(df1, aes(x = x, y = y, fill = group)) + 
  geom_tile() +
  geom_polygon(data = df2, aes(x = x1, y = y1, group = z))

Created on 2022-02-16 by the reprex package (v2.0.1)

Upvotes: 3

Related Questions