In many online resources we know how to project the contours of a surface. For example, in section “Surface Plot With Contours” from https://plotly.com/r/3d-surface-plots/. The method is to use argument contours
in plotly::add_surface
and specify project=list(z=TRUE)
.
My question is, how to project an arbitrary line on the surface (in either x
, y
, or z
direction)? This arbitrary line is defined using plotly::add_paths
.
A dumb solution is to plot the projected curve on the (x,y)
plane where z=0
.
Is there a smarter solution, as in the case of contours, that the projected curve stays on the (x,y)
grid when I change plotly::layout(scene = ...)
?
library(plotly)
dim(volcano) # [1] 87 61
f1 = plot_ly(z = ~volcano)
add_surface(
p = f1,
contours = list(z = list(show = TRUE, usecolormap = TRUE, project = list(z = TRUE)))
) # projection of contours, great!
# use a lighter color scale
(f2 = plot_ly(z = ~volcano) |> add_surface(colorscale = list(c(0, 1), c('beige', 'lightgreen'))))
# add an arbitrary line
(f3 = add_paths(f2, x = 0:60, y = 0:60, z = volcano[cbind(1:61,1:61)], line = list(width = 4)))
# now how do I project this arbitrary line to (x,y), (x,z) or (y,z) plane ?
# dumb solution
# draw the projected curves on (x,y=0,z) and (x=0,y,z) plane manually
f3 |>
add_paths(x = 0, y = 0:60, z = volcano[cbind(1:61,1:61)], line = list(width = 3)) |>
add_paths(x = 0:60, y = 0, z = volcano[cbind(1:61,1:61)], line = list(width = 3))
# do we have a smarter solution?
This works for plotting a line, even one you randomly choose. However, I’m not entirely sure what you’re trying to do your data creation for add_paths
.
I’m sure there is more than one way to do this, but this works for me.
First, I set up the data as x, y, z. Since this is a matrix of elevations, I need to know what the grid is, then I use that build x & y, then I collapse z.
dim(volcano)
# [1] 87 61
volc <- data.frame(x = rep(seq(0, 60, by = 1), 87), # grid is 61 x 87
y = rep(seq(0, 86, by = 1), each = 61),
z = as.vector(t(volcano)))
Now I can build whatever I want on the surface using this data. Straight lines are pretty straightforward, filter for x == ...
or y == ...
.
I can use add_paths
or add_trace(type = "scatter3d", mode =...
add_trace(f2, type = "scatter3d", mode = "lines", # with trace, where y = 50
data = volc[volc$y == 50, ], x = ~x, y = ~y, z = ~z)
# with paths, where y = 50
add_paths(f2, data = volc[volc$y == 50, ], x = ~x, y = ~y, z = ~z)
If wanted a line between (0, 70) and (50, 20) I can do that using the formula for a line.
First a UDF, to make this a bit easier – using the to and from coordinates and the data frame (as dfa
). The coordinates can be in any order, as long as they are x, y, x, y order.
It can be a bit hard to see the line in the images here, so I’ve added markers to make it more visible.
cords <- function(x0, y0, x1, y1, dfa) {
mx <- ifelse(x0 != x1, which(c(x0, x1) == max(c(x0, x1))), 2)
dx <- c(x0, x1)[mx] - c(x0, x1)[-mx] # change in X
dy <- c(y0, y1)[mx] - c(y0, y1)[-mx] # change in Y
intc <- dy/dx * x0 - y0 # y-intercept
# find the relevant rows, using Ax - By + C = 0
with(dfa, which(dy * x - dx * y - dx * intc == 0))
} # returns row indices in dfa
add_trace(f2, type = "scatter3d", mode = "lines+markers", # use both for hi-vis
marker = list(size = 2), # but a bit smaller...sheesh
data = volc[cords(0, 70, 50, 20, volc), ], # call the line maker for coords
x = ~x, y = ~y, z = ~z)
3