tinyplot is a lightweight extension of base R graphics providing automatic legends, facets, themes, and other enhancements. Zero recursive dependencies — only base R.
- Main function:
tinyplot()(alias:plt()) - Add layers:
tinyplot_add()/plt_add() - Themes:
tinytheme() - Parameters:
tpar()
When working on tinyplot interactively, always use pkgload::load_all() to load the development version — never library(tinyplot). This ensures you're testing your local changes, not an installed copy.
pkgload::load_all()
# Then test interactively, e.g.
plt(Sepal.Length ~ Petal.Length | Species, data = iris)R/— Package source. The main entry point istinyplot.R(~57KB). Plot types live intype_*.Rfiles. Other key files includelegend.R,facet.R,by_aesthetics.R,tinytheme.R,tpar.R, andenvironment.R. Input validation helpers follow thesanitize_*.Rnaming convention. Utility functions are inutils.R.inst/tinytest/— Test suite (tinytest+tinysnapshot). Snapshot SVGs are in_tinysnapshot/.man/— roxygen2-generated.Rdfiles.vignettes/— Package vignettes (qmd format).SCRATCH/— Developer scratch files and experiments (not part of the package).
tinyplot has zero recursive dependencies — it imports only base R packages (graphics, grDevices, stats, tools, utils). All contributions must preserve this. Do not add new package dependencies under Imports or Depends.
# Use = not <-
x = 5
# Use function() not \() — package requires R >= 4.0.0 compatibility
fn = function(x) x^2
# Prefer [[ over $ for element access (no partial matching, works with variables)
legend_args[["title"]]
settings[["datapoints"]]
# NOT: legend_args$title, settings$datapointsThe package targets R >= 4.0.0, so the base pipe |> (introduced in 4.1) is not available. Use intermediate variables or nested calls instead.
Wrap at ~80 characters. Break long function calls across lines.
The main pipeline in tinyplot.default() follows this sequence:
- Save par state and the call (for
tinyplot_add()replay) - Build a
settingsenvironment with all inputs - Sanitize inputs: type → axes → labels → facets → datapoints
- Run the type's
datafunction (type_data(settings)) to transform data - Handle flipping, bubble sizing, axis limits
- Compute group aesthetics (colours, pch, lty, etc.)
- Prepare legends
- Draw the facet grid (if any)
- Nested drawing loop: outer loop over facets, inner loop over
bygroups — each iteration calls the type'sdrawfunction with per-group data - Save end par state for layer recall
Each plot type is a tinyplot_type S3 object created by a type_*() constructor:
type_boxplot = function(range = 1.5, ...) {
out = list(
draw = draw_boxplot(range = range, ...), # closure: does actual plotting per group
data = data_boxplot(...), # closure: preprocesses data, injects into settings
name = "boxplot" # string identifier
)
class(out) = "tinyplot_type"
return(out)
}drawfunction signature:function(iby, ix, iy, ipch, ilty, icol, ibg, ...)- Called once per group (
iby= group index) - Receives per-group subsetted data
- Called once per group (
datafunction signature:function(settings, ...)- Receives the
settingsenvironment - Reads from settings via
env2env(settings, environment(), keys) - Writes back via
env2env(environment(), settings, keys) - Can modify
datapoints,xlabs,col,bg,by,facet,group_offsets, legend args, etc.
- Receives the
Individual tinyplot() calls store plot state in a temporary settings environment. Type-specific data functions read/write to this environment using env2env(). This avoids copying large objects and allows types to customize behaviour.
Managed via get_environment_variable() / set_environment_variable() in environment.R:
.last_call— last tinyplot call (used bytinyplot_add()).saved_par_before/.saved_par_after/.saved_par_first— par state for layer restoration.tpar_hooks— theme hooks.group_offsets— dodge offsets for layering (used by jitter-on-boxplot etc.)
Coordinate-dependent calculations (especially legends) must be wrapped in recordGraphics() so they replay correctly on device resize:
recordGraphics(
tinylegend(legend_env),
list = list(legend_env = legend_env),
env = getNamespace("tinyplot")
)Themes use before.plot.new hooks. Legend code must preserve/restore hooks:
oldhook = getHook("before.plot.new")
setHook("before.plot.new", function() par(new = TRUE), action = "append")
plot.new()
setHook("before.plot.new", oldhook, action = "replace")- Inner positions:
"right","topleft", etc. - Outer positions (with
!):"right!","bottom!", etc. - Outer legends adjust plot margins via
par(oma=...)andpar(mar=...)
When multiple groups share the same x-position (boxplot, violin, etc.), offsets are computed in the type's data function and stored as group_offsets + offsets_axis. These are saved to .tinyplot_env so that tinyplot_add() layers (e.g., jitter) can align correctly.
Tests use tinytest + tinysnapshot. Snapshot tests produce SVG output with Liberation fonts and are meant to run on Linux (helpers.R sets tinysnapshot_os = "Linux"); they only run when the environment variable NOT_CRAN is set to the lowercase string "true". Non-snapshot tests (logical assertions, error checks) run anywhere.
On Linux you can generally just run the suite directly and expect it to match CI. Snapshots are byte-compared against amd64 reference images, so the usual things that trip people up are (a) not being on Linux/amd64, and (b) a stale render toolchain — e.g. distro/PPM package binaries built against an older R after a minor bump, which make svglite/systemfonts/textshaping/rsvg/magick error or drift. A handful of pure text/glyph-metric diffs (titles, themed labels, plotmath) almost always means one of those, not a code regression; a larger or structural set of failures is the real thing. CI is the final arbiter.
Pick the path that fits your setup:
Native Linux (amd64) — fastest. make testall runs directly against your R install and should match CI. Snapshots only run with NOT_CRAN=true set (lowercase). If the render stack errors ("Graphics API version mismatch") or snapshots drift, it's the stale-toolchain gotcha above — reinstall svglite, magick, rsvg, systemfonts, textshaping from source.
VS Code devcontainer (any OS). Open the repo in VS Code → Command Palette → "Dev Containers: Reopen in Container" (deps install automatically), then make testall. Also works via GitHub Codespaces.
Terminal container runner (any OS with Docker/Podman/Finch/nerdctl). No editor required:
make testall-docker— full suite in a Linux container at your native architecture.make testall-ci— same, forced to amd64 (--platform linux/amd64). On Apple Silicon this is the CI-faithful run: arm64-native rendering differs subtly from the amd64 reference SVGs, so a native-arch run shows a couple of spurious text-snapshot diffs. It's emulated (slower), so iterate withtestall-dockerand confirm withtestall-cibefore pushing.
make testall # native Linux host
make testall-docker # container, native arch
make testall-ci # container, amd64 (CI-faithful; needed on Apple Silicon)
make testone testfile="inst/tinytest/test-legend.R" # single file, native
.devcontainer/run-tests.sh inst/tinytest/test-legend.R # single file, container
# Via R (Linux, NOT_CRAN=true)
tinytest::run_test_dir("inst/tinytest")
tinytest::run_test_file("inst/tinytest/test-legend.R")PRs against main automatically trigger GitHub Actions CI, which runs R CMD check (including the full test suite) on Ubuntu with both R-release and R-devel. Snapshot tests are included in this CI run, so even if you can't run them locally, CI will catch any regressions.
The test suite (snapshot SVGs in particular) adds significant size to the installed package. Tests are only intended to run locally and on CI — not on CRAN.
Pre-submission checklist:
- Create a new branch
cran_v<x.x.x>from main - Verify version is updated and aligned in both
DESCRIPTIONandNEWS.md - Verify
Datefield inDESCRIPTIONmatches the submission date - Update
cran-comments.mdwith new version and release type - Open a PR from the branch to main
- Verify CI passes on the PR
- Run reverse dependency checks: push a
revdep-v<x.x.x>branch to trigger the revdep workflow - Uncomment the
inst/tinytest/line in.Rbuildignoreto exclude tests from tarball - Run
devtools::check_win_devel()and wait for results - Run
R CMD check --as-cranlocally - Fix any issues / notes and update cran-comments.md as necessary
- Submit to CRAN
Post-acceptance:
- Re-comment the
inst/tinytest/line in.Rbuildignoreand push to PR so CI continues to pick up tests - Merge the PR to main
- Create a new GitHub release tagged
v<x.x.x>
Some features require manual testing, particularly:
- Window resize behaviour (legends, facets, layers should stay aligned)
- Positron IDE compatibility
- Interactive device behaviour
All contributions should go through a pull request against main.
- Always work on a fresh branch. Every PR-based contribution must start on a new branch created from the latest
origin/main(git fetch && git switch --no-track -c <branch> origin/main, thengit push -u origin <branch>on first push). The--no-trackmatters: without it the branch tracksorigin/main, so a plaingit pushor an IDE "sync" pushes straight tomain. Never commit directly tomain, and never reuse or stack work onto an existing feature branch. Overriding this requires explicit opt-in from the maintainer.
make help # Show all available commands
make document # Generate documentation (devtools::document)
make check # Full R CMD check
make install # Install package locally
make website # Build documentation website (altdoc)Where possible, reuse existing draw_*() and data_*() functions rather than writing new ones from scratch. Many types are thin wrappers that combine existing building blocks with custom data transformations. For example:
type_lm,type_glm, andtype_loessall usedraw_ribbon()— they only differ in theirdata_*()functionstype_barplotandtype_histogramboth usedraw_rect()type_jitterandtype_pointrangeboth usedraw_points()type_splineandtype_summaryboth usedraw_lines()type_errorbarreusesdata_pointrange()entirelytype_areahas no draw function at all — its data function setstype = "ribbon"to delegate drawing
Steps:
- Create
R/type_<name>.Rwith thetype_<name>()constructor,draw_<name>(), and optionallydata_<name>() - Register the type in
R/sanitize_type.R: add the string name to theknown_typesvector and a corresponding entry in theswitchstatement that maps it to the constructor - Add
@exporttag to the constructor - Run
make documentto update NAMESPACE - Add tests in
inst/tinytest/test-type_<name>.R - Add snapshot SVGs by running tests on Linux (devcontainer)
- Add the new page to the website navigation in
altdoc/quarto_website.yml
Important: Step 7 applies to any new exported function or page, not just plot types. Whenever you add or rename an exported function that gets its own .Rd page, add it to altdoc/quarto_website.yml under the appropriate section.
Type-specific legend customizations should go in the type's data function by modifying settings$legend_args:
settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22
settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5- Legend misalignment on resize: Ensure coordinate-dependent calculations are inside
recordGraphics(). See PRs #438, #540, #541. - Layer alignment with grouped types: When adding jitter/points on top of boxplot/violin, the layer needs access to
group_offsetsfrom.tinyplot_env. See PR #561. - Theme hook corruption: Always save and restore
before.plot.newhooks when callingplot.new()in legend code. - R 4.0.0 compat: No
|>pipe, no\()lambda.