---
title: "Animating taichi diagrams"
author: |
Youzhi Yu
University of Chicago
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Animating taichi diagrams}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
warning = FALSE,
message = FALSE,
fig.width = 7,
fig.height = 5,
fig.align = "center",
dpi = 120
)
# The gganimate package is in Suggests. When it is not available the code
# chunks below are still displayed (so the recipe is readable) but not
# evaluated, so this vignette builds on systems without gganimate.
has_gganimate <- requireNamespace("gganimate", quietly = TRUE)
knitr::opts_chunk$set(eval = has_gganimate)
library(ggplot2)
library(ggtaichi)
if (has_gganimate) library(gganimate)
set.seed(2026)
```
## Why animate?
A taichi (yin-yang) symbol is *cyclical*, so motion is unusually on-brand for
this geom. Instead of forcing a third variable (e.g. `week`) onto the x-axis
and shrinking every glyph, we can turn it into an **animation frame**. Each
frame is then a clean `category` x `state` grid of large, readable taichi.
This vignette shows how to combine `geom_taichi()` with
[gganimate](https://gganimate.org). `gganimate` is a *Suggested* dependency;
install it with `install.packages("gganimate")` if you have not already.
```r
library(ggplot2)
library(ggtaichi)
library(gganimate)
```
## How `geom_taichi()` composes with gganimate
`geom_taichi()` returns a pair of layers separated by
`ggnewscale::new_scale_fill()`, so each fish keeps its own fill scale and
legend. gganimate works at the *layer* level --- it splits every layer's data
by the transition variable and builds one frame per state. Because the two
fish layers are ordinary `ggplot2` layers, gganimate treats them independently
and the two fill scales continue to apply frame-by-frame. In short: **the
ggnewscale layer stack composes cleanly with gganimate**, so no special
wrapper is needed. (This is not just theory: every recipe below has been
rendered frame-by-frame against gganimate 1.0.11, with both legends and the
per-fish fills intact in every frame.)
## Animating over a third variable
Use `transition_states()` when the frame variable is discrete (e.g. week
number), or `transition_time()` when it is a continuous time. Both drive the
underlying fish layers. The bundled `states_tg` data is a perfect showcase:
instead of squeezing its 30 weeks onto the x-axis, keep a `category` x
`state` grid of large glyphs and let the weeks play out as frames. Because
the fills are continuous, `tweenr` interpolates the values smoothly between
consecutive weeks.
```{r}
p <- ggplot(states_tg, aes(x = category, y = state)) +
geom_taichi(yin = Twitter, yang = Google) +
theme_taichi() +
labs(title = "Week {closest_state}") +
transition_states(week, transition_length = 1, state_length = 1)
# Render to a GIF (requires the gifski package)
# animate(p, renderer = gifski_renderer())
# anim_save("taichi.gif", p)
```
## Keeping glyphs round
Taichi symbols are always drawn round (they are sized in square units, like
`grid::circleGrob()`), but `coord_fixed()` keeps the *cells* square too, so
the glyphs fill them evenly. Match the animation dimensions to the grid:
```{r}
p_fixed <- ggplot(states_tg, aes(x = category, y = state)) +
geom_taichi(yin = Twitter, yang = Google) +
coord_fixed() +
theme_taichi() +
transition_states(week, transition_length = 1, state_length = 1)
# animate(p_fixed, width = 800, height = 600, fps = 10)
```
## Spin animation
With the `angle` argument (see `?geom_taichi`) you can rotate each glyph.
Mapping `angle` to an expression of the frame variable and animating produces
the iconic "turning taichi" --- with `eyes = TRUE` each eye rides around in
its own fish's head:
```{r}
spin <- data.frame(
x = 1, y = 1, yin = 5, yang = 5,
frame = 1:36
)
p_spin <- ggplot(spin, aes(x, y)) +
geom_taichi(yin = yin, yang = yang, angle = frame * 10,
eyes = TRUE) +
coord_fixed() +
theme_taichi() +
transition_states(frame, transition_length = 0, state_length = 1)
# animate(p_spin, width = 300, height = 300, fps = 12)
```
Note the `state_length = 1` with `transition_length = 0`: each frame *is* a
state, so the rotation advances in crisp steps (at least one of the two
lengths must be positive, or gganimate cannot allocate frames). For a
grow-in reveal instead, keep a positive `transition_length` and add
`enter_grow()` to the plot.
## Export helpers
`gganimate::anim_save()` writes the animation to a file. For MP4 output use
`ffmpeg_renderer()` (requires a system `ffmpeg`); for GIF use
`gifski_renderer()` (requires the `gifski` package). Aim for 10--15 fps and
enough frames that the motion is smooth; `coord_fixed()` keeps every glyph
round in the rendered video.