12  3D color plots

You may want to plot your data as a color map, like the evolution of a Raman spectrum as a function of temperature, pressure or position. In some cases you’ll have a 3-columns data.frame with x, y, and z values (e.g. intensity of a peak as a function of the position on the sample), in some cases you can have a list of spectra evolving with a given parameter.

12.1 The ggplot2 solution

Let’s create a dummy set of spectra that we will gather in a tidy tibble.

library(tidyverse)
Nspec <- 40                           # Amount of spectra
N     <- 500                          # Size of the x vector
# Create a fake data tibble
fake_data <- tibble(T = round(seq(273, 500, length=Nspec), 1)) |> 
    mutate(spec = map(T, ~tibble(w = seq(0, 100, length = N),
                         Intensity = 50*dnorm(w, mean = (./T[1])*20 + 25, 
                                                  sd  = 10+runif(1,max=5)))))
fake_data
#> # A tibble: 40 × 2
#>        T spec              
#>    <dbl> <list>            
#>  1  273  <tibble [500 × 2]>
#>  2  279. <tibble [500 × 2]>
#>  3  285. <tibble [500 × 2]>
#>  4  290. <tibble [500 × 2]>
#>  5  296. <tibble [500 × 2]>
#>  6  302. <tibble [500 × 2]>
#>  7  308. <tibble [500 × 2]>
#>  8  314. <tibble [500 × 2]>
#>  9  320. <tibble [500 × 2]>
#> 10  325. <tibble [500 × 2]>
#> # ℹ 30 more rows
fake_data <- fake_data |> unnest(spec)
fake_data
#> # A tibble: 20,000 × 3
#>        T     w Intensity
#>    <dbl> <dbl>     <dbl>
#>  1   273 0        0.0112
#>  2   273 0.200    0.0117
#>  3   273 0.401    0.0122
#>  4   273 0.601    0.0127
#>  5   273 0.802    0.0133
#>  6   273 1.00     0.0139
#>  7   273 1.20     0.0144
#>  8   273 1.40     0.0151
#>  9   273 1.60     0.0157
#> 10   273 1.80     0.0164
#> # ℹ 19,990 more rows

OK, so now we have some fake experimental data stored in a tidy tibble called fake_data. We want to plot it as a color map in order to grasp the evolution of the spectra. This can be done through the use of geom_contour() and geom_contour_filled() functions and by providing the z aesthetics, or by using the geom_raster() or geom_tile() functions with a fill aesthetics. Both methods can be combined, as shown below:

# Plotting
colors <- colorRampPalette(c("white","royalblue","seagreen",
                             "orange","red","brown"))
Nbins <- 10
ggplot(data=fake_data, aes(x=w, y=T, z=Intensity)) + 
      geom_contour_filled(bins = Nbins) + 
      ggtitle("Some fake data") + 
      scale_fill_manual(values = colors(Nbins),
                        name = "Intensity\n[arb. units]") +
      labs(x = "Fake Raman Shift [1/cm]",
           y = "Fake Temperature [K]") +
      theme_bw()

ggplot(data=fake_data, aes(x = w, y = T)) + 
      geom_raster(aes(fill = Intensity)) + #geom_tile would work
      geom_contour(aes(z = Intensity), color = "black", bins = 5)+
      ggtitle("Some fake data") + 
      scale_fill_gradientn(colors = colors(10), 
                           name = "Intensity\n[arb. units]") +
      labs(x = "Fake Raman Shift [1/cm]",
           y = "Fake Temperature [K]") +
      theme_bw()

Another option is to make a “ridge plot”, or a stacking of plots:

colors <- colorRampPalette(c("royalblue","seagreen","orange",
                             "red","brown"))(length(unique(fake_data$T)))
ggplot(data = fake_data, 
       aes(x = w, 
           y = Intensity + as.numeric(factor(T))-1,
           color = factor(T))
       ) + 
    geom_line() + 
    labs(x = "Fake Raman Shift [1/cm]", 
         y = "Fake Intensity [arb. units]") +
    coord_cartesian(xlim = c(25,75)) +
    scale_color_manual(values=colors,name="Fake\nTemperature [K]") +
    theme_bw()

ggplot(data=fake_data, 
       aes(x = w, 
           y = Intensity + as.numeric(factor(T))-1, 
           color = T, 
           group = T)
       )+
    geom_line() + 
    labs(x="Fake Raman Shift [1/cm]", y="Fake Intensity [arb. units]") +
    scale_color_gradientn(colors=colors,name="Fake\nTemperature [K]") +
    coord_cartesian(xlim = c(25,75)) +
    theme_bw()

12.2 The base graphics solution

