create a video from plotly 3d chart - r

I'm wondering if anyone knows of a way to export plotly 3d charts as a video (more specifically if this can be done natively or requires bodging)?
Exporting a static image is simple, and exporting the interactive plots is fine for embedding in HTML etc.
Lets say I have a 3D chart that I want so simply rotate slowly, this seems like it could be pretty straight forward if the image can be rotated a given interval, an image taken, rotated further ad infinitum, perhaps in a loop - but I'm wondering if this isn't somehow supported natively?
Anyone know of a good strategy?
Solution ideally for R/RStudio, but since plotly is cross-platform, any solutions considered.

For future reference:
The key to being able to iterate multiple perspectives turns out to lie in the camera control "eye", the plotly help centre pointed me toward this:
https://plot.ly/r/reference/#layout-scene-camera
camera = list(eye = list(x = 1.25, y = 1.25, z = 1.25))) #1.25 is default
This is kind of answered here, though searching for my specific query as above didn't find it:
enter link description here
I've used a for loop in the script to pass iterators to a trigonometric function that plots out a circle for the camera co-ordinates, rendering a new image at each step.
(x,y) = cos(theta) + sin(theta)
The eventual code looked like this:
# assume dataset read in, manipulated and given as a matrix in "matrix"
matrix.list <- list(x = temp, y = scan, z = matrix)
font.pref <- list(size=12, family="Arial, sans-serif", color="black")
x.list <- list(title = "X", titlefont = font.pref)
y.list <- list(title = "Y", titlefont = font.pref)
z.list <- list(title = "Z",titlefont = font.pref)
zoom <- 2
for(i in seq(0,6.3,by=0.1){
# 6.3 is enough for a full 360 rotation
outfile <- paste(file,"plot",i, sep = "_")
graph <- plot_ly(matrix.list, x = temp, y = scan, z = z,
type="surface") %>%
layout(scene=list(xaxis = x.list,
yaxis = y.list,
zaxis = z.list,
camera = list(eye = list(x = cos(i)*zoom, y = sin(i)*zoom, z= 0.25))))
# The above camera parameters should orbit
# horizontally around the chart.
# The multiplier controls how far out from
# from the graph centre the camera is (so
# is functionally a 'zoom' control).
graph
plotly_IMAGE(graph, username="xxx", key="xxx",
out_file = paste(outfile,"png", sep="."))
}
NB Number of files and the resolution of them could end up taking up a fair amount of space.
NB 2 I had forgotten while constructing this that plotly's free API limits you to 50 API calls per day, so if you want to render a video, adjust your frames etc, accordingly...

Related

3D pipe/tube plots in R - creating plots of tree roots

I'm trying to create 3D plots of simulated tree roots in R. Here is an example of a root system growing over time:
This is essentially a 3D network of cylinders, where the cylinder diameter (and, optionally, color) represents the size of the root. The available data includes:
x, y, z of the root centroid
direction of "parent" root (e.g. +x, -x, +y, -y, +z, -z), although this information could be captured in several different ways, including by calculating the x, y, z of the parent directly prior to plotting.
size of root
Example 3D data is here, but here is my first attempt at it in just 2D using ggplot2::geom_spoke:
dat <- data.frame(x = c(0,1,-1,0,1,-1),
y = c(-1,-1,-1,-2,-2,-2),
biomass = c(3,1.5,1.5,1,1,1),
parent.dir = c("+y","-x","+x","+y","+y","+y"))
dat$parent.dir <- as.numeric(as.character(factor(dat$parent.dir,
levels = c("-x", "+x", "-y", "+y"),
labels = c(pi, 0, pi*3/2, pi/2))))
ggplot(dat, aes(x = x, y = y)) +
geom_point(x = 0, y = 0, size = 20) +
geom_spoke(radius = 1,
aes(angle = parent.dir,
size = biomass)) +
coord_equal()
I prefer a solution based in the ggplot2 framework, but I realize that there are not a ton of 3D options for ggplot2. One interesting approach could be to creatively utilize the concept of network graphs via the ggraph and tidygraph packages. While those packages only operate in 2D as far as I know, their developer has also had some interesting related ideas in 3D that could also be applied.
The rgl library in seems to be the go-to for 3D plots in R, but an rgl solution just seems so much more complex and lacks the other benefits of ggplot2, such as faceting by year as in the example, easily adjusting scales, etc.
Example data is here:
I don't understand the format of your data so I'm sure this isn't the display you want, but it shows how to draw a bunch of cylinders in rgl:
root <- read.csv("~/temp/root.csv")
segments <- data.frame(row.names = unique(root$parent.direction),
x = c(-1,0,1,0,0),
y = c(0,1,0,0,-1),
z = c(0,0,0,0.2,0))
library(rgl)
open3d()
for (i in seq_len(nrow(root))) {
rbind(root[i,2:4],
root[i,2:4] - segments[root$parent.direction[i],]) %>%
cylinder3d(radius = root$size[i]^0.3, closed = -2, sides = 20) %>%
shade3d(col = "green")
}
decorate3d()
This gives the following display (rotatable in the original):
You can pass each cylinder through addNormals if you want it to look smooth, or use sides = <some big number> in the cylinder3d to make them look rounder.

Using R: how to animate multiple 3D objects in single rgl canvas?

I have data where a participant had positional data on each hand:
left:
right:
positional data has
pos.x
pos.y
pos.z
at some time t
The initial question was about importing OBJ to work with RGL: https://stackoverflow.com/posts/46626767/revisions
I have figured that part out. I have written functions to improve rgl mesh framework for open-source OBJ files.
I placed the required functions online: https://gist.github.com/MonteShaffer/d142210cddf346c86aeab1ea2d1d7e9d
The positional data should be captured on a wrist watch, so I want to be able to show two or more 3D data objects and animate each independently based on positional/time data.
That is, treat the hand like a rigid object with the wrist-watch region moving appropriately.
I placed the hand.OBJ file online: https://gist.github.com/MonteShaffer/6c0057b1431364caf120220db77dde4b
I am aware of basic graphing, updating, spinning:
library(rgl)
mymesh = buildBasicMeshFromOBJ(parseFileOBJ("hand.OBJ"));
open3d()
shade3d(mymesh, col = "pink")
par3d(userMatrix = rotate3d(par3d("userMatrix"), 0.1, 1,0,0))
play3d(spin3d(axis = c(1, 0, 0)))
My goal is to plot multiple objects on the same rgl canvas.
e.g.,
righthand = mymesh
lefthand = mymesh
head = buildBasicMeshFromOBJ(parseFileOBJ("head.obj"));
and have each element move independently as an animation over time based on positional/time data for each element. For now the head stays still, but each hand moves based on the rigid idea with the position representing the wrist.
It would be a bonus that the mouse drag could still occur (to change 3D views around the hands and head) while the animation is going on.
You can move objects within the scene by using a function like rotate3d. Despite its name, it allows for quite general kinds of movement: see the help page.
So instead of using par3d to move things, move the objects themselves.
rgl doesn't provide a way to modify objects that are already in the scene (though the WebGL display of rgl objects does...), so the basic idea is the following:
Plot the object, and save the ids (e.g. ids <- shade3d(mymesh))
Then, in a loop:
Turn off updates (using par3d(skipRedraw=TRUE))
Delete the object (e.g. rgl.pop(id=ids))
Move and replot the object
Turn on updates.
The play3d function provides a framework to automate this, but you don't need to use it. For example, this plots two icosahedra and randomly rotates them independently:
orig1 <- icosahedron3d()
id1 <- shade3d(orig1, col = "green")
orig2 <- translate3d(orig1, 4, 0, 0)
id2 <- shade3d(orig2, col = "blue")
repeat {
orig1 <- rotate3d(orig1, 0.01, rnorm(1, 1), rnorm(1), rnorm(1))
orig2 <- rotate3d(orig2, 0.05, rnorm(1, -1), rnorm(1), rnorm(1))
par3d(skipRedraw = TRUE)
rgl.pop(id = c(id1, id2))
id1 <- shade3d(orig1, col = "green")
id2 <- shade3d(orig2, col = "blue")
par3d(skipRedraw = FALSE)
}
Another way to do this is to set up two separate subscenes. Then each
can be controlled by its own par3d() setting. For example,
icos <- icosahedron3d()
ids <- mfrow3d(1,2)
shade3d(icos, col = "red")
next3d()
shade3d(icos, col = "green")
Now each of the icosahedra can be manipulated independently with the mouse. If you want to do it with par3d, use something like par3d(..., subscene = ids[1]) to affect the left pane, par3d(..., subscene = ids[2]) for the right pane.

R: Interactive 3D plotly scatterplot rgl-style with hover info

I'm using plotly to draw an interactive 3D scatter plot with spheres, e.g.:
df <- data.frame(x = c(1,3,5), y = c(1,3,5), z = c(1,3,5), size = c(1,3,5))
library(plotly)
plot_ly(df, type = "scatter3d", x = ~x, y = ~y, z = ~z, size = ~size,
marker = list(symbol = 'circle', sizemode = 'diameter'))
The problem is: I hate plotly's spheres. I much prefer rgl's spheres which have a "shine" effect to them, e.g.:
library(rgl)
spheres3d(x = df$x, y = df$y, z = df$z, radius = 0.8 * sqrt(df$size))
And I think there's even a way in which rgl objects can be embedded in web page like plotly objects. However they lack the ability to present data on hover and other attractive capabilities of plotly.
Is there a way to get my plotly spheres to have a "shine" effect and look more like rgl's spheres? Or a way to make rgl plots have some of plotly's features especially hove info?
This should be possible with a lot of Javascript programming, since when it's embedded in a web page, rgl is using Javascript for everything. However, there's no built-in support for reacting to mouse movements like that. Within R (using Windows or X11 to display the scene, not a browser) it's probably harder, but the select3d and identify3d functions do related things (though not event-driven).

spplot/lattice: objects not drawn/overdrawn

I have a grid and I want to produce a map out of this grid with some map elements (scale, north arrow, etc). I have no problem drawing the grid and the coloring I need, but the additional map elements won't show on the map. I tried putting first=TRUE to the sp.layout argument according to the sp manual, but still no success.
I reproduced the issue with the integrated meuse dataset, so you may just copy&paste that code. I use those package versions: lattice_0.20-33 and sp_1.2-0
library(sp)
library(lattice) # required for trellis.par.set():
trellis.par.set(sp.theme()) # sets color ramp to bpy.colors()
alphaChannelSupported = function() {
!is.na(match(names(dev.cur()), c("pdf")))
}
data(meuse)
coordinates(meuse)=~x+y
data(meuse.riv)
library(gstat, pos = match(paste("package", "sp", sep=":"), search()) + 1)
data(meuse.grid)
coordinates(meuse.grid) = ~x+y
gridded(meuse.grid) = TRUE
v.uk = variogram(log(zinc)~sqrt(dist), meuse)
uk.model = fit.variogram(v.uk, vgm(1, "Exp", 300, 1))
meuse[["ff"]] = factor(meuse[["ffreq"]])
meuse.grid[["ff"]] = factor(meuse.grid[["ffreq"]])
zn.uk = krige(log(zinc)~sqrt(dist), meuse, meuse.grid, model = uk.model)
zn.uk[["se"]] = sqrt(zn.uk[["var1.var"]])
meuse.sr = SpatialPolygons(list(Polygons(list(Polygon(meuse.riv)),"meuse.riv")))
rv = list("sp.polygons", meuse.sr, fill = "lightblue")
sampling = list("sp.points", meuse.riv, color = "black")
scale = list("SpatialPolygonsRescale", layout.scale.bar(),
offset = c(180500,329800), scale = 500, fill=c("transparent","black"), which = 4)
text1 = list("sp.text", c(180500,329900), "0", cex = .5, which = 4)
text2 = list("sp.text", c(181000,329900), "500 m", cex = .5, which = 4)
arrow = list("SpatialPolygonsRescale", layout.north.arrow(),
offset = c(181300,329800),
scale = 400, which = 4)
library(RColorBrewer)
library(lattice)
trellis.par.set(sp.theme())
precip.pal <- colorRampPalette(brewer.pal(7, name="Blues"))
spplot(zn.uk, "var1.pred",
sp.layout = list(rv, sampling, scale, text1, text2),
main = "log(zinc); universal kriging standard errors",
col.regions=precip.pal,
contour=TRUE,
col='black',
pretty=TRUE,
scales=list(draw = TRUE),
labels=TRUE)
And that's how it looks...all naked:
So my questions:
Where is the scale bar, north arrow, etc hiding? Did I miss something? Every example I could find on the internet looks similar to that. On my own dataset I can see the scale bar and north arrow being drawn at first, but as soon as the grid is rendered, it superimposes the additional map elements (except for the scale text, that is shown on the map - not the bar and north arrow for some reason I don't seem to comprehend).
The error message appearing on the map just shows when I try to add the sampling locations sampling = list("sp.points", meuse.riv, color = "black"). Without this entry, the map shows without error, but also without additional map elements. How can I show the sampling points on the map (e.g. in circles whose size depends on the absolute value of this sampling point)?
This bothered me for many, many hours by now and I can't find any solution to this. In Bivand et al's textbook (2013) "Applied Spatial Data Analysis with R" I could read the following entry:
The order of items in the sp.layout argument matters; in principle objects
are drawn in the order they appear. By default, when the object of spplot has
points or lines, sp.layout items are drawn before the points to allow grids
and polygons drawn as a background. For grids and polygons, sp.layout
items are drawn afterwards (so the item will not be overdrawn by the grid
and/or polygon). For grids, adding a list element first = TRUE ensures that
the item is drawn before the grid is drawn (e.g. when filled polygons are added). Transparency may help when combining layers; it is available for the
PDF device and several other devices.
Function sp.theme returns a lattice theme that can be useful for plots
made by spplot; use trellis.par.set(sp.theme()) after a device is opened
or changed to make this effective.
However, also with this additional information I wasn't able to solve this problem. Glad for any hint!
The elements you miss are being drawn in panel four, which does not exist, so are not being drawn. Try removing the which = 4.
meuse.riv in your example is a matrix, which causes the error message, but should be a SpatialPoints object, so create sampling by:
sampling = list("sp.points", SpatialPoints(meuse.riv), color = "black")
When working from examples, my advice is to choose examples as close as possible to what you need, and only change one thing at a time.

Annotate ggplot2 graphs using tikzAnnotate in tikzDevice

I would like to use tikzDevice to include annotated ggplot2 graphs in a Latex document.
tikzAnnotate help has an example of how to use it with base graphics, but how to use it with a grid-based plotting package like ggplot2? The challenge seems to be the positioning of the tikz node.
playwith package has a function convertToDevicePixels (http://code.google.com/p/playwith/source/browse/trunk/R/gridwork.R) that seems to be similar to grconvertX/grconvertY, but I am unable to get this to work either.
Would appreciate any pointers on how to proceed.
tikzAnnotate example using base graphics
library(tikzDevice)
library(ggplot2)
options(tikzLatexPackages = c(getOption('tikzLatexPackages'),
"\\usetikzlibrary{shapes.arrows}"))
tikz(standAlone=TRUE)
print(plot(15:20, 5:10))
#print(qplot(15:20, 5:10))
x <- grconvertX(17,,'device')
y <- grconvertY(7,,'device')
#px <- playwith::convertToDevicePixels(17, 7)
#x <- px$x
#y <- px$y
tikzAnnotate(paste('\\node[single arrow,anchor=tip,draw,fill=green] at (',
x,',',y,') {Look over here!};'))
dev.off()
Currently, tikzAnnotate only works with base graphics. When tikzAnnotate was first written, the problem with grid graphics was that we needed a way of specifying the x,y coordinates relative to the absolute lower left corner of the device canvas. grid thinks in terms of viewports and for many cases it seems the final coordinate system of the graphic is not known until it is heading to the device by means of the print function.
It would be great to have this functionality, but I could not figure out a way good way to implement it and so the feature got shelved. If anyone has details on a good implementation, feel free to start a discussion on the mailing list (which now has an alternate portal on Google Groups) and it will get on the TODO list.
Even better, implement the functionality and open a pull request to the project on GitHub. This is guaranteed to get the feature into a release over 9000 times faster than if it sits on my TODO list for months.
Update
I have had some time to work on this, and I have come up with a function for converting grid coordinates in the current viewport to absolute device coordinates:
gridToDevice <- function(x = 0, y = 0, units = 'native') {
# Converts a coordinate pair from the current viewport to an "absolute
# location" measured in device units from the lower left corner. This is done
# by first casting to inches in the current viewport and then using the
# current.transform() matrix to obtain inches in the device canvas.
x <- convertX(unit(x, units), unitTo = 'inches', valueOnly = TRUE)
y <- convertY(unit(y, units), unitTo = 'inches', valueOnly = TRUE)
transCoords <- c(x,y,1) %*% current.transform()
transCoords <- (transCoords / transCoords[3])
return(
# Finally, cast from inches to native device units
c(
grconvertX(transCoords[1], from = 'inches', to ='device'),
grconvertY(transCoords[2], from = 'inches', to ='device')
)
)
}
Using this missing piece, one can use tikzAnnotate to mark up a grid or lattice plot:
require(tikzDevice)
require(grid)
options(tikzLatexPackages = c(getOption('tikzLatexPackages'),
"\\usetikzlibrary{shapes.arrows}"))
tikz(standAlone=TRUE)
xs <- 15:20
ys <- 5:10
pushViewport(plotViewport())
pushViewport(dataViewport(xs,ys))
grobs <- gList(grid.rect(),grid.xaxis(),grid.yaxis(),grid.points(xs, ys))
coords <- gridToDevice(17, 7)
tikzAnnotate(paste('\\node[single arrow,anchor=tip,draw,fill=green,left=1em]',
'at (', coords[1],',',coords[2],') {Look over here!};'))
dev.off()
This gives the following output:
There is still some work to be done, such as:
Creation of a "annotation grob" that can be added to grid graphics.
Determine how to add such an object to a ggplot.
These features are scheduled to appear in release 0.7 of the tikzDevice.
I have made up a small example based on #Andrie's suggestion with geom_text and geom_polygon:
Initializing your data:
df <- structure(list(x = 15:20, y = 5:10), .Names = c("x", "y"), row.names = c(NA, -6L), class = "data.frame")
And the point you are to annotate is the 4th row in the dataset, the text should be: "Look over here!"
point <- df[4,]
ptext <- "Look over here!"
Make a nice arrow calculated from the coords of the point given above:
arrow <- data.frame(
x = c(point$x-0.1, point$x-0.3, point$x-0.3, point$x-2, point$x-2, point$x-0.3, point$x-0.3, point$x-0.1),
y = c(point$y, point$y+0.3, point$y+0.2, point$y+0.2, point$y-0.2, point$y-0.2, point$y-0.3, point$y)
)
And also make some calculations for the position of the text:
ptext <- data.frame(label=ptext, x=point$x-1, y=point$y)
No more to do besides plotting:
ggplot(df, aes(x,y)) + geom_point() + geom_polygon(aes(x,y), data=arrow, fill="green") + geom_text(aes(x, y, label=label), ptext) + theme_bw()
Of course, this is a rather hackish solution, but could be extended:
compute the size of arrow based on the x and y ranges,
compute the position of the text based on the length of the text (or by the real width of the string with textGrob),
define a shape which does not overlaps your points :)
Good luck!

Resources