Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ docs/
doc
Meta
*.bak
.positai
110 changes: 110 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Ternary Package — Agent Memory

## Overview

**Ternary** (v2.3.6) creates ternary plots (simplex/Gibbs triangles) and
Holdridge life zone diagrams using base R graphics.
Maintained by Martin R. Smith (martin.smith@durham.ac.uk).
Repository: <https://github.com/ms609/Ternary/>

## Project Layout

```
Ternary/
├── DESCRIPTION
├── NAMESPACE # Generated by roxygen2
├── R/ # Source code (12 files)
│ ├── Ternary-package.R # Package-level docs
│ ├── TernaryPlot.R # TernaryPlot(), AddToTernary(), wrapper fns
│ ├── Contours.R # TernaryPointValues(), TernaryContour(),
│ │ # TernaryDensity(), ColourTernary(), etc.
│ ├── Coordinates.R # TernaryCoords/TernaryToXY, XYToTernary
│ ├── Holdridge.R # HoldridgePlot() and Holdridge* wrappers
│ ├── Annotate.R # Annotate()
│ ├── helpers.R # Internal helpers (.PlotGrid, .TitleAxis, …)
│ ├── dot-TrianglePlot.R # Triangle plot internals
│ ├── SetRegion.R # .SetRegion() S3 methods
│ ├── Polygons.R # Polygon geometry (re-exported from PlotTools)
│ ├── shiny.R # TernaryApp() launcher
│ ├── data.R # Dataset documentation
│ └── zzz.R # .onLoad / .onAttach
├── man/ # 27 .Rd files (generated by roxygen2)
├── man-roxygen/ # Reusable roxygen templates
│ ├── MRS.R # @author template
│ └── dotsToContour.R # Contour parameter docs
├── tests/
│ ├── testthat.R
│ └── testthat/ # 10 test files (testthat ed. 3)
│ ├── helper.R # vdiffr visual regression helper
│ └── _snaps/ # SVG snapshots
├── vignettes/ # 5 Rmd vignettes
│ ├── Ternary.Rmd # Main guide
│ ├── new-users.Rmd # R beginners
│ ├── Holdridge.Rmd # Holdridge plots
│ ├── interpolation.Rmd # Contour / interpolation
│ └── annotation.Rmd # Annotating points
├── data/ # 8 .rda datasets (palettes, Holdridge classes)
├── data-raw/ # Scripts that produce data/*.rda
├── inst/
│ ├── TernaryApp/ # Shiny app (git submodule)
│ ├── _pkgdown.yml # pkgdown site config (Bootstrap 5)
│ └── CITATION
├── .github/workflows/ # CI: R-CMD-check, pkgdown, rhub, codemeta
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── NEWS.md
└── codemeta.json
```

## Key Conventions

- **Naming**: TitleCase for exported functions, camelCase for variables.
- **Spelling**: Oxford English ("colour", "centre", "-ize" not "-ise").
- **Style**: Google R style guide.
- **Documentation**: roxygen2 with markdown enabled (RoxygenNote 7.3.3).
- `@template MRS` for author.
- `@template dotsToContour` for shared contour params.
- `@family` tags group related functions.
- `@describeIn` / `@rdname` for grouping related functions on one page.
- `@order` controls section order within grouped docs.
- **Testing**: testthat edition 3, vdiffr for visual regression. Parallel
tests disabled. ARM visual tests skipped.
- **CI/CD**: GitHub Actions (R-CMD-check on Win/macOS/Ubuntu; pkgdown site
with shinylive).
- **Changelog**: Bullet at top of `NEWS.md` for user-facing changes.

## Architecture Notes

### Wrapper pattern

`AddToTernary(PlottingFunction, coordinates, ...)` is the core dispatcher —
it converts ternary coordinates to XY then calls the graphics function.
Convenience wrappers (`TernaryPoints`, `TernaryLines`, `TernaryText`, …)
delegate to it.

`AddToHoldridge(PlottingFunction, pet, prec, ...)` follows the same pattern,
converting Holdridge coordinates via `HoldridgeToXY()` first.

### Plot state

