Animation of 3d charts (specifically rotation)

Is there any way to animate plotlys graphics?

For example, for usage cases where embedding the interactive chart isnt an option, and space might be limited, I’d like to render a gif or move of a 3d chart rotating, as if it were being rotated by hand in the RStudio panel.

So my question is in 2 parts I guess:

  1. Is there any kind of in build functionality that could achieve this natively or with a bit of hacking?

or

  1. Does the plotly viewer itself (in RStudio in this case) have an “API” of sorts, whereby I can specify the plot’s rotation in a for loop, then I could iteratively export pictures and then composite them together in to a video afterwards?

Hi there,
For the 1st part of your question, Plotly doesn’t have a feature that will export a gif.
For the second part, you can define the camera angles in the API to rotate the chart programatically. You can see the camera attributes here: https://plot.ly/r/reference/#layout-scene-camera

Thats great thank you, I knew it must be possible. Related to this I would like to change the level of zoom before I render programmatically too, but can’t seem to figure it out. Does zoom use the centre vector of the eye control in that link you provided, or is there a ‘proper’ zoom parameter?

Nevermind, I cracked it! Full code ended up using a trig function to control the camera in a circular manner. Just giving a multipler to the trig function moved the camera out, so that solved it.

args <- commandArgs(TRUE)
df <- read.csv(args[1], sep=",", check.names=FALSE, row.names = 1)
 args <- c("Documents/Warwick/PhD/Structural\ Work/Tail\ Fibres/CD\ Spectra/lumt.csv")
library(reshape2)
library(plotly)
library(ggplot2)
library(RColorBrewer)
file <- strsplit(basename(args[1]), '[.]')[[1]]
colnames(df)[1] <- "BL" # Keep baseline incase subtration needs to be done before plotting.
###
# Manipulate dataframe to remove baseline and parse the x and y vector needed for plotting.
###
df$BL <- NULL #
scan <- as.numeric(rownames(df))
temp <- as.numeric(colnames(df))
matrix <- as.matrix(df[,1:15])
dimnames(matrix) = NULL

matrix.list <- list(x = temp,
                y = scan,
                z = matrix)

font.pref <- list(size=13,family="Arial, sans-serif",color="black")
x.list <- list(title = "Temperature (˚C)",titlefont = font.pref)
y.list <- list(title = "Wavelength (nm)",titlefont = font.pref)
z.list <- list(title = "CD Intensity",titlefont = font.pref)



for(i in seq(0,6.3,by=0.1)){
outfile <- paste(file[1],"plot",round(i,digits=2), sep = "_")
cam.zoom = 2
ver.angle = 0
graph <- plot_ly(matrix.list,x = temp,y = scan,z = z,
             type="surface",
             colors=c("black","grey","red"),
             colorbar = list(title="CD Intensity")) %>%
              layout(scene=list(xaxis = x.list,
                                yaxis = y.list,
                                zaxis = z.list,
                                camera = list(eye = list(x = cos(i)*cam.zoom,y = sin(i)*cam.zoom, z=0.2),
                                              center = list(x = 0,
                                                            y = 0,
                                                            z = 0
                                                            )
                                              )
                                )
                     )
graph


cat("Now rendering iteration:", i,"\n")
plotly_IMAGE(graph,
         width = 1200,
         height = 1050,
         format = "png",
         username="xxx",
         key="xxxx",
         scale = 1,
         out_file = paste(outfile,"png", sep="."))
}

Hi, I’m using a similar animated rotation thing for a 3D plot — thanks for this code!!

I do have a weird thing happening though which maybe someone can advise on. When I view the plots in RStudio, the colors look sweet:

But when I use plotly_IMAGE to export enough frames for a nice gif, everything works except for the colors, which somehow look very gray, especially on the surface plot:

No other code has changed here, so I find this pretty weird… the surface plot’s attributes are

plot_ly() %>%
    add_surface(x = 1:6, 
                y = 1:6, 
                z = dancplane.m, 
                type = "surface", 
                contours = list(x = list(highlight = FALSE), y = list(highlight = FALSE), z = list(highlight = FALSE)),
                hoverinfo = "skip",
                opacity = .4,
                colors = c('#6d98f3','#6d98f3'),
                showscale = FALSE)

I’m no expert on this at all, but I’d guess its just some idiosyncracies between the RStudio renderer and the exported image renderer 9if indeed, they are different!)

I’ve noticed a similar thing, particularly with greys, but my hacky solution to date has just been to fiddle with the alpha/opacity till I’m happy with it. You could try the code I’ve moved on to below, which circumvents the limitation on free plotly renders, and uses an RSelenium server instead. The rendering may be a little more faithful.

#!/usr/bin/env Rscript

# Plotting surface plots via ggplot2/plotly
# Usage:
# $ Rscript CDmeltplot.R -i data.csv -o filename


############################################################
# General purpose heatmap plotting script for consistency. #
# This script can be slow as it was designed to be pretty  #
# bullet-proof when loading/installing necessary packages. #
#                                                          #
# By J. Healey                                             #
# Version 3
############################################################

# Load packages:
list.of.packages <- c("reshape2",
                      "plotly",
                      "argparse",
                      "ggplot2",
                      "RColorBrewer",
                      "tools",
                      "stringr",
                      "htmltools",
                      "magrittr",
                      "RSelenium")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)

