Search code examples
rplotplotlyrasterrayshader

How can I plot 4D results as a surface in R?


I have a model including a three-way interaction term (XYZ). I have used this model to create predictions across the parameter space. Thus, I have four pieces of information to convey: X, Y, Z, and Probability. If I had a simple interaction, I would plot a heatmap with X~Y and color would be Probability, but now I need that as a 3D plot.

example_results <- expand.grid(x = 1:8,
                  y = seq(from = -1.85, to = 3.58, length.out = 15),
                  z = seq(from = 0, to = 2.41, length.out = 15)) %>% 
  mutate(probability = sample(seq(from = 0, to = 1, length.out = 3000), 1800))

plot_ly seems to understand its own relationships within data, whereas I want just to plot my results. rayshader seemed perfect, but is apparently unable to decouple height (z) from fill (probability). rasterVis objects to cuts.

I will greatly appreciate any advice on how to plot all the information in one image in R.

Thanks!

rayshader outcome

cubic outcome


Solution

  • You can do this using rayshader and ggplot2. One problem is that the example data you have posted uses expand.grid(). Each value of x and y has every value of z, so you won't see a difference in heights with this data. I'll just select random rows for each value of x and y for demonstration purposes:

    library(dplyr)
    dat <- example_results |>
        group_by(x, y) |>
        filter(row_number() == sample(row_number(), 1))
    

    Once you've done this you can use ggplot2 to create two separate 2d heatmaps, one for the height and one for the fill. Make sure to set color in both to the height value (z in this case):

    library(ggplot2)
    heatmap_fill <- ggplot(dat) +
        geom_tile(aes(x, y, fill = probability, color = z)) +
        theme_void() +
        theme(legend.position = "none")
    
    heatmap_height <- ggplot(dat) +
        geom_tile(aes(x, y, fill = z, color = z)) +
        theme_void() +
        theme(legend.position = "none")
    

    Then you can just use rayshader to render these, providing the fill heatmap as the ggobj and the height heatmap as ggobj_height. I've turned off shadows in this case so it renders quickly:

    library(rayshader)
    plot_gg(
        ggobj = heatmap_fill,
        ggobj_height = heatmap_height,
        multicore = TRUE, width = 5, height = 5,
        shadow = FALSE,
        raytrace = FALSE,
        scale = 500, windowsize = c(1280, 720), zoom = 0.85,
        phi = 90, theta = 0
    )
    

    Output:

    enter image description here

    The quality is much better than this - I had to make it very small and reduce the colours to 64 get it under the 2mb image upload limit. See here for the higher quality version.