Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions .CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# tinyplot - AI Assistant Context

## Package Overview

**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()`

## Quick Reference

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.

```r
pkgload::load_all()

# Then test interactively, e.g.
plt(Sepal.Length ~ Petal.Length | Species, data = iris)
```

## Repository Structure

- `R/` — Package source. The main entry point is `tinyplot.R` (~57KB). Plot types live in `type_*.R` files. Other key files include `legend.R`, `facet.R`, `by_aesthetics.R`, `tinytheme.R`, `tpar.R`, and `environment.R`. Input validation helpers follow the `sanitize_*.R` naming convention. Utility functions are in `utils.R`.
- `inst/tinytest/` — Test suite (`tinytest` + `tinysnapshot`). Snapshot SVGs are in `_tinysnapshot/`.
- `man/` — roxygen2-generated `.Rd` files.
- `vignettes/` — Package vignettes (qmd format).
- `SCRATCH/` — Developer scratch files and experiments (not part of the package).

## Code Style & Conventions

### Zero Dependency Requirement
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`.

### Assignment & Syntax
```r
# 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$datapoints
```

### No Pipes in Package Code
The package targets R >= 4.0.0, so the base pipe `|>` (introduced in 4.1) is not available. Use intermediate variables or nested calls instead.

### Line Length
Wrap at ~80 characters. Break long function calls across lines.

## Architecture & Key Patterns

### Execution Flow
The main pipeline in `tinyplot.default()` follows this sequence:
1. Save par state and the call (for `tinyplot_add()` replay)
2. Build a `settings` environment with all inputs
3. Sanitize inputs: type → axes → labels → facets → datapoints
4. Run the type's `data` function (`type_data(settings)`) to transform data
5. Handle flipping, bubble sizing, axis limits
6. Compute group aesthetics (colours, pch, lty, etc.)
7. Prepare legends
8. Draw the facet grid (if any)
9. **Nested drawing loop**: outer loop over facets, inner loop over `by` groups — each iteration calls the type's `draw` function with per-group data
10. Save end par state for layer recall

### Type System
Each plot type is a `tinyplot_type` S3 object created by a `type_*()` constructor:
```r
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)
}
```

- `draw` function signature: `function(iby, ix, iy, ipch, ilty, icol, ibg, ...)`
- Called once per group (`iby` = group index)
- Receives per-group subsetted data
- `data` function signature: `function(settings, ...)`
- Receives the `settings` environment
- 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.

### Settings Environment
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.

### Package-Level State (.tinyplot_env)
Managed via `get_environment_variable()` / `set_environment_variable()` in `environment.R`:
- `.last_call` — last tinyplot call (used by `tinyplot_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.)

### recordGraphics() for Resize Handling
Coordinate-dependent calculations (especially legends) must be wrapped in `recordGraphics()` so they replay correctly on device resize:
```r
recordGraphics(
tinylegend(legend_env),
list = list(legend_env = legend_env),
env = getNamespace("tinyplot")
)
```

### Theme System
Themes use `before.plot.new` hooks. Legend code must preserve/restore hooks:
```r
oldhook = getHook("before.plot.new")
setHook("before.plot.new", function() par(new = TRUE), action = "append")
plot.new()
setHook("before.plot.new", oldhook, action = "replace")
```

### Legend Positioning
- Inner positions: `"right"`, `"topleft"`, etc.
- Outer positions (with `!`): `"right!"`, `"bottom!"`, etc.
- Outer legends adjust plot margins via `par(oma=...)` and `par(mar=...)`

### Group Offsets / Dodge
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.

## Testing & CI

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:
1. Open repo in VS Code
2. Command Palette → "Dev Containers: Reopen in Container"
3. 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. 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.

### Running Tests
```bash
# Via Makefile
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")
```

### Continuous Integration
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.

### CRAN Submissions
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. Before a CRAN submission, uncomment the `inst/tinytest/` line in `.Rbuildignore` to exclude the test directory from the built tarball. Remember to re-comment it after submission so that CI continues to pick up the tests.

### Manual Testing
Some features require manual testing, particularly:
- Window resize behaviour (legends, facets, layers should stay aligned)
- Positron IDE compatibility
- Interactive device behaviour

## Development Workflow

### Makefile Commands
```bash
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)
```

### Adding a New Plot Type
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`, and `type_loess` all use `draw_ribbon()` — they only differ in their `data_*()` functions
- `type_barplot` and `type_histogram` both use `draw_rect()`
- `type_jitter` and `type_pointrange` both use `draw_points()`
- `type_spline` and `type_summary` both use `draw_lines()`
- `type_errorbar` reuses `data_pointrange()` entirely
- `type_area` has no draw function at all — its data function sets `type = "ribbon"` to delegate drawing

Steps:
1. Create `R/type_<name>.R` with the `type_<name>()` constructor, `draw_<name>()`, and optionally `data_<name>()`
2. Register the type in `R/sanitize_type.R`: add the string name to the `known_types` vector and a corresponding entry in the `switch` statement that maps it to the constructor
3. Add `@export` tag to the constructor
4. Run `make document` to update NAMESPACE
5. Add tests in `inst/tinytest/test-type_<name>.R`
6. Add snapshot SVGs by running tests on Linux (devcontainer)

### Modifying Legend Behaviour
Type-specific legend customizations should go in the type's `data` function by modifying `settings$legend_args`:
```r
settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22
settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5
```

## Common Pitfalls

- **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_offsets` from `.tinyplot_env`. See PR #561.
- **Theme hook corruption**: Always save and restore `before.plot.new` hooks when calling `plot.new()` in legend code.
- **R 4.0.0 compat**: No `|>` pipe, no `\()` lambda, no `%||%` (package defines its own for R <= 4.4.0).

## Links

- Docs: https://grantmcdermott.com/tinyplot/
- Issues: https://github.com/grantmcdermott/tinyplot/issues
- CRAN: https://CRAN.R-project.org/package=tinyplot
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ inst/Psychoco2026
Makefile
^.devcontainer
Rplots.pdf
^\.CLAUDE\.md$
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ where the formatting is also better._
- We now encourage type-specific legend customizations within the individual
`type_<type>` constructors. (#531 @grantmcdermott)
- Change maintainer email address.
- Add `.CLAUDE.md` context file for AI-assisted development. (#563 @grantmcdermott)

### Documentation

Expand Down
Loading