In some cases you end up with a matrix z, and two vectors x and y. This is easy to plot using the base image() function. For the sake of example, let’s just pivot our 3-columns data.frame to such a matrix using pivot_wider():

x <- sort(unique(fake_data$w))
y <- sort(unique(fake_data$T))
z <- as.matrix(fake_data |> 
                pivot_wider(values_from = Intensity, names_from = T) |> 
                select(-w)
               )
colors <- colorRampPalette(c("white","royalblue","seagreen","orange","red","brown"))(50)
par(mar = c(4, 4, .5, 4), lwd = 2)
image(x, y, z, col = colors)

You can add a legend by using the image.plot function:

library(fields)
par(mar=c(4, 4, .5, 4), lwd=2)
image.plot(x,y,z, col = colors)

12.3 The plotly solution

And finally, if you want to make this an interactive plot, you can use plot_ly():

library(plotly)
aX <- list(title = "Raman Shift [1/cm]")
aY <- list(title = "Temperature [K]")
# Weird but you need to use t(z) here:
z <- t(z)
# Color plot
plot_ly(x = x, y = y, z = z, type = "heatmap", colors = colors) |> 
   layout(xaxis = aX, yaxis = aY)

Or, very cool, an interactive surface plot:

plot_ly(x=x, y=y, z=z, type = "surface", colors=colors) |>
   layout(scene = list(xaxis = aX, yaxis = aY, dragmode="turntable"))

12.4 The case of non-regular data

In case you have a set of non-regular data, plotting it as a color map can get tricky: how do we tell the plotting device what color should be in a place where there is no data point?

The solution is to use a spline (or linear, but spline looks usually nicer) interpolation of your 2D data. For this, we can use the akima package and its interp() function, like so:

# let's make our data irregular and see the plot is now not working:
irreg.df <- fake_data[sample(nrow(fake_data), nrow(fake_data)/3),]
# let's plot these irregular data
colors <- colorRampPalette(c("white","royalblue","seagreen",
                             "orange","red","brown"))(500)
ggplot(data=irreg.df, aes(x=w, y=T, fill=Intensity)) + 
      geom_raster() + #geom_tile would work
      ggtitle("Some irregular and ugly fake data") + 
      scale_fill_gradientn(colors=colors,name="Intensity\n[arb. units]") +
      labs(x = "Fake Raman Shift [1/cm]",
           y = "Fake Temperature [K]") +
      theme_bw()

# now let's interpolate the data on a 100x100 regular grid
# linear = FALSE -> cubic interpolation
library(akima)
irreg.df.interp <- with(irreg.df, 
    interp(x=w, y=T, z=Intensity, nx = 100, ny = 100,
           duplicate = "median", extrap = FALSE, linear = FALSE)
    )
# irreg.df.interp is a list of 2 vectors and a matrix
str(irreg.df.interp)
#> List of 3
#>  $ x: num [1:100] 0 1.01 2.02 3.03 4.04 ...
#>  $ y: num [1:100] 273 275 278 280 282 ...
#>  $ z: num [1:100, 1:100] NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ...
# Regrouping this list to a 3-columns data.frame
irreg.df.smooth <- expand.grid(w = irreg.df.interp$x, 
                               T = irreg.df.interp$y) |> 
                        tibble() |> 
                        mutate(Intensity = as.vector(irreg.df.interp$z)) |> 
                        na.omit()
# Plotting
irreg.df.smooth |> 
    ggplot(aes(x=w, y=T, fill=Intensity)) + 
        geom_raster() + 
        ggtitle("Some irregular fake data that have been interpolated with cubic splines") + 
        scale_fill_gradientn(colors=colors, name="Intensity\n[arb. units]") +
        labs(x = "Fake Raman Shift [1/cm]", 
             y = "Fake Temperature [K]") +
        theme_bw()

12.5 2D density of points

In case you want to plot a density of points, you have a variety of solutions:

df <- tibble(x=rnorm(1e3, mean=c(1,5)),
             y=rnorm(1e3, mean=c(5,1)))
p1 <- ggplot(data=df, aes(x=x,y=y))+ geom_density2d() + ggtitle('geom_density2d()')
p2 <- ggplot(data=df, aes(x=x,y=y))+ geom_hex() + ggtitle('geom_hex()')
p3 <- ggplot(data=df, aes(x=x,y=y))+ geom_bin2d() + ggtitle('geom_bin2d()')
p4 <- ggplot(data=df, aes(x=x,y=y))+ ggtitle('stat_density2d()') +
        stat_density2d(aes(fill = ..density..), geom = "tile", contour = FALSE, n = 200) +
        scale_fill_continuous(low = "white", high = "dodgerblue4")
library(cowplot)
plot_grid(p1,p2,p3,p4)

Or the base smoothScatter() function could do the trick: