Search code examples
rggplot2heatmap

ggplot R: X, Y, Z dotplot to hexagonal heatmap using fixed / equally separated X, Y coordinates


Is it possible to convert the following dotplot into a touching hexagonal heatmap using the "value" column to fill?

library(ggplot2)

rows = 1:10; cols = 1:10
grid_data = expand.grid(rows, cols)
colnames(grid_data) <- c("x", "y")
set.seed(123)
grid_data$value = sample(1:100, size = nrow(grid_data), replace = TRUE)

ggplot(grid_data, aes(x = x, y = y, color = value)) + geom_point() + theme_void()

enter image description here

Many thanks!

I am expecting a touching hexagonal heatmap or dotplot colored by with "value".


Solution

  • It's not possible to directly transform a square grid of values into a hexagonal grid of values without significantly distorting the relative position of the points.

    An alternative is to perform a manual 2D interpolation of the values onto a hexagonal grid:

    library(tidyverse)
    
    m1 <- interp::interp(grid_data$x, grid_data$y, grid_data$value, 
                         xo = 0:11, yo = seq(0, 11, sqrt(3)/2))
    
    m2 <- interp::interp(grid_data$x, grid_data$y, grid_data$value, 
                         xo = 0:11 + 0.5, yo = seq(0, 11, sqrt(3)/2))
    
    m1$y <- m1$y[  seq_along(m1$y) %% 2 == 1]
    m1$z <- m1$z[, seq_along(m1$x) %% 2 == 1]
    m2$y <- m2$y[  seq_along(m2$y) %% 2 == 0]
    m2$z <- m2$z[, seq_along(m2$x) %% 2 == 0]
    
    interp::interp2xyz(m1) |>
      as.data.frame() |>
      rbind(interp::interp2xyz(m2) |> as.data.frame()) %>%
      filter(!is.na(z)) %>%
      mutate(group = row_number()) %>%
      rowwise() %>%
      reframe(x = x + hexbin::hexcoords(0.5, sqrt(3)/6)$x,
              y = y + hexbin::hexcoords(0.5, sqrt(3)/6)$y,
              z = first(z),
              group = first(group)) %>%
      ggplot(aes(x, y)) + 
      geom_polygon(aes( fill = z, group = group), color = 'white') + 
      scale_fill_viridis_c() +
      theme_minimal() +
      coord_equal(xlim = c(0, 11), ylim = c(0, 11))
    

    enter image description here


    EDIT

    If you want a space-filling interpolation of your values, where actual data points are preserved, there is no need for hexagonal bins:

    library(akima)
    
    with(grid_data, interp(x, y, value, nx = 500, ny = 500, linear = FALSE)) |> 
      interp::interp2xyz() |>
      as.data.frame() |>
      ggplot(aes(x = x, y = y, fill = z)) + 
      scale_fill_viridis_c("value") +
      geom_raster() + 
      theme_void(base_size = 20) +
      coord_equal()
    

    enter image description here

    If we overlay our original points, we can see the original values are preserved at each location:

    with(grid_data, interp(x, y, value, linear = FALSE, nx = 500, ny = 500)) |> 
      interp::interp2xyz() |>
      as.data.frame() |>
      ggplot(aes(x = x, y = y, fill = z)) + 
      scale_fill_viridis_c("value", limits = c(-10, 110), na.value = NA) +
      geom_raster() + 
      geom_point(data = grid_data, shape = 21, size = 5, aes(fill = value)) +
      theme_void(base_size = 20) +
      coord_equal()
    

    enter image description here

    And you can use filled contours if you prefer:

    with(grid_data, interp(x, y, value, nx = 500, ny = 500, linear = FALSE)) |> 
      interp::interp2xyz() |>
      as.data.frame() |>
      ggplot(aes(x = x, y = y, z = z)) + 
      geom_contour_filled() + 
      theme_void(base_size = 20) +
      coord_equal()
    

    enter image description here