Related
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
)
I have got two vectors and a 2D-matrix, from which I want to create a 3D surface plot. I already have split my data into X and Y (vectors (time "t" and wavelength "w") and Z (matrix; absorbance "NIR" at time and wavelength) with the same number of rows/columns respectively:
t = matrix(1:456, ncol= 1)
w = matrix(1350:1650, nrow = 1)
NIR = as.matrix(read.table("NIR_alle_pur.txt", header = TRUE, dec =","))
colnames(NIR) = c(paste0("NIR.", 1350:1650))
dim(NIR)
# [1] 456 301
dput(NIR_example)
structure(c(60771.93, 57230.56, 56235.96, 41617.47, 41709.93,
57466.6, 59916.97, 63376.4, 41966.73, 41254.34, 65535, 61468.76,
65535, 41238.03, 42530.97, 56936.03, 65009.4, 65535, 40375.5,
41021.6, 62757, 65455.44, 63795.6, 41349.6, 41178.2), .Dim = c(5L,
5L), .Dimnames = list(NULL, c("NIR.Spectrum_1350.0000000", "NIR.Spectrum_1351.0000000",
"NIR.Spectrum_1352.0000000", "NIR.Spectrum_1353.0000000", "NIR.Spectrum_1354.0000000"
)))
I tried to insert those into the rgl.surface function, but I get the following error message:
Error in rgl.surface(x, y, z, coords = 1:3) : Bad dimension for rows
I've also tried to plot them with plotly, but my success was equally low.
Can someone give me an input how I can get my spectral data to look like the last ones (multiple surfaces) on this site, individually? I'll try the overlay of the surfaces with plotlylater on!
I am happy for every extra input and information on my level!
Thank you!
After looking at the source code, I'd guess the problem is that you stored your x and y vectors as matrices. If they are matrices, they need to be identical in shape to z.
As I mentioned in a comment, you should avoid using rgl.surface (and the other rgl.* functions in most cases), and use surface3d instead, or persp3d if you want axes.
The *3d functions are higher level functions that act more like other R functions, and they will lead to fewer problems in the long run.
You haven't posted any data, so I'll post a completely artificial example. Let's suppose z = x^2 + y^2 + a, where a is a different constant for each surface. Then you can plot it like this:
x <- seq(-2, 2, length = 7)
y <- seq(-3, 3, length = 5) # I've chosen different ranges
# and lengths just to illustrate.
z <- outer(x, y, function(x, y) x^2 + y^2)
colours <- heat.colors(100)
minval <- min(z)
maxval <- max(z) + 10
col <- colours[(z - minval)/(maxval - minval)*99 + 1]
persp3d(x, y, z, col = col) # get axes the first time
z <- outer(x, y, function(x, y) x^2 + y^2 + 5)
col <- colours[(z - minval)/(maxval - minval)*99 + 1]
surface3d(x, y, z, col = col)
z <- outer(x, y, function(x, y) x^2 + y^2 + 10)
col <- colours[(z - minval)/(maxval - minval)*99 + 1]
surface3d(x, y, z, col = col)
aspect3d(1, 1, 1) # Make axes all equal
That produces this plot:
I would like to achieve that the points I add to the plot have their size adjusted to obtain a better 3D impression. I know that I somehow have to use the transformation matrix that is returned to compute the length of the vector orthogonal to the 2d plane to the respective point in 3d, but I don't know how to do that.
Here is an example:
x1 <- rnorm(100)
x2 <- 4 + rpois(100, 4)
y <- 0.1*x1 + 0.2*x2 + rnorm(100)
dat <- data.frame(x1, x2, y)
m1 <- lm(y ~ x1 + x2, data=dat)
x1r <- range(dat$x1)
x1seq <- seq(x1r[1], x1r[2], length=30)
x2r <- range(dat$x2)
x2seq <- seq(x2r[1], x2r[2], length=30)
z <- outer(x1seq, x2seq, function(a,b){
predict(m1, newdata=data.frame(x1=a, x2=b))
})
res <- persp(x1seq, x2seq, z)
mypoints <- trans3d(dat$x1, dat$x2, dat$y, pmat=res)
points(mypoints, pch=1, col="red")
You can use the function presented here to determine distance to the observer, then scale the pointsize (cex) to that distance:
# volcano data
z <- 2 * volcano # Exaggerate the relief
x <- 10 * (1:nrow(z)) # 10 meter spacing (S to N)
y <- 10 * (1:ncol(z)) # 10 meter spacing (E to W)
# draw volcano and store transformation matrix
pmat <- persp(x, y, z, theta = 35, phi = 40, col = 'green4', scale = FALSE,
ltheta = -120, shade = 0.75, border = NA, box = TRUE)
# take some xyz values from the matrix
s = sample(1:prod(dim(z)), size=500)
xx = x[row(z)[s] ]
yy = y[col(z)[s]]
zz = z[s] + 10
# depth calculation function (adapted from Duncan Murdoch at https://stat.ethz.ch/pipermail/r-help/2005-September/079241.html)
depth3d <- function(x,y,z, pmat, minsize=0.2, maxsize=2) {
# determine depth of each point from xyz and transformation matrix pmat
tr <- as.matrix(cbind(x, y, z, 1)) %*% pmat
tr <- tr[,3]/tr[,4]
# scale depth to point sizes between minsize and maxsize
psize <- ((tr-min(tr) ) * (maxsize-minsize)) / (max(tr)-min(tr)) + minsize
return(psize)
}
# determine distance to eye
psize = depth3d(xx,yy,zz,pmat,minsize=0.1, maxsize = 1)
# from 3D to 2D coordinates
mypoints <- trans3d(xx, yy, zz, pmat=pmat)
# plot in 2D space with pointsize related to distance
points(mypoints, pch=8, cex=psize, col=4)
Could you give me an example on how to use rgl to plot 3 variables at the axes x, y and z and a fourth one with different colours?
thanks
You use a combination of persp and colour according to a separate function. Here's some example code:
## Create a simple surface f(x,y) = -x^2 - y^2
## Colour the surface according to x^2 only
nx = 31; ny = 31
x = seq(-1, 1, length = nx)
y = seq(-1, 1, length = ny)
z = outer(x, y, function(x,y) -x^2 -y^2)
## Fourth dim
z_col = outer(x, y, function(x,y) x^2)
## Average the values at the corner of each facet
## and scale to a value in [0, 1]. We will use this
## to select a gray for colouring the facet.
hgt = 0.25 * (z_col[-nx,-ny] + z_col[-1,-ny] + z_col[-nx,-1] + z_col[-1,-1])
hgt = (hgt - min(hgt))/ (max(hgt) - min(hgt))
## Plot the surface with the specified facet colours.
persp(x, y, z, col = gray(1 - hgt))
persp(x, y, z, col=cm.colors(32)[floor(31*hgt+1)], theta=-35, phi=10)
This gives:
RGL
It's fairly straightforward to use the above technique with the rgl library:
library(rgl)
## Generate the data using the above commands
## New window
open3d()
## clear scene:
clear3d("all")
## setup env:
bg3d(color="#887777")
light3d()
surface3d(x, y, z, color=cm.colors(32)[floor(31*hgt+1)], alpha=0.5)
There is an example in ?plot3d if you are talking about plotting points in a 3d space and colouring them:
x <- sort(rnorm(1000))
y <- rnorm(1000)
z <- rnorm(1000) + atan2(x,y)
plot3d(x, y, z, col=rainbow(1000))
But if you mean to colour the points by a 4th variable, say a grouping variable, then we can modify the example above to do this by creating a grouping variable
grp <- gl(5, 200) ## 5 groups 200 members each
## now select the colours we want
cols <- 1:5
## Now plot
plot3d(x, y, z, col=cols[grp])
OK, is this more what you want?
X <- 1:10
Y <- 1:10
## Z is now a 100 row object of X,Y combinations
Z <- expand.grid(X = X, Y = Y)
## Add in Z1, which is the 3rd variable
## X,Y,Z1 define the surface, which we colour according to
## 4th variable Z2
Z <- within(Z, {
Z1 <- 1.2 + (1.4 * X) + (-1.9 * Y)
Z2 <- 1.2 + (1.4 * X) - (1.2 * X^2) + (1.9 * Y) + (-1.3 * Y^2)
Z3 <- 1.2 + (1.4 * X) + (-1.9 * Y) + (-X^2) + (-Y^2)})
## show the data
head(Z)
## Set-up the rgl device
with(Z, plot3d(X, Y, Z1, type = "n"))
## Need a scale for Z2 to display as colours
## Here I choose 10 equally spaced colours from a palette
cols <- heat.colors(10)
## Break Z2 into 10 equal regions
cuts <- with(Z, cut(Z2, breaks = 10))
## Add in the surface, colouring by Z2
with(Z, surface3d(1:10,1:10, matrix(Z1, ncol = 10),
color = cols[cuts], back = "fill"))
with(Z, points3d(X, Y, Z1, size = 5)) ## show grid X,Y,Z1
Here's a modification where the plane surface Z1 is curved (Z3).
## Set-up the rgl device plotting Z3, a curved surface
with(Z, plot3d(X, Y, Z3, type = "n"))
with(Z, surface3d(1:10,1:10, matrix(Z3, ncol = 10),
color = cols[cuts], back = "fill"))
The detail of what I did to get Z2 probably doesn't matter, but I tried to get something like the graph you linked to.
If I've still not got what you want, can you edit your Q with some example data and give us a better idea of what you want?
HTH
Take a look at example(points3d).
The r3d help page shows you how to draw axes.
x <- c(0, 10, 0, 0)
y <- c(0, 0, 100, 0)
z <- c(0, 0, 0, 1)
i <- c(1,2,1,3,1,4)
labels <- c("Origin", "X", "Y", "Z")
text3d(x,y,z,labels)
segments3d(x[i],y[i],z[i])
Now you add some points
dfr <- data.frame(x = 1:10, y = (1:10)^2, z = runif(10), col = rainbow(10))
with(dfr, points3d(x, y, z, col = col))
Could you give me an example on how to use rgl to plot 3 variables at the axes x, y and z and a fourth one with different colours?
thanks
You use a combination of persp and colour according to a separate function. Here's some example code:
## Create a simple surface f(x,y) = -x^2 - y^2
## Colour the surface according to x^2 only
nx = 31; ny = 31
x = seq(-1, 1, length = nx)
y = seq(-1, 1, length = ny)
z = outer(x, y, function(x,y) -x^2 -y^2)
## Fourth dim
z_col = outer(x, y, function(x,y) x^2)
## Average the values at the corner of each facet
## and scale to a value in [0, 1]. We will use this
## to select a gray for colouring the facet.
hgt = 0.25 * (z_col[-nx,-ny] + z_col[-1,-ny] + z_col[-nx,-1] + z_col[-1,-1])
hgt = (hgt - min(hgt))/ (max(hgt) - min(hgt))
## Plot the surface with the specified facet colours.
persp(x, y, z, col = gray(1 - hgt))
persp(x, y, z, col=cm.colors(32)[floor(31*hgt+1)], theta=-35, phi=10)
This gives:
RGL
It's fairly straightforward to use the above technique with the rgl library:
library(rgl)
## Generate the data using the above commands
## New window
open3d()
## clear scene:
clear3d("all")
## setup env:
bg3d(color="#887777")
light3d()
surface3d(x, y, z, color=cm.colors(32)[floor(31*hgt+1)], alpha=0.5)
There is an example in ?plot3d if you are talking about plotting points in a 3d space and colouring them:
x <- sort(rnorm(1000))
y <- rnorm(1000)
z <- rnorm(1000) + atan2(x,y)
plot3d(x, y, z, col=rainbow(1000))
But if you mean to colour the points by a 4th variable, say a grouping variable, then we can modify the example above to do this by creating a grouping variable
grp <- gl(5, 200) ## 5 groups 200 members each
## now select the colours we want
cols <- 1:5
## Now plot
plot3d(x, y, z, col=cols[grp])
OK, is this more what you want?
X <- 1:10
Y <- 1:10
## Z is now a 100 row object of X,Y combinations
Z <- expand.grid(X = X, Y = Y)
## Add in Z1, which is the 3rd variable
## X,Y,Z1 define the surface, which we colour according to
## 4th variable Z2
Z <- within(Z, {
Z1 <- 1.2 + (1.4 * X) + (-1.9 * Y)
Z2 <- 1.2 + (1.4 * X) - (1.2 * X^2) + (1.9 * Y) + (-1.3 * Y^2)
Z3 <- 1.2 + (1.4 * X) + (-1.9 * Y) + (-X^2) + (-Y^2)})
## show the data
head(Z)
## Set-up the rgl device
with(Z, plot3d(X, Y, Z1, type = "n"))
## Need a scale for Z2 to display as colours
## Here I choose 10 equally spaced colours from a palette
cols <- heat.colors(10)
## Break Z2 into 10 equal regions
cuts <- with(Z, cut(Z2, breaks = 10))
## Add in the surface, colouring by Z2
with(Z, surface3d(1:10,1:10, matrix(Z1, ncol = 10),
color = cols[cuts], back = "fill"))
with(Z, points3d(X, Y, Z1, size = 5)) ## show grid X,Y,Z1
Here's a modification where the plane surface Z1 is curved (Z3).
## Set-up the rgl device plotting Z3, a curved surface
with(Z, plot3d(X, Y, Z3, type = "n"))
with(Z, surface3d(1:10,1:10, matrix(Z3, ncol = 10),
color = cols[cuts], back = "fill"))
The detail of what I did to get Z2 probably doesn't matter, but I tried to get something like the graph you linked to.
If I've still not got what you want, can you edit your Q with some example data and give us a better idea of what you want?
HTH
Take a look at example(points3d).
The r3d help page shows you how to draw axes.
x <- c(0, 10, 0, 0)
y <- c(0, 0, 100, 0)
z <- c(0, 0, 0, 1)
i <- c(1,2,1,3,1,4)
labels <- c("Origin", "X", "Y", "Z")
text3d(x,y,z,labels)
segments3d(x[i],y[i],z[i])
Now you add some points
dfr <- data.frame(x = 1:10, y = (1:10)^2, z = runif(10), col = rainbow(10))
with(dfr, points3d(x, y, z, col = col))