0

I'm trying to make some nice rgl 3d scatterplots I can include in an HTML document. They look fine when I display them in the rgl pane in RStudio - here: good output. But as soon as they're captured with scene3d() things go wrong. If I call the object assigned output from rglwidget(), all the colour is gone from the spheres and the planes don't appear, instead I get this: bad plot. I'm on R 4.1 and Ubuntu 20.04, and as far as I'm aware I've installed all the dependencies. I've tried my best with the doc etc. but I'm finding it a bit difficult to parse. Thanks!

code to reproduce:

library(rgl)
foo <- cbind(runif(20, -1, 1), runif(20, -1, 1), runif(20, -1, 1), c(rep(1,10), rep(2,10)))

plot3d(x = foo[,1], 
       y = foo[,2], 
       z = foo[,3],
       xlab = '',
       ylab = '',
       zlab = '',
       col = ifelse(foo[,4] == 1, '#636EFA', '#EF553B'),
       type = 's',
       size = 2,
       xlim = c(floor(min(foo[,1])), ceiling(max(foo[,1]))),
       ylim = c(floor(min(foo[,2])), ceiling(max(foo[,2]))),
       zlim = c(floor(min(foo[,3])), ceiling(max(foo[,3]))),
       axes = F)

axes3d(fig, edges = c('x-', 'y-', 'z-+'), labels = TRUE, tick = TRUE, nticks = 5, 
       expand = 1.03, floating = F)
title3d(xlab = 'PC1', ylab = 'PC2', level = 5, floating = F)
title3d(zlab = 'PC3', line = 5, level = -20, floating = F)


planes3d(1,0,0,-ceiling(max(foo[,1])), color = "#333777", alpha = 0.6, 
         emission="#333777", specular='black', shininess=5, floating = F) #x
planes3d(0,1,0,-ceiling(max(foo[,2])), color = "#333777", alpha = 0.6, 
         emission="#333777", specular='black', shininess=5, floating = F) #y
planes3d(0,0,1,-floor(min(foo[,3])), color = "#333777", alpha = 0.6, 
         emission="#333777", specular='black', shininess=5, floating = F) #z


widge <- rglwidget()
widge
LJᛃ
  • 7,655
  • 2
  • 24
  • 35
blex-max
  • 45
  • 4
  • I'm on a Mac (vs. your Ubuntu), and I see different things for both the `rgl` rendering and the `rglwidget` than what you see. When I ran the code too quickly, I would get one or two elements; that's it. They aren't the same, just as your views aren't the same. Are you set on using `rgl`? I could help you using `plotly`. – Kat Jan 17 '22 at 04:52
  • I would happily use `plotly`, I want to use `rgl` but it all seems a little arcane. I'd definitely appreciate the help! – blex-max Jan 17 '22 at 11:10
  • The main problem is that you are trying to draw planes outside the range of the data. Since planes are infinite, rgl picks a subset to draw, and it's the intersection with the data bounding box. Yours don't intersect it at all. But your code also reveals some bugs in the bounding box calculation (which is why you got something showing in the non-WebGL display). No idea why you got the bad color in the spheres, I don't see that. – user2554330 Jan 17 '22 at 19:11

2 Answers2

0

This is with plotly, as I mentioned in my comment. There are means of adding lighting and all that, but it won't every render quite like rgl. That being said, the colors looked pretty washed out. You will find that your original colors are in the comments next to the colors I used (should you want to change them back.