`TernaryPlot()` stores a list of plot parameters in the global option
`.Last.triangle`. Direction stored in `ternDirection`; region in
`ternRegion`. Internal helpers read these via `getOption()`.

### Contour / tile system

`TernaryPointValues(Func, resolution)` evaluates `Func(a, b, c)` at triangle
centres generated by `TriangleCentres()`, returning an x/y/z/down matrix.
`ColourTernary()` fills tiles from this matrix.

`TernaryContour(Func, resolution)` evaluates on a rectangular grid via
`outer()`, masking points outside the triangle with `sp::point.in.polygon`.

## Data

| Dataset | Contents |
|---------|----------|
| `cbPalette8/13/15` | Colourblind-safe palettes |
| `holdridge` | Sample Holdridge data |
| `holdridgeClasses` / `holdridgeClassesUp` | 38 classification names |
| `holdridgeLifeZones` / `holdridgeLifeZonesUp` | Life zone names |
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Ternary v2.3.7

- Document use of `Func` as a table lookup in `TernaryPointValues()` and
`TernaryContour()`, with worked examples in documentation and the
'Interpolating and contouring' vignette.

# Ternary v2.3.6 (2026-02-02)
- Enable [web app](http://ms609.github.io/Ternary/app).
- Upgrade `Annotate()` to use LAPJV in place of Hungarian algorithm.
Expand Down
43 changes: 43 additions & 0 deletions R/Contours.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#' returns a numeric vector of length _n_.
#' `a`, `b` and `c` will each be a vector of length _n_. Together, they
#' specify the series of coordinates at which the function should be evaluated.
#' `Func` need not compute values analytically; it may also look up
#' pre-computed values from a table – see the 'Interpolating and contouring'
#' vignette for an example.
#' @param resolution The number of triangles whose base should lie on the longest
#' axis of the triangle. Higher numbers will result in smaller subdivisions and smoother
#' colour gradients, but at a computational cost.
Expand Down Expand Up @@ -43,6 +46,26 @@
#' density <- TernaryDensity(coordinates, resolution = 10L)
#' ColourTernary(density, legend = TRUE, bty = "n", title = "Density")
#' TernaryPoints(coordinates, col = "red", pch = ".")
#'
#' # Func can also look up values from a pre-computed table rather than
#' # evaluate an analytic formula. Here a data frame stores z-values at a
#' # handful of ternary coordinates; Func snaps each queried point to its
#' # nearest tabulated neighbour.
#' lookup <- data.frame(
#' a = c(0.7, 0.5, 0.3, 0.1, 0.5),
#' b = c(0.1, 0.3, 0.5, 0.7, 0.2),
#' z = c( 9, 5, 3, 1, 7)
#' )
#' lookup$c <- 1 - lookup$a - lookup$b
#'
#' NearestLookup <- function(a, b, c) {
#' distSq <- outer(a, lookup$a, `-`)^2 + outer(b, lookup$b, `-`)^2
#' lookup$z[apply(distSq, 1, which.min)]
#' }
#'
#' TernaryPlot(alab = "a", blab = "b", clab = "c")
#' ColourTernary(TernaryPointValues(NearestLookup, resolution = 12L))
#' TernaryPoints(lookup[c("a", "b", "c")], pch = 20, cex = 2)
#' @family contour plotting functions
#' @template MRS
#' @export
Expand Down Expand Up @@ -687,6 +710,26 @@ ColorTernary <- ColourTernary
#' # Re-draw edges of plot triangle over fill
#' TernaryPolygon(diag(3))
#'
#' # Func can also perform a table lookup rather than evaluate a formula.
#' # TernaryContour() passes continuous vectorised (a, b, c) values to Func,
#' # so the lookup must snap queried coordinates to tabulated values.
#' lookup <- data.frame(
#' a = c(0.7, 0.5, 0.3, 0.1, 0.5),
#' b = c(0.1, 0.3, 0.5, 0.7, 0.2),
#' z = c( 9, 5, 3, 1, 7)
#' )
#' lookup$c <- 1 - lookup$a - lookup$b
#'
#' NearestLookup <- function(a, b, c) {
#' distSq <- outer(a, lookup$a, `-`)^2 + outer(b, lookup$b, `-`)^2
#' lookup$z[apply(distSq, 1, which.min)]
#' }
#'
#' TernaryPlot(alab = "a", blab = "b", clab = "c")
#' ColourTernary(TernaryPointValues(NearestLookup, resolution = 12L))
#' TernaryContour(NearestLookup, resolution = 36L)
#' TernaryPoints(lookup[c("a", "b", "c")], pch = 20, cex = 2)
#'
#' # Restore plotting parameters
#' par(originalPar)
#' @family contour plotting functions
Expand Down
1 change: 1 addition & 0 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ R's
RGBA
Tsakalos
WP
Voronoi
Zenodo
ac
aracters
Expand Down
25 changes: 24 additions & 1 deletion man/TernaryContour.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion man/TernaryPointValues.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 62 additions & 1 deletion vignettes/interpolation.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,67 @@ TernaryContour(FunctionToContour, resolution = 36L, legend = TRUE,

```

# Plotting from a pre-computed table

`Func` is an ordinary R function, so it need not compute z-values
analytically.
It can equally well **look up pre-computed values** from a matrix or data
frame.
This is useful whenever z-values are already known at a set of discrete
ternary coordinates – for instance, tabulated rates indexed by age, period,
and cohort in a Lexis analysis.

The key point is that both `TernaryPointValues()` and `TernaryContour()`
call `Func` with **vectors** of `a`, `b`, `c` coordinates, so the lookup
must be vectorised.
A nearest-neighbour lookup is a simple, general approach: for each queried
coordinate, find the closest row in the table and return its z-value.

```{r table-lookup-tpv, fig.asp = 1}
par(mar = rep(0.2, 4))

# A small table of z-values at known (a, b, c) coordinates.
# In practice this would be read in from a file.
lookup <- data.frame(
a = c(0.7, 0.5, 0.3, 0.1, 0.5),
b = c(0.1, 0.3, 0.5, 0.7, 0.2),
z = c( 9, 5, 3, 1, 7)
)
lookup$c <- 1 - lookup$a - lookup$b

# Func: snap each queried point to the nearest tabulated neighbour.
NearestLookup <- function(a, b, c) {
distSq <- outer(a, lookup$a, `-`)^2 + outer(b, lookup$b, `-`)^2
lookup$z[apply(distSq, 1, which.min)]
}

TernaryPlot(alab = "a", blab = "b", clab = "c")
ColourTernary(TernaryPointValues(NearestLookup, resolution = 12L))
# Mark the tabulated points
TernaryPoints(lookup[c("a", "b", "c")], pch = 20, cex = 2)
```

The coloured tiles show the Voronoi-like regions centred on each tabulated
point.
`TernaryContour()` accepts the same lookup function:

```{r table-lookup-contour, fig.asp = 1}
par(mar = rep(0.2, 4))
TernaryPlot(alab = "a", blab = "b", clab = "c")
ColourTernary(TernaryPointValues(NearestLookup, resolution = 12L))
TernaryContour(NearestLookup, resolution = 36L)
TernaryPoints(lookup[c("a", "b", "c")], pch = 20, cex = 2)
```

Note that `TernaryContour()` evaluates `Func` on a dense rectangular
Cartesian grid (not just at triangle centres), so the queried `a`, `b`, `c`
values will be continuous and will generally not coincide with table grid
points.
The nearest-neighbour lookup handles this naturally; the contour lines then
correspond to the boundaries between adjacent tabulated values.
For smoother contours, consider interpolating the table first – see the
inverse distance weighting example below.

If it is not computationally feasible to execute a function at every point,
it is possible to interpolate between known values.

Expand Down Expand Up @@ -98,7 +159,7 @@ TernaryContour(PredictABC, resolution = 36L, legend = 6,
TernaryPoints(abc, pch = 3, col = "#cc3333")
```

More sophisticated interpolation approaches are possible, e.g.
More sophisticated interpolation approaches are possible, e.g.
[kriging](https://desktop.arcgis.com/en/arcmap/10.3/tools/3d-analyst-toolbox/how-kriging-works.htm); these may help to alleviate artefacts of sampling intensity as observed towards the bottom of the example plot above.

# Interpolation within a sampled region
Expand Down
Loading