A major release that wires countryatlas into the database-rendering
world via ‘ggsql’, widens the map vocabulary, and fixes several
correctness issues found by auditing 1.0.0. The version is bumped to
2.0.0 because the bug fixes change the output of
world_map() (quantile binning), bubble_map() /
flow_map() (de-duplicated symbols),
geom_country_labels() (label placement) and
convert_country() (override-only entities) — code that
depended on the old behaviour may see different maps or values.
as_ggsql_source() exports a curated, ISO-reconciled,
WDI-joined table (with sf geometry WKB-encoded) as a ggsql source — a DuckDB connection, a
Parquet file, or a nanoarrow stream. countryatlas does the
reconciliation ggsql’s static bundled world can’t; ggsql does the
database push-down and Vega-Lite output countryatlas doesn’t.world_query() emits a ggsql spatial query
(VISUALISE … DRAW spatial PROJECT TO … SCALE … LABEL …) — a
dependency-free string builder.interactive_map(engine = "ggsql") registers the data
and renders the map in DuckDB, returning a Vega-Lite widget.ggsql, duckdb, DBI and
nanoarrow are optional Suggests. See the new
countryatlas and ggsql vignette.globe_map() — an orthographic globe choropleth, with
backend = "sf" (smoothest limb) or
backend = "polygon" (needs only maps +
mapproj).spin_globe() — a rotating-globe animated GIF (one
globe_map() frame per central longitude, assembled with
gifski or magick).facet_map() — small-multiple choropleths (the static
counterpart to animate_world()).wdj_crs() gains eight projections
(mercator, winkel_tripel,
eckert4, gall_peters,
orthographic, azimuthal_equal_area,
north_polar, south_polar);
world_map() / world_geometry() accept them
all.locate_country() — point-in-polygon lookup tagging
lon/lat with iso3c.repair_country_names() — the “act on it” companion to
check_country_match(): auto-applies confident
string-distance fixes.country_join_all() — reduce-join many messy country
tables on the ISO spine.growth_rate(), index_to(),
share_of_world() — panel analysis helpers.country_overrides() — preferred name for
wdj_overrides() (kept as an alias) after the rename to
countryatlas.country_groups_tbl gains Mercosur,
GCC, Nordic and Visegrad.country_borders() — a tidy adjacency edge list built
from polygon topology (sf::st_touches()), with
neighbors() for a vectorised per-country lookup.distance_between() — great-circle (haversine) distance
between two countries’ centroids; needs neither sf nor the
network.dorling_map() — the Dorling cartogram promoted to a
first-class verb, with k/itermax tuning;
cartogram_map() itself gains ... passthrough
to the underlying cartogram::cartogram_*() call.historical_codes — a curated, dated crosswalk of
dissolved entities (Soviet Union, Yugoslavia, Czechoslovakia, East
Germany, Netherlands Antilles, North/South Yemen, pre-2011 Sudan, United
Arab Republic, Tanganyika/Zanzibar, North/South Vietnam, Serbia and
Montenegro) to their successor states, with retired ISO codes where they
existed. Kosovo is included among the Yugoslav successors on a territory
basis (documented).dissolve_country() — resolve a mixed vector of
historical and modern names to successor iso3c
rows (one-to-many, dated); modern names pass through as single rows, so
a whole messy column pipes in unchanged.check_country_match() gains a historical
column. It flags dissolved entities even when countrycode
“matches” them — the headline case is "USSR",
which countrycode silently resolves to Russia’s RUS, so
Soviet-era data becomes Russian data with no warning.correlate_indicators() — pairwise indicator
correlations on the spine (pearson/spearman, pairwise-complete, per-pair
n), tidy long output.beta_convergence() / sigma_convergence() —
the two standard convergence diagnostics: the growth-on-initial-level
regression (with implied convergence speed and half-life) and per-year
cross-country dispersion.gini() and theil() — inequality across
countries, population-weightable; theil() decomposes
exactly into between/within components when a grouping (continent,
income) is supplied.lag_by_country() / diff_by_country() —
panel lag and difference grouped by iso3c and ordered by
year, completing the panel toolkit around
growth_rate() / index_to() /
complete_years().morans_i() — global Moran’s I with a permutation
pseudo-p-value, computed on the row-standardised
country_borders() adjacency. No spdep
dependency: the weights come from the package’s own curated
topology.spike_map() — triangular spikes at country centroids
(height ∝ value), the overplotting-resistant cousin of
bubble_map(); needs only maps.convert_country() accepts
to = "name_<lang>" ("name_fr",
"name_es", "name_zh", …) for localized country
names via countrycode’s CLDR tables.world_map(style = "binned") legends now show
SI-formatted breaks (4M, not 4e+06) when
scales is installed; the continuous scale uses the same
formatter.bivariate_map() errored on every call (“the condition
has length > 1”, pre-dating 2.0.0). The two fill columns were
injected into biscale::bi_class() with
!!rlang::sym(), but bi_class() reads them with
as.character(substitute(...)) rather than tidy eval, so the
injection deparsed into a multi-element vector inside
biscale. The happy path is now covered by a test (the old
one only checked that the function errors cleanly when sf
is absent).as_ggsql_source() and
interactive_map(engine = "ggsql") errored on any
sf input – the whole point of the ggsql bridge.
sf::st_as_binary() returns a classed WKB
object, which tibble rejects (“all columns must be
vectors”); the geometry column is now the plain list of raw vectors that
nanoarrow encodes as binary and DBI writes as
a BLOB.projection = "winkel_tripel" errored on every render –
one of the eight projections this release adds. The CRS built fine and
the geometry projected fine, but ggplot2::coord_sf()’s
graticule collapses to a degenerate single-point segment under PROJ’s
Winkel Tripel, which GEOS rejects (“point array must contain 0 or >1
elements”). The graticule is now skipped for that projection only;
[theme_world_map()] blanks panel.grid anyway, so nothing
visible changes. All 13 projections are now covered by a full-render
test.world_geometry("coastline", geometry = "sf") errored
with a GEOS TopologyException in every projection except
"plate_carree": a couple of Natural Earth rings are
self-intersecting and sf::st_union() (unlike the spatial
predicates) refuses them. The geometry is repaired before the
union.world_geometry(region = c(xmin, ymin, xmax, ymax)) –
and world_data() / attach_geometry() with a
bounding-box region – errored on the sf
backend (“Loop 0 is not valid”), because sf::st_crop() runs
under the strict S2 engine on unprojected geometry. It now clips with
the GEOS planar predicate, as country_borders() /
locate_country() already did.convert_country()’s warn argument was
documented but silently ignored (every internal
countrycode() call is wrapped in
suppressWarnings(), because countrycode also warns on
intermediate hops that convert_country() goes on to
recover). It now reports inputs that match no country, like
standardize_country() does. A recognised country whose
destination value is genuinely missing still returns NA
quietly.world_map() / globe_map()’s
na_label was accepted and silently ignored. The
"quantile", "jenks" and
"categorical" legends now label their missing-data key with
it (the continuous and binned colourbars have no NA key to
name, which the documentation now says).XKX resolves for country and
flag from from = "iso3c", not just from its
name. It has no row at all in countrycode::codelist, so
everything derived from the code was NA – which surfaced as
country_borders() / neighbors() returning
NA names for Kosovo’s four land borders,
locate_country(add = "country") returning NA
for points inside it, and
standardize_country(add = c("country", "flag")) doing the
same. The curated fallback table now carries the name and flag too.per_capita() without an explicit pop
column died with an opaque vctrs error (“Can’t subset
columns that don’t exist: .wdj_pop”) when the World Bank
population fetch failed or timed out – fetch_wdi()
deliberately degrades to a keys-only tibble in that case. It now reports
the failed fetch and points at the pop argument.theil() returns NA shares (not
NaN) for a perfectly equal distribution, where the total is
0 and the shares are undefined – matching how
gini() and share_of_world() treat a zero
denominator.country_join() / country_join_all() no
longer cross-join rows whose iso3c is NA:
unmatched countries used to collapse to a single NA key and
fan out into a Cartesian product. The joins now pass
na_matches = "never" (#4).country_join_all() validates the length of
origin (must be 1 or one per table) instead of failing with
a cryptic “missing value where TRUE/FALSE needed” error (#16).join_world()’s auto-detection
(detect_country_col()) honours the candidate priority order
instead of picking the first column by data-frame position, so a
region column no longer shadows a real country
column (#6).standardize_country(add = ...) accepts any raw
countrycode destination (e.g. "iso3n") again
instead of erroring with “subscript out of bounds” (#5).standardize_country(origin = "iso3c") now validates
codes: strings that are not real ISO 3166-1 alpha-3 codes become
NA (and are flagged by warn) rather than
passing through uppercased and unchecked (#12).country_data(latest = TRUE) /
world_data(latest = TRUE) for a single year now returns
each country’s most recent non-NA value: the fetch window
is widened so an earlier observation can actually be found (#7).fetch_wdi() keeps iso2c /
country for a country that appears only in a non-first
indicator (they are coalesced across indicators) instead of leaving them
NA (#8).world_map() / globe_map() with
style = "quantile" / "jenks" no longer error
on a constant, single-country, or all-NA value column;
degenerate breaks now fall back to a single bin (#9).flow_map() returns the base map (instead of erroring)
when no origin-destination pair resolves to a centroid (#10).aggregate_regions(fun = "min"/"max") returns
NA for an all-NA group instead of
Inf / -Inf (#11).share_of_world() returns NA (not
NaN/Inf) when the (per-year) total is zero or
non-finite (#13).gini() returns NA with a warning for
negative input rather than a value outside the documented
[0, 1] range (#14).spike_map() no longer produces NaN spike
coordinates when every height is zero (#15).world_query() honours transform even when
palette = NULL, emitting a standalone
SCALE fill VIA <transform> clause (#17).world_map(style = "quantile"/"jenks") computed breaks
over polygon vertices, so a country’s geometric
complexity biased the quantiles and the bins held unequal numbers of
countries. Breaks are now computed on one value per country.bubble_map(backend = "sf") placed bubbles in projected
metres on a degrees base map (off the map). The base map and bubbles now
share one projected CRS via coord_sf().iso3c codes (overrides map several names — Azores/Madeira →
PRT — to one code), fanning out joins in bubble_map() /
flow_map(). Centroids are now one antimeridian-safe row per
country (the largest piece).geom_country_labels() placed labels at the bounding-box
midpoint over all of a country’s pieces, so the US / Fiji / NZ labels
drifted into the wrong ocean. Labels now sit on each country’s largest
piece.projection = "plate_carree" built an incoherent PROJ
string (+proj=longlat … +units=m); it is now true
equirectangular (+proj=eqc).convert_country() only applied
wdj_overrides() for to = "iso3c", so
override-only entities (e.g. “Canary Islands”, “Azores”, “Bonaire”)
returned NA for every other destination (continent, region,
iso2c, flag, currency, country name, …). It now resolves the
override-corrected iso3c first and derives every other
destination from that.XKX needed extra care: it has no row at all in
countrycode::codelist, so deriving destinations purely via
the iso3c round-trip above is NA for
everything — which would have regressed
flag/region/country, since 1.0.0
already resolved those via direct name matching (verified against the
actual 1.0.0 code). convert_country() now recovers from the
original name when the iso3c round-trip comes back empty,
and fills iso2c/continent (which neither path
classifies) from the same curated fallback
standardize_country() uses. Net effect versus 1.0.0: zero
regressions, plus newly-working
continent/iso2c for Kosovo — which also fixes
locate_country(..., add = "continent") for points inside
it.interactive_map(..., tooltip = ) was accepted but
silently ignored by every engine (pre-dating 2.0.0). The
"ggiraph" and "leaflet" engines now use the
supplied tooltip column, defaulting to fill as
before when omitted.world_data(overrides = ) (and
attach_geometry(overrides = )) accepted a custom name ->
iso3c override set but silently ignored it (pre-dating 2.0.0) – the
geometry backend always matched with the default
wdj_overrides(). The override set now flows through to both
the polygon and sf matchers, so a custom mapping actually
changes which polygons a country claims.repair_country_names() no longer records a no-op
“repair” when a dissolved entity’s own name (e.g. “Yugoslavia”, which
exists in the codelist but has no ISO code) comes back as its closest
suggestion; dissolve_country() is the right tool there and
is what the report now points to.ggplot2 as a bare “object ‘x’ not found”
from inside a layer, or out of vctrs as a subscript error.
world_map(), globe_map(),
facet_map(), tile_map(),
bubble_map() (both size and
color), spike_map(), flow_map()
(from, to and weight),
interactive_map() (fill and
tooltip) and morans_i() all validate up front,
matching the message per_capita() /
rank_countries() / bivariate_map() already
gave. morans_i() in particular used to blame the geometry
(“not enough bordering countries with data”) for a column that simply
wasn’t there.audit_coverage(indicator = ) silently reported
n_missing = 0 and na_rate = NaN for a column
name that isn’t in data; it now errors.world_map() / globe_map() errored
(“‘length = 2’ in coercion to ‘logical(1)’”) when na_label
was longer than one element. The first element is used to label the
single NA key, and a NULL / NA
label still leaves the default formatter alone.per_capita() sent start = Inf to the World
Bank when data had a year column that was
entirely NA; it now falls back to last year, as it already
did for a frame with no year column at all. Its
degraded-fetch guard also covers the join keys now, so a partial
population fetch produces the actionable “pass a population column”
error rather than a raw vctrs subscript error.theil() returned NaN when every weight was
zero; it now returns NA, matching gini().world_map() / globe_map() with
style = "categorical" and a numeric fill
column let ggplot2 raise “Continuous value supplied to a
discrete scale” at build time, naming neither the column nor
the style. They now error at the call, name the column, and point at
"quantile" / "jenks" /
"binned".bivariate_map() no longer leaks biscale’s
“var has missing values, omitted in finding classes” warning, which
fired on essentially every call because real indicators always have gaps
(the classes were valid either way). Any other biscale
warning still passes through.region given as lowercase iso3c codes
silently lost countries. Falling through to name matching resolved some
codes by accident (countrycode’s country-name regex is case-insensitive,
so "usa" matched) but not others ("can" did
not), so region = c("usa", "can") subset to the USA alone.
Codes are now recognised in any case, matching what
standardize_country(origin = "iso3c") already accepted; an
all-uppercase unknown code is still taken at face value rather than
reinterpreted as a country name.country_codes() silently dropped a column name it did
not recognise, so a typo returned a table quietly missing that column;
it now errors and lists the available shortcuts.sf-backed call printed three or more lines of
sf internals to the console –
"Spherical geometry (s2) switched off",
st_intersection’s
"although coordinates are longitude/latitude ... assumes that they are planar",
and the matching "switched on". The source was
sf::st_break_antimeridian(), which toggles the s2 engine
and runs an intersection internally, and which sits on the path of
every sf call: a plain
attach_geometry(geometry = "sf") emitted them, as did
world_map(), world_geometry(),
country_borders(), neighbors(),
morans_i(), locate_country() and
simplify_geometry(). It is now wrapped in the same
quietly_sf() helper the other sf calls already
used, so those paths are silent. (The notices bypass R’s condition
system, so suppressMessages() could not have caught
them.)cartogram_map() / dorling_map() never
validated their weight or fill column, the one
place the rest of the package’s existence checks were missed. A bad
weight reached cartogram as
"missing value where TRUE/FALSE needed" (or, for the
Dorling variant, a warning about max() and then a wrong
picture), and a bad fill was not caught at all.index_to() likewise never checked its value column, so
a typo produced a dplyr error from inside
mutate(); base_year and to are
validated too.NA
names the argument instead of surfacing as
"missing value where TRUE/FALSE needed",
classInt’s "n less than 2", or a
PROJ complaint about lat_0. Covers
n_bins (world_map() /
globe_map(), on the binned path as well as the
quantile/jenks one),
lon/lat/recenter/lat0
(globe_map() on both backends – the
polygon one goes to coord_map() and previously accepted a
nonsense orientation silently), n
(flow_map()), max_height / width
/ alpha (spike_map()), max_size /
alpha (bubble_map()), keep
(simplify_geometry()), threshold
(repair_country_names()), n_perm
(morans_i()), and n_frames / fps
/ width / height
(spin_globe()).max_height drew
spike_map()’s spikes upside down, and
globe_map(lat = ) beyond +/-90 built a CRS
PROJ rejects, which only surfaced later as
coord_sf()’s
"crs not found: is it missing?".geom_country_labels(repel = TRUE) silently drew plain
labels when ggrepel was not installed – the one degraded
optional backend the package did not announce, where
classInt, gganimate and
rmapshaper all report theirs. It now says so once per
session (the argument defaults to TRUE, so reporting on
every call would be noise), and stays quiet when
repel = FALSE was asked for.classInt is installed. For a fractional
n_bins, classInt truncated internally while
the base-quantile fallback passed the fraction to
seq(length.out = ) and produced one break more, so the same
call binned differently in different environments. n_bins
is now truncated to a whole number of bins before either backend sees
it.simplify_geometry(keep = 0) errored under
rmapshaper but was silently accepted by the
sf::st_simplify() fallback, so the same call behaved
differently depending on which optional package the caller had
installed. A proportion of zero keeps no vertices, and both paths now
reject it.as.integer(), which returns NA past
2^31-1 – so n_perm = 1e10 or
n_bins = 1e10 produced “NAs introduced by coercion” or,
worse, “missing value where TRUE/FALSE needed”. n_bins,
n (flow_map(), lag_by_country(),
diff_by_country()), n_perm and
n_frames now name the range. morans_i() also
dropped a max(0L, ...) clamp that the validation had made
unreachable.lag_by_country() / diff_by_country()
clamped n <= 0 up to 1, so a lag of
0 quietly returned a lag of 1; it now
errors.complete_years(value = ) silently ignored a column name
that wasn’t in data under the default
method = "none", while erroring from all_of()
for "locf" / "linear"; it now errors
consistently.interactive_map(engine = "ggsql") now gates on
ggsql >= 0.4.1 rather than mere presence.
DRAW spatial – the clause world_query() emits
– arrived in the ggsql engine at 0.4.0, while the ggsql R
package is still 0.3.3, which accepted the call and then failed inside
its own SQL front end on a clause it did not know. The gate now refuses
with an actionable message instead. ?world_query records
that the clause has shipped in the engine but not yet in the R bindings,
and that PROJECT TO additionally needs a spatial backend
(for DuckDB, its spatial extension);
world_query() itself remains a dependency-free string
builder.R CMD check no longer writes to the checking user’s
persistent cache. The \donttest{} examples fetch from the
World Bank, so the memoised on-disk cache was being populated under
tools::R_user_dir() during a check; under check it now
lives in the session temp directory instead. Normal use is unchanged,
and options(countryatlas.cache_dir = ) still overrides
both. ?clear_wdi_cache now documents where the cache lives
and how to disable it.na_matches = "na", so an
NA ISO code matched another NA ISO code – and
Natural Earth carries Somaliland as a polygon with no ISO code. Any row
whose country failed to resolve therefore joined onto Somaliland’s
geometry and was plotted there; with two or more unmatched rows the join
also fanned out many-to-many, duplicating that polygon once per row so
the visible fill was whichever happened to be drawn last. Affected
attach_geometry(geometry = "sf") (and so
join_world() and world_map() downstream of it)
and bubble_map(backend = "sf"). All country-keyed joins in
the package now pass na_matches = "never", which
country_join() and country_join_all() already
did; the keyless polygon is still drawn, now correctly as a no-data
feature. A test asserts the invariant across the whole namespace so a
new join cannot reintroduce it.world_query() emitted a silently malformed query for
any argument that was not a single string. sprintf()
vectorises, so projection = c("a", "b") produced two
PROJECT TO clauses, source = character(0)
deleted the FROM line entirely, and title = NA
became the literal text 'NA' – each of which surfaced only
later, as a parse error inside ggsql’s SQL front end.
world_query() and as_ggsql_source() now
validate their string arguments up front and name the offending one.
NULL still omits an optional clause, and an empty
title is still allowed.as_ggsql_source(format = "parquet") built its
COPY ... TO '<path>' statement by string
interpolation, so a path containing an apostrophe – legal in a filename
– closed the SQL literal early and broke the statement. The path is now
quoted with DBI::dbQuoteString(), matching the
dbQuoteIdentifier() treatment the table name already
had.suffix = character(0) made the whole computation
vanish. suffix is paste0()-ed onto the value
column’s name, and dplyr’s "{character(0)}" := is a silent
no-op – so growth_rate(x, g, suffix = character(0))
returned x unchanged, with no growth column and no error.
suffix = NA produced a column named gNA, and
suffix = "" overwrote the source column in place.
per_capita(), growth_rate(),
index_to(), share_of_world(),
lag_by_country() and diff_by_country() now
require a single non-empty string
(lag_by_country()/diff_by_country() still
accept NULL for the default suffix).convert_country(to = c("country", "continent")) – a
plausible attempt at two destinations – raised “the condition has length
> 1”, and a zero-length to or from raised
“argument is of length zero”.origin did the same across every function that resolves
country names. It is now validated once in the shared internal, so
standardize_country(), country_join(),
country_join_all(), join_world(),
flow_map(), neighbors(),
distance_between(), in_group(),
repair_country_names() and
check_country_match() are all covered.attach_geometry(by = character(0)) raised “argument is
of length zero”.aggregate_regions(by = character(0)) raised nothing at
all: it grouped by no columns and silently collapsed the world into a
single row. by remains documented as plural, so multiple
grouping columns still work.borders argument of world_map() and
globe_map() fed a bare if (), so a bad value
raised one of four opaque base R errors (“missing value where TRUE/FALSE
needed”, “argument is of length zero”, “the condition has length >
1”, “argument is not interpretable as logical”) – none naming
borders. Elsewhere the value went through
isTRUE(), which never errors but silently turns anything
that is not TRUE into FALSE, so the caller got
the opposite of what they asked:
rank_countries(x, v, desc = "yes") ranked ascending,
putting the lowest value at rank 1, and
gini(x, na.rm = "yes") kept the NAs and
returned NA. All 23 logical arguments across the package
now require TRUE or FALSE and name themselves
when they do not get it.gini() and theil() returned a wrong number
for a wrong-length weights vector. Both recycled it with
rep_len(), which accepts any length silently, so
gini(1:10, weights = c(1, 2)) returned 0.2902
– computed from an alternating 1,2 pattern – where the
correctly-weighted answer is 0.3. Someone weighting by
population and mistakenly passing a vector of the wrong length got a
plausible figure and no indication anything was wrong.
weights (and theil()’s groups)
must now be length 1 or the length of x, and
weights must be numeric. complete_years()
likewise rejects a years vector that is non-numeric, empty,
or contains NA, all of which it previously coerced to
NA behind base R’s warning.rlang::as_name(), which
raises argument "x" is missing, with no default for a
missing value – naming rlang’s own parameter, and none of these
functions has an argument called x.
world_map(), tile_map(),
facet_map(), spike_map(),
bubble_map(), globe_map(),
interactive_map(), rank_countries(),
growth_rate(), per_capita(),
aggregate_regions(), index_to(),
share_of_world(), lag_by_country(),
diff_by_country(), beta_convergence(),
sigma_convergence(), morans_i(),
world_query(), country_join(),
bivariate_map(), cartogram_map() and
flow_map() now report e.g. `fill` is required.
Optional tidy-eval arguments are unaffected.distance_between() paired the wrong countries when
a and b had mismatched lengths. It combined
them through vectorised arithmetic, so R’s recycling applied: 2
countries against 3 returned a[1]-b[1],
a[2]-b[2] and
a[1]-b[3], behind only base R’s “longer object
length is not a multiple” warning, and 2 against 4 recycled cleanly with
no warning at all. Equal lengths, or a length-1 side for
one-against-many, are now required – the same rule
locate_country() has always enforced for
lon/lat.?locate_country said lon/lat were
“recycled together” when the function has always required equal lengths,
and ?gini / ?theil promised that
weights was “recycled against x the usual R
way”, which is precisely the behaviour removed above. All three now
describe what the functions do. locate_country()’s length
error also read “or an points sf object”.\donttest{} examples and rebuild vignettes, and CRAN
policy does not allow either to fail for want of a network connection.
world_data() and country_data() degrade a
failed fetch to a warning and a metadata-only frame, and
wdi_search() reads WDI’s bundled indicator
list rather than the API; all three are now asserted, so a change that
made any of them require a connection would break the suite.memoise::cache_filesystem() does not validate the directory
it is given: it constructs successfully and only fails when something is
written, which happens deep inside the fetch. So with a
read-only or otherwise unusable cache location,
country_data(cache = TRUE) reported
Could not fetch indicator "..." from the World Bank API and
returned the country spine with every indicator NA –
blaming the API for a local permission problem, while the same call with
cache = FALSE returned the data perfectly. (The
tryCatch() that was meant to fall back never fired, because
constructing the cache never errored.) The directory is now checked
before use, with a fallback to session-only caching and a one-time
message naming the real cause.options(OutDec = ",") – the ordinary setting in
comma-decimal locales. The PROJ strings are built by pasting numbers, so
recenter = 48.9 became +lon_0=48,9, which PROJ
rejects; the invalid CRS then surfaced as sf’s opaque “crs not found: is
it missing?”. Every number destined for a machine-readable string is now
formatted with an explicit decimal mark.options(scipen = -10), which formats a double in
scientific notation: sf::st_crs(4326) became
EPSG:4.326e+03 and yielded an NA CRS
(surfacing later as st_crs(x) == st_crs(y) is not TRUE from
locate_country()), and Natural Earth’s scale
110 became 1.1e+02, so
world_geometry(geometry = "sf") failed with
'countries1.1e+02' is not an exported object. Every EPSG
code and Natural Earth scale is now an integer literal, which
scipen does not affect.simplify_geometry() and
world_geometry("graticule") are insulated from two upstream
bugs of the same family, both reproducible without this package:
rmapshaper serialises keep for V8, which
rejects the 0,1 that options(OutDec = ",")
produces, and sf::st_graticule() overflows the node stack
under options(scipen = -10). Both calls now run with those
two options normalised, and the caller’s settings are restored
immediately afterwards.rank_countries() silently ranked within groups when
handed a grouped frame. Its mutate() honoured the caller’s
group_by(), so the same data ranked 4, 1, 3, 2
ungrouped and 2, 1, 2, 1 after an incidental
group_by(region) upstream in the pipe – with
within = NULL in both cases, which documents a global
ranking. rank, percentile and
z_score were all affected. within is now the
only thing that sets the ranking scope, matching every other function
here, which imposes its own grouping rather than inheriting the
caller’s. A test asserts that a grouped input changes no answer, and
that nothing leaks grouping into its return value.read.csv()
on a column with one stray non-numeric entry gives you one – and
arithmetic on it failed four different ways: growth_rate()
and per_capita() returned a column of NAs
behind base R’s “‘/’ not meaningful for factors”;
share_of_world(), rank_countries() and
aggregate_regions() raised an opaque error from inside
dplyr::mutate(); gini() managed “missing value
where TRUE/FALSE needed”; and morans_i() quietly returned a
plausible-looking statistic. These, plus index_to(),
diff_by_country(), beta_convergence(),
sigma_convergence() and theil(), now name the
column and its actual type. lag_by_country() is
deliberately unchanged: it does no arithmetic, so lagging a factor or
character column remains legitimate.aggregate_regions() reported a figure for groups it had
no data for. Values are dropped before aggregating, so a group whose
every value is missing had nothing left – and each base function got
that wrong differently: "sum" returned 0,
"mean" and "weighted_mean" NaN,
and "min"/"max"
-Inf/Inf plus a warning. “This region’s total
is 0” is a claim, not an absence, which matters in a package built
around honest missing-data handling. All six now return NA,
as "min"/"max" were already meant to; groups
that do have data are unaffected, and a partially-missing group still
aggregates the values it has. ?aggregate_regions documents
this.complete_years() failed on a zero-row panel, where
every other panel helper returns zero rows. With no years
it reached seq(min(numeric(0)), max(numeric(0))) and died
on base R’s “‘from’ must be a finite number”; with years
supplied it died on tidyr’s “Can’t recycle year (size 3) to
size 0”. Neither message names anything the caller did. It now returns
the empty frame, columns intact, for all three method
values – while still reporting a bad years or
value argument.bivariate_map() and cartogram_map() (and
so dorling_map()) failed inside their optional dependency
when no row carried the values they need. This is easier to hit than it
sounds: attach_geometry() joins geometry-on-the-left, so a
frame with nothing in it arrives at the plotting verb as full-length
columns of NA. biscale then indexed
sVar[1:(length(sVar) - 1)], which becomes
1:-1, and reported “only 0’s may be mixed with negative
subscripts”; cartogram compared NA in
if (meanSizeError < maxSizeError) and reported “missing
value where TRUE/FALSE needed”. Neither mentions the data. Both now say
which columns are empty, as spike_map() already did, and
both reject a non-numeric column by name. Partly-missing columns still
draw from the rows that do have values.gini() could kill the R session. It computed the
weighted mean absolute difference with outer(), an n-by-n
matrix – fine for the ~200 countries it is written for, but it is
exported and accepts any numeric vector. A geometry-joined column is
99,338 rows, needing about 79 GB, and the process was killed outright:
no error, no message, no result. The kernel is now the sorted cumulative
form, O(n log n) in time and O(n) in memory, which agrees with the
pairwise definition to floating-point noise (verified across ties, zero
weights, single values and 340 random cases) and handles a million
values in well under a second. Every documented figure is
unchanged.aggregate_regions() silently multiplied its answer when
given a frame with map geometry attached. The polygon backend expands
each country into hundreds of vertex rows, so a row-wise total counts it
once per vertex: for the bundled snapshot a regional total of 497,265
came out as 280,951,373. It is reachable directly off
world_data(geometry = "polygon"). It cannot de-duplicate on
iso3c, since by = c("region", "year") roll-ups
legitimately repeat a country, so it now warns and says what to do
instead. Country-level tables and panels are unaffected.?audit_coverage described the raw list it returns
without mentioning that the object is classed and has a
print() method, so what you actually see at the console is
a formatted report rather than the list. Both are now documented.world_geometry("ocean") drew nothing at all, in every
projection. Under the S2 engine – sf’s default since 1.0, so everywhere
– st_as_sfc(st_bbox(-180, -90, 180, 90)) collapses to a
two-point, zero-area polygon, and the collapse is invisible because
st_bbox() reports the stored extent instead of recomputing
it from the (empty) coordinates. The rectangle is now built by
constructing the ring explicitly, which S2 never gets to reinterpret,
and its edges are densified so a curved projection has points to bend:
the layer comes out at Earth’s true surface area (5.1e14 m2) and covers
98-100% of the countries layer across all nine world projections.
st_break_antimeridian() is no longer applied here at all –
with lon_0 = 0 it cut the outline at +/-180, its own edges,
taking it down to two thirds of the globe and, under Mollweide, to
nothing.world_geometry() now says so instead of returning an
invisible layer: the four hemispheric projections
("orthographic", "azimuthal_equal_area",
"north_polar", "south_polar") show half the
globe, and a whole-globe rectangle cannot be recentred without covering
only part of the map.world_geometry("coastline") and
world_geometry("ocean") returned a bare sfc
rather than the sf object ?world_geometry
promises (and that the other four what values deliver), so
dplyr verbs failed on exactly those two. Both are now sf;
the geometry is unchanged.?world_geometry’s @return was a single
line naming no columns. It now lists what each what
returns, and warns that the sf backend’s
centroid_lon/centroid_lat are in the object’s
own CRS – projected metres, not degrees, so centroid_lon
for France is 174097, not 2.1. Use
country_meta$centroid_lon for degrees.sf::st_coordinates() failed on
world_geometry("countries", geometry = "sf"), in every
projection including the default. Natural Earth supplies 177 uniform
MULTIPOLYGONs, but st_break_antimeridian()
runs an st_intersection() internally that collapses a
single-part MULTIPOLYGON to a POLYGON, leaving
148 POLYGON + 29 MULTIPOLYGON – an
sfc_GEOMETRY column, which st_coordinates()
does not support. Extracting vertices from the package’s own geometry,
an ordinary thing to want, therefore errored with “not implemented for
objects of class sfc_GEOMETRY”. The column is cast back to
MULTIPOLYGON, which is a type change only: row count, codes
and land area are unchanged. ?world_geometry also now notes
that a hemispheric projection returns empty geometries for the far side,
where the same st_coordinates() limitation applies for a
different and correct reason.world_map() accepted a frame with no geometry, returned
a ggplot object without complaint, and then failed only
when the plot was printed – with ggplot2’s “Problem while computing
aesthetics … Caused by error in .data$long”, which names
nothing the caller did. It validated the fill column but
never the long/lat/group columns
the polygon path needs. Forgetting attach_geometry() is the
easiest mistake to make here, and it is easy precisely because the other
plotting verbs do not need it: tile_map(),
bubble_map(), spike_map() and
globe_map() all take a country-level frame, so
world_map(snap, gdp) looks like it should work too. It now
says so at the call, and names the fix. facet_map()
delegates to world_map() and is covered by the same check;
the four country-level verbs are deliberately unchanged, and a test pins
that asymmetry.interactive_map(engine = "ggiraph") reported a missing
geometry differently from the other engines. It assembles its own
ggplot rather than calling world_map(), so it
bypassed that check and failed at render time on
.data$long, while engine = "plotly" named the
problem properly. Both now give the same message.
?interactive_map also documents that the
"leaflet" engine attaches geometry itself if handed a
country-level table, which the others do not.style = "continuous" and "binned" reached
ggplot2 and failed only when the plot was printed (“Discrete value
supplied to a continuous scale”, “Binned scales only support continuous
data”), neither naming the column; "quantile" and
"jenks" did not fail at all – the break computation returns
early on a non-numeric column, so the fill fell through to the discrete
scale and drew a plausible map whose legend claimed quantile bins it had
never computed. The reverse direction,
style = "categorical" on a numeric column, was already
guarded, so this closes the pair. animate_world() and
facet_map() inherit it.attach_geometry() no longer warns when given a panel.
Joining one row per country-year against polygon vertices is
legitimately many-to-many – it is what animate_world() and
facet_map() are for – so dplyr’s “unexpected many-to-many
relationship” warning was noise. The relationship is now declared, as
the cache merge already did.?repair_country_names now says which way the
stringdist fallback errs. The threshold
argument already noted that the metric changes when
stringdist is absent (Jaro-Winkler versus a
length-normalised edit distance); it now adds that the fallback is the
more conservative of the two, repairing a subset of what Jaro-Winkler
would – mainly missing transposed letters, as in “Frnace” – and never
choosing a different country. Measured over 120 single-typo names: 98
repaired with stringdist, 77 without, none repaired that
stringdist did not, and no wrong repairs either way. A test
now holds the package to that.simplify_geometry() undid the geometry-type fix above.
Both simplifiers collapse a single-part MULTIPOLYGON to a
POLYGON, so a homogeneous input came back as a mixed
sfc_GEOMETRY column and sf::st_coordinates()
failed on the result – the same defect as
world_geometry("countries"), restored one step downstream.
The output is cast back; row count and geometry are unchanged.simplify_geometry()’s keep argument now
means roughly the same thing with and without rmapshaper.
The sf::st_simplify() fallback was given a fixed
dTolerance of (1 - keep) * 10000, i.e. metres
whatever the coordinate system: 9 km on a projected frame, which barely
simplified anything (79% of vertices kept at keep = 0.1),
and 9000 degrees on a lon/lat frame, which is meaningless and
only survivable because preserveTopology keeps a husk. The
tolerance is now scaled to the object’s own extent, so the fallback
behaves the same on either coordinate system and responds monotonically
to keep. ?simplify_geometry says that only
rmapshaper honours keep as a true
proportion.gini()’s negative-value warning said
Returning "NA", which reads as the two-character string
rather than the missing value. It now renders as NA,
matching the same correction already made elsewhere.options(scipen = -10) to exercise the scientific-notation
bugs fixed above, but R 4.6 clamps scipen to a minimum of
-9 and warns (“invalid ‘scipen’ -10, used -9”), so an exact round-trip
assertion failed and three warnings were raised – an ERROR
under R CMD check on current R, even though the package
code itself was correct. The tests now use -9 and compare against the
value R actually stored.wdj_overrides() soft-deprecation notice told the
wrong people. It lived in the shared function body, so in an interactive
session it fired for country_overrides() – the very
replacement it recommends – and for every public function that takes the
override table as a default argument
(standardize_country(), convert_country(),
attach_geometry(), check_country_match(),
repair_country_names(), world_data() and the
geometry backends). Callers were advised to stop using a function they
had never written, and the advice was unactionable. The notice now fires
only for a direct call to wdj_overrides(); the table itself
is unchanged. The documented default is now
country_overrides(), so ?attach_geometry and
friends name a function the reader can actually look up.?country_borders’s whole-world example runs again. It
was wrapped in \dontrun{} on the grounds that the
whole-world adjacency is expensive, but it takes about a quarter of a
second from a cold session – only 2.7 times the
region = "Europe" subset that already ran live. CRAN
discourages \dontrun{} for code that can be executed, so it
is now a guarded \donttest{} and is actually exercised by
the check. That leaves six \dontrun{} topics, each
genuinely unrunnable: a live World Bank fetch, an HTML widget, a DuckDB
connection, a 60-frame GIF, and a call that deletes files.?country_overrides’s advice for accented names in a
non-UTF-8 locale did not work in that locale. It offered de-accenting
with iconv(x, to = "ASCII//TRANSLIT") as an alternative to
running under UTF-8, but //TRANSLIT is itself
locale-dependent: under LC_CTYPE=C it returns
NA, or replaces each accent with ? when given
an explicit from = "UTF-8", so nothing resolves either way.
The section now says de-accenting has to happen while still in a UTF-8
locale, and that the ASCII spellings the override table carries are what
work everywhere.options(countryatlas.workers) reached
mclapply(). The option is advertised in this file, so a
stray value is reachable: "abc", NA and
Inf all became NA workers and surfaced as
“missing value where TRUE/FALSE needed” from deep inside a parallel
fetch, while c(2, 4) silently used the larger of the two.
It is now checked, and the message names the option. Values below one
are still clamped to one, as before – that path was never the
problem.options(countryatlas.cache_dir) did the same
thing one layer down. The option is documented in
?clear_wdi_cache, and a stray value reached
dir.exists()/dir.create(): NA, a
number and TRUE each gave “invalid filename argument”,
character(0) gave “argument is of length zero”, and a
two-element vector gave “the condition has length > 1”. It is now
checked and the message names the option. An empty string is still
accepted and still degrades to session-only caching;
NA_character_, which used to degrade silently, now errors
like the other bad values.dir.create("") warns and returns FALSE on R
4.4, so the fallback to session-only caching worked; on R 4.6 it
errors with “zero-length ‘path’ argument”, which escaped the
surrounding suppressWarnings() and propagated. An empty
path is now recognised as “no disk cache” before the filesystem is
touched, so the behaviour is the same on every R version.?theil now says that a row with a missing
group is dropped along with rows whose value is missing, so the
decomposition’s total is computed over the grouped subset
and can differ from the ungrouped theil(x). On the bundled
snapshot that difference is entirely Puerto Rico, which has no
region. theil() also gains numeric anchors on
the bundled data, which gini() already had.sf-backed verb leaked sf’s internal
chatter as message() conditions. The console was already
clean, but silencing it by redirecting the message stream
leaves the conditions themselves travelling to whatever handler the
caller installed, so purrr::quietly(),
testthat::expect_silent() or a plain
withCallingHandlers() around
attach_geometry(), neighbors(),
country_borders(), locate_country() or
morans_i() still saw three to nine “Spherical geometry (s2)
switched off” / “assumes that they are planar” notices. They are now
muffled as well as redirected. (The comment claiming these notices
bypass R’s condition system was simply wrong – they are ordinary
message()s, and only a few GDAL diagnostics need the stream
redirect.)?world_geometry called all four azimuthal projections
“hemispheric” and said the far side comes back as empty geometries. That
is true of "orthographic" alone;
"azimuthal_equal_area", "north_polar" and
"south_polar" are Lambert equal-area and image the
whole globe, with the far side stretched around the rim and
nothing dropped. The page now distinguishes them and points at
region for a genuine polar view, and the error
"ocean" raises in those projections no longer gives “it
shows one hemisphere” as the reason.?world_geometry now documents the Natural Earth
features that have no ISO code and so come back with iso3c
NA – Somaliland at every scale, plus the Indian Ocean
Territories and Ashmore and Cartier Islands from "medium"
on.?theme_world_map said the theme is used by all
the package’s plotting functions. bivariate_map() is the
exception – it applies biscale::bi_theme() so the map
matches biscale’s own legend, and its axis titles, panel grid and
background differ as a result. The page now names the exception.?simplify_geometry documented keep as a
proportion “(0-1)”, but keep = 0 is rejected on both
simplifier paths (it would leave nothing to draw). The range now reads
“greater than 0 and at most 1”.complete_years(value = ) fabricated data in the columns
it was not given. A numeric column left out of
value was classified as a static attribute and
carry-filled, so naming fewer columns invented
more figures – and even method = "none",
which exists to complete the grid and fill nothing, produced a
carried-forward value for the missing year. Measure columns are now
excluded from the attribute carry whether or not they are named; an
unnamed one stays NA in the rows complete()
adds. value = NULL behaves exactly as before.aggregate_regions() silently ignored
weight for every fun except
"weighted_mean", returning the unweighted figure – on
European GDP per capita, 38,323 where the population-weighted answer is
29,896. Passing weight with any other fun now
errors, mirroring the existing abort when
fun = "weighted_mean" is given no weight.flow_map() dropped a flow whose endpoint it could not
resolve without a word, and when nothing resolved it returned a bare
world map with no arc layer at all. It now warns, naming the values it
could not place and pointing at origin – feeding it
iso3c codes while origin still defaults to
"country.name" is the usual cause, and it drew a blank
map.?in_group now says that a value origin
cannot resolve answers FALSE, indistinguishable from a
country that is genuinely outside the group, and points at
check_country_match() for telling the two apart.world_map() / globe_map() passed
palette, title, legend and
na_label to viridisLite and
ggplot2 unchecked, so a mistake came back in their
vocabulary rather than the package’s:
palette = c("magma", "viridis") reached a bare
switch() and reported “EXPR must be a length 1 vector”, and
a numeric palette was accepted without a word. A length-2
title or legend was accepted too, and
ggplot2 then drew both strings on top of each other. All
four are now checked; world_query() already validated the
two of them it takes (palette and title).
na_label keeps its documented tolerance – there is one
NA key, so the first element wins and a length-1
NA still means “leave the default formatter alone” – but it
now says so instead of doing it silently. A number is still a perfectly
good label.country_data() / world_data() resolved a
conflict between latest and the shape arguments silently,
and with opposite precedence depending on the year: a multi-year
year overrode latest = TRUE, while a single
year had latest = TRUE override panel = TRUE.
The winner is unchanged – both were already the documented behaviour –
but the call now warns, naming the argument being dropped, instead of
returning a shape nobody asked for.locate_country(tolerance_km = ) was unvalidated, and a
character value did not merely give an opaque message – it produced a
wrong answer. R compares
dkm <= tolerance_km as strings when the tolerance is
character, and "2650" <= "a" is TRUE, so
every unmatched point snapped to its nearest country however far away it
was: a mid-Pacific point came back as Fiji, where the documented
behaviour is that open ocean stays NA. Now checked, before
the sf gate.correlate_indicators(min_n = ) (an NA gave
“missing value where TRUE/FALSE needed”, a length-2 value “the condition
has length > 1”), and dorling_map(k = ) /
dorling_map(itermax = ) (which reported
cartogram’s “all sizes are missing and/or non-positive” and
an assertion naming its internal maxiter).interactive_map(engine = "ggsql") reported a missing
ggsql >= 0.4.1 for a frame that simply had no geometry –
sending the caller after a package that has not shipped in the R
bindings at all, only to meet the real error afterwards. The
sf check now runs ahead of the package gates.
spin_globe() had the same inversion for its
fill column: its scalars were moved ahead of the animation
gate in an earlier pass, but the column check was not, so a mistyped
column still asked for gifski.join_world() could not read a column of ISO codes
without being told to. Its fallback detector tried each character column
with origin = "country.name", which does not match most
alpha-3 codes, so a column of them was rejected outright – and a column
named iso3c was found by name but still read as
country names, so
join_world(tibble(iso3c = c("FRA", "JPN"))) warned that
nothing matched and returned all NA. Detection now tries
the code schemes as well and carries the one that worked through to the
conversion, so iso3c, iso_a3,
iso2c and an unrecognised name like code all
resolve. An explicit origin still wins, and a column whose
name implies a scheme is only read that way if the scheme actually
resolves it.region on the polygon
backend only filters vertices; it cannot clip a polygon, so a country
crossing the edge keeps a truncated ring that
geom_polygon() closes with a straight chord (France loses
202 of 605 vertices and the two ends sit 15 degrees apart). Nothing in
the returned tibble showed it, and the vignette presented the box as
clipping the shapes. It now warns and points at
geometry = "sf", where the clip is a real
sf::st_crop(); ?world_geometry and the
vignette say which is which.attach_geometry() on a frame that already had geometry
multiplied the rows instead of refusing. The join is by country, and a
polygon frame holds one row per vertex, so re-attaching joins a
country’s vertices against themselves: the bundled snapshot went from
99,338 rows to 310,977,360. The call declares
relationship = "many-to-many" – correctly, since one
country really does have many vertices – which switches off dplyr’s own
guard against exactly this. It now errors, and says to pass the
country-level table.share_of_world() on a grouped frame with no
year column returned a share of the group,
not of the world: sum() inside mutate() is per
group, so a frame grouped by continent came back with a column that
summed to 5 instead of 1, under a name and a help page that both say
“world”. A panel was already safe by accident, because the function
regroups by year and that replaces the caller’s groups. It
now ignores the grouping in both cases, and says so where it would have
mattered.rank_countries() overwrote rank,
percentile and z_score, and
per_capita(), share_of_world(),
growth_rate(), index_to(),
lag_by_country() and diff_by_country()
overwrote their target column, all in silence – a user’s own
rank column simply vanished. It is a warning, not an error,
because re-running a verb on its own output is legitimate.
standardize_country() is deliberately exempt:
add names the columns literally, so replacing an existing
continent is what was asked for.geom_country_labels() on an sf map failed
with rlang’s internal “Column long not found
in .data”. The layer reads the polygon backend’s
long/lat columns, and its own
aes() was evaluated against the sf frame before the guard
inside could run. It now errors with its own message and points at
ggplot2::geom_sf_text(aes(label = iso3c)), which is
documented on the help page too.geom_country_labels() put every country that crosses
the antimeridian on the far side of the planet when the frame had no
group column. group is what identifies a
country’s separate pieces, and the label belongs on the largest; without
it the fallback averaged the raw longitude range, so measured against
the largest-piece centroid Fiji was 177.8 degrees out, New Zealand
169.6, and even the USA 96.6 – its Aleutian tail dragging the mid-range
to the Gulf of Guinea. Averaging in wrapped coordinates cuts those to
0.2, 6.9 and 31.4. It remains an approximation, and the help page now
says that placement is exact only while group is
present.gini() and theil() returned a silent
NaN when the input contained an infinity. Inf
is not NA, so it passed na.rm and (for Theil)
the non-positive filter, then made the mean infinite and every share
Inf/Inf. Every other verb propagates an infinity visibly –
Inf in, Inf out, which the caller can see –
but an inequality index has no such value to report, so both now warn
and return NA, as they already did for a zero total weight.
Infinite weights are caught too. NaN is still
treated as NA and dropped.
?theil promised a tibble whenever
groups is supplied, but every degenerate path – nothing
left after na.rm, a zero total weight, an infinity –
returns a single NA instead. The help page now says
so.
dorling_map(k = 0) passed validation and then failed
inside cartogram with “all sizes are missing and/or
non-positive”. check_number()’s bounds are inclusive, so
lo = 0 admitted a value the next layer cannot use – the
same hole the integer ceilings were added to close, at the other end of
the range. It is now rejected with a message naming k;
anything above zero still works.
?countryatlas gains an Options
section. Two of the three options the package reads –
countryatlas.workers and
countryatlas.gdp_compat – were described only in this
changelog, which is not reference documentation, so a reader of the help
pages had no way to find them. (wdj_workers()’s own comment
noted that the option was “advertised in NEWS”, which is how a bad value
became reachable.) All three are now documented where they are looked
for, and a test fails if a future option is read without being
listed.
The test suite runs in a quarter of the time
(R CMD check’s test phase went from 389s to 92s). One block
asked neighbors() for each country in turn and then again
for each of that country’s neighbours, to confirm the reverse edge – and
neighbors() recomputes the whole world’s
sf::st_touches() adjacency on every call, so that was ~465
rebuilds and 292 seconds, four fifths of the suite.
neighbors() is vectorised, so one call does the same work.
The same properties are asserted (irreflexive, no repeated pair, every
edge symmetric), and a failure now names the offending countries rather
than only counting them.
?neighbors now says to pass a vector rather than
loop, since that cost is invisible from the outside: every call rebuilds
the whole world’s adjacency, so asking about one country costs the same
as asking about all of them, and adding countries to a single call only
adds the filtering. Measured here, one country takes about as long as
153 of them, which makes a loop over them roughly two orders of
magnitude more work. The note names country_borders() as
where the cost comes from. A test pins the fact the advice rests on –
one country_borders() call per neighbors()
call, whatever the length of x.
?attach_geometry claimed that Gibraltar, Hong Kong,
Macao, Tuvalu and the British Virgin Islands have geometry in “no
backend at any scale”. Only Gibraltar does not: the other four are
carried by the sf backend at scale = "medium",
which is the very fix the preceding sentence recommends for microstates.
It was the no-tile list from ?world_tiles –
coincidentally the same five names – pasted into a paragraph about
geometry. Corrected, and the section’s coverage counts (215 snapshot
countries; 210, 169 and 214 carried) are now pinned by a test, so an
upstream Natural Earth change surfaces as a failure rather than as
silently wrong advice.
as_ggsql_source(format = "parquet") wrote into the
working directory when no path was given:
the default was the bare relative path
"<name>.parquet". CRAN policy is that a package
writes nowhere but the session’s temporary directory unless the caller
says otherwise. The default is now a file of that name under
tempdir(), and since the function returns the path the
workflow is unchanged; an explicit path still writes
exactly where you point it. spin_globe() already defaulted
to tempfile(), and no other export writes at all.
?country_meta and ?world_snapshot now
say that their country columns disagree, and why.
country_meta carries countrycode’s English
names and world_snapshot the World Bank’s, so 38 of the 215
shared countries are labelled differently (“South Korea” against “Korea,
Rep.”). Each table is faithful to its own source and neither is wrong,
but joining the two on iso3c leaves you holding two
country columns with nothing to explain the difference –
the very reconciliation country_join() advertises, using
the same example. The count is pinned by a test, along with the
referential consistency of all five code columns against
country_meta.
?world_data and ?country_data now say
where the country label comes from, because the same call
produces two different spellings: a successful fetch carries the World
Bank’s names (“Korea, Rep.”), while the country spine used when the
fetch returns nothing carries the countrycode names (“South
Korea”) – as does every other function in the package. Nothing can
reconcile that offline, since the World Bank spelling only exists in the
response, so both pages now point at iso3c as the stable
key and at convert_country(iso3c, to = "country") for one
consistent set of labels.
utils::globalVariables() declared 29 names where 7
are needed. Emptying it and reading what R CMD check
actually reports showed the rest were covered by the
.data$x idiom the code uses throughout, which needs no
declaration at all; three of them (subregion,
NY.GDP.PCAP.KD, gdp_per_capita_2015) never
appeared as bare symbols anywhere, only in a comment or as string
literals. A stale entry is worse than clutter: it silences the “no
visible binding” NOTE for a new bare use of the same name,
which is the warning that would otherwise catch a typo. A test now fails
if a declared name is not a real bare symbol in
R/.
Six exported functions failed when the package was loaded but not
attached – countryatlas::dissolve_country(),
distance_between(), country_groups(),
in_group(), tile_map() and
world_geometry(region = <group name>) all died with
“object ‘historical_codes’ not found” or similar. They referred to the
bundled datasets by bare name, and a bare name resolves only while the
package is on the search path: under countryatlas::fn() in
a script with no library() call, the lazy-data objects are
not reachable. They are now countryatlas::-qualified. Every
test in the suite attaches the package, so nothing caught this; a static
check now fails if a bare reference reappears.
per_capita() failed when the caller’s frame already
had a column named .wdj_pop, the internal name used for the
fetched population. The join suffixed both sides to
.wdj_pop.x / .wdj_pop.y, so the column the
division reads came back NULL and base R reported
“replacement has 0 rows, data has 2”. Any pre-existing column of that
name is now dropped before the join. Only the branch that fetches
population was affected – passing pop explicitly never
touched it.
Two verbs leaked someone else’s message on an empty frame.
facet_map() gave ggplot2’s “Faceting variables
must have at least one value”, which names neither the argument nor the
package; it now says the frame has no rows to facet, and notes that the
other map verbs draw an empty panel instead.
geom_country_labels() ran the centroid summary over
nothing, where range() warns twice and dplyr
adds a deprecation note on top – it now returns early, silent as it is
on a full frame. Every other plotting verb already handled a zero-row
frame cleanly, either drawing an empty panel or naming the reason it
cannot.
?attach_geometry now says that geometry is attached
once per row, not once per country. A panel wants
exactly that – one row per country-year, each carrying the shape – but a
frame that repeats a country by accident draws it more than once, and
only the last one painted is visible. dplyr’s own many-to-many warning
is suppressed by the relationship the join declares, so nothing signals
it.
?country_data’s example quoted a
retired World Bank indicator. The bank replaced the
EN.ATM.CO2E.* carbon series with the AR5 greenhouse-gas
series, and the bundled common_indicators table had already
been updated, but the example still asked for
EN.ATM.CO2E.KT – so anyone copying it got a warning and an
all-NA column. It now uses
EN.GHG.CO2.MT.CE.AR5, which returns data.
R CMD check reports examples “OK” without failing on the
warning, so nothing surfaced this; a test now checks every indicator
code quoted in R/, man/ or the vignettes
against the bundled table.
country_groups_tbl was out of date by two years in
four places, while carrying an as_of stamp of 2026-06-01
that claimed otherwise. Sweden was missing from NATO
(acceded 7 March 2024; Finland had been added, so the table had been
maintained to 2023 and no further), Angola was still in
OPEC (left 1 January 2024), BRICS still held only its
original five (Egypt, Ethiopia, Iran and the UAE joined in
January 2024, Indonesia in January 2025), and The Gambia was
missing from the Commonwealth (rejoined 2018). Corrected, so
the counts are now NATO 32, OPEC 12, BRICS 10 and Commonwealth 56. Saudi
Arabia is deliberately still absent from BRICS: it was invited in the
2024 round but has never confirmed accession.
in_group("Sweden", "NATO") returned FALSE
before this.
options(countryatlas.cache_dir = ) was ignored once
a cached fetch had happened. The memoised fetcher was built on first use
and kept for the rest of the session, so relocating the cache afterwards
silently kept writing to the original directory – and
?clear_wdi_cache offers that option as the way to relocate
the cache without saying it has to be set first. It only ever took
effect because clear_wdi_cache() happened to reset the
state. The fetcher is now rebuilt when the directory changes, and the
“cannot write to the cache directory” notice is once per
directory rather than once per session, so a second unwritable
location is not swallowed.
A corrupt cache entry was reported as a World Bank outage. An
interrupted write leaves a truncated or empty .rds, and
readRDS()’s “unknown input format” surfaced under “Could
not fetch indicator … from the World Bank API”, sending the caller off
to debug a connection that was fine – the same misattribution already
fixed for an unwritable cache directory, now fixed on the read
side. The warning names the cache and gives the recovery command, which
matters because the bad entry persists: every later call degrades to the
country spine until clear_wdi_cache(disk = TRUE) is run. A
genuine network failure still blames the network.
When the cache directory was unwritable, caching stopped working
altogether instead of falling back to the session. The in-memory memo
that stands in for the disk cache lives in one process, but multiple
indicators are fetched with parallel::mclapply(), so each
worker warmed a memo and then exited with it: every call re-fetched
every indicator, hitting the World Bank API again and again with nothing
to show for it. Fetching is now serial when the memo is memory-only –
the repeated round-trips cost far more than the one-shot parallel
speedup – and unchanged when the disk cache is available, since a disk
memo is shared by every worker.
A failed indicator was dropped from the result without a word,
whenever more than one indicator was requested.
fetch_one_safe() degrades gracefully and warns – “Could not
fetch indicator … from the World Bank API”, or the corrupt-cache variant
– but it runs inside parallel::mclapply(), which brings
back a worker’s value and discards the conditions it signalled.
Since having several indicators is exactly what makes the fetch fork,
and parallel = TRUE is the default, the common case was the
silent one: a column simply missing from the table with no explanation.
A single indicator, which never forks, warned correctly – which is why
this went unnoticed. Conditions are now carried back and re-signalled in
the calling process, on the serial path too so both report identically,
and one problem is reported once even if two entries name the same
series.
Country lookups silently returned NA in Turkish,
Azeri and Crimean Tatar locales. toupper() and
tolower() follow LC_CTYPE, and in those
locales i and I are not a case pair:
toupper("idn") returns a dotted capital I, not
"IDN", and tolower("ISO3C") returns a
dotless i. Five places folded an ASCII identifier that way and
then compared it against plain ASCII, so every ISO code containing an
i (IDN, IND, IRL, IRN, ISL, ISR, ITA, BIH, CIV, FIN, …)
failed to resolve. The failures were quiet and the blast radius uneven:
world_geometry(region = c("ind", "chn")) returned Ivory
Coast, Indonesia, Isle of Man and India – one unfoldable element made
the whole vector fall through to name matching, which then
matched on substrings; dissolve_country("SOUTH VIETNAM")
stopped finding its alias; and join_world() on a frame with
ISO3C and geo columns picked geo,
joining on the wrong column entirely. Identifier folding is now done
with an explicit ASCII table (ascii_upper() /
ascii_lower()) and does not consult the locale.
This fixes countryatlas’s own folding, which covers every path keyed
on an ISO code, a column name or an alias. It cannot fix matching on a
country name: that goes through countrycode, whose
regexes are themselves locale-sensitive
(countrycode("Ireland", "country.name", "iso3c") is
NA under tr_TR). So a user with
LC_COLLATE=tr_TR still sees name-keyed gaps – notably the
polygon backend, which labels its geometry by joining region
names to codes, so world_geometry(region = "IND")
comes back with 21 rows of Siachen Glacier instead of India. Working
around that would mean forcing the C locale around every
countrycode call, which risks mangling accented names for
everyone else; it is left for upstream.
The two-core cap that CRAN policy requires of a check was applied
only when _R_CHECK_LIMIT_CORES_ held the exact string
"TRUE". R CMD check --as-cran does set it to
that – but only when it is not already set, so the value that actually
arrives is whatever the check flavour or CI exported, and R’s own parser
for these variables reads "true", "True",
"T", "1", "yes",
"Yes" and "YES" as true as well. Under any of
those spellings the cap did not apply and a multi-indicator fetch forked
detectCores() - 1 workers in the middle of a check. The
test is now inverted: a value that is set and does not explicitly parse
as false means “limit”, which also covers "warn". An
explicit "false", "F", "0" or
"no" is still honoured as a deliberate opt-out.
?world_data’s example called
world_data(2020) unconditionally, and the default
geometry = "polygon" backend comes from the suggested
maps package. R CMD check runs
\donttest{} blocks, so on a check flavour configured
without suggested packages – CRAN runs one – that example failed with
“The package "maps" is required for the polygon geometry backend”,
taking the whole examples step down with it. Writing R Extensions
requires code that uses a suggested package to be conditional, examples
included; the call is now guarded with
requireNamespace("maps"). The second call in the block
passes geometry = "none" and needs nothing beyond the hard
dependencies, so it is left to run unconditionally.
The beyond-the-choropleth vignette failed to build
wherever rnaturalearth was absent. Its chunk guard was
has_sf <- requireNamespace("sf"), but the sf geometry
backend gates on three packages – sf,
rnaturalearth and rnaturalearthdata – so on a
machine with sf but without the Natural Earth data the guarded chunk
evaluated to TRUE, ran, and stopped
R CMD build with “The packages "rnaturalearth" and
"rnaturalearthdata" are required for the sf geometry backend”. The other
two vignettes already tested for rnaturalearth; all three
now test for the same trio the code itself gates on. Three tests had the
same incomplete guard and errored rather than skipping in that
configuration; they now share a skip_if_no_sf_geometry()
helper.
New hex logo, drawn by the package itself
(data-raw/hex_logo.R): an orthographic globe —
globe_map()’s projection — carrying a viridis choropleth of
world_snapshot GDP per capita on Natural Earth geometry
joined by attach_geometry(), with
spike_map()-style population spikes rising off the horizon
and the binned-legend swatches under the wordmark.
The gdp_per_capita_2015 compatibility alias (a
one-cycle deprecation shim from 1.0.0) is now opt-in: set
options(countryatlas.gdp_compat = TRUE) to restore it. The
default is FALSE, so world_data() no longer
emits a duplicate column.
world_snapshot refreshed to year
2024 (was 2022) and rebuilt with the latest WDI data
and curated overrides.
country_groups_tbl membership date bumped to
2026-06-01 (was 2024-01-01).
?world_snapshot was out of sync with the rebuilt
data (missing the “Snapshot year: 2024” note); regenerated.
Fixed a stray orphaned code fence at the end of the countryatlas and ggsql vignette that broke its markdown structure.
.Rbuildignore now excludes the session-local
.claude/ directory, which git ignores but
R CMD build does not, so it was shipping in the tarball and
tripping R CMD check’s “hidden files and directories”
NOTE.
Comments in R/overrides.R are ASCII-only, so no
source file carries non-ASCII characters outside a deliberate
\U escape.
?world_snapshot no longer splits a code span across
two source lines, which had left the checked-in .Rd
disagreeing with what roxygen2 regenerates.
beta_convergence() failed with a bare
"subscript out of bounds" when the initial levels had no
spread across countries. A constant predictor makes lm()
return an NA coefficient, which summary() then
drops entirely, so the lookup for it fell off the end. It now says that
the initial levels have no spread and why that matters.
The forking path is now tested.
fetch_wdi(parallel = TRUE) is the default for a
multi-indicator request, so wdj_lapply()’s
mclapply branch runs on one of the package’s busiest code
paths, yet no test reached it – every other test used a single indicator
or passed parallel = FALSE. Confirmed: a parallel fetch is
identical to the serial one, forking preserves order, ...
reaches the workers, wdj_workers() honours
options(countryatlas.workers) and CRAN’s two-core limit,
and an error inside a fork is surfaced rather than left as a
try-error for downstream code to trip over.
The World Bank fetch and assembly path is now tested offline. It
needs the network, so it had no coverage at all despite holding the
least obvious logic in the package: fetch_wdi()’s
multi-indicator reduce-merge (shared keys are coalesced rather than
suffixed, and values stay aligned per country-year), its degradation
when one indicator of several fails,
country_data(latest = TRUE)’s “most recent non-NA” collapse
(which opens the window at 1960 and skips a missing latest year), the
panel key, the duplicate-key case where two iso2c codes map
to one iso3c, and that cache = TRUE really
short-circuits a repeated fetch. No defects were found; the tests pin
the behaviour.
spin_globe() validates n_frames /
fps / width / height /
lat before gating on gifski /
magick, matching how globe_map() orders the
two: a bad argument is the caller’s bug and the message should not
depend on which optional packages happen to be installed.
tile_map() phrased the missing-iso3c
error differently from the three other verbs performing the identical
check; all four now read the same.
Half the examples that were marked \dontrun{} now
actually run: it covered 14 of the 55 documented topics and now covers
7. Nine verbs gained executable examples –
locate_country(), country_borders(),
neighbors(), morans_i(),
simplify_geometry(), bivariate_map(),
cartogram_map(), dorling_map() and the
polygon-backend globe_map() – having been unrunnable only
because their examples made a live World Bank fetch. Driven by the
bundled world_snapshot instead, and guarded with
requireNamespace(), they are \donttest{}
examples that execute in under half a second each. The safe form of
clear_wdi_cache() is now a live example too.
\dontrun{} remains only where the code genuinely cannot run
in a check (a network fetch, an HTML widget, a written GIF,
ggsql >= 0.4.1, or deleting files).
The tests guarding this release’s fixes were verified by mutation: each fix was reverted in a scratch copy and the suite had to fail. 35 mutations, all now detected – but four were not at first, and each pointed at a real gap:
is.na() is TRUE for NaN as
well, so the checks on theil()’s zero-weight result and its
shares at perfect equality passed whether the value was the fixed
NA or the NaN the bug produced. Both now
assert the exact value.world_map() or
globe_map()’s polygon backend, broke no test. There are now
checks on the helper, on both call sites, and on the property that
matters: roughly equal numbers of countries per colour.interactive_map(tooltip = ) was unprotected: the
existing tests assert the returned object’s class, which passes whether
the argument is honoured or silently dropped – exactly the pre-2.0.0
bug. Both the "ggiraph" and "leaflet" engines
are now checked on the column they are actually handed.Degenerate-but-valid input is now covered by tests: perfect
equality (gini() / theil() return
0, not NaN), zero-variance columns
(NaN z-scores and NA correlations rather than
errors), duplicate (iso3c, year) panel rows, poles and
antipodal great circles, collinear rings, all-NA and
all-zero fill columns, and single-country frames.
?distance_between and ?country_meta now
state which countries have no bundled centroid.
country_meta is assembled from
countrycode::codelist, which has no Kosovo row, so
distance_between("Kosovo", "Serbia") is NA
even though neighbors("Kosovo") and
country_borders() know about it – and ten small or
dependent territories have a row but no centroid. The
@return already said NA was possible; it did
not say which countries, and the asymmetry with the geometry backends
was surprising.
The same double-counting affected three more places, all gated on
the presence of a group column – which polygon frames have
and sf frames do not:
correlate_indicators() reported n and
r over geometry rows rather than countries. That column
exists precisely so a correlation computed on a few countries cannot
masquerade as a world fact, so an inflated n defeated the
point.audit_coverage() reported the wrong country count and a
wrong NA rate for every indicator.bubble_map(backend = "sf") drew two bubbles for a
divided country, where the polygon path already guaranteed one per
country. All three now reduce whenever an iso3c column is
present.Quantile and jenks breaks on the sf backend
double-counted divided countries. The de-duplication added in this
release skipped the sf path on the assumption that Natural
Earth is one row per country, but it is not: Cyprus occupies two rows
sharing one iso3c at 110m, as do Cyprus and India at 50m.
Breaking on the raw column shifted the cut points enough to move real
countries into the wrong bin – Saudi Arabia and Libya changed colour in
the bundled snapshot at n_bins = 5. Both backends now
de-duplicate on the key, so “one value per country” holds exactly rather
than nearly.
convert_country(x, to = "calling_code") returned
alpha-3 country codes instead of telephone calling codes –
"FRA" where 33 was meant. The shortcut was
mapped to countrycode’s genc3c column, which
is an ISO-style three-letter code, not a dialling prefix; it now maps to
the telephone column, so France gives 33, the
USA 1 and Japan 81. A source comment claimed
the limitation was “documented as best-effort”, but
?convert_country never mentioned calling_code
at all; the shortcut is now listed there.
The Description field – the text CRAN renders on the
package page – advertised nine map idioms while the package ships
eleven: spike_map() and facet_map() (small
multiples) were missing from the vocabulary list, and the
analysis-helper examples predated this release’s inequality and
convergence statistics. Every idiom it names now corresponds to an
exported verb.
?countryatlas listed morans_i() under
“Core data assembly” while _pkgdown.yml listed it under
“Analysis helpers”, so the two navigational indexes described the same
function as two different kinds of thing. It is a spatial statistic, so
it now sits with gini(), theil() and the
convergence measures on both surfaces.
The @seealso cross-references are reciprocal. All
three the package had pointed one way only: a reader of
?gini was sent to theil() but a reader of
?theil was sent nowhere, and likewise for
beta_convergence()/sigma_convergence() and
dissolve_country()/check_country_match().
?theil did not link to gini() at all – it
named Gini in prose without a cross-reference. The four related
diagnostics (check_country_match(),
repair_country_names(), dissolve_country(),
plus historical_codes) now all reference each
other.
Every page taking a projection argument now says
where the valid values are. Only world_map() enumerated the
13 projections and only world_geometry() pointed at it; the
other eight entries said no more than “Projection.”
(bivariate_map()) or “Projection options for the
sf backend”, leaving a reader with nothing to go on.
Relatedly, the three pages that document projection and
recenter together described only the projection, so
recenter’s meaning – a central meridian – was missing from
world_data(), join_world() and
attach_geometry().
The rnaturalearthhires requirement is now documented
on every page that takes a scale argument, not just
?world_geometry. Seven topics – world_data(),
join_world(), attach_geometry(),
locate_country(), country_borders(),
neighbors() and morans_i() – described
scale without mentioning that "large" is
unobtainable from CRAN, so a reader of any of those pages met the gate
with no warning.
scale = "large" was offered as a plain option but
needs the rnaturalearthhires package, which is not on CRAN
and is not in Suggests. Left ungated,
rnaturalearth responded by trying to install it into the
user’s library from a non-CRAN repository and then failing obscurely. It
is now gated with a message naming the package and the repository to get
it from, and pointing at scale = "medium" (50m) as the
option that needs nothing extra. ?world_geometry documents
the requirement, and the sf & projections vignette no
longer demonstrates the scale most readers cannot run.
?country_borders recommended a graph recipe that
produced nonsense. igraph::graph_from_data_frame() treats
the first two columns as the edge endpoints, and
country_borders() returns iso3c_a,
country_a, iso3c_b, country_b –
so columns 1 and 2 are both endpoint A, and passing the whole
tibble built edges from each country’s code to its own name (56 vertices
instead of 37 for Europe, every French edge running FRA to
"France"). The documented call now passes only the two code
columns.
?neighbors and ?country_borders now
warn that igraph also exports a neighbors() –
taking a graph and a vertex rather than country names – so whichever
package is attached later wins. ?country_borders recommends
igraph for turning the adjacency into a graph, which walks
users straight into the clash, so both pages now say to qualify the call
as countryatlas::neighbors(). It is the only collision
between this package’s exports and any of dplyr,
ggplot2, tidyr, tibble,
sf, maps, WDI,
countrycode, scales, leaflet,
plotly, igraph, raster,
terra, purrr, stringr,
forcats, readr or the base packages.
?country_overrides now documents why every name in
the override table is plain ASCII: ASCII spellings match in any locale,
whereas accented spellings rely on countrycode’s own
matching and resolve to NA under a non-UTF-8 locale
(LC_CTYPE=C). The note points at
iconv(x, to = "ASCII//TRANSLIT") for input that may carry
accents.
The test suite is now green under CRAN’s noSuggests
configuration (_R_CHECK_DEPENDS_ONLY_=true), which runs
with every optional package absent. Four tests called
globe_map(backend = "polygon") or morans_i()
without guarding on mapproj / sf, so they
errored on the dependency gate instead of exercising what they were
written to check.
The quantile/jenks binning that world_map() and both
globe_map() backends perform lived as three near-copies
kept in step by hand; it is now one internal helper. Verified
behaviour-preserving by comparing the rendered fill of every style x
n_bins x backend combination before and after (50
fingerprints, ~2.6M values, all identical).
Documented \value claims are now asserted as
executable contracts, so Rd prose cannot drift from the code in silence.
The 36 exports whose \value makes a specific structural
promise – named columns, a single row, an attached "model"
object, a length matching the input – are covered; the rest return a
ggplot, a layer or a widget and are checked by their own
tests. Every claim audited was already accurate; the tests keep it that
way.
The four exports that had no test call site at all –
wdi_search(), clear_wdi_cache(),
animate_world() and cartogram_map() – are
covered, including cartogram_map(type = "contiguous") (the
default type, previously never exercised) and
wdi_search()’s zero-match and single-match paths.
The README’s “optional features at a glance” table is corrected
against what the code actually gates on: spin_globe() was
listed as needing only maps + mapproj when it
also hard-requires gifski or magick; the
ggsql row overstated the requirements of
as_ggsql_source() (which never needs ggsql)
and understated the version
interactive_map(engine = "ggsql") needs; and
locate_country(), flow_map() and
bubble_map() were missing.
The countryatlas and ggsql vignette said
DRAW spatial “was added in 0.4.1” as plain fact; it now
says that version is newer than what CRAN ships, which is why the
query-executing chunks are shown but not evaluated.
The package-level overview (?countryatlas) was
missing simplify_geometry() and
clear_wdi_cache() from its section list, though
_pkgdown.yml had both.
The set of ISO codes the package treats as countries was computed in two places (name matching and the World Bank aggregate filter); it now comes from one internal helper, which region resolution uses as well, so the three callers cannot drift apart.
New offline test suites pin the things a structural test cannot:
closed-form anchors for the hand-rolled numerical kernels (haversine
distance, spherical polygon area, great-circle interpolation,
Gini/Theil, sigma and beta convergence, Moran’s I against an
independently built weights matrix) and internal-consistency checks on
every bundled dataset (no duplicate or unknown iso3c,
coordinates in range, one country per world_tiles cell,
historical_codes in step with its alias table, and
country_meta centroids still agreeing with
polygon_centroids()).
README and vignettes now demonstrate every exported function:
wdi_search(), country_codes(),
complete_years(), growth_rate() /
index_to(), repair_country_names(),
country_join_all(), locate_country() and
facet_map() gained worked examples, and the vignettes
prefer country_overrides() over the soft-deprecated
wdj_overrides(). The README’s rendered output and figures
were stale (pre-dating the quantile-breaks fix and the
gdp_per_capita_2015 opt-in) and have been re-rendered from
the 2.0.0 code.
geofacet is dropped from Suggests: no
code ever used it, and ?tile_map / the README claimed a
geofacet-backed small-multiples feature that did not exist.
Facet a tile_map() like any other ggplot, or
use facet_map() for choropleth small multiples.
The README’s figures are shipped in the tarball again, so the
images on the CRAN package page resolve. .Rbuildignore
excluded the generated .pngs but not the (much larger)
.gif, which left six of the seven images broken.
?world_snapshot’s @format said “two
elements” while listing three, and advertised an sf element
that is NULL in the released package. It now documents what
actually ships and points at attach_geometry() for
geometry.
?attach_geometry documents which countries each
geometry backend actually carries. Rows with no matching geometry are
dropped silently, and the sf backend’s scale
changes which countries exist rather than only how detailed
they are: of the 215 countries in world_snapshot, the
default scale = "small" (110m) maps 169 and
scale = "medium" maps 214. Five territories (Gibraltar,
Hong Kong, Macao, Tuvalu, the British Virgin Islands) are in no backend
at any scale.
?world_tiles and ?tile_map said “one
square per country” without saying how many. The grid is the 239
country_meta rows that have a bundled centroid, so the 10
without one have no tile and tile_map() silently drops
data rows keyed on them. Both are now documented, and a
test pins the grid to that definition.
A single, comprehensive release that takes the package from a one-function proof of concept to a complete toolkit for joining world data to maps. The spirit is unchanged — ISO codes as the universal join key, one call to a map-ready table — but pushed to its full potential.
world_data() is generalised but backward-compatible:
world_data(2020) still returns the classic polygon-backed,
GDP-per-capita tibble. The only visible change is the column name
gdp_per_capita_2015 → gdp_per_capita. A
one-cycle deprecation shim keeps gdp_per_capita_2015
available as an alias (toggle with
options(countryatlas.gdp_compat = FALSE)).wdj_overrides()] instead of
deleted, so they appear on maps. Diffs of map output will show increased
coverage.world_data() gains indicator (one or many
WDI codes; named vectors drive clean column names), multi-year
panels, an sf backend
(geometry = "sf"), region subsetting,
latest, projections and caching.country_data() — the lightweight, one-row-per-country
analysis table.world_geometry() — projected, region-subset geometry
(countries, centroids, coastline, borders, graticule, ocean).standardize_country(), join_world(),
attach_geometry(), country_join().check_country_match(), wdj_overrides(),
audit_coverage() — never lose a country silently.convert_country() (flags, currency, tld, research
codes), country_codes(), country_groups() /
in_group(), wdi_search().world_snapshot,
country_meta, common_indicators,
country_groups_tbl, world_tiles.per_capita(), aggregate_regions(),
rank_countries(), complete_years().world_map() (continuous / binned / quantile / jenks /
categorical), bubble_map(), bivariate_map(),
cartogram_map(), tile_map(),
flow_map(), animate_world(),
interactive_map(), geom_country_labels(),
theme_world_map().parallel::mclapply)
where supported. See clear_wdi_cache().world_snapshot lets every example, test and
vignette run offline and deterministically.@importFrom instead of
blanket @import).cli / rlang
errors.testthat (3e) suite; network calls are skipped
offline and on CRAN.pkgdown site.sf,
rnaturalearth, cartogram,
biscale, geofacet, gganimate,
leaflet, …) are all in Suggests and gated by
rlang::check_installed(), so the base install stays
light.Group memberships in country_groups_tbl are
point-in-time as of 2024-01-01.
world_data(year) function.