Bring your plots to the next dimension with plotly
and rayshader
!
2D plots with an x- and a y-axis displaying the relationships between two variables are probably the most common type of plots in our everyday life. If we want to incorporate more information into the plots, we can use different appearances (colors, sizes, shapes, etc.) of points and lines (or other graphic elements). But sometimes, visualizing the data in a 3D space can be more appealing and may even help us gain deeper insights into the data distribution. Although ggplot does not have geoms for 3D plots (at least by the time of this post), there are other R packages that do the job. And in this post, I’m going to introduce two of these packages to you: plotly
and rayshader
, both of which have functions for generating cool 3D plots. So buckle up and let’s get ready to bring our plots to the next dimension!
plotly
The first package we’ll be using is plotly
. This package features interactive plots and contains functions for generating a wide array of figures. Moreover, plotly
also provides the function ggplotly()
, which converts ggplots to the corresponding interactive plots.
Here, we’re going to explore the plotly
functions for creating 3D plots. Specifically, we’ll create a 3D scatterplot with fitted lines and a 3D surface plot with contours and a path.
The syntax for plotly
is pretty similar to ggplot: we call the global function plotly()
to create a plotting canvas (akin to ggplot()
) and add other graphical elements using the function add_XXX()
(akin to geom_XXX()
).
To create a basic 3D scatterplot, we use the function add_markers()
(similar to geom_point()
). Simply pass the x-, y-, and z-coordinates to the function and specify the variable you would like to color the points by.
To add the lines, we use the function add_path()
(similar to geom_path()
). Again, simply pass the x-, y-, and z-coordinates to the function and specify the variable you would like to color the lines by.
We can modify the the appearance of the axes and legend using the function layout()
(see here for a complete list of things you can adjust!).
library(tidyverse)
library(plotly)
### Model predictions
iris_lm_prediction <- iris %>%
group_by(Species) %>%
summarise(Model = list(lm(Petal.Length ~ Sepal.Length + Sepal.Width))) %>%
rowwise() %>%
mutate(Newdata = list(data.frame(Sepal.Length = range(Model$model$Sepal.Length),
Sepal.Width = range(Model$model$Sepal.Width)))) %>%
mutate(Prediction = list(cbind(Newdata, Prediction = predict(Model, newdata = Newdata)))) %>%
dplyr::select(Species, Prediction) %>%
unnest(col = Prediction)
### Create a 3D scatterplot with the fitted lines
scatterplot3d <- plot_ly() %>%
# add the points
add_markers(data = iris,
x = ~ Sepal.Length,
y = ~ Sepal.Width,
z = ~ Petal.Length,
color = ~ Species,
colors = c("#BF382A", "#0C4B8E", "#018571"),
size = 2) %>%
# add the lines
add_paths(data = iris_lm_prediction,
x = ~ Sepal.Length,
y = ~ Sepal.Width,
z = ~ Prediction,
color = ~ Species,
colors = c("#BF382A", "#0C4B8E", "#018571"),
showlegend = F) %>%
layout(legend = list(x = 0.9, y = 0.5),
scene = list(xaxis = list(title = "Sepal length", range = list(4, 8)),
yaxis = list(title = "Sepal width", range = list(1, 5)),
zaxis = list(title = "Petal length", range = list(1, 7))))
scatterplot3d
The second plot we’re going to create is a 3D surface plot with contour lines. This can be done via the function add_surface()
, which takes a matrix (instead of a dataframe column or a vector) as the input for the argument “z”. The contour lines are added via the argument “contour”.
In the example below, we first estimated the kernel density of the Sepal.Length and Sepal.Width in the iris
dataset using the kde2d()
from the MASS
package. The function returns a matrix of the kernel density estimates, with Sepal.Length as the row and Sepal.Width as the column. This matrix can be passed directly to the “z” argument in add_surface()
to generate a 3D surface plot. Additionally, we can specify the x- and y-axis via the argument “x” and “y”. Also note that the matrix column is treated as the x-axis (which is “Sepal.Width”) and the matrix row is treated as the y-axis (which is “Sepal.Length”) in the plot, so make sure you’re specifying the correct variables for the axes (it’s a bit confusing I know!).
Finally, we can add a random path that traverses across the 3D landscape surface, using the function add_paths()
.
library(MASS)
### 2D kernel density estimates of Sepal.Length and Sepal.Width
### The result is a 100-by-100 matrix with Sepal.Length as the row and Sepal.Width as the column
iris_kd_matrix <- kde2d(iris$Sepal.Length, iris$Sepal.Width, n = 100)[[3]]
### Create a 3D surface plot with contours
### Note that the matrix column (Sepal.Width) will be plotted as the x-axis and the matrix row (Sepal.Length) will be plotted as the y-axis
surfaceplot3d <- plot_ly() %>%
add_surface(x = seq(min(iris$Sepal.Width), max(iris$Sepal.Width), length.out = 100), # x is the matrix column, corresponding to Sepal.Width
y = seq(min(iris$Sepal.Length), max(iris$Sepal.Length), length.out = 100), # y is the matrix row, corresponding to Sepal.Length
z = iris_kd_matrix, # takes a matrix as the input
contours = list(z = list(start = 0,
end = 0.5,
size = 0.1,
show = T,
usecolormap = F))) %>%
layout(scene = list(xaxis = list(title = "Sepal width"),
yaxis = list(title = "Sepal length"),
zaxis = list(title = "Density")))
### Create a dataframe of a random path across the 3D surface
set.seed(123)
random_path_df <- data.frame(x = 1, y = 1)
max_random_path_df <- 1
i <- 1
while (max_random_path_df < 100) {
random_path_df[i + 1, ] <- random_path_df[i, ] + sample(c(0, 1), size = 2, replace = F)
max_random_path_df <- max(random_path_df)
i <- i + 1
}
random_path_df <- random_path_df %>%
rowwise() %>%
mutate(Density = iris_kd_matrix[x, y]) %>%
mutate(Sepal.Length = seq(min(iris$Sepal.Length), max(iris$Sepal.Length), length.out = 100)[x],
Sepal.Width = seq(min(iris$Sepal.Width), max(iris$Sepal.Width), length.out = 100)[y])
### Add the random path to the plot
surfaceplot3d <- surfaceplot3d %>%
add_paths(data = random_path_df,
x = ~ Sepal.Width, # again the x-axis is Sepal.Width
y = ~ Sepal.Length, # again the y-axis is Sepal.Length
z = ~ Density,
line = list(width = 4))
surfaceplot3d
To save the plot, we can set the configuration options using config()
and specify the file format, file name, figure width and height (in pixels), and the scale of the figure (a scale larger than 1 will enlarge the figure). After that, adjust the plot to a desired view and click the camera icon “Download plot as a png” on the mode bar to download it.
Alternatively, we can install the application Orca and save the plot directly using the function orca()
. See this page for more details. Of course, we can modify the default plot view by specifying the x, y, and z values in the “eye” argument before we save it.
### Option 1. Download the plot from the mode bar
### Set the configuration options and click the download icon
config(surfaceplot3d, toImageButtonOptions = list(format = "png",
filename = "Surfaceplot3d",
height = 500,
width = 700,
scale = 2))
### Option 2. Save the plot using the function orca()
# modify the plot view
surfaceplot3d_newview <- surfaceplot3d %>%
layout(scene = list(camera = list(eye = list(x = 1.5, y = -1.5, z = 1.5))))
surfaceplot3d_newview
# save the plot
orca(surfaceplot3d_newview, file = "Surfaceplot3d.png", width = 700, height = 500, scale = 2)
rayshader
The second package we’re going to explore is rayshader
. rayshader
features awesome 3D elevation maps using a combination of raytracing and hillshading techniques. Besides maps, the package also has the function plot_gg()
that converts ggplots to 3D plots, which you’ll see in a moment. plot_gg()
will recognize the “fill” and “color” aesthetic and ignore other aesthetics such as “shape” and “linetype” that cannot be rendered into a 3D view. Let’s take a look at some examples below.
In this example, we first created a contour plot in ggplot and then passed the ggplot object to plot_gg()
. We can specify additional arguments for the 3D plot: the width and height of the plot, the height of the z-axis, the shadow intensity, the isometric field of view angle, the zoom level, the angle of rotation around the z-axis, etc. The 3D plot will be displayed in a pop-up window. To save the plot as a png file, simply run rander_snapshot(filename = )
.
library(rayshader)
### Create a contour ggplot
contourplot <- ggplot(data = iris) +
geom_density_2d_filled(aes(x = Sepal.Length, y = Sepal.Width, fill = after_stat(nlevel)),
n = 100, bins = 20, contour = TRUE) +
scale_fill_viridis_c() +
theme_bw()
### Convert the contour ggplot to a 3D plot
plot_gg(contourplot,
width = 3.5, # the width of the 3D plot in inch
height = 3, # the height of the 3D plot in inch
scale = 250, # the scaling factor controlling the height of the z-axis
shadow_intensity = 0.8, # 0 means full shade and 1 means no shade
fov = 25, # the isometric field of view angle
zoom = 0.6, # the zoom factor; a value smaller than 1 zooms in on the plot
theta = 0, # the rotation around the z-axis
windowsize = c(900, 900) # the size of the pop-up window
)
render_snapshot(filename = "Contourplot3d") # save the 3D plot as a png file
Here is another example of converting a ggplot heatmap into a 3D plot:
### 2D kernel density estimates of Sepal.Length and Sepal.Width
sepal_length_vec <- seq(min(iris$Sepal.Length), max(iris$Sepal.Length), length.out = 30)
Sepal_width_vec <- seq(min(iris$Sepal.Width), max(iris$Sepal.Width), length.out = 30)
iris_kd_vec <- kde2d(iris$Sepal.Length, iris$Sepal.Width, n = 30)[[3]] %>%
as.vector()
iris_kd_df <- expand.grid(sepal_length_vec, Sepal_width_vec) %>%
mutate(Density = iris_kd_vec) %>%
rename(Sepal_length = Var1,
Sepal_width = Var2)
### Create a ggplot heatmap
heatmap <- ggplot(iris_kd_df) +
geom_tile(aes(x = Sepal_length, y = Sepal_width, fill = Density)) +
scale_fill_viridis_c() +
theme_bw()
### Convert the heatmap to a 3D plot
plot_gg(heatmap,
width = 3.5,
height = 3,
scale = 250,
shadow_intensity = 0.8,
fov = 25,
zoom = 0.6,
theta = 0,
windowsize = c(900, 900))
render_snapshot(filename = "Heatmap3d")
To recap, we explored two packages for 3D data visualization in R. The first one is plotly
, which creates a variety of interactive plots, and we made a 3D scatterplot and a 3D contour plot with it. The second one is rayshader
, which has the function plot_gg()
to convert a 2D ggplot into a 3D plot, and we did that using examples of a contour plot and a heatmap. There are a lot more different kinds of 3D plots you can make with these two packages—the sky’s the limit, and we only scratched the surface here. But at least now you know the basics and you can go on to explore more!
Hope you learn something useful from this post and don’t forget to leave your comments and suggestions below if you have any!
If you see mistakes or want to suggest changes, please create an issue on the source repository.