suppressMessages(library("reshape2"))
suppressMessages(library("plotly"))
suppressMessages(library("argparse"))
suppressMessages(library("ggplot2"))
suppressMessages(library("RColorBrewer"))
suppressMessages(library("tools"))
suppressMessages(library("stringr"))
suppressMessages(library("htmltools"))
suppressMessages(library("magrittr"))
suppressMessages(library("RSelenium"))

parser <- ArgumentParser()

parser$add_argument('-i',
                    '--infile',
                    action='store',
                    required=TRUE,
                    help="Plain csv matrix file for plotting.")
parser$add_argument('-d',
                    '--device',
                    action='store',
                    default='svg',
                    choices=c('png','svg','jpeg','pdf','webp'),
                    help='Output image format/device. Default SVG.')
parser$add_argument('-o',
                    '--outfile',
                    action='store',
                    default='None',
                    help='Filename and path to save the image to. Defaults to the same as the infile with a new extension.')
parser$add_argument('-z',
                    '--zoom',
                    action='store',
                    default=2.5,
                    help='The zoom value of the camera, larger values move the camera away from the centre. Default 2.5')
parser$add_argument('-v',
                    '--vangle',
                    action='store',
                    default=0.4,
                    help='The vertical angle of the camera. Increased values will rotate the camera \'overhead\'. Default 0.4')
parser$add_argument('--movie',
                    action='store',
                    default='0.5:0.5:1',
                    help='Render multiple frames to create a video. Specify start:end:increment.')
parser$add_argument('--set_iter',
                    action='store',
                    default=0,
                    help='Set the iteration counter for the output images when rendering in multiple sessions. Keeps images numerically ordered to make video creation easier.')


args <-parser$parse_args()

# Prep variables for use
infile <- args$infile
outfile <- args$outfile
device <- args$device
movie <- args$movie
cam.zoom <- as.numeric(args$zoom)
ver.angle <- as.numeric(args$vangle)
set_iter <- as.numeric(args$set_iter)
browser <- args$browser
port <- as.numeric(args$port)
from <- as.numeric(strsplit(movie,":")[[1]][1])
to <- as.numeric(strsplit(movie,":")[[1]][2])
increment <- as.numeric(strsplit(movie,":")[[1]][3])

if (outfile == 'None'){
  outfile <- basename(file_path_sans_ext(infile))
}

cat("Starting RSelenium server", "\n")
RS_server <- rsDriver(verbose = FALSE)

# Read in and prepare data
df <- read.csv(infile, sep=",", check.names=FALSE, row.names = 1, header = TRUE)
matrix <- as.matrix(df)

scan <- as.numeric(rownames(df))
temp <- as.numeric(colnames(df))

matrix.list <- list(z = matrix,
                    x = temp,
                    y = scan)

font.pref <- list(size=18, family="Arial, sans-serif", color="black")
x.list <- list(title = "Temperature (˚C)",
               titlefont = font.pref,
               ticklen = 20,
               tickcolor = "white",
               zerolinecolor = "black",
               zerolinewidth = 2)
y.list <- list(title = "Wavelength (nm)",
               titlefont = font.pref,
               ticklen = 20,
               tickcolor = "white",
               zerolinecolor = "black",
               zerolinewidth = 2)
z.list <- list(title = "CD Intensity",
               titlefont = font.pref,
               ticklen = 20,
               tickcolor = "white",
               zerolinecolor = "black",
               zerolinewidth = 2)

iter = set_iter
for(i in seq(from, to, by=increment)){
  iter = iter + 1
  theta = i/100
  
  graph <- plot_ly(z =~matrix, x=temp, y=scan, type="surface",
                   colors= c('black','gray','red'),
                   colorbar = list(title="CD Intensity",
                                   yanchor="middle",
                                   len=1.5)) %>%
                   layout(scene=list(xaxis = x.list,
                                     yaxis = y.list,
                                     zaxis = z.list,
                                     camera = list(eye = list(x = cos((i/10))*cam.zoom,
                                                              y = sin((i/10))*cam.zoom,
                                                              z=ver.angle)
                                                  )
                                    )
                          )

  outfile_iter <- paste(outfile, ".", str_pad(iter, 4, pad ="0"), sep = "")
  outfile_iter_ext <- paste(outfile_iter, device, sep = ".")
  
# Render images:
export(p = graph, outfile_iter_ext, RS_server)

if (device == "pdf"){
    convert.installed <- system("convert --version", ignore.stdout = TRUE) == 0

    if (convert.installed == TRUE){
      cat("Calling ImageMagick to convert to formats other than PNG/JPEG", "\n")
      cat(sprintf('convert %s %s',
                  paste(outfile_iter, device, sep = "."),
                  paste(outfile_iter, "pdf", sep = ".")), "\n")
      system(sprintf('convert %s %s',
                     paste(outfile_iter, "png", sep = "."),
                     paste(outfile_iter, device, sep = ".")))
    } else {
      cat("Can't convert to that device. Make sure ImageMagick `convert` is in your PATH.", "\n")
    }
  }
  cat("Outfile:", outfile_iter_ext, '\n')


}


# Kill the webserver

RS_server[["server"]]$stop()
cat("Webserver exited. All done.", "\n")