Add regression plane in R using Plotly - r

I recently tried to plot a regression pane in RStudio using the plotly library and read this post: Add Regression Plane to 3d Scatter Plot in Plotly
I followed the exact same procedure and ended up with a regression plane, which is obviously not correct:
EDIT: I followed the proposal in the first answer and my result looks like this:
Here is my code, I commented every step: sm is the data.frame I used
library(reshape2);
sm <- read.delim("Supermodel.dat", header = TRUE);
x1 <- sm$age
x2 <- sm$years
y <- sm$salary
df <- data.frame(x1, x2, y);
### Estimation of the regression plane
mod <- lm(y ~ x1+x2, data = df, na.action =
na.omit);
cf.mod <- coef(mod)
### Calculate z on a grid of x-y values
x1.seq <- seq(min(x1),max(x1),length.out=231)
x2.seq <- seq(min(x2),max(x2),length.out=231)
z.mtx <- t(outer(x1.seq, x2.seq, function(x1,x2)
cf.mod[1]+cf.mod[2]*x1+cf.mod[3]*x2))
#### Draw the plane with "plot_ly" and add points with "add_trace"
library(plotly)
# Draw plane with plotly surface plot
plane <- plot_ly(x=~x1.seq, y=~x2.seq, z=~z.mtx, colors =
c("#f5cb11", #b31d83"),type="surface") %>%
add_trace(data=df, x=x1, y=x2, z=y, mode="markers",
type="scatter3d",
marker = list(color="black", opacity=0.7, symbol=105)) %>%
layout(scene = list(aspectmode = "manual", aspectratio = list(x=1,
y=1, z=1), xaxis = list(title = "Age", range = c(12,24)), yaxis =
list(title = "Work experience (years)", range = c(0,10)), zaxis =
list(title = "Salary p.a. (k)", range = c(0,90) )))
plane
I checked with the str-function, if the x1.seq and x2.seq have the same number of entries, and they both have 231 number values in them. The plane gets calculated and is shown, but it obviously still wrong.
PS: If you want to run the code, just download the file Supermodel.dat from Andy Fields website (https://studysites.uk.sagepub.com/dsur/study/articles.htm) under Regression.
Thanks in advance,
rikojir

Here is an illustrative example that shows how the observed points and the regression plane can be plotted together in a 3D plot generated using the plotlty package.
Hope it can help you.
### Data generating process
set.seed(1234)
n <- 50
x1 <- runif(n); x2 <- runif(n)
x3 <- rnorm(n)>0.5
y <- 2*x1-x2+rnorm(n, sd=0.25)
df <- data.frame(y, x1, x2, x3)
### Estimation of the regression plane
mod <- lm(y ~ x1+x2)
cf.mod <- coef(mod)
### Calculate z on a grid of x-y values
x1.seq <- seq(min(x1),max(x1),length.out=25)
x2.seq <- seq(min(x2),max(x2),length.out=25)
z <- t(outer(x1.seq, x2.seq, function(x,y) cf.mod[1]+cf.mod[2]*x+cf.mod[3]*y))
#### Draw the plane with "plot_ly" and add points with "add_trace"
cols <- c("#f5cb11", "#b31d83")
cols <- cols[x3+1]
library(plotly)
p <- plot_ly(x=~x1.seq, y=~x2.seq, z=~z,
colors = c("#f5cb11", "#b31d83"),type="surface") %>%
add_trace(data=df, x=x1, y=x2, z=y, mode="markers", type="scatter3d",
marker = list(color=cols, opacity=0.7, symbol=105)) %>%
layout(scene = list(
aspectmode = "manual", aspectratio = list(x=1, y=1, z=1),
xaxis = list(title = "X1", range = c(0,1)),
yaxis = list(title = "X2", range = c(0,1)),
zaxis = list(title = "Y", range = pretty(z)[c(1,8)])))
print(p)
Here is the 3D plot generated by the above code:

Related

How to use the pdp in R to compute 3d partial dependence plots?

I have a Random forest model in R similar to this:
library("randomForest")
library("caret")
library("pdp")
data("cars")
my_data<-cars[1:5]
my_rf <- randomForest( Price ~ ., data=my_data)
price_mil<- partial(my_rf, pred.var = c("Price", "Mileage"))
plotPartial(price_mil, levelplot = FALSE, zlab = "Price", colorkey = TRUE)
However, I would like to have some 3d partial dependence plots, including the values of parameters on the axis. How can I do this with pdp?
First of all, In your example you used "price" in the partial() function. This does not make sense to me, as you essentially just plot a 2d partial dependence plot that way. I changed that in my example code below.
However, to get the requested partial plots you can use
plotPartial(price_mil, zlab = "Price", levelplot = F, scale = list(arrows = F))
If you want to have more control, I would advise to use the underlying functions of the package to construct your formula and wireframe object and then call wireframe() with scale=list(arrows = F) to add the values to the axes.
library("randomForest")
library("caret")
library("pdp")
data("cars")
my_data <- cars[1:5]
my_rf <- randomForest( Price ~ ., data=my_data)
object <- pdp::partial(my_rf, pred.var = c("Cylinder", "Mileage"))
form <- stats::as.formula(paste("yhat ~", paste(names(object)[1L:2L],
collapse = "*")))
wireframe(form, data = object, drape =T, zlab = "Price", scale = list(arrows = F))
yields
Interactive 3D Partial Dependence Plot with plotly
# Random seed to reproduce the results
set.seed(1)
# Create artificial data for a binary classification problem
y <- factor(sample(c(0,1), size = 100, replace = TRUE), levels = c("0", "1"))
d <- data.frame(y = y, x1 = rnorm(100), x2 = rnorm(100), x3 = rnorm(100))
# Build a random forest model
library(randomForest)
rf1 <- randomForest::randomForest(y ~., n.trees = 100, mtry = 2, data = d)
###### Bivariate partial dependency plots ######
# Step 1: compute the partial dependence values
# given two variables using the pdp library
library(pdp)
pd <- rf1 %>% partial(pred.var = c("x1", "x2"), n.trees = 100)
# Step 2: construct the plot using the plotly library
library(plotly)
p <- plot_ly(x = pd$x1, y = pd$x2, z = pd$yhat, type = 'mesh3d')
# Step 3: add labels to the plot
p <- p %>% layout(scene = list(xaxis = list(title = "x1"),
yaxis = list(title = "x2"),
zaxis = list(title = "Partial Dependence")))
# Step 4: show the plot
show(p)
Interactive Contour Plot (i.e. flattened 2-variable PDP) with a color scale for the partial dependence values using plotly
###### Bivariate PDPs with colored scale ######
# Interpolate the partial dependence values
dens <- akima::interp(x = pd$x1, y = pd$x2, z = pd$yhat)
# Flattened contour partial dependence plot for 2 variables
p2 <- plot_ly(x = dens$x,
y = dens$y,
z = dens$z,
colors = c("blue", "grey", "red"),
type = "contour")
# Add axis labels for 2D plots
p2 <- p2 %>% layout(xaxis = list(title = "x1"), yaxis = list(title = "x2"))
# Show the plot
show(p2)
Interactive 3D Partial Dependence Plot with a color scale for the partial dependence values using plotly
###### Interactive 3D partial dependence plot with coloring scale ######
# Interpolate the partial dependence values
dens <- akima::interp(x = pd$x1, y = pd$x2, z = pd$yhat)
# 3D partial dependence plot with a coloring scale
p3 <- plot_ly(x = dens$x,
y = dens$y,
z = dens$z,
colors = c("blue", "grey", "red"),
type = "surface")
# Add axis labels for 3D plots
p3 <- p3 %>% layout(scene = list(xaxis = list(title = "x1"),
yaxis = list(title = "x2"),
zaxis = list(title = "Partial Dependence")))
# Show the plot
show(p3)

Find the exact coordinates of a contour on a surface and plot it manually in R plotly

I am drawing a surface plot and would like to "manually" draw a contour line using plotly. In the code below I:
simulate the data for drawing the surface plot
calculate the coordinates of the contour line at a specific z level using the contoureR package
draw the surface plot and contour line
# Load packages
library(plotly) # for interactive visualizations
library(contoureR) # for calculating contour coordinates
# Simulate the data for plotting
x <- y <- seq(from = 0, to = 100, by = 1)
z1 <- outer(X = x, Y = y, FUN = function(x, y) x^0.2 * y^0.3) # data for surface plot
# Obtain coordinates of contour for z = 5
z_level <- 5
r <- contourLines(x = x, y = y, z = z1, levels = z_level)
plot_ly(
type = "surface",
x = x,
y = y,
z = z1,
) %>%
add_trace(
type = "scatter3d",
x = r[[1]]$x,
y = r[[1]]$y,
z = z_level
)
I am aware that these are all approximations, so I also tried to pass the x and y coordinates produced by contourLines() to the formula used to create z1above and use the corresponding values to plot my contour line (instead of using z_level = 5, but I still do not obtain the desired result:
plot_ly(
x = x,
y = y,
z = z1,
type = "surface"
) %>%
add_trace(
type = "scatter3d",
x = r[[1]]$x,
y = r[[1]]$y,
z = r[[1]]$x^0.2*r[[1]]$y^0.3
)
I alo know that plotly enables me to draw specific contour lines (see my question and answer here: Add a permanent contour line to a surface plot in R plotly). However, I would like to draw my contour line myself (after getting their coordinates) so it can "pull" by cursor and show me the tooltip info whenever I hover over it. Ideally, if there was a way to obtain the contour lines coordinates as computed by plotly itself, that would be great.
Thank you for your help.
I was able to find two solutions to this problem.
Solution 1: transpose the z1 matrix
The first solution was given me by #nirgrahamuk and it consists in transposing the z1 matrix:
library(plotly) # for interactive visualizations
# Simulate the data for plotting
x <- y <- seq(from = 0, to = 100, by = 1)
z1 <- outer(X = x, Y = y, FUN = function(x, y) x^0.2 * y^0.3) # data for surface plot
# Obtain coordinates of contour for z = 5
z_level <- 6
r <- contourLines(x = x,
y = y,
z = z1,
levels = z_level)
plot_ly(
type = "surface",
z = t(z1), # *** WE TRANSPOSE THE MATRIX HERE! ***
) %>%
add_trace(
type = "scatter3d",
x = r[[1]]$x,
y = r[[1]]$y,
z = z_level
)
Solution 2: use the isoband package
The second solution is to compute the contour lines coordinates with the isoband::isolines() function:
library(plotly) # for interactive visualizations
library(isoband) # for find contour lines coordinates
# Simulate the data for plotting
x <- y <- seq(from = 0, to = 100, by = 1)
z1 <- outer(X = x, Y = y, FUN = function(x, y) x^0.2 * y^0.3) # data for surface plot
# Obtain coordinates of contour for z = 5
z_level <- 6
r <- isolines(x = x, # *** WE USE THE isolines() FUNCTION HERE ***
y = y,
z = z1,
levels = z_level)
plot_ly(
type = "surface",
z = z1,
) %>%
add_trace(
type = "scatter3d",
x = r[[1]]$x,
y = r[[1]]$y,
z = z_level
)

R + plotly: solid of revolution

I have a function r(x) that I want to rotate around the x axis to get a solid of revolution that I want to add to an existing plot_ly plot using add_surface (colored by x).
Here is an example:
library(dplyr)
library(plotly)
# radius depends on x
r <- function(x) x^2
# interval of interest
int <- c(1, 3)
# number of points along the x-axis
nx <- 20
# number of points along the rotation
ntheta <- 36
# set x points and get corresponding radii
coords <- data_frame(x = seq(int[1], int[2], length.out = nx), r = r(x))
# for each x: rotate r to get y and z coordinates
# edit: ensure 0 and pi are both amongst the angles used
coords %<>%
rowwise() %>%
do(data_frame(x = .$x, r = .$r,
theta = seq(0, pi, length.out = ntheta / 2 + 1) %>%
c(pi + .[-c(1, length(.))]))) %>%
ungroup %>%
mutate(y = r * cos(theta), z = r * sin(theta))
# plot points to make sure the coordinates define the desired shape
coords %>%
plot_ly(x = ~x, y = ~y, z = ~z, color = ~x) %>%
add_markers()
How can I generate the shape indicated by the points above as a plotly surface (ideally open on both ends)?
edit (1):
Here is my best attempt so far:
# get all x & y values used (sort to connect halves on the side)
xs <-
unique(coords$x) %>%
sort
ys <-
unique(coords$y) %>%
sort
# for each possible x/y pair: get z^2 value
coords <-
expand.grid(x = xs, y = ys) %>%
as_data_frame %>%
mutate(r = r(x), z2 = r^2 - y^2)
# format z coordinates above x/y plane as matrix where columns
# represent x and rows y
zs <- matrix(sqrt(coords$z2), ncol = length(xs), byrow = TRUE)
# format x coordiantes as matrix as above (for color gradient)
gradient <-
rep(xs, length(ys)) %>%
matrix(ncol = length(xs), byrow = TRUE)
# plot upper half of shape as surface
p <- plot_ly(x = xs, y = ys, z = zs, surfacecolor = gradient,
type = "surface", colorbar = list(title = 'x'))
# plot lower have of shape as second surface
p %>%
add_surface(z = -zs, showscale = FALSE)
While this gives the desired shape,
It has 'razor teeth' close to the x/y plane.
The halves parts don't touch. (resolved by including 0 and pi in the theta vectors)
I didn't figure out how to color it by x instead of z (though I didn't look much into this so far). (resolved by gradient matrix)
edit (2):
Here is an attempt using a single surface:
# close circle in y-direction
ys <- c(ys, rev(ys), ys[1])
# get corresponding z-values
zs <- rbind(zs, -zs[nrow(zs):1, ], zs[1, ])
# as above, but for color gradient
gradient <-
rbind(gradient, gradient[nrow(gradient):1, ], gradient[1, ])
# plot single surface
plot_ly(x = xs, y = ys, z = zs, surfacecolor = gradient,
type = "surface", colorbar = list(title = 'x'))
Surprisingly, while this should connect the two halves orthogonal to the x / y plane to create the full shape,
it still suffers from the same 'razor teeth' effect as the above solution:
edit (3):
It turns out the missing parts result from z-values being NaN when close to 0:
# color points 'outside' the solid purple
gradient[is.nan(zs)] <- -1
# show those previously hidden points
zs[is.nan(zs)] <- 0
# plot exactly as before
plot_ly(x = xs, y = ys, z = zs, surfacecolor = gradient,
type = "surface", colorbar = list(title = 'x'))
This could be caused by numerical instability of the substraction when r^2 and y get too close, resulting in negative input for sqrt where the actual input is still non-negative.
This seams unrelated to numerical issues as even when considering +-4 'close' to zero, the 'razor teeth' effect can not be avoided completely:
# re-calculate z-values rounding to zero if 'close'
eps <- 4
zs <- with(coords, ifelse(abs(z2) < eps, 0, sqrt(z2))) %>%
matrix(ncol = length(xs), byrow = TRUE) %>%
rbind(-.[nrow(.):1, ], .[1, ])
# plot exactly as before
plot_ly(x = xs, y = ys, z = zs, surfacecolor = gradient,
type = "surface", colorbar = list(title = 'x'))
interesting question, I've struggled to use the surface density to improve on your solution. There is a hack you could do with layering multiple lines, that comes out nice for this e.g. Only changes made to the original eg is to use lots more x points: nx to 1000, and change add_markers to add_lines. Might not be scalable, but works fine for this size of data :)
library(dplyr)
library(plotly)
# radius depends on x
r <- function(x) x^2
# interval of interest
int <- c(1, 3)
# number of points along the x-axis
nx <- 1000
# number of points along the rotation
ntheta <- 36
# set x points and get corresponding radii
coords <- data_frame(x = seq(int[1], int[2], length.out = nx), r = r(x))
# for each x: rotate r to get y and z coordinates
# edit: ensure 0 and pi are both amongst the angles used
coords %<>%
rowwise() %>%
do(data_frame(x = .$x, r = .$r,
theta = seq(0, pi, length.out = ntheta / 2 + 1) %>%
c(pi + .[-c(1, length(.))]))) %>%
ungroup %>%
mutate(y = r * cos(theta), z = r * sin(theta))
# plot points to make sure the coordinates define the desired shape
coords %>%
plot_ly(x = ~x, y = ~y, z = ~z, color = ~x) %>%
add_lines()
Best,
Jonny
This doesn't answer your question, but it will give a result you can interact with in a web page: don't use plot_ly, use rgl. For example,
library(rgl)
# Your initial values...
r <- function(x) x^2
int <- c(1, 3)
nx <- 20
ntheta <- 36
# Set up x and colours for each x
x <- seq(int[1], int[2], length.out = nx)
cols <- colorRampPalette(c("blue", "yellow"), space = "Lab")(nx)
clear3d()
shade3d(turn3d(x, r(x), n = ntheta, smooth = TRUE,
material = list(color = rep(cols, each = 4*ntheta))))
aspect3d(1,1,1)
decorate3d()
rglwidget()
You could do better on the colours with some fiddling: you probably want to create a function that uses x or r(x) to set the colour instead of just repeating the colours the way I did.
Here's the result:
I have had another crack at it and have a closer solution, using the "surface" type. What helped was looking at the results of your first surface plot with nx = 5 and ntheta = 18. The reason it's jaggardy is because of the way its linking up the columns in zs (across the x points). It's having to link from part way up the larger ring around it, and this causes the density to spike up to meet this point.
I can't get rid of this jaggardy behaviour 100%. I've made these changes:
add some small points to theta around the edges: where the two densities are joined. This reduces the size of the jaggardy part as there are some more points close to the boundary
calculation to mod zs to zs2: ensure that each ring has an equal dimension to the ring outside, by adding the 0's in.
increased nx to 40 and reduced ntheta to 18 - more x's makes step smaller. reduce ntheta for run time, as I've added on more points
the steps come in how it tries to join up the x rings. In theory if you have more x rings it should remove this jaggardiness, but that's time consuming to run.
I don't think this answers the Q 100%, and I'm unsure if this library is the best for this job. Get in touch if any Q's.
library(dplyr)
library(plotly)
# radius depends on x
r <- function(x) x^2
# interval of interest
int <- c(1, 3)
# number of points along the x-axis
nx <- 40
# number of points along the rotation
ntheta <- 18
# set x points and get corresponding radii
coords <- data_frame(x = seq(int[1], int[2], length.out = nx), r = r(x))
# theta: add small increments at the extremities for the density plot
theta <- seq(0, pi, length.out = ntheta / 2 + 1)
theta <- c(theta, pi + theta)
theta <- theta[theta != 2*pi]
inc <- 0.00001
theta <- c(theta, inc, pi + inc, pi - inc, 2*pi - inc)
theta <- sort(theta)
coords %<>%
rowwise() %>%
do(data_frame(x = .$x, r = .$r, theta = theta)) %>%
ungroup %>%
mutate(y = r * cos(theta), z = r * sin(theta))
# get all x & y values used (sort to connect halves on the side)
xs <-
unique(coords$x) %>%
sort
ys <-
unique(coords$y) %>%
sort
# for each possible x/y pair: get z^2 value
coords <-
expand.grid(x = xs, y = ys) %>%
as_data_frame %>%
mutate(r = r(x), z2 = r^2 - y^2)
# format z coordinates above x/y plane as matrix where columns
# represent x and rows y
zs <- matrix(sqrt(coords$z2), ncol = length(xs), byrow = TRUE)
zs2 <- zs
L <- ncol(zs)
for(i in (L-1):1){
w <- which(!is.na(zs[, (i+1)]) & is.na(zs[, i]))
zs2[w, i] <- 0
}
# format x coordiantes as matrix as above (for color gradient)
gradient <-
rep(xs, length(ys)) %>%
matrix(ncol = length(xs), byrow = TRUE)
# plot upper half of shape as surface
p <- plot_ly(x = xs, y = ys, z = zs2, surfacecolor = gradient,
type = "surface", colorbar = list(title = 'x'))
# plot lower have of shape as second surface
p %>%
add_surface(z = -zs2, showscale = FALSE)
One solution would be to flip your axes so that you are rotating around the z axis rather than the x axis. I don't know if this is feasible, given the existing chart that you are adding this figure to, but it does easily solve the 'teeth' problem.
xs <- seq(-9,9,length.out = 20)
ys <- seq(-9,9,length.out = 20)
coords <-
expand.grid(x = xs, y = ys) %>%
mutate(z2 = (x^2 + y^2)^(1/4))
zs <- matrix(coords$z2, ncol = length(xs), byrow = TRUE)
plot_ly(x = xs, y = ys, z = zs, surfacecolor = zs,
type = "surface", colorbar = list(title = 'x')) %>%
layout(scene = list(zaxis = list(range = c(1,3))))

3D Biplot in plotly - R

I want to build a 3D PCA bi-plot using plotly package because the graph is nice and interactive in html format (something that I need).
My difficulty is to add the loading. I want the loading to be presented as straight lines from the point (0,0,0) (i.e. the equivalent to 2D biplots)
So all in all I don't know how to add straight lines starting from the centre of the 3D graph.
I have calculated the scores and loading using the PCA function;
pca1 <- PCA (dat1, graph = F)
for scores:
ind1 <- pca1$ind$coord[,1:3]
x <- ind1[,1] ; y <- ind1[,2] ; z <- ind1[,3]
for loadings:
var1 <- pca1$var$coord[,1:3]
xl <- var1[,1] ; yl <- var1[,2] ; zl <- var1[,3]
and using the code bellow the 3D score plot is generated;
p <- plot_ly( x=x, y=y, z=z,
marker = list(opacity = 0.7, color=y , colorscale = c('#FFE1A1', '#683531'), showscale = F)) %>%
layout(title = "3D Prefmap",
scene = list(
xaxis = list(title = "PC 1"),
yaxis = list(title = "PC 2"),
zaxis = list(title = "PC 3")))
Here are some ideas that could be useful for the development of a 3D biplot.
# Data generating process
library(MASS)
set.seed(6543)
n <- 500
mu <- c(1,-2,3,-1,3,4)
Sigma <- diag(rep(1,length(mu)))
Sigma[3,1] <- Sigma[1,3] <- 0.1
Sigma[4,6] <- Sigma[6,4] <- 0.1
X <- as.data.frame(mvrnorm(n, mu=mu, Sigma=Sigma))
# PCA
pca <- princomp(X, scores=T, cor=T)
# Scores
scores <- pca$scores
x <- scores[,1]
y <- scores[,2]
z <- scores[,3]
# Loadings
loads <- pca$loadings
# Scale factor for loadings
scale.loads <- 5
# 3D plot
library(plotly)
p <- plot_ly() %>%
add_trace(x=x, y=y, z=z,
type="scatter3d", mode="markers",
marker = list(color=y,
colorscale = c("#FFE1A1", "#683531"),
opacity = 0.7))
for (k in 1:nrow(loads)) {
x <- c(0, loads[k,1])*scale.loads
y <- c(0, loads[k,2])*scale.loads
z <- c(0, loads[k,3])*scale.loads
p <- p %>% add_trace(x=x, y=y, z=z,
type="scatter3d", mode="lines",
line = list(width=8),
opacity = 1)
}
print(p)

3D Data with ggplot

I have data in the following form:
x <- seq(from = 0.01,to = 1, by = 0.01)
y <- seq(from = 0.01,to = 1, by = 0.01)
xAxis <- x/(1+x*y)
yAxis <- x/(1+x*y)
z <- (0.9-xAxis)^2 + (0.5-yAxis)^2
df <- expand.grid(x,y)
xAxis <- df$Var1/(1+df$Var1*df$Var2)
yAxis <- df$Var2/(1+df$Var1*df$Var2)
df$x <- xAxis
df$y <- yAxis
df$z <- z
I would like to plot te (x,y,z) data as a surface and contour plots, possibily interpolating data to obtain as smooth a figure as possible.
Searching I reached the akima package which does the interpolation:
im <- with(df,interp(x,y,z))
I am having trouble plotting the data with this output. Ideally I would like to use ggplot2 since I want to add stuff to the original plot.
Thanks!
I'm a bit puzzled as to what you are looking for, but how about something like this?
im <- with(df, akima::interp(x, y, z, nx = 1000, ny = 1000))
df2 <- data.frame(expand.grid(x = im$x, y = im$y), z = c(im$z))
ggplot(df2, aes(x, y, fill = z)) +
geom_raster() +
viridis::scale_fill_viridis()
For contour plots, I use the "rgl" package. This allows real-time manipulation of the plot in order to have the best view.
library("rgl")
x <- seq(from = 0.01,to = 1, by = 0.01)
y <- seq(from = 0.01,to = 1, by = 0.01)
#z <- (0.9-xAxis)^2 + (0.5-yAxis)^2
df <- expand.grid(x,y)
xAxis <- df$Var1/(1+df$Var1*df$Var2)
yAxis <- df$Var2/(1+df$Var1*df$Var2)
df$z <- (0.9-xAxis)^2 + (0.5-yAxis)^2
surface3d(x=x, y=y, z=df$z, col="blue", back="lines")
title3d(xlab="x", zlab="z", ylab="y")
axes3d(tick="FALSE")
The rgl package is comparable to the ggplot2 package to customize the final plot. The 0.01 grid spacing is more than close enough for this type of smooth surface.

Resources