Constrain axis limits in chordDiagram (circlize) when making gifs

102 views Asked by At

I hope somebody will be able to help me with this chordDiagram visualisation I am trying to create. I am well aware that maybe this visualization type was not suitable for this particular data, but somehow it was something I had in my head (or how I wanted to visualize this data) and what I wanted to create, and now I think it is too late to give it up :) too curious how one can fix it. It is my first real post here, though I am an active user of stackoverflow and I genuinely admire the audience here.

So I have this data on the change in the size of area in km2 over time (d0) and I am trying to create a GIF out of it using example here: https://guyabel.com/post/animated-directional-chord-diagrams/

The data "d0":

Time <- as.numeric(c(10,10,10,100,100,100,200,200,200,5,5,5,50,50,50,0,0,0))
Year <- as.character(c(2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200))
Area_km2 <- as.numeric(c(4.3075211,7.1672926,17.2780622,5.9099250,8.2909189,16.9748961,6.5400554,8.9036313,16.5627228,3.0765610,6.3929883,18.0708108,5.3520782,8.4503856,16.7938196,0.5565978,1.8415855,12.5089476))

(d0 <- as.data.frame(cbind(Time,Year,Area_km2)))

I also have the color codes stored in a separate dataframe (d1) following the above mentioned example. The data "d1":

year <- as.numeric(c(2050,2100,2200))
order1 <- as.character(c(1,2,3))
col1 <- c("#40A4D8","#33BEB7","#0C5BCE")

(d1 <- as.data.frame(cbind(year,order1,col1)))

So the idea was to have self-linking flows within each sector increasing in size over time, which will look like just growing segments in a final animated GIF (or like growing pie segments), but I noticed that regardless how hard I try I can't seem to manage to constrain the axis of each segment to limits of that particular year in an every single frame. It seems that the axis is being added on and keeps on adding over time, which is not what I want.

Like for example in the first figure (figure0) or "starting frame" the size of the links matches well the dataframe:

figure0

So it is

orig_year Area_km2 .frame
2050 0.557 0
2100 1.84 0
2200 12.5 0

But when one plots next figure (figure1), the axis seems to have taken the values from the starting frame and added on the current values (4, 7.4 and 19 respectively) instead of (3.08, 6.39 and 18.1) or what should have been the values according the data frame:

figure1

orig_year Area_km2 .frame
2050 3.08 1
2100 6.39 1
2200 18.1 1

And it keep on doing so as one loops through the data and creates new plots for the next frames. I wonder whether it is possible to constrain the axis and create the visualization in a way that the links just gradually increase over time and the axis is, so to say, following the increase or does also increase gradually following the data???

Any help is highly appreciated! Thanks.

My code:

Sort decreasing

(d0 <- arrange(d0,Time))

Copy columns

(d0$Dest_year <- d0$Year)

Re-arrange data

library(tweenr)
(d2 <- d0 %>%
mutate(corridor=paste(Year,Dest_year,sep="->")) %>%
dplyr::select(Time,corridor,Area_km2) %>%
mutate(ease="linear") %>%
tweenr::tween_elements('Time','corridor','ease',nframes=30) %>%
tibble::as_tibble())

(d2 <- d2 %>%
separate(col=.group,into=c("orig_year","dest_year"),sep="->") %>%
dplyr::select(orig_year,dest_year,Area_km2,everything()))

d2$Time <- NULL

Create a directory to store the individual plots

dir.create("./plot-gif/")

Fixing scales

scale_gap <- function(Area_km2_m,Area_km2_max,gap_at_max=1,gaps=NULL) {
p <- Area_km2_m/Area_km2_max
if(length(gap_at_max)==1 & !is.null(gaps)) {
gap_at_max <- rep(gap_at_max,gaps)
}
gap_degree <- (360-sum(gap_at_max))*(1-p)
gap_m <- (gap_degree + sum(gap_at_max))/gaps
return(gap_m)
}

Function to derive the size of gaps in each frame for an animated GIF

(d3 <- d2 %>% group_by(orig_year) %>% mutate(gaps=scale_gap(Area_km2_m=Area_km2,Area_km2_max=max(.$Area_km2),gap_at_max=4,gaps=9)))

library(magrittr)

Get the values for axis limits

(axmax <- d2 %>% group_by(orig_year,.frame) %>% mutate(max=mean(Area_km2)))

Creating unique chordDiagrams for each frame

library(circlize)
for(f in unique(d2$.frame)){
png(file=paste0("./plot-gif/figure",f,".png"),height=7,width=7,units="in",res=500)
circos.clear()
par(mar=rep(0,4),cex=1)
circos.par(start.degree=90,track.margin=c(-0.1,0.1),
gap.degree=filter(d3,.frame==f)$gaps,
points.overflow.warning=FALSE)
chordDiagram(x=filter(d2,.frame==f),directional=2,order=d1$year,
grid.col=d1$col1,annotationTrack=c("grid","name","axis"),
transparency=0.25,annotationTrackHeight=c(0.05,0.1),
direction.type=c("diffHeight"),
diffHeight=-0.04,link.sort=TRUE,
xmax=axmax$max)
dev.off()
}
    

Now make a GIF

library(magick)
img <- image_read(path="./plot-gif/figure0.png")
for(f in unique(d2$.frame)[-1]){
img0 <- image_read(path=paste0("./plot-gif/figure",f,".png"))
img <- c(img,img0)
message(f)
}
img1 <- image_scale(image=img,geometry="720x720")
ani0 <- image_animate(image=img1,fps=10)
image_write(image=ani0,path="./plot-gif/figure.gif")
1

There are 1 answers

0
Zuguang Gu On

I will start with your d0 object. I first construct the d0 object but I do not convert everything to characters, just put them as the original numeric format. Also I reorder d0 by Time and Year:

Time = c(10,10,10,100,100,100,200,200,200,5,5,5,50,50,50,0,0,0)
Year = c(2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200,2050,2100,2200)
Area_km2 = c(4.3075211,7.1672926,17.2780622,5.9099250,8.2909189,16.9748961,6.5400554,8.9036313,16.5627228,3.0765610,6.3929883,18.0708108,5.3520782,8.4503856,16.7938196,0.5565978,1.8415855,12.5089476)

d0 = data.frame(Time = Time,
    Year = Year,
    Area_km2 = Area_km2,
    Dest_year = Year)

d0 = d0[order(d0$Time, d0$Year), ]

The key thing is to calculate proper values for "gaps" between sectors so that the same unit from data corresponds to the same degree in different plots.

We first calculate the maximal total width of the circular plot:

width = tapply(d0$Area_km2, d0$Time, sum)
max_width = max(width)

We assume there are n sectors (where n = 3 in d0). We let the first n-1 gaps to be 2 degrees and we dynamically adjust the last gap according to the total amount of values in each plot. For the plot with the largest total value, the last gap is also set to 2 degrees.

n = 3
degree_per_unit = (360 - n*2)/max_width

Now degree_per_unit can be shared between multiple plots. Every time we calculate the value for last_gap:

for(t in sort(unique(Time))) {
    l = d0$Time == t
    
    d0_current = d0[l, c("Year", "Dest_year", "Area_km2")]

    last_gap = 360 - (n-1)*2 - sum(d0_current$Area_km2)*degree_per_unit

    circos.par(gap.after = c(rep(2, n-1), last_gap))
    chordDiagram(d0_current, grid.col = c("2050" = "red", "2100" = "blue", "2200" = "green"))
    circos.clear()

    title(paste0("Time = ", t, ", Sum = ", sum(d0_current$Area_km2)))

    Sys.sleep(1)
}

enter image description here