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 must be run on Linux (options("tinysnapshot_os" = "Linux")). For non-Linux users, we provide a dedicated .devcontainer for running tests via VS Code or GitHub Codespaces:
- Open repo in VS Code
- Command Palette → "Dev Containers: Reopen in Container"
- Dependencies install automatically
Non-snapshot tests (logical assertions, error checks, etc.) run fine on any platform. Even with the devcontainer, a small number of snapshot tests (~2-3) may produce false positive failures on macOS hosts due to imperceptible rendering differences. These show up in inst/tinytest/_tinysnapshot_review/ but the visual differences are too small to detect by eye. Known false positives:
xaxl_yaxlpalette_manual_continuous
This is a known quirk — don't worry about these specific persistent failures. However, if you see more than ~3 snapshot failures, something real is likely broken and needs investigation.
The canonical test workflow is to open the devcontainer and run make testall. Do not attempt to run the full test suite outside the devcontainer — snapshot tests will fail due to font/rendering differences, and even non-snapshot tests may pull in snapshot comparisons.
# Via Makefile (inside devcontainer)
make testall # Run all tests
make testone testfile="inst/tinytest/test-legend.R" # Run single test file
# Via R
tinytest::run_test_dir("inst/tinytest")
tinytest::run_test_file("inst/tinytest/test-legend.R")All contributions should go through a pull request. 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
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)
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.