Search code examples
r3dplotly

3D full volume surface plot in R with plotly


been working around a little bit in the aesthetics on a 3dgraph I managed to get from some help here. Although I read in extense plotly library cant figure it out. I want to get a graph that looks like the image below, but I cant find the correct arguments to do so.

enter image description here

*Not an image of my property. For further information, please read: https://doi.org/10.1111/1365-2435.12268

My dataframe looks like this:

> data.ttl.gp
# A tibble: 3,000 x 4
       X surv.prob temp    time
   <int>     <dbl> <chr>  <dbl>
 1     1     1     42.0   3.26 
 2     1     1     44.0   0.686
 3     1     1     46.0   0.144
 4     2     0.999 42.0   6.53 
 5     2     0.999 44.0   1.37 
 6     2     0.999 46.0   0.288
 7     3     0.998 42.0   9.79 
 8     3     0.998 44.0   2.06 
 9     3     0.998 46.0   0.432
10     4     0.997 42.0  13.1  
# ... with 2,990 more rows

My basic R skills led me to something like this:

pgp<-plot_ly(scene='scene2')%>%
  add_trace(x=data.ttl.gp$surv.prob, y=data.ttl.gp$time, z=data.ttl.gp$temp,
             type="mesh3d", opacity=0.8)%>%
  layout(
    scene2=list(
      xaxis=list(title='Survival probability'),
      yaxis=list(title='Time (min)'),
      zaxis=list(title='Temperature (°C)')))

And 3Dgraph output is the one below:

enter image description here

But the image above does not have any "full surface under the curve" as the first image. I understand this is because my df have just one vector for every x,y,z, so it is understandable the output graph. So my question is: Is there any other library I could use to make a full surface plot? Is there any other efficent way to plot it?

Thanks in advance!

EDIT

As Russ requested, sample data from dput(head(data.ttl.gp, n=30))

structure(list(X = c(1L, 1L, 1L, 2L, 2L, 2L, 3L, 3L, 3L, 4L, 
4L, 4L, 5L, 5L, 5L, 6L, 6L, 6L, 7L, 7L, 7L, 8L, 8L, 8L, 9L, 9L, 
9L, 10L, 10L, 10L), surv.prob = c(1, 1, 1, 0.999, 0.999, 0.999, 
0.998, 0.998, 0.998, 0.997, 0.997, 0.997, 0.996, 0.996, 0.996, 
0.995, 0.995, 0.995, 0.994, 0.994, 0.994, 0.993, 0.993, 0.993, 
0.992, 0.992, 0.992, 0.991, 0.991, 0.991), temp = c("42.0", "44.0", 
"46.0", "42.0", "44.0", "46.0", "42.0", "44.0", "46.0", "42.0", 
"44.0", "46.0", "42.0", "44.0", "46.0", "42.0", "44.0", "46.0", 
"42.0", "44.0", "46.0", "42.0", "44.0", "46.0", "42.0", "44.0", 
"46.0", "42.0", "44.0", "46.0"), time = c(3.2647446560386, 0.6858, 
0.144060773368623, 6.5294893120772, 1.3716, 0.288121546737246, 
9.79423396811581, 2.0574, 0.432182320105869, 13.0589786241544, 
2.7432, 0.576243093474492, 16.323723280193, 3.429, 0.720303866843115, 
19.5884679362316, 4.1148, 0.864364640211738, 22.8532125922702, 
4.8006, 1.00842541358036, 26.1179572483088, 5.4864, 1.15248618694898, 
29.3827019043474, 6.1722, 1.29654696031761, 32.647446560386, 
6.858, 1.44060773368623)), row.names = c(NA, -30L), class = c("tbl_df", 
"tbl", "data.frame"))


Solution

  • I know that this probably doesn't answer your question, but I found your question interesting, so this might give you an idea. Before moving on:

    1. I'm not familiar with R, so I'll use Python... Hopefully you'll be able to adapt it.
    2. I'm not going to create a full volume plot, but I'll add vertical walls to the surface. This should allows to create a nice plot like the one you posted.

    Since I have no access to your full dataset, I assume that the points are non-uniformly distributed over the surface. The approach that follows requires the points to be "uniformly distributed": essentially, we need x, y, z to be 2D arrays, similar to what (Numpy) np.mgrid creates. You should be able to get grid-like x,y,z with scipy interpolate.

    After that, you don't need to worry about triangulation. x, y represents a grid on a plane. We can just add:

    • a column on the left: copy the first column of x and y. On z, set it to a minimum value.
    • a column on the right: copy the last column of x and y. On z, set it to a minimum value.
    • a row to the top: copy the first row of x and y. On z, set it to a minimum value.
    • a row to the bottom: copy the last row of x and y. On z, set it to a minimum value.

    Then, Plotly is going to do the rest.

    import numpy as np
    import plotly.graph_objects as go
    
    # assuming your data are in a grid-like layout
    x, y = np.mgrid[-2:2:50j, -2:2:50j]
    # surface
    z = np.cos(x**2 + y**2)
    
    add_rows = lambda arr, r1, r2: np.vstack([r1, arr, r2])
    add_cols = lambda arr, c1, c2: np.concatenate([np.array([c1]).T, arr, np.array([c2]).T], axis=1)
    
    # Chose an appropriate minimum z value
    zz = -2 * np.ones_like(x[:, 0])
    x = add_cols(x, x[:, 0], x[:, -1])
    y = add_cols(y, y[:, 0], y[:, -1])
    z = add_cols(z, zz, zz)
    
    # Chose an appropriate minimum z value
    zz = -2 * np.ones_like(x[0, :])
    x = add_rows(x, x[0, :], x[-1, :])
    y = add_rows(y, y[0, :], y[-1, :])
    z = add_rows(z, zz, zz)
    
    fig = go.Figure(data=[
        go.Surface(x=x, y=y, z=z, **{ # draw wire frame
            "opacity": 1,
            "contours.x.show":True,
            "contours.x.color":"#101010",
            "contours.x.width":1,
            "contours.x.start":-2,
            "contours.x.end":2,
            "contours.x.size":0.25,
            "contours.y.show":True,
            "contours.y.color":"#101010",
            "contours.y.width":1,
            "contours.y.start":-2,
            "contours.y.end":2,
            "contours.y.size":0.25,
        })
    ])
    fig
    

    enter image description here