LandR provides three functions for summarizing a
national forest raster over a set of polygons:
calc_raster_counts() — the value-frequency table of
raster values within each polygon;calc_raster_stats() — min, mean, max, quartiles and the
proportion of zeroes, per polygon;plot_raster_stats() — a per-polygon figure combining a
histogram, a map, and the statistics.This vignette applies them to a national stand age map, summarized by ecozone.
The rasters these functions are designed for are large: SCANFI at 30 m is 2.1 × 1010 pixels, and a single pass reads tens of GB. That is not something a CI runner can do — and the ecostratification polygons are served from a host that throttles CI IP ranges — so this vignette is not computed from the full raster when the package is built.
Instead the work is cut at its natural seam.
calc_raster_counts() is the only step that touches every
pixel, and its output is small: a few thousand rows. That table is
computed once on a workstation by data-raw/precompute-vignettes.R
and committed, together with a coarsened raster used only for drawing
maps and a simplified copy of the ecozone polygons.
Everything below — every statistic, histogram and map — is then
recomputed from those artifacts when this vignette is built, in seconds
and offline. So the numbers you are reading cannot drift away from the
code in R/: they are produced by it, right now.
What can drift is the committed counts table, if
calc_raster_counts() itself changes. That is guarded: the
manifest records a hash of the normalized source of the functions that
produced the table, and
tests/testthat/test-vignette-artifacts.R fails when the two
diverge, naming the script to re-run.
Note that the summary statistics are deliberately not part of that hash. They are an exact function of the counts table, so a change to, say, the quantile definition simply shows up in the next build — it must not force a multi-hour regeneration of a counts table that is still perfectly valid.
| Generated | 2026-08-24 08:58:06 MDT |
| LandRVersion | 1.2.0.9009 |
| RVersion | R version 4.6.1 (2026-06-24) |
| DataSource | SCANFI |
| DataYear | 2020 |
| DataVersion | V2 |
| SourceRaster | SCANFI_age_median_2020_v2_20260119.tif |
| SourceResolution | 30 m |
| PolygonSource | https://sis.agr.gc.ca/cansis/nsdb/ecostrat/zone/ecozone_shp.zip |
| CountsFile | scanfi_age_2020_counts_ecozone.csv.gz |
| DisplayFile | scanfi_age_2020_display.tif |
| DisplayResolution | 5010 m |
| PolygonFile | ecozones.gpkg |
| counts_hash | a81fda035290bb8489c08e669b2c6e0b |
library(LandR)
extdata <- function(f) system.file("extdata", f, package = "LandR")
## exact value-frequency table, computed from the full-resolution raster
counts <- utils::read.csv(extdata(manifest$CountsFile))
## coarsened raster -- used for maps only, never for statistics
display <- terra::rast(extdata(manifest$DisplayFile))
ecozones <- sf::st_read(extdata(manifest$PolygonFile), quiet = TRUE)
str(counts)
#> 'data.frame': 4493 obs. of 3 variables:
#> $ ID : chr "Arctic Cordillera" "Arctic Cordillera" "Arctic Cordillera" "Arctic Cordillera" ...
#> $ value: int 0 1 2 3 4 5 6 7 8 9 ...
#> $ count: int 971 211 818 375 451 258 12 24 17 23 ...Every statistic is an exact function of the value-frequency table, so these are the true national numbers at full resolution — not an approximation from the coarsened raster used for the maps below.
stats <- calc_raster_stats(raster = NULL, counts_df = counts)
knitr::kable(stats, digits = 1, row.names = FALSE)| ID | min | mean | max | q25 | q50 | q75 | prop_zero |
|---|---|---|---|---|---|---|---|
| Arctic Cordillera | 0 | 34.6 | 258 | 15 | 20 | 35 | 0 |
| Atlantic Maritime | 0 | 58.2 | 167 | 34 | 65 | 82 | 0 |
| Boreal Cordillera | 0 | 93.9 | 398 | 42 | 85 | 128 | 0 |
| Boreal PLain | 0 | 71.2 | 331 | 40 | 80 | 97 | 0 |
| Boreal Shield | 0 | 68.7 | 279 | 35 | 76 | 96 | 0 |
| Hudson Plain | 0 | 59.3 | 190 | 30 | 62 | 81 | 0 |
| MixedWood Plain | 0 | 56.9 | 147 | 30 | 64 | 77 | 0 |
| Montane Cordillera | 0 | 118.8 | 548 | 36 | 128 | 173 | 0 |
| Northern Arctic | 0 | 24.1 | 332 | 15 | 20 | 25 | 0 |
| Pacific Maritime | 0 | 194.1 | 535 | 69 | 233 | 291 | 0 |
| Prairie | 0 | 37.8 | 240 | 20 | 35 | 49 | 0 |
| Southern Arctic | 0 | 39.1 | 334 | 15 | 25 | 43 | 0 |
| Taiga Cordillera | 0 | 55.3 | 259 | 25 | 51 | 85 | 0 |
| Taiga Plain | 0 | 62.6 | 359 | 27 | 63 | 91 | 0 |
| Taiga Shield | 0 | 63.4 | 384 | 26 | 57 | 87 | 0 |
Note the quartiles use the inverse-ECDF definition (the smallest value whose cumulative proportion reaches p). For an integer raster with a large point mass at zero, an interpolated definition would report a non-zero median even where the majority of cells are zero.
library(ggplot2)
binned <- counts |>
subset(!is.na(value)) |>
transform(bin = floor(value / 10) * 10) |>
aggregate(count ~ ID + bin, data = _, FUN = sum)
ggplot(binned, aes(x = bin, y = count)) +
geom_col(fill = "steelblue") +
facet_wrap(~ID, scales = "free_y", ncol = 4) +
theme_minimal(base_size = 9) +
labs(x = "stand age (years)", y = "pixel count")plot_raster_stats() combines the histogram, a map and
the statistics into one figure per polygon. Here it is driven from the
precomputed tables — passing counts_df and
stats_df skips the expensive recomputation — and from the
coarsened display raster, so the whole set renders in seconds.
## Write the figures where knitr puts its own, rather than under `tempdir()`:
## pkgdown knits each article in a separate process and then only rewrites
## absolute `<img src>` paths relative to the site -- it never copies the files
## -- so a temp path is both unreachable and already deleted by then, and the
## site build dies with "Failed to realize ... no such file or directory".
## A path under `fig.path` is carried to the output by every knitr front end:
## base64-embedded into the self-contained vignette by `R CMD build`, and
## deployed with the article by pkgdown.
fig_dir <- paste0(knitr::opts_chunk$get("fig.path"), "ecozones")
plot_raster_stats(
raster = display,
polygons = ecozones,
polygon_id = "ZONE_NAME",
counts_df = counts,
stats_df = stats,
aggregate_factor = 1, ## already coarsened
raster_label = "stand age (years)",
inset_canada = TRUE,
## dissolving the ecozones gives the same national outline the inset would
## otherwise download from GADM, so this build stays offline
inset_polygons = sf::st_union(ecozones),
bin_width = 10,
output_dir = fig_dir,
## web-sized rather than the print-sized 12 x 7.5 in @ 300 dpi default
fig_width = 10,
fig_height = 6.25,
fig_dpi = 96
)To run the same summary against the raster at native resolution — or
against ecoprovinces or ecoregions instead of ecozones — see
data-raw/precompute-vignettes.R. It is parameterized by
environment variable:
LANDR_VIGNETTE_SOURCE=SCANFI LANDR_VIGNETTE_YEAR=2020 LANDR_VIGNETTE_VERSION=V2 \
Rscript data-raw/precompute-vignettes.RSCANFI is distributed through a Google Drive folder that
prepInputsStandAgeMap() can only reach with credentials for
it. If you already have a local copy, skip the download by pointing the
script straight at it:
LANDR_VIGNETTE_RASTER=~/data/SCANFI/v2/2020/SCANFI_age_median_2020_v2_20260119.tif \
Rscript data-raw/precompute-vignettes.RProcess large rasters on disk with bounded RAM, and point
tempdir at fast local storage:
Prefer memmax over memfrac:
memfrac is a fraction of free RAM rather than a
hard cap, and memfrac = 0.0 crashes (terra’s block size
collapses to zero). Never point tempdir at NFS, and never
at a tmpfs such as /tmp on systems where that is RAM-backed
— a raster “copied to disk” there really sits in RAM.