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

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.

Related

Highlight a Specific Section of a Treemap using Treemap package in R

I am using the Treemap package in R to highlight the number of COVID outbreaks in different settings. I am making a number of different reports using R Markdown. Each one describes a different type of settings and I would like to highlight that setting in the treemap for each report, showing what proportion of total outbreaks occur in the setting in question. For example you I am currently working on the K-12 school report and would like to highlight the box representing that category in the figure.
I was previously using an exploded donut pie chart however there were two many subcategories and the graph became hard to read.
I am picturing a way to change the label or border on one specific box, ie. put a yellow border around the box or make the label yellow. I found a way to do both these things for all the boxes but not just one specific box. I made this image using the snipping tool to further illustrate what the desired outcome might look like. The code to generate the treemap can be found in the link below. It looks like this:
# library
library(treemap)
# Build Dataset
group <- c(rep("group-1",4),rep("group-2",2),rep("group-3",3))
subgroup <- paste("subgroup" , c(1,2,3,4,1,2,1,2,3), sep="-")
value <- c(13,5,22,12,11,7,3,1,23)
data <- data.frame(group,subgroup,value)
# treemap
treemap(data,
index=c("group","subgroup"),
vSize="value",
type="index"
)
This is the most straightforward information I can find about the package, this is where I took the sample image and code from: https://www.r-graph-gallery.com/236-custom-your-treemap.html
It looks like the treemap package doesn't have a built-in way to do this. But we can hack it by using the data frame returned by treemap() and adding a rectangle to the appropriate viewport.
# Plot the treemap and save the data used for plotting.
t = treemap(data,
index = c("group", "subgroup"),
vSize = "value",
type = "index"
)
# Add a rectangle around subgroup-2.
library(grid)
library(dplyr)
with(
# t$tm is a data frame with one row per rectangle. Filter to the group we
# want to highlight.
t$tm %>%
filter(group == "group-1",
subgroup == "subgroup-2"),
{
# Use grid.rect to add a rectangle on top of the treemap.
grid.rect(x = x0 + (w / 2),
y = y0 + (h / 2),
width = w,
height = h,
gp = gpar(col = "yellow", fill = NA, lwd = 4),
vp = "data")
}
)

How to specify order of glyph layers in rbokeh?

I am trying to build a network visualisation using the rbokeh package.
library(igraph)
library(rbokeh)
library(dplyr)
g <- random.graph.game(n=100,p=0.3)
L <- as.data.frame(igraph::layout_with_fr(g)) %>% rename(x=V1,y=V2)
url1 <- 'http://icons.veryicon.com/png/Business/Flat%20Finance/person.png'
p <- figure(xlab = "x", ylab = "y", height = 500,width=1000,xgrid=F,ygrid=F,webgl = T,
xaxes = F,yaxes = F,h_symmetry = T,v_symmetry = T) %>%
ly_lines(x = L$x,y=L$y,color = '#FFA700', width = 4, alpha = 0.2) %>%
ly_image_url(x = L$x, y=L$y, image_url = url1, w = rep(0.1,vcount(g)), h=rep(0.2,vcount(g)),
anchor = "center",lname = 'nodes')
The resulting visualisation looks as intended except for the fact that the lines are drawn on top of the image glyphs. Is there a way to control the visual order of the layers in a way that the nodes (images) are drawn on top with lines drawn behind?
The issue here is using webgl = TRUE. For more details, see the Bokeh documentation (see both the "support" and "notes" sections. The main points there are that not all glyphs can be rendered with WebGL (yes for lines, no for images) and that "Glyphs drawn using WebGL are drawn on top of glyphs that are not drawn in WebGL."
If you get rid of webgl = TRUE, you should be in good shape!
Also, to further answer the overall question of how to control the order of layers, the answer is that outside of edge cases like this where one layer was rendered with WebGL and the other wasn't, layers are drawn in the order that they are specified.

create a video from plotly 3d chart

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...

How do I exclude parameters from an RDA plot

I'm still relatively inexperienced manipulating plots in R, and am in need of assistance. I ran a redundancy analysis in R using the rda() function, but now I need to simplify the figure to exclude unnecessary information. The code I'm currently using is:
abio1516<-read.csv("1516 descriptors.csv")
attach(abio1516)
bio1516<-read.csv("1516habund.csv")
attach(bio1516)
rda1516<-rda(bio1516[,2:18],abio1516[,2:6])
anova(rda1516)
RsquareAdj(rda1516)
summary(rda1516)
varpart(bio1516[,2:18],~Distance_to_source,~Depth, ~Veg._cover, ~Surface_area,data=abio1516)
plot(rda1516,bty="n",xaxt="n",yaxt="n",main="1516; P=, R^2=",
ylab="Driven by , Var explained=",xlab="Driven by , Var explained=")
The produced plot looks like this:
Please help me modify my code to: exclude the sites (sit#), all axes, and the internal dashed lines.
I'd also like to either expand the size of the field, or move the vector labels to all fit in the plotting field.
updated as per responses, working code below this point
plot(rda,bty="n",xaxt="n",yaxt="n",type="n",main="xxx",ylab="xxx",xlab="xxx
Overall best:xxx")
abline(h=0,v=0,col="white",lwd=3)
points(rda,display="species",col="blue")
points(rda,display="cn",col="black")
text(rda,display="cn",col="black")
Start by plotting the rda with type = "n" which generates an empty plot to which you can add the things you want. The dotted lines are hard coded into the plot.cca function, so you need either make your own version, or use abline to hide them (then use box to cover up the holes in the axes).
require(vegan)
data(dune, dune.env)
rda1516 <- rda(dune~., data = dune.env)
plot(rda1516, type = "n")
abline(h = 0, v = 0, col = "white", lwd = 3)
box()
points(rda1516, display = "species")
points(rda1516, display = "cn", col = "blue")
text(rda1516, display = "cn", col = "blue")
If the text labels are not in the correct position, you can use the argument pos to move them (make a vector as long as the number of arrows you have with the integers 1 - 4 to move the label down, left, up, or right. (there might be better solutions to this)

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.

Resources