ggtaichi 0.3.0
This release closes the structural gap in the design and brings the package up to date with ggplot2 4.x. A taichi grid is a superposition comparison: it shows two sources in one position, which is what makes spatial patterns directly comparable, and which is also why it can say “which is bigger here?” but not “by how much?”. 0.3.0 answers the second question three ways — as a third channel of the glyph, as a companion heatmap, and as a table — and adds the two things a colour-encoded chart needs to be trustworthy: an interactive route to the exact values, and a way to check that its two colour ramps are a fair pair.
Nothing about the default appearance changes.
Explicit encoding: the relationship, not just the two levels
-
explicit =computes a third channel from the two sources —"difference","ratio","log_ratio"or"z"— andexplicit_channel =decides where it goes:-
"eye_size"(the default) puts the gap in the eyes, which already exist and are visually subordinate to the fills. Cells where the two sources agree exactly get no eye, so a plain glyph means agreement. -
"angle"puts it in the glyph’s tilt. Direction is read far more accurately than shading, so this is the most precise of the four: upright means the sources agree, and the lean shows which way and how far. -
"border"puts it in the outline width, and"radius"in the glyph’s size, scaled by area (radius proportional to the square root of the statistic) rather than by diameter.explicit_range =sets the output range. The statistic is rescaled across the whole layer, so facets stay comparable, and driving the same channel by hand as well is an error rather than a silent override.
-
-
geom_taichi_diff()draws the same statistic as a diverging heatmap, with limits symmetric about “the two sources agree”. Sometimes the right chart for “how much bigger?” is not a glyph, and the package would rather say so than insist. -
taichi_summary()returns the numbers per cell — both values, the difference, the ratio, the log ratio, the standardised difference, which source dominates, and the cell’s rank by the size of the gap. - A ratio of a zero or negative value is
NAwith a warning, neverInf, in all three.
Interactivity
-
interactive = TRUEmakes the fish and their eyes ggiraph grobs, soggiraph::girafe()turns the plot into a widget. This matters more than convenience: fill is the least accurate channel there is, and hovering is how a colour-encoded chart supplies exact values without abandoning its encoding. The default tooltip carries both values, their difference and the cell’s coordinates. -
data_id_by =decides what a hover highlights:"cell"(both fish of one glyph, the default),"fish", or"source"— which lights up every fish of one source at once, temporarily turning the superposition display into a single-source one. That is the one thing a static superposition cannot do. -
tooltip,data_idandonclicktake a data column to override any of it. - The static path is untouched: with
interactive = FALSEthe package does not load ggiraph at all, and the interactive geometry is identical to the static geometry. ggiraph is a Suggests-only dependency. - plotly remains unsupported and will stay that way:
ggplotly()cannot translate the custom grobs this package draws. The help page now says so.
Palettes are a correctness problem, and now they are measurable
-
taichi_check_palette()measures a pair of ramps: per-step luminance and chroma, the largest luminance mismatch, whether each ramp is monotone, and — with colorspace installed — how far apart the two stay under deuteranopia, protanopia and tritanopia, against a normal-vision baseline. Run on the package’s own defaults it returns FAIL: the grey yin ramp spans the full luminance range while the red yang ramp stops around L* 41, a mismatch of about 41 units, so equal values have never read as equal ink. That is documented rather than quietly fixed — see below. -
taichi_palette_pair()builds a pair that differs only in hue, sharing one luminance and one chroma trajectory, so step for step the two fish carry the same visual weight. -
palette =ongeom_taichi()selects a ready-made pair:"balanced"(the recommended one),"diverging","viridis_pair","brewer_pair","greyscale_safe"(a grey ramp and a hued ramp on the same luminance trajectory, so the figure survives greyscale printing), or"default".taichi_palette()returns any of them for inspection. - The defaults do not change. Every existing figure is unaffected. The honest fix is a different default, and that is a 1.0.0 decision with a prominent note, not something to slip into a minor release.
Fill scales, including binned ones
- A family of ready fill scales:
scale_taichi_yin_c()/scale_taichi_yang_c(),scale_taichi_yin_d()/scale_taichi_yang_d(),scale_taichi_yin_binned()/scale_taichi_yang_binned(), andscale_taichi_yin_viridis_c()/_d()with their yang counterparts. Pass them toyin_scale/yang_scale, or use them directly with the fish geoms. - Binning is the cheapest accuracy win available on a dense grid, and the documentation now says so: matching a patch to one of five labelled bins is much closer to a categorical lookup than reading a position on a continuous luminance ramp.
-
shared_limitsnow composes with a supplied scale. Previously the limits ggtaichi computed were dropped as soon as anyone brought their own scale, soshared_limits = TRUEsilently did nothing next to a binned or viridis scale. They are now pushed into it (a scale that sets its own limits still wins), which is what makes binning both fish against one set of breaks work.shared_legendlikewise drops the duplicate yang guide from a supplied scale.
ggplot2 4.x currency
-
The geoms follow the theme. ggplot2 4.0 lets a theme set geom defaults through
theme(geom = element_geom(ink, paper, accent)); ggtaichi’s fallbacks were hard-coded, so on a dark theme the fallback fish was nearly invisible and the two eyes were the wrong way round. Fill, outline colour, linewidth, linetype and both eye colours now read from the theme, resolving to exactly the previous values on any light theme. The eye-colour arguments default toNULL, meaning “ask the theme”. The ggplot2 floor stays at 3.4.0: the theme-aware defaults are installed at load time when the installed ggplot2 supports them, and the literal fallbacks are used otherwise. Raising the floor to 4.0.0 is a 1.0.0 decision. -
draw_key_taichi(): legend keys are now small taichi symbols with the layer’s own fish filled, rather than plain rectangles — and they grow eyes when the layer has them.key_glyph = "rect"restores the old keys, andkey_glyphis a new argument ofgeom_taichi(),geom_yin_fish()andgeom_yang_fish(). Keys only appear for discrete fills; a continuous fill still gets a colourbar. -
inst/CITATION, socitation("ggtaichi")gives a proper entry. - New tests pin the things ggplot2’s S7 migration could quietly break:
ggplot_add()dispatch, the+chain, and the theme-aware defaults resolving to the historical appearance. The suite moves to testthat edition 3, addsexpect_snapshot()coverage of every print method and error message, and adds a vdiffr case per new channel. - CI gains a coverage job and a weekly, non-blocking spelling and URL check.
New aesthetics on the fish geoms
geom_yin_fish() and geom_yang_fish() additionally understand radius (a proportion of the cell’s own radius, so 0.5 draws a half-size glyph in the same cell), border (a per-cell outline width in mm, overriding linewidth), and tooltip / data_id / onclick. Both gain interactive and key_glyph arguments.
Bug fixes
-
gganimate transitions collapsed to a single frame. Every animation this package has ever been able to produce was static. gganimate tracks which rows belong to which frame by encoding the frame into the
groupcolumn, as a"<id>"suffix; the geom’ssetup_data()resetgrouptoseq_len(nrow(data))and threw that away, sotransition_states(),transition_manual()and the rest all rendered one frame. The rewrite was dead code from the package’s first commit — nothing in the draw path readsgroup, since each panel is batched into one polygon that numbers its own vertices — and removing it changes no static output (every vdiffr snapshot is unchanged). It went unnoticed becausevignette("animations")builds thegganimobject but leaves everyanimate()call commented out for CI, so the frames were never rendered. There is now a test that renders frames withgganimate::file_renderer(), which needs no gifski and no system libraries, and asserts the count. -
The yin eye vanished on a theme with no background.
theme_void(), and any theme built withrect = element_blank(), leaves the theme’spaperfully transparent, so the new theme-aware default painted the yin eye#00000000. A fully transparentpapernow falls back to white (and a transparentinkto black), which also stops the fallback fill being mixed towards transparency instead of towards the page.
Fixes and corrections in this cycle
Found by auditing 0.3.0 against its own documentation before release. None of these has appeared in a CRAN release, so all are corrections rather than breaking changes.
-
The yin and yang legends could swap places between sessions. Both auto-built fill guides were left at ggplot2’s default
order, and the tie was broken by something that is not stable across R sessions: the same plot, the same package and the same ggplot2 could put yin first in one render and yang first in the next. One of the committed vdiffr references had in fact recorded the wrong order, which is how it was found. Yin is now pinned before yang, matching the argument order and every example in the documentation. An explicitguidepassed through...still wins, andshared_legendstill drops the yang guide. -
vignette("animations")now really renders its animations. Everyanimate()call in it was commented out, because gifski is not installed on every check machine — and since building agganimobject succeeds whether or not the transition works, nothing ever noticed that the geom was collapsing every animation to a single frame. The calls now execute throughgganimate::file_renderer(), which needs no gifski and no system libraries, and emit a GIF on top of that wherever gifski exists. A demonstration that is never run is not a demonstration. -
palette = "print_safe"is renamed"greyscale_safe". The preset guarantees that the two ramps collapse to the same ink in greyscale; it says nothing about the CMYK gamut, which is what most readers understand by “print safe”. The name over-promised. Renamed now because the preset is new in this cycle and has never been released. -
explicit_channel = "radius"gainsradius_exponent, defaulting to 0.57. The radius was scaled bysqrt()— strict area scaling — which was a silent choice. Cartography’s answer for proportional symbols is the apparent-magnitude (Flannery) exponent of about 0.57, because readers systematically underestimate the area ratio between large and small circles.radius_exponent = 0.5restores the previous behaviour. -
taichi_check_palette()now names the colour space it measured in. Every number it prints is space-dependent and none of them said so, which made them impossible to check against another tool. -
taichi_summary()documents a caveat onrank. The widest gap in a 96-cell grid is frequently the largest noise;ranklists places to look, not findings. -
geom_taichi()documents two things the mark cannot do. A sequential discrete palette asserts an ordering, which suits an ordered factor and overstates an unordered one; and putting time onxencodes the series in fill rather than position, so slope is not encoded at all.
Deprecations and notes
- The
sizeargument ofgeom_taichi(), soft-deprecated in favour oflinewidthsince 0.2.0, will be removed in 1.0.0. It still works, and still warns. -
geom_taichi()now takes around thirty arguments, which is a design smell the roadmap has flagged. Grouping them into option objects (taichi_eyes(),taichi_scales(), …) is intended to land before the next wave of glyph channels, not after. -
The lifecycle badge moves to
stable. The API has been through three releases without a breaking change: 0.3.0 added arguments but removed and altered nothing, and every plot written against 0.1.0 or 0.2.0 still draws the same picture.stableis a promise about breakage, not a promise to stop adding features, and that promise the package can keep.
ggtaichi 0.2.0
CRAN release: 2026-08-24
New features
-
Data-driven eyes (
eyes = TRUE): draw the classic taichi dots, each centred in its own fish’s head (yin in the top bulb, yang in the bottom bulb).yin_eye_size/yang_eye_sizeandyin_eye_colour/yang_eye_colouraccept either a constant or an unquoted data column, so a single glyph can now encode up to six dimensions (x, y, two fills, two eyes) (#3b). -
Rotation (
angle): rotate each glyph by a constant number of degrees or by a data column, encoding a directional or temporal variable as orientation, and unlocking spin animations (#3a). -
Categorical fill support:
geom_taichi()inspects the plot data at+time and auto-selectsscale_fill_manual()for discrete (factor / character / logical)yin/yangvalues — including computed expressions such asfactor(week)— andscale_fill_gradientn()for continuous ones. With the default color vectors, discrete categories sample the ramp evenly while skipping its palest end, so no category is invisible on a white panel. Custom scales (objects or constructors) can be supplied viayin_scale/yang_scale(#4a, BUG-4). -
Shared scales for directly comparable sources (#4b):
-
shared_limits = TRUEgives both auto-built fill scales common limits — the union range of the two sources (or the union of levels when both are discrete) — so equal values read as equal ink. Explicitlimitspassed through...still win, and mixing a discrete with a continuous source warns and ignores the flag. -
shared_legend = TRUEtreats the sources as one measure: it implies shared limits, paints both fish withyin_colors, drops the duplicate yang guide, and titles the single legend “yin/yang” unlessyin_nameis supplied.
-
-
The fish geoms are exported (#4d):
geom_yin_fish()andgeom_yang_fish()are now documented exports (with theGeomYinFish/GeomYangFishggproto objects available for extension packages), for users who want a single fish or full manual control over scale stacking. -
remove_padding()auto mode: called with no arguments it now detects each axis’s scale type from the plot it is added to; the explicit"c"/"d"arguments remain as overrides. -
New dataset
cafes_tg: a small, clearly synthetic (seeded) espresso vs. matcha dataset whose two columns share units — an evergreen demo for the shared-scale features and a break from the COVID-era examples. The generating script ships indata-raw/. -
yinandyangalso accept strings naming a column (yin = "Twitter"), which previously produced a meaningless constant fill. -
geom_taichi()now returns an object with a friendlyprint()method instead of dumping raw list internals at the console. -
Animation vignette:
vignette("animations")documents howgeom_taichi()composes with gganimate (transition_states(), spin animations viaangle, export recipes) — verified frame-by-frame against gganimate 1.0.11. gganimate is a Suggests-only dependency.
Performance
-
Vectorized rendering: each layer now draws all of its cells as one id-batched polygon (plus one batched circle grob for the eyes), resolved against the physical panel size at draw time via
makeContent(). Glyphs stay perfectly round under resize, and large grids render an order of magnitude faster than the per-cell grob building used in 0.1.0: a 1200-cell grid with eyes takes 0.24 s to build and draw versus ~3.5 s with the per-cell approach (~15x, same machine), with pixel-identical output.
Bug fixes
-
Mapped
eye_size = 0drew an eye (#1): a mapped eye-size of0was rescaled to a positive radius, so an eye was drawn despite the documented “0 → no eye” rule. Zeros are now preserved and drawn without an eye. -
A zero disabled the eye-size pass-through for the whole column: because
0is excluded from the documented(0, 0.5]pass-through range, a single “no eye here” zero made every other value in an otherwise-proportional column go through the[0.05, 0.3]rescale instead —c(0, 0.2, 0.4)drew eyes of0, 0.175, 0.3. Zeros are markers rather than measurements, so they no longer take part in that decision; the column now draws0, 0.2, 0.4, and the two documented rules compose as intended. -
shared_legendpalette mismatch (#2): withshared_legend = TRUEand discrete fills, the yang fish was painted with a differently-interpolated palette than the yin fish when only one ofyin_colors/yang_colorswas supplied. Both fish now useyin_colors, so identical categories read as identical ink. -
states_tgdocumentation (#3): the dataset spans 31 weeks (the bundled data has 31); the documentation said 30. Corrected to 31 (the data is unchanged). -
...routing (BUG-1): geom parameters (alpha,colour,linewidth,linetype,width,height,na.rm,show.legend) are now real, documented arguments ofgeom_taichi()and are forwarded to the underlying fish layers....is reserved for options applied to both fill scales (e.g. sharedlimits); per-fish scale control goes throughyin_scale/yang_scale. -
linewidthaesthetic (BUG-2): the outline width now uses the modernlinewidthaesthetic. Passingsizetogeom_taichi()still works but warns and is routed tolinewidth, and an inheritedaes(size = ...)mapping is renamed through ggplot2’s built-in deprecation path. ggtaichi now requires ggplot2 >= 3.4.0. -
Missing-argument validation (BUG-3): omitting
yinoryangerrors immediately with a clear message instead of silently producing a degenerate grey plot, and ayin/yangcolumn that does not exist in the plot data errors at+time with the offending name. - Categorical fills (BUG-4): factor / character columns no longer trigger the cryptic “Discrete value supplied to a continuous scale” error (see the categorical fill support above).
-
Non-finite
angleandeye_sizevalues: the guards testedis.na(), which isTRUEforNAandNaNbut not forInf/-Inf. An infinite angle therefore reachedcos()/sin(), turned every vertex of that glyph intoNaNand drew nothing while warning “NaNs produced”; an infinite mapped eye size asked grid for a circle of infinite radius. Both now testis.finite(), so a non-finite angle falls back to no rotation and a non-finite eye size means no eye, exactly asNAalready did. -
Non-numeric
anglecolumns: mappingangleto a character column failed at draw time with the base error “non-numeric argument to binary operator”, and mapping it to a factor silently drew unrotated glyphs alongside'*' not meaningful for factorswarnings. Both now error at build time with a clear message, matching how a non-numericeye_sizecolumn is already handled. -
Explicit discrete
limits: passinglimitsthrough...for a factor / character fish aborted with “Insufficient values in manual scale” whenever the limits held more entries than the data had levels. The auto-built palette is now sized against the limits. -
A custom scale for the wrong aesthetic drew the wrong plot silently: passing e.g.
yin_scale = scale_colour_viridis_cattached the scale tocolour, which the fish never map, so the fish fell back to ggplot2’s default blue fill gradient with no error at all.yin_scale/yang_scaleare now checked to govern a fill aesthetic, and a value that is neither a scale object nor a constructor function reports that instead of the base error “‘what’ must be a function or character string”. -
Non-numeric cell
width/heightfailed insidesetup_data()with “non-numeric argument to binary operator”; now reported directly. -
...colliding with the scale optionsgeom_taichi()sets itself:guidetogether withshared_legend = TRUEaborted with the base error “formal argumentguidematched by multiple actual arguments”. The internally computed options now take precedence, so the yang guide is still dropped while a user-suppliedguidestyles the shared legend. Passingname,values,colors, orcoloursthrough...now reports which per-fish argument to use instead of raising the same base error. -
theme_taichi()no longer clips text at the plot edges: the title is now aligned with the whole plot area (plot.title.position = "plot") and slightly smaller (15 instead of 18), so realistic titles fit at typical figure sizes, and the right plot margin is a touch wider so an axis label sitting on the panel boundary (common withremove_padding()) is not cut off. -
theme_taichi()element inheritance: because the theme is composed with%+replace%, three propertiestheme_bw()had set were silently dropped and fell back to the generictext/rectparents. The rice-paper canvas picked up a near-black 1px border around the whole plot, the y-axis tick labels lost their right alignment and their gap from the panel, and the legend title lost its left alignment. All three are restored.
Documentation
- New pkgdown-only gallery article showing palettes, data-driven eyes, rotation, categorical fills, shared scales, and dense-grid texture.
- New “When (not) to use taichi” section in the intro vignette: honest guidance on dense grids, luminance precision, colorblind-safe palettes (viridis via
yin_scale/yang_scale), and NA visibility. - New Styling section in
?geom_taichi:alpha,colour,linewidthandlinetypeare layer-wide constants there (each has a concrete default, so it is always forwarded as a layer parameter and outranks an inherited mapping) — map those throughgeom_yin_fish()/geom_yang_fish()instead.widthandheightdefault toNULLand are forwarded only when supplied, so a plot-levelaes(width = ...)does size the cells per row. -
?theme_taichinow spells out its two surprising choices — the blanked y axis title (solabs(y = )has no effect) and the 90-degree legend text — together with thetheme()calls that put either back. -
?remove_paddingnow states that...reaches both position scales, so with axes of different types only arguments common to continuous and discrete scales work there, and that auto-detection reads the plot’s mapping (name the type explicitly whenx/yare mapped in a layer instead). -
?pitts_emojisnow documents the actual format (HTML<img>tags aligned row-for-row withpitts_tg) and notes that the remote images it points at are no longer served.
Internal
- Added a testthat suite (argument validation,
taichi_fish()geometry down to a Monte-Carlo tiling check, parameter routing, rotation, eyes, discrete-scale selection, grob-level rendering checks) plus vdiffr visual-regression snapshots. - Two gaps in that suite are closed. It now covers non-square cells — the per-cell box following
widthon x andheighton y, and the glyph radius coming from the shorter cell side — which everycoord_fixed()snapshot is blind to. It also pins the direction of all three places rotation is applied (taichi_fish(), the vectorised body rotation inmakeContent(), and the eye placement), so a sign error in any one of them fails a test. -
tests/testthat/setup.Rholds a null device open for the run, so the suite no longer leaves a strayRplots.pdfintests/testthat/. -
geom_taichi()now returns aggtaichi_plotobject added to the plot via aggplot_add()method, which is what makes data-aware scale selection and shared limits possible.
ggtaichi 0.1.0
CRAN release: 2026-06-24
- Initial version.
-
geom_taichi()turns each cell of a grid into a taichi (yin-yang) diagram, filling the two fish with values from two data sources. - Added
theme_taichi()andremove_padding()helpers. - Bundled the
pitts_tg,states_tg, andpitts_emojisdata sets.