I used your foo data (but didn't reiterate the code here). I set the seed to 199, so that I would see the results consistently if you wanted to see the same output.

First, some setup:

  • data for the planes (instead of using background)
  • layout content
  • content repeated for each plane (lighting)
    library(plotly)
    
    # this data will be used for all three planes
    planeXy = data.frame(x = rep(c(-1.1, 1.1), 2),
                         y = rep(c(-1.1, 1.1), each = 2),
                         z = rep(-1.1, 4))
    
    # set names for the layout of the axes
    aLbls <- list(title = "PC1", title = "PC2", title = "PC3")
    
    # set styles that apply to all three axes
    # you can set a background here (instead of mesh planes),
    #     but opacity attached to the hex is the only way to control it
    #     it didn't look right, so I added the planes
    bkg <- list(range = c(-1.2, 1.2),
                showgrid = FALSE,  # hide grid lines
                zeroline = FALSE,  # don't emphasize the 0 line
                tickangle = -45,   # -45 degree angle (labels only)
                showline = TRUE,   # the outside line/connected to ticks
                linewidth = 4,     # thickness of th line in px
                ticks = "outside", # so the ticks are seen with grid hidden
                ticklen = 10,      # default is 5px
                tickwidth = 2,     # line thickness of ticks
                nticks = 5)        # same meaning as your rgl plot
    
    # set lighting for mesh planes (this is reused for each axis)
    lights <- list(ambient = 1,  # .7 for orig colors/ avail range is 0:1
                   specular = 1) # shininess/ avail range 0:2
    
    # set light position for mesh planes (this is reused for each axis)
    lightpos <- list(x = -.3, y = -.5, z = .1)

Here is the plot. It may look like a lot, but the three add_trace are nearly identical.

plot_ly(data = as.data.frame(foo),  # change it back to a data frame
        x = ~V1,
        y = ~V2,
        z = ~V3,
        type = "scatter3d", # scatter plot
        mode = "markers",   # use points
        showlegend = F,     # no legend
        name = "Foo Data Point",
        marker = list(
                size = 12,  # point size
                color = ifelse(foo[,4] == 1, 
                               '#1F51FF',   # '#636EFA', # original color
                               '#FF3131'),  # '#EF553B')
                # this gives the marker points a slightly darker outer ring
                line = list(
                        color = ifelse(foo[,4] == 1, 
                                       '#2837a2',  #'#45499f', # darker of orig color
                                       '#BA0000'), # '#9a3b2a')
                        width = 1))) %>% 
        # add the planes, the only things that differ are
        # the names, the arrang. of xyz and the delauney axis
        add_trace(inherit = F, # xy plane
                  data = planeXy,
                  x = ~x,
                  y = ~y,
                  z = ~z,
                  facecolor = rep("#333777", nrow(planeXy)),
                  opacity = .6,
                  hoverinfo = "skip",
                  lighting = lights,
                  lightposition = lightpos,
                  name = "xy",
                  type = "mesh3d") %>%
        # uses the same data, but the xyz is zxy
        add_trace(inherit = F, # yz plane
                  data = planeXy,
                  x = ~z * -1,
                  y = ~x,
                  z = ~y,
                  facecolor = rep("#333777", nrow(planeXy)),
                  opacity = .6,
                  delaunayaxis = "x", # perpendicular plane
                  hoverinfo = "skip",
                  lighting = lights,
                  lightposition = lightpos,
                  name = "yx",
                  type = "mesh3d") %>%
        # uses the same data, but the xyz is yzx
        add_trace(inherit = F, # yz plane
                  data = planeXy,
                  x = ~y,
                  y = ~z * -1,
                  z = ~x,
                  facecolor = rep("#333777", nrow(planeXy)),
                  opacity = .6,
                  delaunayaxis = "y", # perpendicular plane
                  hoverinfo = "skip",
                  lighting = lights,
                  lightposition = lightpos,
                  name = "yx",
                  type = "mesh3d") %>%
        # labels, axes, and initial perspective
        layout(scene = list(  # have to use scene when using 3D
                xaxis = append(aLbls[1], bkg),
                yaxis = append(aLbls[2], bkg),
                zaxis = append(aLbls[3], bkg),
                camera = list(eye = list(x = -1, # the initial view
                                         y = -2, 
                                         z = .1))
        ))

Because plotly uses JSON, this should work regardless of where you view it. Let me know if you have any questions or if I missed something.

This is what it looks like for me:

enter image description here

Kat
  • 15,669
  • 3
  • 18
  • 51
0

As mentioned in my comment, you shouldn't really be using planes3d for this. One simple way to do it is to draw a cube with invisible back faces, but it will show more than 3 planes in some orientations. Another is to use the bbox3d function in rgl and ask it to draw the back faces in a solid color, but that will exactly line up with the axes, and your method didn't do that.

So it's a little complicated, but I'd think drawing 3 quadrilaterals is the simplest. Just include this code (possibly with a different formula for the limits) instead of the planes3d calls:

xlim <- c(-1, 1)
ylim <- c(-1, 1)
zlim <- c(-1, 1)
quads3d(xlim[c(2,2,2,2,1,1,2,2,1,1,2,2)],
        ylim[c(1,1,2,2,2,2,2,2,1,2,2,1)],
        zlim[c(1,2,2,1,1,2,2,1,1,1,1,1)],
        color = "#333777", alpha = 0.6, 
        emission="#333777", specular='black', shininess=5)

enter image description here

user2554330
  • 37,248
  • 4
  • 43
  • 90