rast() - open the GeoTIFF; terraOptions(progress = 0) - silence progress bars
library(terra)
library(here)
terraOptions(progress = 0)
FBUY <- here("dataverse-files", "v2", "FBUY.tif")
stopifnot(file.exists(FBUY))When each 250 m of the United States was first built on, 1810–2020
2026-08-24
HISDAC-US records, for every 250 m cell of the conterminous United States, the year its first structure appears in the property record. One raster, 1810–2020. Because a cell becomes built and stays built, the series is cumulative - which is what lets the whole animation come from a single layer and a moving threshold, rather than from 43 separate rasters.
Opens the GeoTIFF as a SpatRaster - a lazy handle, not 214 million values in memory - and silences terra’s progress bars, which otherwise print a row of pipes under every chunk.
library(terra)
library(here)
terraOptions(progress = 0)
FBUY <- here("dataverse-files", "v2", "FBUY.tif")
stopifnot(file.exists(FBUY))clamp() sends anything outside 1810–2020 to NA. aggregate(fun = "min") reduces 250 m cells to 2.5 km blocks, keeping the earliest year in each - the minimum, never the mean, because averaging build years produces dates that are in no property record. rasterize() burns the state polygons onto that same grid so the two can never drift apart.
r <- rast(FBUY)
r <- clamp(r, 1810, 2020, values = FALSE)
a <- aggregate(r, fact = params$agg_factor, fun = "min", na.rm = TRUE)
states <- rnaturalearth::ne_states(country = "United States of America",
returnclass = "sf")
states <- states[!states$postal %in% c("AK", "HI"), ]
sv <- vect(sf::st_transform(states, crs(a)))
land <- rasterize(sv, a)ifel() is the whole trick: a vectorised if-else evaluated on the raster itself, so each frame is a masked to cells built at or before year y - no loop over cells, no melting to a data frame. breaks pins values to colour bins explicitly; range fails in the 1810 frame, where a single distinct value gives terra nothing to build a scale from. The HUD is drawn on a fresh 0–1 canvas because par("usr") describes the map’s coordinates, not the visible frame.
pal <- colorRampPalette(c("#C2410C", "#DB6A16", "#E9A23B",
"#F2C36F", "#F9DFAB", "#FFF3DC"))(100)
brk <- seq(1810, 2020, length.out = length(pal) + 1)
draw <- function(y) {
op <- par(bg = "#10131A", xpd = FALSE, new = FALSE)
on.exit(par(op), add = TRUE)
plot(land, col = "#1B2130", legend = FALSE, axes = FALSE, mar = c(0, 0, 4, 0))
plot(ifel(a <= y, a, NA), col = pal, breaks = brk, add = TRUE, legend = FALSE)
par(new = TRUE, mar = c(0, 0, 0, 0), xpd = NA)
plot.new()
plot.window(xlim = c(0, 1), ylim = c(0, 1), xaxs = "i", yaxs = "i")
text(0.02, 0.960, "Two centuries of American settlement",
adj = c(0, 1), cex = 1.9, font = 2, col = "#E8E6E1")
text(0.02, 0.920, "Year each cell was first built on, where records exist - HISDAC-US, 1810–2020",
adj = c(0, 1), cex = 1.05, col = "#8A8F9A")
text(0.02, 0.155, y, adj = c(0, 0), cex = 3.4, font = 2, col = "#E8E6E1")
text(0.02, 0.115, "First built", adj = c(0, 0), col = "#A8AEB8", cex = 1.25)
bx <- seq(0.02, 0.26, length.out = length(pal) + 1)
rect(head(bx, -1), 0.075, bx[-1], 0.100, col = pal, border = NA)
text(0.02, 0.040, "1810", adj = c(0, 0), col = "#A8AEB8", cex = 1.3)
text(0.26, 0.040, "2020", adj = c(1, 0), col = "#A8AEB8", cex = 1.3)
text(0.98, 0.040, "Ahn, Leyk, Uhl & McShane (2023) · CC BY 4.0",
adj = c(1, 0), col = "#5C6472", cex = 0.95)
segments(0.02, 0.018, 0.98, 0.018, col = "#2A3140", lwd = 4)
segments(0.02, 0.018, 0.02 + 0.96 * (y - 1810) / (2020 - 1810), 0.018,
col = "#E9A23B", lwd = 4)
}Steps annually rather than every five years. The data is unchanged - still 5-year buckets, each simply held for five frames - but 43 frames at 10 fps was over in 4.3 seconds, before a scrolling viewer had registered it was moving. av_capture_graphics() opens a graphics device, runs the loop, and encodes straight to H.264 without ever writing PNGs to disk.
frames <- c(seq(1810, 2020, by = 1), rep(2020, 30))
invisible(av::av_capture_graphics(
for (y in frames) draw(y),
output = here("settlement.mp4"),
width = 1200,
height = 860,
framerate = params$fps
))draw(2020)Ahn, Leyk, Uhl & McShane (2023), Gridded Settlement Data for the Conterminous United States Over 200 Years, Harvard Dataverse, doi:10.7910/DVN/HHFM5E. Derived from Zillow ZTRAX, Parcel Atlas and BuildZero Open City Model. Dates are when a property record says a structure was built, not when a place was settled.