--- title: "Introduction to ggtaichi" author: | Youzhi Yu
University of Chicago output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to ggtaichi} %\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 = 7, fig.align = "center", dpi = 120 ) ``` ## Why taichi? A heat map drawn with `ggplot2::geom_tile()` carries three dimensions of information: the `x` position, the `y` position, and a single value mapped to fill. That is plenty when there is one number per cell, but it forces you to *facet* (or to draw two separate maps) the moment you want to compare two data sources on the same footing. `ggtaichi` removes that limitation by replacing each cell with a **taichi** (yin-yang) diagram. The symbol is a circle split by an S-curve into two interlocking "fish": - the **yang** (light) fish is shaded by one data source, and - the **yin** (dark) fish is shaded by the other. Because both fish live in the same cell, a single `geom_taichi()` layer encodes **four** dimensions at once: `x`, `y`, `yin`, and `yang`. The two sources keep their own color scales and legends, so they can be read independently while still being compared side by side. By default there are no decorative eyes or markers -- every drop of ink on the plot is mapped to data -- and when you do switch the classic eyes on (`eyes = TRUE`, new in v0.2.0), they can carry data too, taking a single glyph up to **six** dimensions. ```{r} library(ggtaichi) library(ggplot2) ``` ## Reading a single symbol It is worth zooming in on one cell to see the anatomy of the glyph. The yang fish (its bulb at the bottom) carries one source; the yin fish (its bulb at the top) carries the other. Each half is filled by its own gradient, so a lighter or darker shade is a smaller or larger value. ```{r, fig.width = 4.5, fig.height = 4.5, fig.alt="A single large taichi diagram, its red yang fish reading a high value and its grey yin fish a low value."} one <- data.frame(x = 1, y = 1, google = 7, twitter = 3) ggplot(one, aes(x, y)) + geom_taichi(yin = twitter, yang = google) + coord_fixed() + theme_taichi() ``` Here the yang (red) fish reads `7` and the yin (grey) fish reads `3`; the deeper the ink, the larger the number relative to the rest of the data. ## The example data `ggtaichi` ships with the same data sets used by its foundational package `ggDoubleHeat`. `pitts_tg` records the 30-week COVID-related Google and Twitter incidence rates for 9 categories in the Pittsburgh Metropolitan Statistical Area (MSA). ```{r} head(pitts_tg) ``` `states_tg` is the larger sibling, repeating the same measurements across four states, and `pitts_emojis` holds the most popular weekly emoji per category. Since v0.2.0 the package also bundles `cafes_tg`, a small *synthetic* espresso-vs-matcha dataset whose two columns share the same units — handy for the shared-scale features shown later. See `?pitts_tg`, `?states_tg`, `?pitts_emojis`, and `?cafes_tg` for the full descriptions. ## A first taichi grid The two value columns are passed to the `yin` and `yang` arguments. Everything else -- the `x`/`y` mapping, faceting, titles -- is plain `ggplot2`. The legend titles default to the column names you supplied (`Twitter` and `Google` here). ```{r, fig.height = 6, fig.alt="A full 30-week by 9-category grid of taichi diagrams for Pittsburgh, red yang fish for Google and grey yin fish for Twitter."} ggplot(pitts_tg, aes(x = week, y = category)) + geom_taichi(yin = Twitter, yang = Google) + theme_taichi() + ggtitle("Pittsburgh Google & Twitter Incidence Rate (%)") ``` Each symbol stays round regardless of the panel's aspect ratio, so you do **not** need `coord_fixed()`. The shape is sized in square units, like the radius of a `grid::circleGrob()`. ## Fewer cells, bigger glyphs Thirty weeks across nine categories is a lot of ink in one panel. When the goal is to *read* individual symbols rather than scan an overall texture, subset the data: fewer cells means each taichi is drawn larger. ```{r, fig.height = 8, fig.alt="A six-week Pittsburgh grid of taichi diagrams drawn large enough to read each fish clearly."} pitts_small <- subset(pitts_tg, week <= 6) ggplot(pitts_small, aes(x = week, y = category)) + geom_taichi(yin = Twitter, yang = Google) + theme_taichi() + ggtitle("The first six weeks, drawn large") ``` ## Which source should be yin? `yin` defaults to a grey (luminance) ramp and `yang` to a red ramp, echoing the "ink and seal" look of a classic taichi. The choice is yours, but a useful rule of thumb is to put the source you want to read as *intensity* on `yin` (the eye reads darkness quickly) and the source you want to read as *warmth* on `yang`. ## Customizing the color scales Each fish gets its own scale. `yang_colors` and `yin_colors` accept any color vector (usually hex codes), and `yang_name` / `yin_name` relabel the legends. Any extra argument in `...` is forwarded to *both* auto-built fill scales, so you can, for example, set common `limits` so the two legends share a range, or pass an `na.value`. When the two fish need *different* scale options -- or an entirely different scale type -- hand a scale object or constructor to `yin_scale` / `yang_scale` and it is used verbatim. ```{r, fig.height = 8, fig.alt="The six-week Pittsburgh grid of taichi diagrams with a blue gradient for Twitter and an orange gradient for Google."} ggplot(pitts_small, aes(x = week, y = category)) + geom_taichi( yin = Twitter, yin_name = "Twitter (%)", yin_colors = c("#deebf7", "#3182bd", "#08306b"), yang = Google, yang_name = "Google (%)", yang_colors = c("#fee6ce", "#e6550d", "#7f2704") ) + theme_taichi() ``` ## Removing the panel padding `ggplot2` leaves a margin around discrete and continuous scales, which can make a taichi grid look like it is floating. `remove_padding()` trims it — as of v0.2.0 it detects each axis's scale type by itself, and you can still spell it out with `"c"` (continuous) / `"d"` (discrete) when you want to override the detection. ```{r, fig.height = 8, fig.alt="The six-week Pittsburgh taichi grid with the surrounding panel padding removed so the symbols reach the plot edges."} ggplot(pitts_small, aes(x = week, y = category)) + geom_taichi(yin = Twitter, yang = Google) + remove_padding() + theme_taichi() ``` ## Comparing places with facets Because `geom_taichi()` is an ordinary layer, faceting works out of the box. The `states_tg` data set carries the same measurements across four states; pairing two of them over a few weeks keeps every glyph large and legible. ```{r, fig.height = 8, fig.alt="Two faceted taichi grids comparing New York and Texas over six weeks, red yang fish for Google and grey yin fish for Twitter."} two_states <- subset(states_tg, state %in% c("New York", "Texas") & week <= 6) ggplot(two_states, aes(x = week, y = category)) + geom_taichi(yin = Twitter, yang = Google) + facet_wrap(~ state, ncol = 1) + remove_padding(x = "c", y = "d") + theme_taichi() + ggtitle("New York vs Texas, weeks 1-6") ``` ## Theming `theme_taichi()` is a light, off-white companion theme that bottoms the legends, drops the panel grid and ticks, and emphasizes the axis labels. It is a normal `ggplot2` theme, so you can override any element afterwards, or skip it entirely and bring your own. ```{r, fig.height = 6, fig.alt="The six-week Pittsburgh taichi grid using theme_taichi() with its off-white background overridden to plain white."} ggplot(pitts_small, aes(x = week, y = category)) + geom_taichi(yin = Twitter, yang = Google) + theme_taichi() + theme(plot.background = element_rect(fill = "white")) + ggtitle("theme_taichi(), then tweaked") ``` ## New in v0.2.0 ### Rotation The `angle` argument rotates each glyph by the given number of degrees. It can be a constant (same angle for every cell) or a column name (one angle per cell), encoding a directional or temporal variable as orientation. ```{r, fig.height = 4, fig.alt="Four taichi diagrams with rotation angles 0, 45, 90, and 180 drawn from a data column."} one_rot <- data.frame( x = c(1, 2, 1, 2), y = c(2, 2, 1, 1), yin = c(3, 5, 7, 9), yang = c(9, 7, 5, 3), rot = c(0, 45, 90, 180) ) ggplot(one_rot, aes(x, y)) + geom_taichi(yin = yin, yang = yang, angle = rot, limits = c(0, 10)) + coord_fixed() + theme_taichi() ``` ### Data-driven eyes Setting `eyes = TRUE` draws the classic taichi dots, each sitting in its own fish's head: the yin eye in the top bulb, the yang eye in the bottom one. With the default white and black dots the glyph looks exactly like the traditional symbol. ```{r, fig.height = 4, fig.alt="Four taichi diagrams with the classic white and black eyes enabled."} one_eye <- data.frame( x = c(1, 2, 1, 2), y = c(2, 2, 1, 1), yin = c(3, 5, 7, 9), yang = c(9, 7, 5, 3) ) ggplot(one_eye, aes(x, y)) + geom_taichi(yin = yin, yang = yang, eyes = TRUE, limits = c(0, 10)) + # shared limits keep the palest fish visible coord_fixed() + theme_taichi() ``` The eyes are not just decoration: `yin_eye_size`, `yang_eye_size`, `yin_eye_colour`, and `yang_eye_colour` all accept either a constant *or an unquoted column name*, so the two dots can encode up to two further variables -- a **fifth and sixth dimension** on top of `x`, `y`, and the two fills. A mapped size column is rescaled to eye radii between 5% and 30% of the glyph radius (values already between 0 and 0.5 are used as exact proportions, and an `NA` suppresses the eye for that cell). ```{r, fig.height = 4, fig.alt="Four taichi diagrams whose eye sizes vary from cell to cell, encoding two extra variables."} one_eye$reach <- c(10, 40, 25, 5) # drives the yin eye one_eye$quality <- c(2, 1, 4, 8) # drives the yang eye ggplot(one_eye, aes(x, y)) + geom_taichi(yin = yin, yang = yang, eyes = TRUE, yin_eye_size = reach, yang_eye_size = quality, limits = c(0, 10)) + coord_fixed() + theme_taichi() ``` ### Categorical fills `geom_taichi()` now automatically detects whether the `yin` / `yang` columns are numeric or discrete (factor / character / logical) and picks the appropriate scale -- computed expressions such as `factor(week)` work too. With the default palettes the discrete colors are sampled from the ramp skipping its palest end, so every category stays visible. ```{r, fig.height = 4, fig.alt="Taichi grid with discrete category fills: methods A to C on the yin fish and win or loss on the yang fish."} disc <- data.frame( x = c(1, 2, 1, 2), y = c(2, 2, 1, 1), method = factor(c("A", "B", "C", "A")), outcome = factor(c("win", "loss", "win", "loss")) ) ggplot(disc, aes(x, y)) + geom_taichi(yin = method, yang = outcome) + coord_fixed() + theme_taichi() ``` For full control, hand any fill scale -- an object or a constructor function -- to `yin_scale` / `yang_scale`; it overrides the auto-detection and the `*_colors` vectors entirely: ```{r, fig.height = 4, fig.alt="The same discrete taichi grid drawn with viridis palettes supplied through yin_scale and yang_scale."} ggplot(disc, aes(x, y)) + geom_taichi(yin = method, yang = outcome, yin_scale = scale_fill_viridis_d, yang_scale = scale_fill_viridis_d(name = "outcome", option = "rocket", begin = 0.4, end = 0.8)) + coord_fixed() + theme_taichi() ``` ### Missing values A fish whose fill value is `NA` is painted in its scale's `na.value` colour (grey by default; pass e.g. `na.value = "transparent"` through `...` to hide it), so one missing source never suppresses the other fish. `na.rm = TRUE` additionally drops rows with missing positions, and an `NA` eye size simply skips that cell's eye. ### Geom parameter routing All standard geom parameters (`alpha`, `colour`, `linewidth`, `linetype`, `width`, `height`, `na.rm`, `show.legend`) are now properly accepted by `geom_taichi()` and forwarded to the underlying fish geoms. The deprecated `size` aesthetic has been replaced with `linewidth`. ```{r, fig.height = 4, fig.alt="Taichi diagrams with custom linewidth, alpha, and colour."} one_lwd <- data.frame( x = c(1, 2, 1, 2), y = c(2, 2, 1, 1), yin = c(3, 5, 7, 9), yang = c(9, 7, 5, 3) ) ggplot(one_lwd, aes(x, y)) + geom_taichi(yin = yin, yang = yang, alpha = 0.7, linewidth = 1.5, colour = "#333333") + coord_fixed() + theme_taichi() ``` ### Shared limits and a single legend When the two sources are measured in the same units, two separate legends are noise. `shared_limits = TRUE` aligns the limits of both fill scales (the union range of the two columns, or the union of levels for two discrete sources), so equal values carry equal ink. `shared_legend = TRUE` goes further: both fish use the yin palette and only one legend is shown. The synthetic `cafes_tg` data is the natural demo — espresso and matcha orders per 100 customers: ```{r, fig.height = 6, fig.alt="A 12-week by 8-neighbourhood taichi grid of espresso versus matcha orders sharing one grey fill scale and a single legend."} ggplot(cafes_tg, aes(x = week, y = neighbourhood)) + geom_taichi(yin = matcha, yang = espresso, shared_legend = TRUE, yin_name = "orders / 100 customers") + remove_padding() + theme_taichi() + ggtitle("Espresso (yang) vs matcha (yin)") ``` For diverging data (values around 0), pass a diverging palette to both color arguments and symmetric limits through `...`, e.g. `limits = c(-5, 5)` — both fish then hinge on the same midpoint. ### The fish geoms are exported `geom_yin_fish()` and `geom_yang_fish()` — the layers `geom_taichi()` is built from — are now exported and documented. Reach for them when you want one fish only, or full manual control over scales and `ggnewscale::new_scale_fill()` stacking. See `?geom_yin_fish`. ### Faster rendering All cells of a layer are now drawn as one batched polygon (and one batch of eye dots) resolved at draw time, instead of one grob stack per cell. Large grids and animation frames render several times faster, and glyphs stay perfectly round when you resize the device. ## When (not) to use taichi A taichi grid is at its best when *comparing two sources cell by cell* is the question — the interlocking fish put both numbers in one glance. A few honest caveats: - **Dense grids become texture.** Past roughly a thousand cells you stop reading symbols and start reading fields; that is still useful for spotting bands and regime changes, but for precise lookup, subset (as done above) or facet. - **Luminance is a coarse channel.** Small differences in a fish's shade are hard to judge; when exact comparison matters, add shared limits (`shared_limits = TRUE`) so at least the two fish are on the same footing, and consider printing the numbers alongside. - **Color-vision deficiency.** The default grey ramp is luminance-only and safe, and the default red ramp varies strongly in luminance as well. For fully colorblind-safe plots, supply viridis scales: `yin_scale = ggplot2::scale_fill_viridis_c` (and a second option like `"magma"` for yang) — every figure in this vignette can be redrawn that way with one argument per fish. - **One source missing?** An `NA` fish keeps its place (painted in `na.value`), so absence is visible rather than silently dropped. ## Acknowledgement `ggtaichi` stands on the shoulders of the [`ggDoubleHeat`](https://CRAN.R-project.org/package=ggDoubleHeat) package, which pioneered the two-source "double" heat map through its `geom_heat_*()` family and supplies the example data used throughout this vignette. Please cite it alongside `ggtaichi`: > Yu Y, Buskirk T (2025). *ggDoubleHeat: A Heatmap-Like Visualization Tool*. > R package version 0.1.3. CRAN: > , GitHub: >