rplotlyscatter3d

Possible to synchronize 3D scatter (plotly) plots in R?


I have create a series of 3D scatter plots in R using the plotly package, where each plot is colored according to a different variable in my dataset. I have managed to combine them into a single "main" plot using either the subplot() function from plotly or combineWidgets() function from the manipulateWidget package. See code below for a representative example using the mtcars dataset:

#Packages
library(dplyr)
library(plotly)
library("manipulateWidget")

#Dataset
library(datasets)
data(mtcars)

#Four different 3d scatter plots
fig_1 <- plot_ly(mtcars, type = "scatter3d", x = ~mpg, y = ~cyl, z = ~ wt, color = ~as.factor(vs), 
                 colors = c("black", "grey"), scene = "scene",
                 mode = "markers", marker = list(size = 5, symbol = "circle"))

fig_2 <- plot_ly(mtcars, type = "scatter3d", x = ~mpg, y = ~cyl, z = ~ wt, color = ~as.factor(am),
                 colors = c("red", "blue"), scene = "scene2",
                 mode = "markers", marker = list(size = 5, symbol = "circle"))

fig_3 <- plot_ly(mtcars, type = "scatter3d", x = ~mpg, y = ~cyl, z = ~ wt, color = ~as.factor(gear),
                 colors = c("yellow", "brown", "orange"), scene = "scene3",
                 mode = "markers", marker = list(size = 5, symbol = "circle"))

fig_4 <- plot_ly(mtcars, type = "scatter3d", x = ~mpg, y = ~cyl, z = ~ wt, color = ~as.factor(carb),
                 colors = c("green", "pink", "cyan", "purple"), scene = "scene4",
                 mode = "markers", marker = list(size = 5, symbol = "circle"))

#Can be combined as subplots:
main_plot <- subplot(fig_1, fig_2, fig_3, fig_4, nrows = 2, margin = 0.06) %>% 
  layout(title = "Main Plot", 
         scene= list(domain = list(x=c(0,0.5), y=c(0.5,1)), aspectmode="cube"), 
         scene2 = list(domain = list(x=c(0.5,1), y=c(0.5,1)), aspectmode="cube"),
         scene3 = list(domain = list(x=c(0,0.5), y=c(0,0.5)), aspectmode="cube"),
         scene4 = list(domain = list(x=c(0.5,1), y=c(0,0.5)), aspectmode="cube")
         )

main_plot


#OR alternatively they can be combined as widgets:
combineWidgets(fig_1, fig_2, fig_3, fig_4) 

So far so good, but I had hoped the combined 3D plots could somehow be synchronized, so that when I drag/zoom/rotate one plot, the 3 other plots would follow suit and adapt to the new "view". Does anyone know if it is possible to accomplish this in the R with combined 3D plots as either subplots or combined widgets?

Similar question have been asked in relation with plotly through Python (https://community.plotly.com/t/synchronize-camera-across-3d-subplots/22236), although I haven't been able to find if something likewise has been or can be done in R.

I will greatly appreciate any help or suggestions I can get. Thanks in advance!


Solution

  • This functionality is not implemented, but a bit of cutsom JavaScript will do the trick:

    main_plot <- subplot(fig_1, fig_2, fig_3, fig_4, nrows = 2, margin = 0.06) %>% 
      layout(title = "Main Plot", 
             scene  = list(domain = list(x = c(0, 0.5), y = c(0.5, 1)), aspectmode = "cube"), 
             scene2 = list(domain = list(x = c(0.5, 1), y = c(0.5, 1)), aspectmode = "cube"),
             scene3 = list(domain = list(x = c(0, 0.5), y = c(0, 0.5)), aspectmode = "cube"),
             scene4 = list(domain = list(x = c(0.5, 1), y = c(0, 0.5)), aspectmode = "cube")
      )
    
    main_plot %>% 
      htmlwidgets::onRender(
        "function(x, el) {
          x.on('plotly_relayout', function(d) {
            const camera = Object.keys(d).filter((key) => /\\.camera$/.test(key));
            if (camera.length) {
              const scenes = Object.keys(x.layout).filter((key) => /^scene\\d*/.test(key));
              const new_layout = {};
              scenes.forEach(key => {
                new_layout[key] = {...x.layout[key], camera: {...d[camera]}};
              });
              Plotly.relayout(x, new_layout);
            }
          });
        }")
    

    Explanation

    Whenever you do some zooming / rotating a plotly_relayout event is fired upon which you can react. The event contains an object holding the new layout data (d) which we can use.

    If there is a camera slot in this object (i.e. we did something which changed the viewing angle/zoom), we first get the original scenes (to keep original settings specifically for domain) and overwrite the camera slot of all scenes with the new camera slot effectively synchronizing all scatter plots.

    We use htmlwidgets::onRender to add the custom JavaScript code et voilĂ .