# animation functions
ani.saveqz <- function (list , dpi=150 , location="frames/frame_" , prefix=1000 ) {
require(animation)
if (missing(list))
list = animation:::.ani.env$.images
lapply(1:length(list), function(x) {
dev.hold()
replayPlot( list[[x]] )
ani.pause()
quartz.save( concat(location,prefix+x,".png") , type = "png", device = dev.cur(), dpi = dpi )
})
invisible(NULL)
}
anim_prior_predictive <- function(
prior, # named list samples from prior
linkf, # function (of x) to display
n=20, # number of samples to show
xlim=c(-2,2), # range of x to show
ylim=c(0,1), # y range
n_pts=100, # number of points in each curve
n_steps=10, # number of transition frames between subsequent samples
n_to_show=1, # number of samples to show at once
cols=c(2,4,5,6,3),
alpha=0.7,
xlab="x value",
ylab="probability",
lwd=5,
pf, # optional function to control setting up plot and axes
vel0=1, accel=-1, # spring parameters
start_from, # optional initial parameter values (for transition from previous seq)
do_reset=TRUE # reset animation stack
) {
# function for 1D path acceleration - distance traveled at time t
faccel <- function(t,x0=0,v0=vel0,a=accel) x0 + v0*t + (1/2)*a*t^2
# interpolate between parameter values with deceleration
interp <- function(p1,p2) {
path <- sapply( seq(from=0,to=1,length=n_steps/2+1) , faccel )
path_seq <- normalize(path)
nn <- length(path_seq)
path_seq <- c( 0.5*(1-path_seq)[nn:2] , 0.5*path_seq[1:(nn-1)] + 0.5 )
#the_seq <- seq( from=s1 , to=s2 , length=n_steps )
path_seq <- path_seq*p2 + (1-path_seq)*p1
return(path_seq)
}
if ( missing(pf) )
pf <- function(samp,step) {
plot(NULL,xlim=xlim,ylim=ylim,xlab=xlab,ylab=ylab,bty="n")
#axis(2,at=c(0,0.5,1),labels=c(0,0.5,1))
}
# setup colors
if ( length(cols) < n_to_show ) cols <- rep(cols,length.out=n_to_show)
cols <- sapply( cols , function(f) col.alpha(f,alpha) )
# x values
x <- seq( from=xlim[1] , to=xlim[2] , length.out=n_pts )
require(animation)
ani.record(reset = do_reset) # clear history before recording
# set up data structure for interpolation
prior_blend <- list()
for ( i in 1:length(prior) )
prior_blend[[ names(prior)[i] ]] <- matrix(NA,nrow=n_to_show,ncol=n_steps)
# check for start_from
if ( !missing(start_from) )
if ( !is.null(start_from) ) {
# insert start_from into prior
for ( j in names(prior) )
for ( k in 1:n_to_show ) {
prior[[j]][ 1 + (k-1)*n ] <- start_from[[j]][ k , n_steps ]
}
}
# animation loop
for ( i in 1:(n-1) ) {
# interpolate
for ( j in names(prior) )
for ( k in 1:n_to_show ) {
s1 <- prior[[j]][ i + (k-1)*n ]
s2 <- prior[[j]][ i+1 + (k-1)*n ]
# prior_blend[[j]][k,] <- seq( from=s1 , to=s2 , length=n_steps )
prior_blend[[j]][k,] <- interp( s1 , s2 )
}
for ( j in 1:n_steps ) {
pf(i,j)
for ( k in 1:n_to_show ) {
prior_now <- list()
for ( kk in names(prior) ) prior_now[[kk]] <- prior_blend[[kk]][k,j]
prior_now[['x']] <- x
y <- eval( body(linkf) , env=prior_now )
lines( x , y , lwd=lwd , col=cols[k] )
}
ani.record() # record the current frame
}
}#i
return(prior_blend)
}
#### DAG STUFF ####
# function to draw compound effect dot at particular coordinate
dagfx_dot <- function(x,y,k,r=1,rr=1,angle1=pi/2,lwd=10,angle_gap=pi/5,pch=16,plwd=2) {
# k should be vector of colors
require(plotrix)
n <- length(k)
d <- rep(NA,n)
d[1] <- r
#if ( n > 1 ) for ( i in 2:n ) d[i] <- d[i-1] + rr
if ( n > 1 ) for ( i in 2:n ) d[i] <- d[1] + rr
angle_inc <- 2*pi / (n-1)
#angle1 <- pi/2
#angle_gap <- pi/8
for ( i in n:1 ) {
if ( i==1 ) {
# draw center circle
points( x , y , pch=16 , cex=d[i]*1.2 , col="black" )
points( x , y , pch=pch , cex=d[i] , col=k[i] , lwd=plwd )
} else {
# draw arc
draw.arc( x , y , radius=d[i]/80 , col=k[i] , lwd=lwd , angle1=angle1 + angle_gap , angle2=angle1 + angle_inc - angle_gap )
angle1 <- angle1 + angle_inc
}
}
}
# plot( NULL , xlim=c(-1,1) , ylim=c(-1,1) ); dagfx_dot(0,0,2:4 , r=2 , lwd=8 )
# master function
# starts animation and tracks compound effects through FRONT DOORS
dagfx_anim_forward <- function( the_dag , Y , X , n_frames , n_loops=3 , path_frames=20 , radius=2 , color_list=c(2,4,3,5,6,7) , fade_rate=0.9 , col="white" , bg="black" , xlim , ylim , buffer=c(0,0.4,0,0) , offset_stack=0.12 , arc_lwd=10 , angle_gap=pi/5 , angle1_inc=pi/path_frames , fix="" , skadoosh=TRUE , accel=-0.7 , vel0=1 , force_on="" , scm , ... ) {
# function to extract colors for each dot
dot_palette <- function( parent ) {
# get parent color
the_col <- color_list[ which( uvars == parent ) ]
# append colors of vars that touched parent
if ( any(touched[ parent , ]>0) ) {
j <- which( touched[ parent , ] > 0 )
for ( k in j ) {
a <- touched[ parent , k ]
the_col <- c( the_col , col.alpha( color_list[ k ] , a ) )
}
}
return(the_col)
}
# set frame count
if ( missing(n_frames) & !missing(n_loops) ) {
n_frames <- n_loops * path_frames
}
# get direct animation paths
the_edges <- edges(the_dag)
the_edges <- the_edges[ the_edges[,3]=="->" , 1:3 ]
the_edges[,3] <- the_edges$v
# calculate plot dimensions so there is space for accumulating on outcome
coords <- coordinates(the_dag)
labels <- names(coords$x)
wx <- sapply( paste0("mm",labels),
function(s) strwidth(s,units="inches") )
wy <- sapply( paste0("\n",labels),
function(s) strheight(s,units="inches") )
if ( missing(xlim) )
xlim <- c( min(coords$x-wx/2) , max(coords$x+wx/2) ) + c( -buffer[1] , buffer[2] )
if ( missing(ylim) )
ylim <- c( -max(coords$y+wy/2) , -min(coords$y-wy/2) ) + c( -buffer[3] , buffer[4] )
# draw and collect path coordinates
par(bg = bg)
coo_arrows <- drawdag( the_dag , col_arrow=col , col_segment=col , col_labels=col , xlim=xlim, ylim=ylim )
coo_nodes <- coordinates( the_dag )
# check fix (conditioning) list for colliders
# if any colliders in fix list, need to add new paths for them
# idea is to "reflect" parents of collider to other side of the collider
# so e.g. A -> Z <- B
# when Z is fixed, then A gets a dose of backdoor B and the reverse
is_collider <- function(z) {
# if z is endpoint for more than one edge, then collider
if ( length( which( the_edges$w==z ) ) > 1 ) {
#print( concat( z , " is a collider" ) )
return(TRUE)
}
return(FALSE)
}
if ( !missing(fix) ) {
for ( v in fix )
if ( is_collider(v) ) {
# turn off normal outbound (frontdoor) paths for v
# we can do this instead by not making them "touch"
ew <- which( the_edges$v==v )
#the_edges[ ew , 3 ] <- NA
# add edges to reflect v back on appropriate parents
ew <- which( the_edges$w==v )
collider_parents <- the_edges$v[ew]
for ( w in collider_parents ) {
for ( ww in collider_parents )
if ( ww != w ) {
# add ghost path from collider v to parent w, which transmits opposite parent ww
cur_edg <- nrow(the_edges) + 1
the_edges[ cur_edg , ] <- c( v , w , ww )
# also copy coordinates into coo_arrows
# can copy path w -> v and then reverse it
old_arrow_i <- which( the_edges$v==w & the_edges$w==v )
new_arrow <- coo_arrows[old_arrow_i, c( 3 , 4 , 1 , 2 , 5 ) ]
coo_arrows[ cur_edg , ] <- new_arrow
}
}#w
}
#print( the_edges )
#print( coo_arrows )
}
# need list of which variables have "touched" others so we can draw the dots right
uvars <- unique( c( unique(the_edges[,1]) , unique(the_edges[,2]) ) )
touched <- matrix(0,nrow=length(uvars),ncol=length(uvars))
colnames(touched) <- uvars
rownames(touched) <- uvars
# data frame, for when scm present
dat <- matrix(NA,nrow=n_loops+2,ncol=length(uvars))
colnames(dat) <- uvars
dat_pos <- 0
dat_sampler <- "" # record which parent updates outcome first
# sample variables
for ( i in 1:nrow(dat) ) {
sv <- rep(NA,length(uvars))
names(sv) <- uvars
for ( u in scm ) {
sv[u$var] <- u$f(sv)
dat[i,u$var] <- sv[u$var]
}
}#i
# init state indexes for each variable
dat_index <- rep(1,length(uvars))
names(dat_index) <- uvars
print(dat)
# organize colors
if ( class(color_list)=="list" ) {
# named list, so sort in order of uvars
color_list <- unlist(color_list)[uvars]
}
# positions of each effect
pos_inc <- 1/path_frames
#pos <- rep( -pos_inc , nrow(the_edges) )
pos <- rep( 0 , nrow(the_edges) ) # init position
# matrix with same dims as touched, used to track things that arrive at Y
Ystack <- touched
# path sequence
# function for 1D path acceleration - distance traveled at time t
fa <- function(t,x0=0,v0=vel0,a=accel) x0 + v0*t + (1/2)*a*t^2
path_seq <- seq( from=0 , to=1 , length=path_frames )
if ( skadoosh==TRUE ) {
x <- sapply( path_seq , fa )
path_seq <- normalize(x)
}
path_seq <- rep( path_seq , length=n_frames )
# animation loop
angle1 <- pi/2
#angle1_inc <- pi/path_frames
ani.record(reset = TRUE)
for ( f in 1:n_frames ) {
# draw the dag again
par(bg = bg)
drawdag( the_dag , col_arrow=col , col_segment=col , col_labels=col , xlim=xlim, ylim=ylim )
# draw boxes on fixed variables
if ( !missing(fix) ) {
for ( v in fix ) {
points( coo_nodes$x[v] , -coo_nodes$y[v] , pch=0 , cex=3 , col=col )
}
}
# fade touched
touched <- touched * fade_rate
# for each effect, update dot position and draw
for ( i in 1:nrow(the_edges) ) {
parent <- the_edges$v[i]
child <- the_edges$w[i]
#if ( !missing(fix) ) {
# if ( parent %in% fix & !is_collider(parent) ) {
# # skip this (conditioned mediator or confound)
# next
# }
#}
turned_off <- parent %in% fix # won't touch child
if ( (
length(parents(the_dag,parent))==0 |
any(touched[parent,]>0) |
parent %in% force_on
) ) {
#pos[i] <- pos[i] + pos_inc
pos[i] <- pos[i] + 1
if ( path_seq[pos[i]] == 1 ) {
# path end!
# run into descendant and pass on our color
# w is descendant, v is parent
# need to check e column of the_edges to know which var does the touching
# update data index tracker
dat_index[ parent ] <- dat_index[ parent ] + 1
if ( !missing(fix) )
if ( !(the_edges$e[i] %in% fix) )
touched[ the_edges$w[i] , the_edges$e[i] ] <- 1
# reset position to origin
#pos[i] <- (-pos_inc)
# if path end is the Y (monitored outcome),
# then stack
if ( !missing(Y) ) {
if ( Y == child ) {
Ystack[parent,] <- touched[parent,]
Ystack[parent,parent] <- 1
if ( !missing(scm) & ( dat_sampler==parent | dat_sampler=="" ) ) {
# update counter for number of rows to display in stack
dat_pos <- dat_pos + 1
dat_sampler <- parent
}#scm
}#Y child
}#Y end
}#path end
# interpolate position along path
px <- (1-path_seq[pos[i]])*coo_arrows[i,1] + path_seq[pos[i]]*coo_arrows[i,3]
py <- (1-path_seq[pos[i]])*coo_arrows[i,2] + path_seq[pos[i]]*coo_arrows[i,4]
# get parent color
the_col <- dot_palette( the_edges$e[i] )
#the_col <- color_list[ which( uvars == parent ) ]
# append colors of vars that touched parent
#if ( any(touched[ parent , ]>0) ) {
# j <- which( touched[ parent , ] > 0 )
# for ( k in j ) {
# a <- touched[ parent , k ]
# the_col <- c( the_col , col.alpha( color_list[ k ] , a ) )
# }
#}
# draw dot
dot_type <- 16
if ( !missing(scm) ) {
# color by state
#dot_idx <- dat[ dat_index[parent] , parent ]
dot_idx <- floor( f / path_frames ) + 1
dot_idx <- dat[ dot_idx , parent ]
dot_type <- c(1,16)[dot_idx+1]
}
dagfx_dot( px , -py , the_col , r=radius , angle1=angle1 , lwd=arc_lwd , angle_gap=angle_gap , pch=dot_type )
}
}#i
# draw the Ystack
if ( !missing(Y) ) {
pos_stack <- 1 # position in visual stack
for ( v in rownames(Ystack) ) {
if ( v != Y ) {
if ( Ystack[v,v] == 1 ) {
# hit, so display
px <- coo_nodes$x[Y]
py <- coo_nodes$y[Y]
# get parent color
the_col <- dot_palette( v )
# draw dot
j <- ifelse( length(the_col) > 1 , 1 , 0 )
px <- px + offset_stack*pos_stack + 0.03*j
dagfx_dot( px , -py , the_col , r=radius , angle1=angle1 , lwd=arc_lwd , angle_gap=angle_gap )
# draw label above
py <- -py + 0.12
text( px , py , v , col=col )
# update position
pos_stack <- pos_stack + 1 + 0.4*j
}
}
}#i
}
angle1 <- angle1 + angle1_inc
# record frame
ani.record()
}#f
# print(coo_arrows)
# print(touched)
# print(Ystack)
return(invisible(dat))
}
#######
# DAG animations using backdoor paths
dagfx_anim_backward <- function( the_dag , Y , X , n_frames , n_loops=3 , path_frames=20 , radius=2 , color_list=c(2,4,3,5,6,7) , col="white" , bg="black" , xlim , ylim , buffer=c(0,0.4,0,0) , fix="" , skadoosh=TRUE , accel=-0.7 , vel0=1 , force_on="" , ... ) {
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.