diff --git a/.gitignore b/.gitignore index 9869f2f8..9e96c3b2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ docs/ doc Meta *.bak +.positai diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..76630e9c --- /dev/null +++ b/AGENTS.md @@ -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: + +## 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 | diff --git a/NEWS.md b/NEWS.md index 925ef57b..3852470b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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. diff --git a/R/Contours.R b/R/Contours.R index 155f3bde..ce5796c7 100644 --- a/R/Contours.R +++ b/R/Contours.R @@ -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. @@ -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 @@ -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 diff --git a/inst/WORDLIST b/inst/WORDLIST index cd27a9b7..d94d5132 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -17,6 +17,7 @@ R's RGBA Tsakalos WP +Voronoi Zenodo ac aracters diff --git a/man/TernaryContour.Rd b/man/TernaryContour.Rd index 1c513419..a9bcd646 100644 --- a/man/TernaryContour.Rd +++ b/man/TernaryContour.Rd @@ -26,7 +26,10 @@ TernaryContour( \item{Func}{Function that takes three arguments named \code{a}, \code{b} and \code{c}, and returns a numeric vector of length \emph{n}. \code{a}, \code{b} and \code{c} will each be a vector of length \emph{n}. Together, they -specify the series of coordinates at which the function should be evaluated.} +specify the series of coordinates at which the function should be evaluated. +\code{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.} \item{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 @@ -133,6 +136,26 @@ TernaryContour(GeneralMax, filled = TRUE, # 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) } diff --git a/man/TernaryPointValues.Rd b/man/TernaryPointValues.Rd index 1d38c2a4..c6847cf2 100644 --- a/man/TernaryPointValues.Rd +++ b/man/TernaryPointValues.Rd @@ -22,7 +22,10 @@ TernaryDensity( \item{Func}{Function that takes three arguments named \code{a}, \code{b} and \code{c}, and returns a numeric vector of length \emph{n}. \code{a}, \code{b} and \code{c} will each be a vector of length \emph{n}. Together, they -specify the series of coordinates at which the function should be evaluated.} +specify the series of coordinates at which the function should be evaluated. +\code{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.} \item{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 @@ -71,6 +74,26 @@ coordinates <- cbind(abs(rnorm(nPoints, 2, 3)), 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) } \seealso{ Other contour plotting functions: diff --git a/vignettes/interpolation.Rmd b/vignettes/interpolation.Rmd index 7e0b6d9a..fc629207 100644 --- a/vignettes/interpolation.Rmd +++ b/vignettes/interpolation.Rmd @@ -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. @@ -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