coord_sf() when sf loads and with the
package's own spherical Equal Earth when it does not; every projection,
recenter and zoom_map() work on it.breaks and diverging midpoints
when you want them.classify_countries()), group membership with spells and
suspensions for nineteen groups, lags and growth keyed on the year, joins
on the year, and aggregates that report their coverage.source_info()); World Bank releases can be pinned
(vintage =) and compared; any SDMX provider reads without a client
package (fetch_sdmx()).inequality(),
funnel plots, bivariate LISA and join counts.check_country_match() says which tier matched each one.projection said. Way back:
projection = "none".maps::map_data("world") (Natural Earth 1:50m as imported in
2013), so the default map needs nothing installed and carries 215 of the
snapshot's 216 countries where it carried 211. country_meta's centroids
and areas, and world_tiles, are rebuilt from the same polygons (247 tiles;
only Bouvet Island, Gibraltar and the U.S. Minor Outlying Islands lack a
centroid now). Way back, for one release: geometry = "maps", deprecated.world_map(), globe_map(), facet_map(), animate_world() and
interactive_map() default to style = "quantile". Way back:
style = "continuous". A factor or character fill still draws its
categories.footnote = "auto"): the
coverage line and, where the data carries a record, the source. Way back:
footnote = FALSE.small_states = "auto"): a country
with a value and no polygon at the chosen scale, most small states on the
sf backend at 1:110m, is a filled point at its centroid on the same fill
scale, counted in the coverage. Way back: small_states = "none"."1.68K to 4.65K" rather than cut()'s
"(1684,4655]", and legend titles default to the source's label and unit
("GDP per capita (constant 2015 US$)") when the data carries one. Way back:
legend = "gdp_per_capita"."Dark 3", not
the "turbo" rainbow. Way back: palette = "turbo".lag_by_country(), diff_by_country() and growth_rate(type = "yoy")
are keyed on the year (by = "year"): the value a year earlier, NA
across a gap, where 3.0.0 took the previous row and warned. Way back:
by = "row".convert_country(from) is origin,
classify_compare(n) is n_bins, flow_map(n) is arc_points, and
repair_country_names(verbose) is quiet (inverted).check_country_match() gains a method column, and name matching
tries German, Spanish, French and Italian names and every CLDR name after
the English patterns, so names 3.0.0 left unmatched now resolve.country_join() and country_join_all() also join on a shared year.
Two country-year panels were joined on the country alone, so two countries
by two years came back as eight rows with year.x and year.y, and no
warning: dplyr shows its many-to-many warning only for joins made at the
console, never from inside a package. Now, when both tables have a year
column, it becomes part of the key and a message says so. Way back:
also_by = character().aggregate_regions() reports coverage, and withholds partial aggregates.
Every row gains n_countries, n_reporting and coverage (and
coverage_weighted with a coverage weight), and a group below two-thirds
coverage is NA with a countryatlas_low_coverage warning naming it, the
World Bank's rule for its own aggregates. Way back: min_coverage = 0.weights = NULL in morans_i(), gearys_c(), getis_ord(),
local_morans(), lisa_map() and spatial_lag() meant land-border
contiguity, which deletes every island: on the bundled snapshot's GDP per
capita, Moran's I was 0.61 on 142 countries (49 of the 191 with data left
out) and is 0.47 on 189. country_weights()'s own default type is now
"knn" too. Way back: weights = country_weights("contiguity").
morans_i(scale =), which only set the old default's resolution, is
deprecated; supplying it still builds contiguity weights, with a warning.local_morans(), getis_ord(local = TRUE) and lisa_map() gain
p_adjust (default "fdr", Benjamini-Hochberg) and return p_adjusted
beside the unchanged p_value; the cluster labels and the LISA map's
significance mask use the adjusted value, and the map's caption names the
method. About 190 tests at alpha = 0.05 expect ten false clusters under
the null. Way back: p_adjust = "none".local_morans() and lisa_map() default to 9,999 permutations. With
999 the smallest attainable p-value, 0.001, is above the first
Benjamini-Hochberg cutoff for 190 tests (0.00026), so an adjusted map could
almost never show anything. The conditional permutation now draws every
neighbour set at once, so 9,999 permutations take under a second for the
whole world (999 took 1.2 s before, 0.14 s now). Way back: n_perm = 999.country_data(latest = TRUE) and world_data(latest = TRUE) add an
<indicator>_year column per indicator, the year each value comes from.
Each indicator took its own most recent value and year was dropped, so a
row could hold GDP from 2023 beside population from 2021 with nothing to
say so. Way back: drop the columns.rank_countries() warns on a panel with no within (class
countryatlas_panel): ranks there pool every country-year, so a country's
2020 and its 2000 compete and z_score carries the time trend. Way back:
within = year, or muffle the class.income and region come from a bundled, dated World Bank table, not
from the installed WDI release. world_data(), country_data() and
country_meta carry the current fiscal year's classes (FY2027), including
the July 2025 region change that moved Afghanistan and Pakistan into
"Middle East, North Africa, Afghanistan & Pakistan"; world_snapshot
carries the classes in force in its year (FY2024). WDI 2.7.10 disagreed
with the World Bank's own API for eight countries, so two users with
different WDI versions got different income groups from the same call.
No way back is needed: the old values were an accident of which WDI was
installed.fetch_owid() takes grapher chart slugs and fetch_oecd() SDMX
dataflows. Neither could return data in 3.0.0: owidR stopped working when
Our World in Data changed its API, and the OECD package's client targets
OECD.Stat, offline since 2024-07-01. owidR and OECD leave Suggests.wdj_overrides() and options(countryatlas.gdp_compat) are removed,
after warning for a release. Use country_overrides(), and the
gdp_per_capita column the alias copied.add_indicator() refuses a source that repeats a key. A source
returning two rows for one country-year fanned the caller's frame out, one
row in and two out. It now aborts with class countryatlas_many_to_many,
naming the keys; aggregate the source first.breaks (fixed class boundaries, open-ended end classes with a
warning) and midpoint (a diverging palette centred on a value) in
world_map(), globe_map() and value_by_alpha_map(); facet_map() and
animate_world() gain breaks_by = "panel" to classify each panel on its
own values."fisher", "headtails", "sd" and "equal"; their breaks
equal classInt's. classify_compare() adds Fisher and head/tail breaks and
reports the goodness of variance fit, the tabular accuracy index and the
fullest class's share.bubble_map(), spike_map(), flow_map(), gridded_cartogram() and
value_by_alpha_map() project on the polygon backend too; spike_map()
and gridded_cartogram() lay their spikes and cells out in projected
metres, so a spike's height and a cell's size do not depend on latitude.
New project_lonlat() places your own layers on the built-in Equal Earth,
and zoom_map() zooms by longitude and latitude keeping the projection.tile_trend_map(): a sparkline per country on the tile grid.ternary_map() for three-part compositions (sector shares of GDP, age
structure): each part has a primary colour and a country mixes them in
proportion to its shares, centred by default on the average country so that
grey means average (Schoeley 2021). The key is the colour triangle with
every country on it.map_alt_text() returns it (Lundgard & Satyanarayan's levels 1 and 2).
New check_palette() simulates three colour-vision deficiencies and warns
when adjacent classes are too close. New map_citation() returns the
references for exactly the data and methods a map used.map_provenance() reads the final plot: a coord or fill scale added after
the verb drew it is reported, with modified = TRUE.projection = "north_polar" on the sf backend painted the whole disc
grey: Natural Earth closes Antarctica along the South Pole, the
projection's antipode, so its ring closed around every other country. What
lies wholly south of 60 degrees S is now left out of that view, on both
backends.classify_compare() labels missing values "No data", as every other map
does, rather than "NA"; flow_map()'s lightest arcs no longer vanish into
the basemap; and geom_country_labels() on a frame without group no
longer places one "NA" label for every uncoded piece.world_geometry(worldview = ), attach_geometry(worldview = ) and
dispute_policy(worldview = ) draw the boundaries as the viewing country
publishes them, on the sf backend.standardize_country() matches and derives once per distinct value rather
than once per row, so the new tiers cost nothing per repeated name.convert_country("XK", origin = "iso2c") reads back the code the package
writes for Kosovo.country_groups_history gains status and several spells per country,
and seven dated groups: SCO, CPTPP, RCEP, EAC, SADC, APEC and the Arab
League, every date sourced in data-raw/. Suspended spells are not counted
as membership.audit_time_coverage() dates independence from Gleditsch & Ward's list,
flagging "before_independence" (Namibia before 1990, Timor-Leste before
2002) with the basis of each flag.country_join() and country_join_all() gain also_by, extra key columns
besides the country: a character vector, or a named one when the two tables
name the column differently (also_by = c(year = "yr")). A panel joined to
a cross-section repeats the cross-section's values across the years, with a
message. A key repeated on both sides now warns with class
countryatlas_many_to_many, naming up to five keys.in_group(as_of =) takes one date per element as well as one for all, so
mutate(eu = in_group(iso3c, "EU", "iso3c", as_of = year)) asks each row
about its own year. A missing date answers NA. The lookup is one
vectorised interval join, so a 50,000-row panel takes a fraction of a
second. country_groups(as_of =) stays scalar, since it returns one table.aggregate_groups() aggregates by dated group membership: an "EU" or
"OECD" series built from the members of each row's own year, with coverage
measured against the full membership on that date, so a member with no row
counts as missing.aggregate_regions() gains min_coverage and coverage_weight. A weighted
mean follows the World Bank's weight rule by default, using weight as the
coverage weight.latest = "common" in country_data() and world_data(): per country,
the most recent year in which every requested indicator is present, so
arithmetic across them is consistent.per_capita(), deflate() and to_ppp() read those <column>_year
columns and warn (class countryatlas_mixed_years) when the two columns
they combine come from different years. per_capita() with no pop now
fetches the population of the year each value comes from, rather than of
last year.correlate_indicators() gains by_year, one correlation table per year of
a panel instead of the earliest year alone, and weight, for a
population-weighted correlation (the average person rather than the average
country).options(countryatlas.timeout), default 60 s) and bounded retries
(options(countryatlas.retries), default 3) with exponential backoff and
jitter on 429, 5xx and dropped connections, honouring Retry-After. A
World Bank outage on 2026-10-01 had held a fetch for 450 seconds; the worst
case is now bounded and documented. A failed download is still a warning
and an empty result, as CRAN asks, unless options(countryatlas.strict = TRUE) makes it an error (class countryatlas_fetch_failed).WDI::WDI(), in pages of 5,000 rows instead of the single 32,500-row page
that failed. On recorded responses it returns the same keys, columns and
values as 3.0.0's path did. WDI stays for wdi_search().vintage = "2024-07" in
fetch_indicator("wdi", ...), country_data() and world_data() reads an
archived release of the World Development Indicators (API source 57); a
series the release does not hold warns and names the nearest releases that
do. New wdi_vintages() lists the 142 releases since 1989, and
compare_vintages() shows how much a series was revised between them
(Johnson, Larson, Papageorgiou & Subramanian 2013). An archived release
never changes, so its cache entries never expire.source_info() and source_info<-: every fetch records, per value
column, the source, indicator, label, unit, provider update date, fetch
date, release, licence and citation. The package's verbs carry the record
through joins and geometry, map_provenance() prints it ("World Bank WDI
NY.GDP.PCAP.KD (constant 2015 US$), release 2026-07, fetched 2026-10-01"),
and footnote = "auto" adds a one-line source note to the caption.compare_sources() refuses to call two series in different units a
disagreement (class countryatlas_unit_mismatch) unless
allow_unit_mismatch = TRUE, reading the unit a registered fetcher states
under its own column name as well as under the source's.fetch_sdmx() reads any SDMX service as SDMX-CSV with no client
package: the OECD, the IMF, the ILO, Eurostat, the ECB, the BIS and UNICEF
by name, or any SDMX 2.1 service by URL. Where the provider publishes the
dataflow's structure, countries goes into the key and the server filters.
A dimension that still varies within a country-year aborts and names the
dimension, rather than keeping a first row. "imf" and "ilo" are new
built-in sources.fetch_owid() reads OWID's Chart API, keyed on OWID's ISO codes (Kosovo
included), and takes "slug/column" to pick one column of a chart.clear_country_cache(source, disk = TRUE) clears just that source. The
cache key moved with the client, so 3.0.0's entries are swept, not read.fetch_indicator("wdi", ...) no longer warns on every call that some fifty
of the World Bank's aggregate codes are "not usable as iso3c"; the
aggregates are dropped, as country_data() always did.country_classifications dataset: the World Bank's income groups for
every economy and fiscal year since FY1989 (from its historical
classification, OGHIST), its regions with the July 2025 change dated, and
the FY2027 lending categories. New classify_countries() adds them to a
frame as in force on each row's date, with the fiscal-year rule stated:
basis = "in_effect" gives the class in force, basis = "data_year" the
class computed from that year's income, two fiscal years later.world_snapshot is refreshed from the 2026-07 release and stays on 2024,
by the rule recorded in its "provenance" attribute: in 2025, life
expectancy and CO2 have no values yet and GDP per capita covers 86% of
countries against 92%. It gains Kosovo (216 countries). country_meta
gains a curated Kosovo row, so distance_between() and the k-nearest
weights include it.local_morans(), getis_ord() and lisa_map(): p_adjust accepts
"fdr", "bonferroni", "holm" or "none". The method is recorded as an
attribute of the result and in lisa_map()'s provenance.eb_morans_i(), Moran's I for rates (Assuncao & Reis 1999), equal to
spdep::EBImoran.mc(); smooth_rates(method = "local_eb") shrinks toward
each neighbourhood's rate (Marshall 1991), equal to
spdep::EBlocal(geoda = TRUE).transition_matrix(), spatial_markov() (with a homogeneity test) and
rank_mobility() for distribution dynamics.inequality(): Gini, Theil T and L, Atkinson, CV, Palma and p90/p10,
unweighted or population-weighted; theil(type = "L") decomposes the mean
log deviation.rate_funnel() with exact Poisson limits and Spiegelhalter's
overdispersion adjustment; new bivariate_lisa() and join_counts().inst/CITATION carries the methods added in this release.clear_wdi_cache() is deprecated in favour of clear_country_cache(),
which it has been an alias of since 3.0.0. It warns, and goes in 5.0.0.
Deprecations now go through lifecycle, which is added to Imports.New scheduled workflows: weekly live contract tests make one real request per provider and upload re-recorded fixtures for review; a monthly dry run rebuilds every dataset from its source; a budget check fails the build when the CRAN-mode tests, the vignette rebuild or the whole check outgrows the time that got 3.0.0 through CRAN. The adapter tests replay real recorded responses instead of mocking the client function, which is how two dead adapters had passed every test.
Every internal join now declares its cardinality (relationship =), so a
lookup that unexpectedly repeats a key errors instead of multiplying rows. A
test parses R/ and fails on any join without one.
R/visualization.R (3,328 lines) is split into map-core.R,
map-symbols.R, map-cartograms.R, map-globe.R, map-interactive.R,
map-panels.R and labels.R; interpolation moves to time.R and the
value-suppressing palette to honest-maps.R. The tests move from
release-named files to one test-<file>.R per source file. Both are pure
moves.
The whole of the roadmap tracked in #19, in one release. 46 new exports and two new datasets take the package from "join World Bank data to a map" to "join anyone's data, to the map as it was in 1950, and say honestly what the picture does and does not support".
The version is 3.0.0 rather than 2.1.0 because three things change behaviour for
existing code: world_map(projection = "mercator") now produces a different
(and usable) map, morans_i() returns two more columns, and
geom_country_labels() takes data as its second argument. Details under
Breaking changes.
geom_country_labels() gains data as its second argument, matching the
geom_*(mapping, data, ...) convention every other ggplot2 geom follows. A
call that passed repel positionally -- geom_country_labels(aes(...), FALSE) -- now binds that FALSE to data and must name it. Named calls are
unaffected.
morans_i() returns n_excluded and excluded. Code that assumed a
five-column result will see seven. The statistic itself is unchanged.
world_map(projection = "mercator") draws a different map -- see the bug
fix below. It was previously unusable, so this is a fix rather than a
regression, but the output does change.
style = "binned" draws different bins in world_map(), globe_map()
and value_by_alpha_map(). n_bins was silently ignored there (see the bug
fix below), so the bin count and boundaries change for any existing call.
style = "quantile" and "jenks" are unaffected.
classification_report = TRUE returns NULL for style = "continuous"
instead of a row-per-distinct-value table, and warns. Code that read the
attribute after a continuous map got a table that described nothing; it now
gets nothing, explicitly.
world_map()'s new arguments were deliberately appended after recenter,
so no existing positional call to it changes meaning.
country_join() and country_join_all() gain warn, defaulting to
TRUE. They now warn about country names that resolve to nothing, so code
that joins on messy names will start emitting a warning it did not before.
The join result is unchanged; pass warn = FALSE for the old silence.
world_map(engine = "tmap") failed opaquely on an older tmap. The
engine builds its scales with tm_scale_intervals(),
tm_scale_continuous() and tm_scale_categorical() -- the tmap 4 API. tmap
3 configured scales through arguments on tm_polygons() and exports none of
them, and because DESCRIPTION pins no version on any Suggests package,
the need_pkg("tmap") gate was satisfied by any tmap at all. An older
install then died on R's own "'tm_scale_intervals' is not an exported object
from 'namespace:tmap'", which names neither the cause nor the cure. The
engine now checks for the constructors it needs and says so, pointing at
either an upgrade or engine = "ggplot2". The check is on the capability
rather than a version number, as as_ggsql_source() already does for
duckdb's shared_home: that is the thing actually required, and it stays
correct whichever release introduced it.
locate_country() measured its snap-back distance from the wrong point.
A point falling just outside the coarse coastline is snapped to the nearest
country when it is within tolerance_km, and that step paired points with
countries using st_nearest_points(..., by_element = TRUE). The argument is
pairwise; by_element is not a formal of it, so sf absorbed the name into
... and ignored it, returning the full n×n cross product instead of
n pairs -- after which each unmatched point was measured against a
different point's country. Cuba's centroid sits 10.8 km off the 110m
coastline and should snap; in a 17-point call it was measured against
American Samoa's point, came out at 10167 km, and was dropped to NA. One
unmatched point was safe, because 1x1 is the same thing as pairwise -- which
is why every single-point example was correct and only multi-point calls
were wrong. The error ran both ways: a bogus distance landing under the
tolerance would have attributed a point to a country it is not in. Fixed,
with a length check so it cannot silently regress to a cross product again.
spatial_lag() did not say which countries its weights excluded. A
country with no neighbour gets NA, which the documentation states -- but
that NA is indistinguishable from one caused by a missing input value, and
nothing in the result said which countries the weights had dropped.
morans_i() and gearys_c() answer this by returning excluded;
getis_ord() and local_morans() omit the row, so a setdiff() recovers
it. spatial_lag() returns the caller's frame, so it now attaches the codes
as the "countryatlas_excluded" attribute -- the same channel this package
already uses for "countryatlas_cartogram" and "countryatlas_clubs". Not
a warning: on real geography some country always lacks a land neighbour, so
a warning would fire on every ordinary call. On a panel the attribute holds
the union across years, since the weights are geography rather than time.
neighbors() could not tell a typo from an island. An unresolved name
becomes NA, and the %in% filter simply never matches it, so a
misspelling returned no rows -- indistinguishable from a country that
genuinely has no land border. The function's own example makes that
collision explicit ("Japan has no land border"). distance_between() and
convert_country() take the same input and both report an unresolved value;
neighbors() now does too, and gains warn = TRUE. Iceland still returns
zero rows in silence, because that zero is real.
country_timeline() said nothing about a name it could not resolve. An
unrecognised input came back as a row of NA, which reads as a country with
no recorded history rather than a name nobody matched -- in a package whose
whole premise is that unmatched names are the problem worth reporting.
dissolve_country() takes the same input shape and has warned about exactly
this since 1.0.0. country_timeline() now gives the same warning, with the
same check_country_match() pointer, and gains warn = TRUE to match its
sibling. A historical name is still silent: "USSR" is meant to fail the
ISO lookup and be resolved by the historical spine, which is why that
lookup's own warning stays suppressed.
Three panel verbs did nothing, silently, on a cross-section. Handed one
year per country, growth_rate(), lag_by_country() and diff_by_country()
each added a column that was NA in every row and said nothing -- the verb
accomplished literally nothing, and the result read as a computation that had
run. They now report it (countryatlas_all_na_result), stating what the verb
needs ("a lag of 3 needs 4 years for the same country") and pointing at
complete_years(). The notice fires only when the whole column is NA and
the source column had data, so the ordinary panel case -- where just the
first year per country is NA -- stays silent.
country_network() validated top_n after building the network. It was
the one verb here that did its whole job and then rejected an argument it
could have rejected at the door. top_n only trims the finished edge list,
so the check is now made first.
share_of_world() returned a column of NA without saying why. A zero
or non-finite total is a dead end -- the division would give NaN or Inf
-- and the guard that turns it into NA was right. Its silence was not:
per_capita() and to_ppp() both report an unusable denominator, under the
same two condition classes, while this one returned nothing but NA and
read as "these countries have no share" rather than "there was no total to
take a share of". It now reports it, naming the years affected on a panel
(only the bad years go NA; the rest keep their shares).
world_map(engine = "tmap") now projects. It took projection and
recenter and drew in the frame's own CRS regardless (see the bug fix
below), so every existing tmap map changes: the default is Equal Earth, as
it already was on the ggplot2 engine. A bad projection name is now an error
there rather than being ignored.
Five verbs now return a tibble rather than the class they were handed.
to_ppp(), smooth_rates(), interpolate_missing(), spatial_lag() and
rank_countries() returned whatever arrived: a data.frame stayed a
data.frame, and a grouped frame stayed grouped (see the bug fixes below).
They now normalise, as their sibling verbs always did. Code that relied on
df[, "col"] dropping to a vector, or on an inherited grouping surviving
the call, changes.
complete_years() returns an sf frame when given one. It previously
dropped the class while leaving the geometry column in place, so a pipeline
that repaired the result with st_as_sf() is now re-wrapping something that
is already sf -- harmless, but no longer necessary.
A duplicated column name is now refused. per_capita() and to_ppp()
accepted a frame with two gdp columns and silently computed from the
first; ten other verbs failed with a message about internals in tibble. All
of them now reject it up front (see the bug fix below).
region is applied when geometry = "none". world_data() and
join_world() ignored it on that branch and returned every country in the
world (see the bug fix below), so a call passing both now gets fewer rows --
the ones it asked for. A bounding box, having nothing to clip against, is
refused rather than silently dropped.
Several backends now warn where they used to accept an argument and drop
it -- scale and projection on the polygon backend, the ggplot2-only
arguments on the tmap and mapgl renderers, title/subtitle on
world_table(engine = "tibble"), and ... on two of interactive_map()'s
engines. The drawings are unchanged; only the silence is. Code running under
options(warn = 2) will now stop where it previously carried on.
Argument values are no longer partially matched. Every exported function
that takes a fixed set of choices now validates with rlang::arg_match()
instead of match.arg(), so style = "quant" errors rather than resolving to
"quantile". The error names the argument, the function and the valid
choices, and suggests the one you meant -- match.arg() reported only
'arg' should be one of ..., naming neither. Spelled-out values are
unaffected.
world_tiles places 171 of its 239 countries in a different cell. The
break in the placement scan left only its inner loop, so a country whose own
cell was taken was re-placed at the last free cell in the search square
rather than the first -- and every cell claimed along the way stayed marked
occupied for nobody. That wasted 129 of 368 cells and pushed later countries
further out still: mean displacement from a country's true position was 1.9
cells with a worst case of 7.1, against 0.9 and 2.8 now. tile_map()
layouts change accordingly. The grid is still 40x24 and still has one
country per cell.
dispute_policy() returns the policy it replaced, not the one you set.
R's convention for a setter -- options(), par(), sf::sf_use_s2() -- so
on.exit(dispute_policy(dispute_policy("neutral"))) now restores the old
value, where before it was a no-op. Reading the current policy with no
argument is unchanged.
world_geometry("coastline") and world_geometry("ocean") name their
geometry column geometry. It was x, from st_as_sf() on a bare sfc,
making these the only two what values whose column was not called what the
return contract says. Code that referred to $x must change; code written
against the other four now works on all six.
gini() and theil() warn instead of returning a bare NA when the
input is empty, or when na.rm = FALSE and values are missing. The value is
unchanged; code running under options(warn = 2) will now stop where it
previously carried an unexplained NA forward.
globe_map() built globes it could not draw. On the default sf
backend, 63 of 216 sampled viewpoints (29%) failed when the plot was
printed, with grid's "Invalid graphics path": a country on the horizon lost
its far-side vertices in the orthographic transform and kept whatever was
left (at lon 120, lat 20 Chad became a single point). The plot object built
fine, so nothing noticed until it was drawn, and a 60-frame
spin_globe(backend = "sf") could not get through one rotation. The
viewpoints that did draw closed every horizon country with a straight chord.
Geometry is now cut at the horizon on the sphere before it is projected, in
globe_map(), world_map(projection = "orthographic") on either engine,
world_geometry(), projection_compare() and tissot_map(), CShapes'
historical borders included; every sampled viewpoint draws,
and the colour scale and coverage are the same from every side.
world_map(na_style = "hatched") built maps it could not draw where sf
does not load. ggpattern draws the stripes through gridpattern, which
clips them with sf when the plot is printed. sf is always installed
alongside, but where its system libraries (udunits, GDAL, GEOS, PROJ) are
missing from the library path it cannot be loaded, so the map built fine and
then failed on print with "unable to load shared object units.so". Hatching
now checks that sf loads and otherwise falls back to grey with a message,
as it already did when ggpattern is absent.
world_map(na_style = "hatched") took half a minute to print on the
polygon backend. ggpattern clipped a fresh set of stripes to every
polygon, and the no-data countries of a world map are many polygons (169
for co2_per_capita in world_snapshot, most of them islands, plus
Antarctica). The stripes are now clipped once, to all of them together:
the same map, with the same stripes, prints in about a second.
Countries with no data vanished from animate_world() and got their own
panel in facet_map(facet = year). attach_geometry() gives a country
the data does not cover an NA year, so it belonged to no frame: the
animation never drew it (gganimate warned twice while dropping it) and the
small multiples drew it in an extra panel labelled NA. It is now drawn,
as no data, in every period.
value_by_alpha_map() and the value-suppressing palette ranked polygon
vertices, not countries. The polygon backend repeats a country's values
down every vertex of its outline, so the default transform = "rank"
weighted each country by how complex the outlines below it are: Chile, at
the 69th percentile of countries by population, was drawn at the 30th, and
the mean opacity error across 210 countries was 0.25. world_map(uncertainty = ) ranked the same way and put 101 of 189 countries in the wrong value x
uncertainty cell. Both now rank one value per country, as the quantile
breaks always have.
attach_geometry() let a column of the caller's replace the map
geometry. A shared name keeps the caller's version, so a group, lat,
long or order column (a treatment group, a capital's latitude) displaced
the polygon backend's own, and every map drawn from the result was nonsense:
a two-value group drew the world as three polygons. That is now refused,
naming the column; the sf backend is unaffected.
historical_geometry() gave the Soviet Union Russia's code. Gleditsch
and Ward use one code for both, so the 1980 USSR came back as iso3c = "RUS" and attach_geometry(year = 1980) painted all fifteen republics with
Russia's value, the anachronism audit_time_coverage() reports as
"before_existence". A code now stays NA until historical_codes says
the state existed. ?historical_geometry also said owner is NA for a
sovereign state (it is the state's own code) and named the Gold Coast as an
entity with no code (CShapes calls it Ghana and it carries "GHA").
A non-breaking space turned the USSR into Russia. The historical-name
lookup trimmed only ASCII whitespace, so "USSR" pasted from a web page
missed its alias and fell through to countrycode, which resolves it to
Russia alone: dissolve_country() returned one row instead of fifteen,
check_country_match() did not flag it historical, and
country_timeline() answered for Russia.
lisa_map() reported two different coverages. Its caption counted the
countries it coloured while map_provenance() counted every country with a
value, including the ones the weights could not connect, which are drawn as
no-data: 142 against 189 on the bundled snapshot. Provenance now counts
what is drawn.
od_map() dropped flows the basemap cannot draw without saying so.
Hong Kong, Macao, Tuvalu, the British Virgin Islands and Gibraltar have no
polygon on the polygon backend, so China's largest destination simply
vanished from China's panel. They are now named in a warning.
convergence_club() dropped countries without a complete series. A
country with one missing year disappeared from the result altogether; it
now comes back as club = NA, the documented "not classified", with a
warning naming it.
smooth_rates() let a negative count into its model. The pooled rate
and excess variance went off their domain, every _shrinkage came back
above 1 (the help promises 0 to 1) and rates were pushed away from the
global one. A negative count is now unusable and reported. The
unusable-denominator warning also counted rows whose numerator was
missing; it counts denominators only. rate_check() reported a standard
error of exactly 0 for a negative rate and now gives NA, with a warning.
world_map(uncertainty = , n_bins = ) said n_bins was ignored while
using it, for the value classes of the palette. The notice is gone there;
n_uncertainty without uncertainty, which really does nothing, now says
so instead.
join_world() replaced a caller's region, continent or iso2c
column in silence, where standardize_country() warns. It now warns too,
when a value actually changes.
One unmatched name in a region vector was dropped in silence.
region = c("France", "Germny") drew France alone; the name that matched
nothing is now reported, and a continent or group name inside a vector,
which region accepts only on its own, is explained rather than called a
failed match.
growth_rate() read the row after an infinity as -100%. Anything
finite over Inf is 0, so type = "yoy" gave -1 after an infinite value
and type = "cagr" gave -1 for every year of a series starting at one.
Both are NA now, with a warning.
audit_coverage() counted an infinite value as present, while
coverage_map(), which both help pages call the same report as a map,
shows it as missing. It counts as missing in both now.
world_map(engine = "tmap", style = "binned") drew pretty breaks. The
ggplot2 engine draws n_bins equal intervals; the tmap engine now does too.
A blank iso3c was reported as an excluded country by morans_i(),
gearys_c() and the other spatial statistics, inflating n_excluded.
A custom weights frame naming one link twice kept only the last weight. It is now refused, with advice to sum repeated flows first.
standardize_subnational() refused a lowercase or padded ISO 3166-2
code ("de-by", "DE-BY "), under a message promising case-insensitive
matching. Codes are upper-cased and trimmed first.
A bad countryatlas.cache_max_age or countryatlas.cache_max_size was
blamed on the cache directory. "a" or NA switched persistent caching
off with "Cannot write to the cache directory"; a negative value was
accepted. Both options are now validated when read, naming the option.
bivariate_map() did not validate dim, so "a", NA, c(2, 3) or
2.5 failed in base R or in biscale; a custom biscale palette also turned
the provenance style into one string per colour.
interactive_map() crashed on a fill with nothing to scale. The
leaflet engine died on "Wasn't able to determine range of domain" for an
all-missing numeric column, and the mapgl engine refused an all-missing
column and failed on a constant one or one with a single country's value
("values and stops must have the same length"). Such a map now draws
as no-data, as the ggplot2 engines always did.
interactive_map(engine = "plotly", projection = "orthographic") failed
inside plotly ("number of columns of matrices must match"), whose
converter cannot take an orthographic view. It now says so and names the
engines that can.
country_factsheet() misreported neighbours it had not computed. With
the lookup unavailable (no sf), France's sheet blamed the 110m basemap for
"excluding" Andorra and Monaco, and Andorra's said "None found". The sheet
now says the neighbours were not computed, and why.
?share_of_world promised a proportion in [0, 1] and ?index_to said
nothing about a negative base year; both now say what a negative value
does.
compare_sources() names a source listed twice rather than failing on
duplicated column names, and a non-finite year passed to world_data() or
country_data() is refused as such rather than leaking a coercion warning.
cartogram_map() and dorling_map() failed in the orthographic
projection, inside cartogram ("all sizes are missing and/or
non-positive"), because the far side's vertices were dropped rather than
cut at the horizon. They now draw the visible hemisphere. A contiguous
cartogram in Winkel Tripel also failed when printed, on a graticule the
theme never shows.
growth_rate(type = "cagr") gave a country with a zero base year NA
without saying so, and when that was every country, blamed the series for
being shorter than two years. It now names them.
deflate() left a row NA in silence when its year's index was zero,
infinite or missing, where to_ppp() and per_capita() report the same
thing. It now says how many rows.
compare_sources() crashed on a year no source covers, with base R's
"subscript out of bounds"; it now returns the empty comparison with a
warning. An infinite value made the relative difference NaN, which
disagrees counted as agreement; it is left out of the comparison and
named.
recenter = 360 or -360 failed on the sf backend with sf's
"polygons require at least 4 points", as did an out-of-range value such as
500, which is now refused by name before any geometry is touched.
bubble_map() ignored projection on its default polygon backend
without saying so, as world_map() already reports; it now names
backend = "sf" as the way to project.
Errors named functions the caller never called. A bare column passed
to one of the string-taking arguments was reported as "Error in
value[[3L]]()" (a tryCatch() handler), a missing column in
country_join_all() as "Error in FUN()", and a bad origin, weights
matrix, scale, region, footnote or override table under an internal
helper's name such as wdj_to_iso3c() or weights_custom(). Each now names
the verb. A bad global option is reported without a call.
The on-disk cache could delete files it had not written. With
options(countryatlas.cache_dir = ) pointed at a folder that held anything
else, the first cached fetch deleted every file in it without an .rds
extension (a sweep meant for entries left by 2.0.x), cachem's 30-day expiry
and size cap applied to every .rds file there, and
clear_wdi_cache(disk = TRUE) removed the folder and everything below it.
Entries now carry their own .countryatlas extension, pruning and clearing
touch only files the cache wrote, and the folder itself is removed only when
that leaves it empty.
local_morans() did not permute conditionally, as it documents. It
shuffled all n values with one permutation shared by every country, so a
country's own value could land among its neighbours. For the extreme values a
hot-spot map is about, that inflated the pseudo p-value: Monaco came out at
0.028 where the conditional reference distribution (and spdep's
localmoran_perm()) gives 0.0035. Each country's neighbours are now drawn
from the other n - 1 values.
convergence_club() depended on row order. pivot_wider() lays its
columns out in order of first appearance and the log-t test reads them by
position, so a shuffled panel, or one whose first country lacked the first
year, formed different clubs from the same data. The years are sorted first.
smooth_rates() shrank every year of a panel toward the earliest year's
global rate, under a warning that said the other years were dropped though
every row came back. The empirical-Bayes prior is now estimated per year,
the design spatial_lag() already uses for a panel.
bubble_map() drew a negative total as a bubble of its absolute value
(France at -1.4e9 as large as China) and an infinite one as an infinite
bubble. bubble_map() and spike_map() now set negative and infinite values
aside with a warning and count them as missing; spike_map() used to drop
them and blame a missing centroid. A missing size on the polygon backend no
longer leaks ggplot2's "Removed 1 row" at print time.
An infinite fill was counted as shown. Every fill scale draws Inf as no
data, but the footnote = "auto" caption, map_provenance() and
coverage_map() counted it as present: "189 of 240 countries shown" over a
map showing 187. It now counts as missing everywhere, na_style = "omit" and
"hatched" treat it as the no-data it is drawn as, and the map verbs name
the countries holding one. interactive_map(engine = "leaflet") no longer
fails on one with leaflet's "Wasn't able to determine range of domain".
flow_map() with a missing or infinite weight leaked "Removed 50 rows",
or failed at print time with grid's "'lwd' must be non-negative and finite".
Those flows are dropped with a warning, as flow_matrix() drops them.
cartogram_map() and dorling_map() likewise name an infinite weight
instead of handing it to cartogram.
growth_rate(type = "yoy") returned Inf and NaN after a zero, where
every sibling verb gives NA for a zero denominator. It now gives NA, with
a warning.
A row with a missing year was treated as the latest year.
lag_by_country(), diff_by_country() and growth_rate() gave it a value
computed from the last real year, beta_convergence() dropped its whole
country, sigma_convergence() and share_of_world() reported a phantom NA
year, and interpolate_missing() crashed on it, and could have overwritten
an undated observed value with NA. Undated rows are now left out of
anything that needs a position in time.
complete_years(method = "locf") carried values in row order, not time
order, when the data held years outside years, and returned the frame out
of order.
The United Kingdom left the EU a day early. country_groups_history
reads to as the first day of non-membership (Austria left EFTA and joined
the EU on 1995-01-01), but the UK's exit was recorded as 2020-01-31, its last
day as a member. It is now 2020-02-01, and the column's help states the
convention.
projection_distortion() measured a WGS84 projection against a sphere.
Equal Earth read between 0.9955 and 1.009 instead of 1, and Mercator showed
up to 0.38 degrees of angular distortion, contradicting both checks its
documentation offers. It now uses the ellipsoid's radii of curvature and
central differences: Equal Earth, Gall-Peters and the Lambert azimuthals read
1, Mercator 0, and the 0.7% that Mollweide and Eckert IV show (PROJ applies
their spherical formulas to the ellipsoid) is documented.
theil(na.rm = FALSE) broke its own decomposition on a missing group.
The row stayed in total and fell out of both components, so total no
longer equalled between + within. It now returns NA, as it already does
for a missing value under na.rm = FALSE.
Reports that said something false. country_timeline() warned that
"Tanganyika", "Zanzibar" and the "United Arab Republic" matched nothing while
returning their history. audit_time_coverage() returned a phantom all-NA
row for an unreadable year, and now names the dissolved codes it flags.
compare_sources() kept unresolved keys as iso3c = NA rows, counted them
in only_x/only_y, and called two of them "a duplicate country".
index_to() and deflate() reported two unidentified countries as "1
country: NA". world_table() gave tied values different ranks.
country_join() and country_join_all() warned that iso3c was "replaced"
when it was the join column itself. subnational_map(projection = ) printed
raw cli markup and advised geometry = "sf". audit_coverage() left a blank
code out of unmatched, and check_dispute_coverage() listed NA and
repeated values as bad codes.
Inputs accepted and then failed somewhere else. A numeric region that
is not a four-number box (region = 250) drew an empty map, and a reversed
box was accepted. Custom country_weights() codes were taken verbatim, so
lowercase ones made every statistic report "Not enough connected countries"
with advice about islands; they are normalised, and codes that differ only in
case are now named as the key problem they are. spatial_lag() on a panel
aborted entirely when one year was too sparse. deflate(), to_ppp() and
rate_check(rate = ) did not check that their value column was numeric, and
per_capita(), to_ppp() and add_indicator() leaked dplyr's join-type
error for a character year. value_by_alpha_map() drew a character
value as categories labelled "quantile". classify_compare(),
projection_compare() and od_map() died on "factor level [2] is
duplicated" for a repeated method, projection or origin.
country_join_all(by = iso3c) gave "object 'iso3c' not found".
historical_geometry(Inf) failed inside as.Date(), locate_country() on
zero points leaked four base R warnings, and
as_ggsql_source(format = "parquet") left its connection open when the
write failed.
interactive_map(engine = "leaflet") hard-wired a numeric colour scale.
A discrete fill reached leaflet::colorNumeric() and died inside leaflet
with "Wasn't able to determine range of domain" -- the same defect
auto_fill_scale() was written to fix for the ggplot2 engines, and that the
mapgl engine handles with match_expr(). ?interactive_map documents no
per-engine restriction on fill, so the leaflet engine now branches on the
column's type and uses colorFactor() for a discrete one.
The leaflet engine could be broken by a column named pal. Its
fillColor = ~ pal(get(fill_name)) formula was evaluated by leaflet against
the data as an environment, so a column of that name was found before the
palette function and then called. The values are computed directly now, which
also removes the only get()-in-a-formula column read in the package.
The leaflet and ggiraph engines read iso3c without checking for it.
check_cols() covered fill and tooltip, and check_map_geometry() does
not require a key, so an sf frame without one failed at render time from
inside leaflet or rlang. Both now name the missing column.
bubble_map(backend = "sf") carried two geometry columns.
as_tibble() strips the sf class but leaves the live sfc column, so the
st_drop_geometry() further down saw a plain tibble and returned it
unchanged -- and the join then produced geometry.x / geometry.y, renaming
the active column out from under coord_sf(). It now uses sf_drop() first,
as four other verbs already do.
index_to() returned an all-NA column in silence, three ways. It and
deflate() take the same base_year and match it the same way, and
deflate() reports every case where the rebasing cannot happen while this
reported none. A Date or POSIXct year was the worst: == coerces the
number to that class, so as.Date("2000-01-01") == 2000 compares against
1970-01-01 plus 2000 days and is false for every row -- every country
indexed to NA, and read.csv() with a date-parsing reader produces
exactly that column. That shape is now refused by name. A base_year the
panel does not cover, and a country whose base-year value is missing or
zero, still come back NA as documented -- but the verb now names the
countries it could not index, so an all-NA column is readable rather than
indistinguishable from a source that had no data. A character year still
works, deliberately: this verb only matches on the year, and "2000" == 2000 is true.
deflate() aborted on a zero-row panel, reporting "Years present: Inf
and -Inf" from range() of nothing, and would have asked the World Bank for
the year range Inf..-Inf. It returns early with the promised column,
typed.
rate_check() warned that a zero-row frame had no usable denominator.
quantile() of an empty vector is NA too, so the guard for an all-NA
denominator fired for a frame that simply had no rows.
beta_convergence() and convergence_club() leaked base R's "essentially
perfect fit: summary may be unreliable". It named neither the verb nor the
column, and from convergence_club() it described an internal log-t
regression the caller does not know exists. beta_convergence() now says so
in the package's own voice -- naming which columns are meaningless as a
result -- and the internal fit is silent.
simplify_geometry() skipped its documented MULTIPOLYGON cast for an
sfc. The validator accepts sf and sfc; the cast gated on sf alone,
so half the accepted inputs came back as a mix of POLYGON and MULTIPOLYGON.
world_table(engine = "gt") rendered year as "2,020". year now joins
rank in the columns fmt_number() leaves alone -- it is an identifier, not
a measurement.
tile_map()'s provenance called a numeric fill "categorical".
auto_fill_scale() picks the scale from the column's type, so a numeric fill
was drawn continuously and recorded as categorical, in the record whose job
is to say what was drawn.
split_antimeridian() wired up only the last crossing. A carry row was
computed and stored for every crossing but only the last was read, so with
two or more crossings the middle segments began at their first raw vertex
instead of at the antimeridian edge, leaving a visible break on the re-entry
side. The code read as though it were general; it handled one.
A region that resolves to NA codes selected uncoded geometry.
resolve_region()'s continent branch reads countrycode's codelist, where a
few rows carry a continent and no iso3c, and %in% treats NA as a value
-- so NA %in% c("FRA", NA) is TRUE and a subset keyed on the result
pulled in every geometry row whose own iso3c was NA. The sf backend
guarded this; the polygon backend did not. The codes are now filtered at the
source, so all callers are covered.
country_overrides() accepted any string as an iso3c value.
wdj_to_iso3c() whitelists every override value as a legitimate code, so
country_overrides(c(Freedonia = "1")) put "1" in the iso3c column and
every join keyed on it. Values must now look like an ISO 3166-1 alpha-3
code. Separately, any(!nzchar(nms)) missed an NA name -- nzchar(NA) is
TRUE -- while wdj_to_iso3c() rejected one, so the two validators
disagreed about what a valid override table is.
world_query() interpolated identifiers into SQL without escaping them. Only title
was escaped, and it is the one value that lands inside quotes; every other
name went in as bare SQL. A column name with a space produced invalid SQL
with no diagnostic, and a name derived from untrusted input reached a string
that ggsql::ggsql_execute() then runs. fill, source, size, facet,
projection, palette and transform are now checked against a plain SQL
identifier.
nuts_geometry(countries = "Germany") said "NUTS covers the EU, EFTA and
candidate countries only" -- true, and not the problem. countries is read
as iso3c, so a country name resolves to nothing; the error now names the
origin that would have worked.
od_map()'s origin and origins are one letter apart and adjacent in
the signature, and mean unrelated things. Both mix-ups used to fail somewhere
else; each is now diagnosed where it was made.
geom_country_labels() did not validate size or mapping, which feed
ggplot2 arithmetic and modifyList() respectively; world_table() checked
title but not subtitle; and ggsql_wkb_frame() overwrote an existing
column of the geometry column's name in silence, where eleven other
column-adding verbs warn.
world_query() silently ignored arguments its layer does not use. size
belongs to layer = "bubble" and n_bins to layer = "binned", and the
other layers took them without a word: n_bins was dropped, since only the
binned layer emits a BIN clause, while size still went into the
VISUALISE list as pop AS size on a choropleth, which has no size channel
to put it on. Both are now reported, together when both are given. The
emitted query is unchanged -- a pass-through builder passing a clause through
is defensible -- but the silence was not, since the abort beside it already
treats a layer/argument mismatch as worth naming.
globe_map() crashed at some viewpoints with an error from the geometry
engine. globe_map(sf_data, lon = 90, lat = 30) died on
IllegalArgumentException: point array must contain 0 or >1 elements, which
names neither the projection nor the viewpoint. coord_sf() draws a
graticule, and on the one genuinely hemispheric projection those lines are
clipped at the horizon -- a clipped line can reduce to a single point,
which GEOS rejects. winkel_tripel already had datum = NA for its own
graticule trouble; "orthographic" now does too, and all 65 sampled
viewpoints draw. The other three azimuthal projections show the whole globe,
so nothing is clipped and they are unaffected.
country_weights() silently ignored the arguments its scheme does not
use. k, cutoff_km, w and scale each belong to exactly one scheme,
and the other three dropped them without a word. The costly case is
country_weights("knn", w = my_matrix): the caller's own adjacency matrix was
discarded and nearest-neighbour weights returned instead, so the result looks
entirely reasonable and is not what was asked for. cutoff_km passed to
"knn" reads as a distance cap that was never applied, and scale only ever
affected "contiguity". All eleven combinations now report which arguments
were ignored and which one the chosen scheme is built from; each scheme's own
argument, the shared style, and a default passed explicitly stay quiet.
Arguments that do not apply were silently ignored by the map verbs. The
package reports an inert argument rather than pretending to honour it --
that is what warn_projection_ignored() and warn_recenter_ignored() are
for, and attach_geometry(), world_geometry(), world_data() and
join_world() all call them. world_map() did not, though it takes both
arguments and is the verb people actually reach for: on the polygon backend,
which draws in unprojected longitude/latitude, projection = "mollweide"
and recenter = 180 looked honoured and changed nothing. n_bins was inert
in the same way under style = "continuous" (a colourbar has no classes)
and style = "categorical" (the classes are the values) -- the same
complaint the 3.0.0 fix for style = "binned" answered. All three are now
reported, under countryatlas_projection_ignored,
countryatlas_recenter_ignored and countryatlas_n_bins_ignored, and
value_by_alpha_map() gets the two that apply to it since it carries its own
copies of those arguments. facet_map() and coverage_map() inherit them by
passing ... through.
attach_geometry(year = ) warned about the caller's frame size, not the
geometry. Its threshold was matched < nrow(geom) * 0.5, so an ordinary
three-country frame -- 3 of the 97 entities CShapes has for 1960 -- was told
"only 3 of 97 historical entities matched" and advised to join on gwcode
"for full historical coverage", which is not what someone asking for three
countries wants. What the message claimed is a property of CShapes rather
than of the request: a few entities never had an ISO code (6 of 97 in 1960,
3 of 174 in 2019) and no iso3c join can reach them. That is what it now
reports, independently of the frame, and the nothing-matched case is left to
the same helper every other geometry verb uses. The whole historical branch
had no test coverage.
A missing-column error named an argument the function does not have.
check_cols() wrote `data` into its message unconditionally, which is
right for the twenty-odd verbs whose frame argument is called that and wrong
for the three whose is not: country_weights(w = ) reported Columns "iso3c" and "neighbor" not found in `data``` and `cartogram_diagnostics(x = )` reported Column "nope" not found in
`data```. The helper now takes the name to use, and the duplicated-column
error beside it does too.
A provider renaming its time column turned the years into the values.
adapter_reshape() -- the shared step that puts a provider's response into
the package's shape, behind fetch_owid(), fetch_eurostat(),
fetch_oecd() and fetch_comtrade() -- checked that the entity and value
columns exist but not the year column. The value column is auto-detected as
the first numeric column other than the year, excluded by name, so with the
year column missing the exclusion removed nothing and the provider's own time
column was picked as the indicator: the mapped values came back as 2000,
2001, 2002 while year was entirely NA, with nothing said. A missing year
column is now reported like the missing value column beside it.
locate_country() gave a raw sf error for a points object with no CRS.
sf::st_as_sf(df, coords = c("lon", "lat")) leaves the CRS missing whenever
crs is forgotten, and the object then reached st_transform() and came
back with sf's "cannot transform sfc object with missing crs" -- naming
neither the argument nor the fix. The non-sf case one line above had already
been given a proper message; this is the same failure one step further in.
It now says which argument is at fault and how to set the CRS.
subnational_map() joined against the wrong column when the caller's code
column collided with the geometry's. The geometry side is meant to be
nuts_id "whatever the caller's column is called", but the guard was
if (!by %in% names(geom)) -- false exactly when by names one of the
geometry's own columns. by = "name" therefore joined the caller's NUTS
codes against the geometry's region names and matched nothing, and
by = "iso3c" silently joined at country granularity instead of regional.
by is documented as "the code column in data", so the geometry side is
now always nuts_id.
subnational_map() blamed the codes for an all-NA indicator. It counted
matches as sum(!is.na(fill)), so a panel whose indicator is entirely NA
-- a real thing to map, and one this package draws with an na.value and
reports in the caption -- was refused with "no rows matched the geometry",
sending the reader to check NUTS vintages for a mismatch that never happened.
The count is now taken on the join key. Both of these sat in code that no
test reached, because the path needs a GISCO download: R/subnational.R was
at 66% coverage where every other file was above 90%. It is now exercised
with a mocked nuts_geometry().
correlate_indicators() let a tidyselect error through for a missing
column. Every other verb reports one as ``Column "x" not found in data```; this one selected through dplyr::select()` and surfaced the
`vctrs_error_subscript_oob` raised by tidyselect -- "Can't select columns
that don't exist" -- which was the last error at this boundary not carrying
the package's class.
The selection is a full tidyselect expression, so it cannot be checked by
name up front; the failure is caught and re-worded instead, and
`starts_with()`, `where()`, ranges, bare columns and the no-argument form all
still work.
Rows whose iso3c did not resolve were treated as one country.
dplyr::group_by() puts every NA in a single group, so a panel carrying
two unmatched names was one country to every verb that reads a neighbouring
row within it: growth_rate() reported the change from one unmatched row to
the next -- 899% between two unrelated countries -- and lag_by_country(),
diff_by_country(), index_to(), interpolate_missing(), deflate() and
beta_convergence() read across them the same way, silently. That frame is
exactly the one a user is most likely to hold, because
standardize_country() has already warned that those names did not match
and left their iso3c as NA. The grouping now falls back to whatever does
identify the row -- the same country, then group order
distinct_countries() uses for its uncoded branch -- so an appended
aggregate row (no iso3c, country = "World") still groups as one series,
while rows nothing identifies each get a key of their own and are no longer
read across. complete_years() had the same conflation twice over: the two
unresolved rows shared one completed year grid, and the geometry carry that
gives invented rows their country's shape matched on iso3c -- where
match() treats NA as equal to NA -- so it copied one unidentified
country's polygon onto the other's invented rows. Both are keyed on the unit
now, and each unresolved row gets its own grid and its own shape.
A blank code is treated the same way. "" is not NA, so every is.na()
guard missed it -- but read.csv() without na.strings = "" gives a blank
for every empty cell, and standardize_country("") already resolves to
iso3c = NA, so a blank identifies no country anywhere else in the package.
Two blank-coded rows produced the same fabricated 899%. Whitespace-only codes
count as blank too, using the same Unicode [\h\v] class
standardize_country() uses rather than trimws()'s ASCII-only default.
The two panel guards were keyed on iso3c while the verbs grouped by the
unit, which made them disagree: four single-year unidentified rows were
reported as one country with gaps between them, and two unidentified rows in
the same year as a duplicated country-year. Both now key on the unit, so
they describe what the verb actually did; a genuinely duplicated
country-year and a genuinely irregular panel are still reported.
The unparseable-year warning printed its own markup and padded its list
with NAs. Two defects in one message. cli does not re-interpolate a
substituted value, so {source_label} emitted the label verbatim: three of
the six call sites showed users `{.arg data}`: 2 time values are not a year and, from the public source extension point, Source {.val {source}}: -- where the braces also referenced a variable that exists only
in the caller's frame. Separately, the list of offending values was sliced to
min(4, sum(bad)), sizing it by the number of bad rows while the values
themselves were de-duplicated, so a column of one repeated placeholder --
"N/A", "..", "-", the common case -- read "N/A", NA, NA, and NA. The
label is now pre-rendered in the caller's environment and the list is taken
with head(), so the warning names its source and shows only values that
are really there.
audit_time_coverage() invented history for a factor iso3c. It looked
the dissolution and succession dates up by indexing a named vector with the
frame's iso3c column -- and indexing a named vector with a factor
selects by the factor's integer codes, not its labels. A frame from
read.csv(stringsAsFactors = TRUE) therefore matched whichever rows of
historical_codes happened to sit at those positions: a clean panel of
France, Germany, Italy and Spain came back reporting France dissolved in
1993, Italy in 1992, Spain in 1991 and Germany as existing only from 2010,
from the one verb whose whole job is catching that kind of mistake. The
year key had already been hardened against the same class of bug (a Date
year became a day count, hence read_year()); the iso3c key had not. The
column is now coerced before the lookup, so a factor iso3c gives the same
answer as a character one -- verified across 27 verbs, none of which now
differ on a factor iso3c or an integer value column.
A stray 60 KB Rplots.pdf shipped inside the source tarball. Printing a
plot with no device open makes R open the default one, which in a
non-interactive session writes Rplots.pdf into the working directory -- for
the test suite, tests/testthat. .Rbuildignore carried ^Rplots\.pdf$,
anchored at the package root, so it matched the root copy and not the nested
one, and R CMD build included the artefact. The pattern is now unanchored,
and a test setup file points the default device at the null file so the file
is not written in the first place -- plots still render, so a drawing error
still fails a test.
A factor year column silently produced wrong numbers. dplyr::arrange()
and order() sort a factor by its level index, not by the label, and
stats::approx() coerces a factor the same way. A year column arrives as a
factor more often than it looks -- read.csv(stringsAsFactors = TRUE), some
importers, and any deliberate factor(year) for plotting -- and every verb
that reads a neighbouring row then read the wrong neighbour.
lag_by_country() and diff_by_country() took the value from the wrong
year, growth_rate() reported -0.75 on a series that had doubled, and
interpolate_missing() left the gap NA because the targets fell outside
the anchors it had mis-placed. Nothing warned: the panel came back the right
shape with the wrong values. warn_irregular_years() had the same trap from
the other side -- as.numeric() on a factor returns level indices, so it
reported a perfectly regular annual panel as having gaps. The nine
year-ordering sites, the linear interpolation path and the gap check now go
through one year_sort_key() helper, so a factor or character year gives the
same answers as a numeric one, and the gap warning is now classed
countryatlas_irregular_years so a deliberately decadal panel can silence
just that one. earliest_per_unit() had carried this coercion since 2.0.0,
which is why the map-drawing path was
unaffected and this survived. A year column that is not numeric at all -- an
ordered factor of period labels -- still sorts by its own order, and
complete_years() still asks for a numeric year, since it has to generate
the grid rather than merely order it.
A bare column name was reported as "object not found" by six arguments.
A handful of arguments take a column name as a string while the verbs
around them take a bare column through tidy eval, so the package converts
that predictable slip into a message naming the argument and showing the
quoted form to write. The conversion had been applied to the value,
indicator, columns, codes and by arguments of interpolate_missing(),
complete_years(), audit_coverage(), world_table(), country_codes()
and aggregate_regions(), and not to the join-key and query arguments of
the same shape: aggregate_regions(by = region) explained
itself while attach_geometry(by = iso3c), subnational_map(by = nuts_id),
register_country_source(key_col = iso3c), world_query(facet = year),
world_query(size = pop) and as_ggsql_source(geometry_col = geom) all
reached the user as base R's "object 'iso3c' not found". All six now report
the argument and the fix. A genuine error inside such an argument still
surfaces as itself.
The plotting verbs rejected a column passed as a string. Every unquoted
column argument in the package goes through one helper, whose own error hint
tells the user to "pass the column unquoted (fill = my_col) or as a
string", and the column check then accepted the string form happily. But
world_map(), tile_map(), bubble_map() and interactive_map() spliced
the captured expression straight into aes(), which honours only the
unquoted form: fill = "value" mapped the constant string "value". The failure then arrived at
build time, from ggplot2, as "Discrete value supplied to a continuous
scale" (or "Binned scales only support continuous data" under style = "binned") -- naming neither the argument, nor the column, nor the fix, which
is the exact failure the helper exists to prevent. world_map(d, "value") --
the package's most common call -- errored while world_map(d, value) drew.
The mapping is now built from the validated column name, so both forms
produce the same plot; coverage_map(), spike_map() and the quantile,
jenks and categorical styles were already correct, which is why this
survived.
interpolate_missing() invented data across decades. max_gap is
documented as "the longest run of consecutive missing years to fill ...
because interpolating across a decade is not interpolation", and it counted
missing rows. On any panel that is not annual the guard was therefore
defeated: a decadal panel of 2000, 2010, 2020 with 2010 missing is one
missing row, so the default max_gap = 3 filled it -- placing an invented
value ten years from either anchor, the exact thing the parameter exists to
refuse. Five-yearly data was interpolated across 15 years the same way.
A run is now refused if it is longer than max_gap in rows -- which is what
an annual panel means by consecutive missing years, so annual behaviour is
unchanged -- or if any filled year sits further than max_gap from its
nearest observation, which is what makes the decadal case wrong. Measuring
the bracketing span instead would have been too blunt: on 2000, 2001, 2005
the filled 2001 is one year from an anchor and interpolating it is sound,
even though the span is five. The documentation was already right; the code
has caught up with it.
interpolate_missing(method = "linear") leaked a dplyr internal for a
year it could not read. Linear interpolation places the filled value
along the year axis via approx(), so a labelled year column arrived as
NA and surfaced "need at least two non-NA values to interpolate" wrapped
in an across() error, naming neither the column nor its type. It is now a
classed error naming the unreadable values and pointing at
method = "locf", which carries the last value forward in row order and
needs no arithmetic. It tests whether the year can be read as a number
rather than whether it is numeric, deliberately: approx() reads "2000"
happily, so a character year still works here --
the difference from deflate() and beta_convergence(), which do the
arithmetic themselves.
gearys_c() and getis_ord() listed their count columns without saying
what they count. morans_i() defines all three -- n (countries used),
n_excluded (countries with data the weights could not reach), n_links
(non-zero weights) -- and the other two named the same columns and left them
undefined, so a reader had to go to a third function's page. The counts are
consistent: verified across all six spatial verbs on one graph, n is 187
everywhere it appears, n_excluded 2, n + n_excluded is the 189 countries
supplied, n_links matches the non-zero weights, and the local forms return
exactly n rows. All of that is now stated where the columns are listed.
audit_coverage() did not say which way round its counts run. Its
@return described the three elements but none of their columns, and both
na_rates$n and by_group$n_countries are denominators -- everything
counted, not everything present. That is the opposite orientation from
map_provenance(), whose n_countries is the numerator, and the two share
that column name: on 215 countries with 24 missing, audit_coverage() gives
n = 215 where map_provenance() gives n_countries = 191. The package had
already spelled this out for map_provenance() ("the numerator, not the
denominator, which its name does not say on its own"); the verb whose whole
purpose is reporting coverage had not. Every column is now defined, with the
contrast stated. The numbers themselves were correct throughout -- verified
against the data, including that the group counts sum to n and each
na_rate is the within-group share.
as_ggsql_source() accepted things that are not data, and gave a bare
error for a closed connection. data is documented as a map-ready frame
and was never checked, so as_ggsql_source(1:5) wrote an integer vector out
as a table and returned a connection -- a "world source" containing no
countries at all -- while NULL surfaced Data frame with at least one
column required, a message from DuckDB rather than from here. A con the caller had already closed reached
dbWriteTable() as base R's unclassed Invalid connection, naming neither
the argument nor its state. Both are now classed errors, checked at the one
point that knows whose connection it is -- which is also why the existing
bookkeeping there closes a connection the function opened and leaves one
passed in alone.
A negative or infinite custom weight was accepted and quietly changed the
answer. country_weights("custom", w = ) validated the matrix's dimnames,
squareness, type and NAs, and the type message even promises "any
non-negative number" -- but nothing enforced it. Row standardisation divides
by the row sum, so one negative weight moved Moran's I from 0.714 to 0.497
with no warning, and an all-negative matrix cancelled to exactly the
all-positive answer, discarding the caller's signs entirely. An infinite
weight normalised to NaN, and the verb then reported not enough connected
countries with data -- diagnosing connectivity when the cause was the
weight. gini(), theil() and the global G all refuse a negative input for
the same reason, so both are now classed errors naming how many entries are
at fault. The long-frame form of w gets the same two checks, and all three
built-in constructors satisfy them.
A partly named custom_match silently ignored the unnamed entries. The
check tested is.null(names()), which catches a wholly unnamed vector but
not c(Freedonia = "FRA", "DEU"), whose names are c("Freedonia", ""). An
entry named "" or NA can never match a spelling, so it sat there doing
nothing while the caller believed it was overriding something -- the same
silent-but-useless entry the surrounding comment was written about, and
invisible in the output, unlike a mistyped code which at least shows up in
the iso3c column. Every element must now be named. Values are still not
checked against the ISO list, deliberately: the package's own overrides map
Kosovo to XKX, a World Bank user-assigned code that is not in
countrycode::codelist$iso3c, so validating that way would reject the very
case overrides exist for.
A custom source that failed, or was written with the wrong signature,
reported neither. register_country_source()'s boundary checks what an
adapter returns thoroughly -- a non-data-frame, a missing key column and a
duplicate key are all named and pointed at the fetch contract -- but not the
call itself. An adapter that raised its own error re-raised it bare, so
"provider is down" reached the caller with nothing to say which source
produced it; and one defined as function(indicator) surfaced R's
unused arguments (countries = countries, years = years), naming neither the
source nor the documented arity. Both are what someone developing an adapter
hits, and both are the one moment the package knows which source it just
called. They are now classed errors that name the source, preserve the
provider's own message verbatim (braces included -- a cli template would
have interpolated them), and point at the contract.
A flow between near-antipodal countries was drawn as a streak across the
top of the map. great_circle() sampled a fixed 50 points along the arc,
and a near-antipodal arc passes within half a degree of a pole, where
longitude turns almost arbitrarily fast: Belgium to Tonga stepped 131 degrees
of longitude between two consecutive points, Greenland to Japan 121, Tonga to
the Netherlands 136. split_antimeridian() only cuts a step wider than 180,
so none of these were cut and geom_path() joined them literally -- the same
failure that function was written to fix for trans-Pacific flows, caused by
the pole rather than the antimeridian. 16 of 8010 real ordered country pairs
hit it, every one at latitude 87 to 89.
The path itself was right; 50 points is simply too coarse where it turns
fastest, and the step shrinks in proportion to the sample count. The
offending segments are now bisected until every step is under 20 degrees, so
all 8010 pairs come in under that, at a cost of at most 63 points instead of
50. An ordinary arc never trips the threshold and keeps its n points
exactly.
gridded_cartogram() now documents that crowded neighbours overlap.
Each country's block is centred on its own centroid with no collision
avoidance -- a deliberate choice, since a global packing solve would push
countries away from where they belong, and it was recorded in a code comment.
It was not in the documentation, though, and it is not marginal: at the
defaults about a third of the cells are drawn on top of a cell belonging to a
different country, across some sixty countries, rising to roughly two thirds
at cells = 2500. A partly hidden block cannot be counted or compared, which
is the one thing this verb asks the reader to do. A new section says so,
quantifies it, and names cell_size as the lever -- dropping it from 2.5 to
1.5 cuts the overlap at cells = 1000 to about a tenth -- with a pointer to
dorling_map(), which resolves collisions by displacement instead.
beta_convergence() returned a silent NA for speed and half_life
on an unbalanced panel, and the documented reason did not apply. Turning
beta into an annual rate inverts beta = -(1 - exp(-lambda T))/T, which
needs 1 + beta * T > 0. That holds whenever every country spans the same
T, but beta is fitted on growth already annualised per country, so a mix
of spans leaves the mean span irreconcilable with the fitted slope. On a
panel where half the countries were observed for 10 years and half for 50 --
ordinary coverage for real data -- beta came back at -0.041 with
p = 9.5e-14 and r_squared = 0.96, unmistakable convergence, while the two
most interpretable columns were blank and the documentation said NA meant
beta >= 0, which it plainly was not. The guard itself was right: log() of
a negative is not a speed. It now warns, naming the range of spans and saying
that beta and its inference are unaffected, and the documented NA
conditions list both cases.
local_morans() returned a lag column that was not the neighbour
average it is documented as. It reported the lag of the centred value,
which is what the statistic and the quadrants are built on, beside a value
column that is raw -- so the two columns in the same tibble were on
different scales, and neither matched spatial_lag()'s column of the same
name. That breaks the Moran scatterplot the pair exists for: with value
centred on 17384 and lag on 0, the quadrant boundaries fall in different
places on the two axes, so the usual reference lines disagree with the
cluster column beside them. The reported lag is now the plain neighbour
average, identical to spatial_lag(). The centred lag is still what ii
and the four quadrants use internally, which is what Anselin (1995) defines
them on, so no statistic, p-value or cluster label changes.
A value-suppressing uncertainty palette drew nothing when only one
country was usable. The two axes are ranked with percent_rank(), which
is (rank - 1)/(n - 1) and therefore NaN for a single observation, so
cut() returned NA and the row got no colour -- a country whose value and
uncertainty were both present, drawn as though neither were. This is not a
one-row-input curiosity: a mostly-missing uncertainty column with a single
usable country blanked the entire VSUP layer. A lone observation has no rank
position relative to others, so it now lands in the middle of each ramp,
claiming neither extreme, which is also where a maximally uncertain value
sits. Rows that are genuinely missing still get no colour, and a palette
with more than one usable row is unchanged.
rate_check() ranked a zero-count row as the most reliable in the
table. The output is documented as "sorted with the least reliable first",
and it sorted on expected_se, which is the Poisson standard error
sqrt(y)/d -- exactly 0 when the count is 0. So a country with no
events out of 251 people sorted last, presented as the most trustworthy
row on the page, while another with one event out of the same 251 sorted
first as the least trustworthy. Observing nothing is not evidence of
precision; it is the small-number problem this verb exists to name, and
nothing else in the output caught it either, because the default threshold
is a quantile of the denominators. The ordering now uses the standard error
a single event would imply, sqrt(max(y, 1))/d, which is identical to
expected_se for every row with at least one event -- so no other row
moves -- and expected_se itself is unchanged, still the plain Poisson
standard error.
convergence_club() found no clubs at all on an indexed panel. The
log-t statistic refused to compute whenever H_1, the cross-sectional
dispersion in the first period, was zero -- and it is exactly zero
whenever every country starts equal, which index_to() guarantees by
construction. So the natural pipeline, convergence_club(index_to(panel, value, base_year = ...), value_index), silently placed nobody: the same
panel in levels found seven clubs covering 16 of 20 countries, and indexed
found none.
H_1 cannot affect the answer. log(H_1/H_t) is log(H_1) - log(H_t), and
log(H_1) is one constant across the regression, so it lands entirely in
the intercept while the statistic returned is the t on the slope --
verified bit-identical for H_1 of 1e-6, 1 and 1e6, and for omitting it
altogether. The guard now requires only the periods inside the regression
window to be positive and finite, which also stops earlier periods being
over-checked when they never enter the fit. A window with genuinely zero
dispersion is still NA, since there is no decay rate to estimate.
repair_country_names() promised something it does not do. The
documentation said the no-stringdist fallback "repairs a subset of what
Jaro-Winkler would ... and never picks a different country". The second half
is false, and not marginally: the two metrics choose the nearest known name
independently, so "Libia" is repaired to Liberia with stringdist and to
Libya without it, both accepted at the default threshold. Across a
42-misspelling sweep the two disagreed on the candidate five times, once
reaching the user as a different substitution. No code change can make two
different string metrics agree on a nearest neighbour -- and normalising the
candidate gate in the fallback, which was the obvious repair, agrees less often
(35/42 against 37/42), so the metric is left alone. The documentation now
says plainly that the engines can land on different countries, recommends
installing stringdist, notes that neither metric is uniformly better
(Jaro-Winkler repairs "Maroco" to Monaco, not Morocco), and points at the
reported substitutions and the "repairs" attribute as the thing to check.
The same paragraph's aside about transpositions was ambiguously worded and is
now explicit with the numbers: the fallback charges two edits for a
transposition, so "Germny" is repaired (0.14) and "Frnace" is not
(0.33), where Jaro-Winkler scores the latter 0.06 and repairs it.
getis_ord(local = FALSE) returned a silent NaN in three cases. The
global G is a ratio of cross-products, and the denominator is zero when
every value is zero (0/0) or non-finite when the products overflow or
underflow at extreme magnitudes (Inf - Inf, or an underflow to 0). The
branch immediately above it already warns and returns NA for negative
values, citing gini() as the precedent for "outside the domain is NA
plus a word about why"; these three slipped past it. They now warn and
return NA too, and the overflow message says that the statistic is
unchanged by a positive scale factor, so rescaling the column fixes it.
The permutation p-values did not say which tail they used. morans_i()
documented its own (one-sided, upper); local_morans() and gearys_c()
documented neither their tail nor that they consume the RNG. That matters
for reading them: local_morans() is two-sided, and gearys_c() is
one-sided on the lower tail -- the opposite way round from Moran's I,
because Geary's C runs from 0 (neighbours identical) up through 1 (no
autocorrelation), so a small c is the evidence of clustering. All three
now give the formula, note that the p-value can never be exactly zero (its
floor is 1/(n_perm + 1)), and say to set a seed for reproducibility.
A categorical map's colours depended on the machine's collation locale.
A character fill column reaches ggplot2 unfactored, and its discrete scale
derives the level order by sorting -- which consults LC_COLLATE. The same
script on the same data gave a legend of Belgium, Chad, Zambia, aland,
Åland under C collation and aland, Åland, Belgium, Chad, Zambia under
en_US, so every category was drawn in a different colour and two people
running identical code got different maps. The level order is now pinned
byte-wise (order(method = "radix"), which is precisely what a plain
sort() does not do), so it is the same everywhere. An incoming factor is
left alone -- the caller has already chosen an order. Same fix applied to
interactive_map()'s mapgl branch, where the sorted values are paired
positionally with the colour stops, and to the classification_report
fallback, whose rows came out in a locale-dependent order.
classify_compare() drew half a panel as grey, and which half depended on
row order. It took the class breaks from one arbitrary row per country --
the same flaw as the break computation behind world_map() -- but here every
row is then cut() against those breaks, so the rows the chosen year did not
cover fell outside the range and came back NA. On a two-year panel one
year classified correctly and the other vanished into na.value, and
swapping the input's row order swapped which. It now de-duplicates
(country, value) pairs, which still collapses the polygon-backend vertex
rows the de-duplication exists for and spans the panel otherwise.
distinct_countries() disagreed with itself. The branch for rows that
resolved to an ISO code picked the earliest year explicitly; the branch for
rows that did not, three lines below, still took distinct()'s first row.
A panel carrying an unmatchable country name in two years therefore kept the
earliest year for every country that resolved and an arbitrary one for the
country that did not. Both branches now use the same rule.
compare_sources() dropped duplicate rows in silence. A provider
answering twice for one country-year hands back whichever row came first,
order-dependently. fetch_wdi() reports exactly this and its comment argues
why -- "this is the response of a third party, which is more reason to" --
and this path said nothing. It now warns with the count and says the first
row was kept.
A map's colour classes depended on the caller's row order. The break
computation de-duplicated to one row per country, which is right for a
polygon-backend frame -- one row per vertex, hundreds per country, all
carrying the same fill, and quantiles must not be weighted by how complex an
outline is. But it picked an arbitrary row when a country's rows genuinely
differ, i.e. a panel: the same panel handed over in a different order gave
quantile breaks of 10-100 or of 1000-10000, so the same data drew a
different map and nothing said so. It now de-duplicates (country, value)
pairs, which collapses the vertex rows exactly as before and spans the whole
panel otherwise -- what facet_map(facet = "year") wants from a shared
scale, and order-independent either way.
Coverage counts depended on row order too. na_coverage() -- behind the
world_map() caption, map_provenance() and the honest-maps verbs -- took
the first row per country, so the same panel reordered reported 2 of 4
countries missing or 0 of 4. It now counts a country as shown if any of its
rows has a value, matching imputed_count(); identical on the map-ready
cross-section it is documented for. This mattered in practice because
facet_map(facet = "year") deliberately hands world_map() the whole
panel, so that arbitrary number was the caption on a plot showing every year.
classification_report = TRUE reported a different table for the same
map. It counts countries per class, so one row per country is the right
shape, but it too used distinct()'s first row. It now takes the earliest
year, the rule distinct_countries() already promised, which is also now
shared between the two rather than written out twice.
map_provenance() reported n_imputed = 0 for every data frame, no
matter how many values were interpolated. The data-frame branch never set
the field, and the fallback below it is %||% 0L -- so unlike the other
unset fields, which default to NA and claim nothing, this one asserted
that nothing had been imputed. A map-ready frame carrying two interpolated
values reported zero of them, in the function whose whole job is reporting
provenance. n_imputed is a data-side fact and is now computed from the
frame; the plot path is unchanged.
imputed_count() picked an arbitrary row per country. It counts once
per country, because a map draws one polygon per country, but it did so via
distinct(.keep_all = TRUE) -- the first row. Identical on the map-ready
cross-section it is documented for, but on a panel the first row is an
arbitrary year, so a value interpolated in any other year counted as nothing
imputed at all. Now "imputed in any row for this country".
Running interpolate_missing() twice turned the imputation flag off,
which the documented hard rule says cannot happen. The flag was recomputed
from scratch on each call as "was NA before, is not now" -- and after the
first call the filled cells are no longer NA, so every TRUE became
FALSE. world_map() reads that column to avoid drawing imputed values as
though they were observed, so a second defensive call -- or a re-run
notebook cell -- silently produced a map with no provenance caption.
"This value was imputed" is a property of the data, not of the call that
produced it, so an existing logical flag is now carried forward and the
operation is idempotent. Carried as a column rather than a vector, because
the pipeline re-sorts by iso3c and year. A flag column that is not
logical is somebody else's, and still warns that it is being overwritten.
One infinite value silently voided a whole z_score column. scale()
drops NA but runs an infinity straight through the mean and the SD, so a
single Inf made every z_score in rank_countries() NaN -- beside a
rank and a percentile that were still correct, because both are
rank-based and untroubled by an infinity. That is the easiest kind of
corruption to miss. z_score is now NA with a warning saying which two
columns are unaffected.
sigma_convergence() returned a silent NaN for a year containing an
infinity, through the same !is.na() filter that beta_convergence()
had: Inf passes both the NA test and > 0, so it reached
sd(log(x)). The filter now tests is.finite(), and the reported n
already says how many observations survived.
An infinite base year made index_to() report a country as having
collapsed to zero. The guard already returned NA for a base that was
missing, NA or 0, but not for one that was infinite -- so every other
year of that country became finite / Inf, a plain and entirely plausible
0, while its neighbours indexed correctly. The guard now tests
!is.finite(), which covers NA and NaN exactly as before.
An infinite value crashed beta_convergence() with a base R error. The
filter kept observations that were !is.na() and > 0, and Inf passes
both, so an infinity reached log() and lm() died with NA/NaN/Inf in
'x' -- unclassed, naming nothing. gini() and theil() already treat an
infinity as unusable; the filter now tests is.finite(), dropping it
alongside the NA and non-positive values it already dropped.
A character year broke the verbs that do arithmetic on it, each in a
different way. read.csv() yields "2000" readily, and most verbs here
tolerate it because they only sort or group. The four that actually compute
with a year did not: beta_convergence() died with base R's non-numeric
argument to binary operator, growth_rate(type = "cagr") surfaced a dplyr
mutate error quoting an internal expression, and deflate() exposed its
internal join as Can't join x$year with y$year. All three now use the
same guard complete_years() already used, so the message names the column
and the conversion. growth_rate(type = "yoy") is unaffected -- it never
does year arithmetic -- and neither are lag_by_country(), index_to(),
sigma_convergence() or convergence_club().
locate_country() crashed on an NA coordinate and went quiet on an
out-of-range one. A missing longitude or latitude -- what a failed
geocoding step produces all the time -- reached sf::st_as_sf() and came
back as base R's missing values in coordinates not allowed, an unclassed
error naming neither argument. Those rows now return NA, which is already
the documented way this function says "no country here", and the rest of the
points are located as usual. Separately, a longitude outside [-180, 180] or
a latitude outside [-90, 90] returned a silent NA, indistinguishable from
open ocean, so a column still in 0-360 degrees -- or with the two swapped --
looked like a world of water rather than a mistake; it is now a classed
error naming the offending pairs and the conversion.
A gap in year made lag_by_country(), diff_by_country() and
growth_rate(type = "yoy") compare rows that are years apart, silently.
These read the previous row, not the previous year, so on a panel of 2000,
2002 and 2005 the 2005 growth rate came back as 27.3% -- the change since
2002 -- in a column the docs call year-on-year and the argument calls
"yoy". The arithmetic is unchanged, because a quinquennial panel is a
legitimate design and re-keying the lag would move everyone's results; but
the three verbs now warn and name the countries whose years are not
consecutive, pointing at complete_years(). type = "cagr" divides by the
real year span and so is silent, as before.
convergence_club() died with a bare simpleError on a repeated
country-year. pivot_wider() folds the repeat into a list-column and
as.matrix() then failed with base R's invalid 'type' (list) of argument,
naming neither the verb nor the offending rows. A repeat is fatal to the
log-t test rather than merely inaccurate, so it is now a classed error that
lists the country-years and points at check_panel_unique().
theil() returned NaN for both components when one group's weights
summed to zero. sw == 0 was guarded for the whole vector, but not per
group: the group mean sum(w[i] * x[i]) / swg was 0/0, and the NaN
poisoned between and within while total came back a perfectly good
number -- with no warning to say which group did it. A group with no weight
has no share of the population, so its contribution to both components is
exactly zero, and the decomposition identity total == between + within
now holds in that case too.
interpolate_missing() stripped a column's class when, and only when, the
series had a gap. The fill went through ifelse(), which drops attributes,
so a Date column came back as bare numbers -- 2020-01-01 returned as
18262. A series with no NA took an early return that preserved the
column, so the same column changed type depending on its data. The fill now
assigns into a copy of the input, which keeps the class.
Passing ISO codes to a verb that expects country names gave no usable
advice. origin defaults to "country.name" throughout, so
country_factsheet("FRA") -- the canonical ISO3 code -- was refused and the
message suggested check_country_match(), which has nothing to say about a
code. When every unresolved value is a valid code under some other origin,
the message now names it: It resolves under origin = "iso3c" -- try that
instead. Applies to country_factsheet(), neighbors(),
distance_between() and the join verbs. A genuinely unknown name still gets
the close-name suggestion and no misleading hint.
Accented country names written in NFD resolved to NA. The same accent
can be encoded as one precomposed code point (NFC) or as a base letter
followed by a combining mark (NFD) -- identical on screen, different strings
to match(). countrycode's tables are NFC, so "Turkiye", "Sao Tome and Principe", "Aland Islands", "Curacao" and "Reunion" in their accented
NFD spellings matched nothing, silently, even in a UTF-8 locale -- which is
precisely where ?country_overrides promises accented names do work. macOS
returns NFD for filenames and several export paths emit it, so this is input
a user gets without choosing it. Cote d'Ivoire was unaffected and hid the
problem: countrycode's regex for it keys on an ASCII substring that
decomposition leaves alone. Names that resolve to nothing are now retried
with their combining marks stripped, which turns an NFD spelling into the
ASCII spelling the package already documents as resolving in any locale.
Only the unresolved values are retried, so the pass can add a match but
never move one: across the 314 names the package and countrycode ship, none
changed.
The README said common_indicators "keeps the 20 you actually use" after
this release grew it to 22 with the two price-conversion series. Corrected,
and the count is now asserted against nrow(common_indicators) so the
prose cannot drift from the data again.
The global getis_ord(local = FALSE) gave the same answer for a variable
and its negation. The general G is a ratio of weighted to total
cross-products, so every x_i * x_j term survives negating the whole
variable: g(x) and g(-x) came back as bit-for-bit the same double, and
the statistic could not tell a coldspot pattern from a hotspot one. Getis &
Ord (1992) define it for a variable with a natural origin and no negative
values, and gini() -- the nearest analogue here, a global index with a
positivity domain -- already warns and returns NA in this case. The global
branch now does the same, naming how many values are negative, instead of
handing back a plausible-looking number computed outside its domain. Zero is
still allowed, positive columns are numerically unchanged, and the local
Gi* branch standardises and is untouched.
growth_rate(type = "cagr") returned a bare NaN for a negative value,
silently. The compound rate is (v_t / v_0)^(1/n) - 1, and the code
guarded v_0 > 0 -- the base of the ratio -- but not v_t. A fractional
power of a negative ratio is NaN, so a single negative year in an
otherwise positive series put a NaN in the column with nothing said. Every
neighbouring measure reports this: theil() drops non-positive values and
says how many, gini() warns about negatives, sigma_convergence() warns
when no value is positive, beta_convergence() errors, and growth_rate()
itself warns when every row comes back NA. The partial case was the one
silent spot, and it is the case real data hits -- a deficit or a net flow
dipping below zero for a single year. Negative values now give NA with a
countryatlas_cagr_negative warning naming how many rows are affected. A
value of exactly 0 is unaffected and still gives -1, an annualised
-100%, which is the right answer for a series that went to nothing;
type = "yoy" is a plain ratio change, is defined for negatives, and is
untouched.
index_to() documents what it does when base_year is not in the data. A
country whose series does not cover the base year indexes to NA rather
than stopping the call, because the rebasing is per country and a partial
answer is still a real one -- so a base_year no row anywhere carries
(including any non-integer value, which no year can equal) gives an all-NA
column rather than an error. That was pinned by a test and explained in a
code comment, but nothing in ?index_to said it, and the sibling
deflate() refuses such a year instead. Both behaviours are unchanged; the
contrast is now stated, with an example.
A units value crashed the numeric guards. sf's st_area() and
st_distance() return units objects, so a threshold computed from geometry
and passed to a numeric argument is an easy mistake -- and is.numeric() is
TRUE for one, so it sailed through the type check. The range comparison
then raised the units package's own "both operands of the expression should
be units objects", naming neither the argument nor this package.
check_number() and check_top_n() now ask whether the comparison is
possible before making it, and say what to do: "cutoff_km must be a plain
number. Got as.numeric(cutoff_km)."
A classed numeric that does compare, and a named scalar, are still accepted
as before.
Three more arguments consumed a function before checking it. A sibling
of the validator fix below, with a different mechanism: the value was used
before any check ran, so the crash came from the consuming call rather than
the message. check_choice() reached as.character(x) on its
default-detection line, which is how world_map(projection = mean) died;
country_groups() reached setdiff(group, valid), which coerces (and a
formula got as far as "duplicated() applies only to vectors"); and
complete_years() called anyNA(years) inside the condition of the guard
meant to catch bad input, which errors outright on an environment. All three
now refuse a function or environment up front and name the class.
Passing a function to a validated argument crashed the validator itself.
check_string(), check_bool(), check_number() and check_top_n() build
their message with {.val {x}}, which coerces the value to character -- and
that fails outright for a closure, with base R's "cannot coerce type
'closure' to vector of type 'character'". The error path therefore crashed
instead of reporting the bad input, and every caller inherited it:
register_country_source(mean, ...), clear_country_cache(source = mean),
as_ggsql_source(name = mean) and world_query(source = mean) all gave a
bare simpleError from base R rather than naming the argument. length()
was no defence -- a closure has length 1, so the length branch never fired,
and length(globalenv()) counts bindings, so an environment reported "Got 5
values". All four now name the class instead: "Got <function>". Ordinary
values, including the value itself and the wrong-length counts, are
unchanged.
A numeric argument turned an "unknown name" error into a cli crash.
Several messages put the pluralisation marker ahead of the value --
"Unknown group{?s}: {.val {bad}}". With no quantity set, cli reaches for
the most recent interpolation to find one, and a numeric vector there is
read as the quantity itself, which must be length 1. So
country_groups(c(1, 2)) died on cli's own "length(object) == 1 is not
TRUE" rather than reporting the bad input, and the same held for
check_cols(), world_table(columns = ) and
classify_compare(methods = ). Character vectors were fine, which is why it
went unnoticed. The five affected messages now set the count with
cli::qty(length(x)), the form reporting.R already used. Note qty(x)
alone is not enough for a numeric x -- it hits the same trap. Singular and
plural now read correctly for both types, and character messages are
unchanged.
The spherical-area helper mis-measured rings at the antimeridian.
ring_area_km2() picks a country's largest polygon piece so the label lands
on the mainland rather than in an ocean. Longitudes arrive wrapped into
[-180, 180), so a raw edge difference jumps by ~360 degrees at the
antimeridian, and the two failure modes pointed opposite ways: a ring
crossing 180 measured 179 times too large (a 2-degree equatorial square
came out at 8.85e6 km2 instead of 49447), while one encircling the pole
cancelled to about zero (a cap at latitude -80 measured 7e-11 instead of
3.87e6). Either can hand the "largest piece" choice to the wrong ring --
exactly the mislabelling the helper exists to prevent. Edges are now
differenced modulo 360, which agrees with the analytic area for an
equatorial square, a wrapped square and a polar cap alike. No centroid
changes: the bundled polygon data splits pieces at the antimeridian, so all
239 still match country_meta. The point is that the result no longer
depends on the data being split that way.
tissot_map() failed for many ordinary radii. Its circles are built by
walking out along azimuths 0..2*pi and wrapping longitude into
[-180, 180). az = 0 and az = 2*pi are the same point, but the wrap
could send the two ends to -180 and +180 -- the same meridian, opposite
signs -- so st_polygon() saw an unclosed ring and sf reported "polygons
not (all) closed", a bare simpleError naming neither the argument nor the
function. Whether it happened depended on the radius, and the default hid
it: at radius_km = 500 no centre in the grid produced such a ring, at
1000 six did and at 5000 fifteen. The failures were not monotonic --
500 and 10000 worked while 1000, 2000, 5000, 8000, 12000 and
20000 did not. The ring is now closed by construction rather than relying
on the arithmetic to reproduce its first vertex, and every radius from 100
to 40000 km draws. The antimeridian guard is unchanged, so a larger radius
still drops the circles that would smear across the map.
An enormous k in dorling_map() surfaced a GEOS exception. k was
bounded below but not above. check_number() already refuses Inf, so
k = Inf errored cleanly, but a merely enormous finite value passed and
then overflowed the coordinate arithmetic inside GEOS, which reported an
illegal-argument exception about encountering non-finite numbers in an
orientation test -- a bare simpleError from a C++ library, naming neither
k nor the function. It is now capped at .Machine$integer.max, the bound
the counting arguments elsewhere use, so the refusal comes from the package
and names the argument. Realistic values are unaffected: k = 1e6 still
draws.
top_n past integer range leaked a base R error. Inf is the
documented "no limit", and world_table() and country_network() both gate
on is.finite(top_n) to detect it -- so a finite value beyond integer
range passed the gate and then broke on the coercion behind it.
as.integer(1e18) is NA, so utils::head(df, NA) surfaced base R's
"invalid 'n' - must contain at least one non-missing element, got none": a
bare simpleError naming neither top_n nor the package. Asking for at
most 1e18 rows of a 191-row table is the same request as Inf, so it is
now normalised to Inf rather than rejected. Representable limits are
unchanged, .Machine$integer.max still works, and 0 / negatives / NA /
non-numeric are still refused with a countryatlas_error. Found by fuzzing
argument values, looking for errors the package had not classified.
The two hand-curated datasets are pinned to the scripts that build them.
country_groups_history and disputed_territories are maintained by hand
in data-raw/, so the script is the source of truth and drift can happen in
either direction -- editing the .rda without the script, or the script
without rebuilding. Both are now re-run during the tests and compared to the
shipped data, the same guard data-raw/overrides_snapshot.R already had.
Both reproduce byte for byte today.
interpolate_missing() no longer carries a second, unreachable
duplicate-column guard. check_panel_unique() grew one of its own earlier
in this cycle, and it runs five lines earlier, so the copy inside
interpolate_missing() could never fire -- its stale comment still claimed
the function "did not" reject duplicate names. Line coverage showed the
block never executing. The rejection itself is unchanged, still raising
countryatlas_duplicate_columns, and is now covered by a test.
audit_time_coverage()'s three console messages are covered by tests. None
of those lines had ever executed, which meant the pluralisation in
"{n} row{?s} fall{?s/} outside the country's existence" had never been
checked at both counts -- and {?s} keys to the most recently interpolated
value, a construct this package has got wrong before. Both readings are
correct ("1 row falls", "2 rows fall") and are now pinned, along with the
no-findings message and the empty-input column contract.
rate_check() reported "no countries flagged" when it meant "unknown".
When no small-denominator threshold can be computed it warns that flagged
is NA throughout -- but is.finite(den) & den < thr yields FALSE, not
NA, for a non-finite denominator, because R short-circuits FALSE & NA to
FALSE. An all-NA denominator therefore produced FALSE for every row,
so sum(out$flagged) returned 0: a confident "nothing is flagged", which
is precisely the misreading the warning was written to prevent. An all-zero
or all-negative denominator did yield NA, so the cases disagreed with each
other as well as with the message. With no threshold, no row can be compared
to one, so flagged is now NA for every row and the warning fires exactly
when that is true. The normal path is unchanged, including its FALSE for a
missing denominator among usable ones, which keeps sum(out$flagged)
working as intended.
getis_ord() computed its spread with the unstable one-pass formula, so
a tightly clustered column produced Inf or NaN z-scores. The spread
was sqrt(sum(x^2)/n - mean(x)^2), which subtracts two nearly equal large
numbers and loses every significant digit once the mean dwarfs the
variation. Measured on 8-country vectors: 1e9 + 1:8 drove the spread to
exactly 0, so the denominator was 0 and every z_score came back Inf
with p_value 0 -- reading as "every country is a significant hotspot";
1e10 + 1:8 gave a spread 90 times too large, so nothing was ever
significant; and 1e12 + 10 * 1:8 went negative under the sqrt, returning
NaN z-scores and leaking base R's warning about them. The
zero-variance guard caught none of it, because such a column is not
constant, only nearly so. Centring before squaring is algebraically the same
quantity and unconditionally stable, and it is how local_morans() already
computed its second moment. Well-conditioned columns are unaffected to
floating-point noise. Gi* z-scores are shift and scale invariant by
construction, which is what the old form violated, so that invariant is now
asserted rather than any particular number.
A trailing space on region silently drew an empty map.
resolve_region() trimmed in its iso3c branch and nowhere else, so one
trailing space gave three different answers: "FRA " resolved, "Europe "
fell through to country-name matching and errored, and "EU " was accepted
as a three-letter code -- nchar("EU ") is 3 and it is already uppercase,
so it reached the branch that passes an unrecognised code through untouched
and came back as the literal string "EU ". Every public caller of that
helper -- world_geometry(), world_data(), attach_geometry(),
join_world() and country_borders() -- therefore subset to nothing and
reported nothing, which is the outcome that branch exists to prevent. Every branch now reads the same trimmed value, so
all 15 shipped groups, the continents, codes and names agree across padding.
The error message still quotes the caller's value untrimmed.
Padding was tolerated for a code but not for an overridden spelling, and
a non-breaking space defeated the trim entirely. trimws()'s default
class is [ \t\r\n], so the iso3c branch's deliberate trim missed every
non-ASCII space -- "FRA " resolved and "FRA\u00a0", what a code pasted
from a web table or a Word document carries, did not. Separately the
override lookup keyed on the raw value while the whitelist was built from
the trimmed one, so with custom_match = c(Somaliland = "SOM"),
"Somaliland" gave SOM and "Somaliland " gave NA; an ordinary
trailing space was enough. Both now use one Unicode-aware trim. A BOM or a
zero-width space is a format character rather than whitespace and is still
rejected rather than silently accepted.
top_n went unvalidated in world_table() and country_network().
Inf is the documented "no limit", which check_number() rejects, so the
guard was a bare is.finite() -- and everything is.finite() rejects then
skipped validation altogether. top_n = "5" and top_n = NA silently
returned every row instead of five, and top_n = NULL failed on if with R's
"argument is of length zero". Both now validate up front and still take Inf.
world_table(subtitle = ) was dropped unless a title came with it. gt
draws the subtitle inside the header block that a title opens, so a lone
subtitle had nowhere to go; it now says so instead of vanishing.
value_by_alpha_map() drew a half-lit map when opacity meant nothing.
The alpha scale had no limits, so it rescaled to whatever spread the frame
happened to have; an equalize column with nothing usable collapsed to one
value and ggplot2 placed it at the midpoint of alpha_range. The result
read as "every country equally weighted" -- the one impression this verb
exists to prevent. Opacity is now absolute (limits = c(0, 1)), so a frame
with no usable equalising variable is drawn at the floor and says so, and two
maps of different subsets are comparable.
rate_check() returned an all-NA flagged column in silence. With no
positive finite denominator anywhere, the tenth-percentile threshold is NA
and every comparison against it is NA too -- so sum(out$flagged), the
obvious next step, came back NA rather than a count. It now says why.
deflate() returned an all-NA country without saying why. A country
with no usable deflator in base_year has nothing to rebase against, so every
one of its values is NA -- correct arithmetic, but in the output
indistinguishable from a country the source never covered. It now names them
and suggests a base year the panel actually spans.
od_map() dropped a named origin without saying so. A country that
appears in the OD table only as a destination has no outflow to draw, so it
was filtered out -- silently, leaving the caller to notice that they asked for
four panels and got three. Named origins now warn (and point at
direction = "in"); trimming the origins = <n> top-N list stays quiet,
which is what top-N means.
distance_between() returned a silent NA for a name that is not a
country. Two causes produce an NA distance and only one is the documented
gap: a country with no bundled centroid is expected -- ?distance_between
says so, and country_weights() already reports it -- while a value that
resolves to no country at all is a mistake, usually the wrong origin, and
handed back a column of NA with nothing said. The second is now reported,
naming the values and the origin in force; the documented gap stays quiet.
simplify_geometry() and theme_world_map() never validated their first
argument. Both check their second one carefully.
simplify_geometry() handed a non-spatial object straight to rmapshaper,
leaking "no applicable method for 'ms_simplify' applied to an object of class
NULL" -- rmapshaper's generic rather than the argument -- and failed
differently again through the sf::st_simplify() fallback, so the message
depended on which optional package the caller happened to have.
theme_world_map() got base R's bare "non-numeric argument to binary
operator". Found by calling all 102 exports with degenerate input and
classifying which errors were the package's own.
Three verbs joined on an unstandardised key and said nothing. Lowercase,
mixed-case and padded iso3c values match nothing, so attach_geometry()
drew every country as no-data, tile_map() drew every tile grey, and
add_indicator() attached a column of pure NA -- each of which reads as a
coverage problem, or as the provider having no data, rather than as a key
problem. An unmatched code stays quiet, because the basemap genuinely holds
fewer countries than the snapshot; matching nothing now says so and points
at standardize_country(), which normalises case and whitespace.
attach_geometry(year = ) already reported its match rate; the ordinary
branches did not.
complete_years() also leaked one on a missing year. It infers the
span with seq(min(year), max(year)), so a single NA produced base R's
"'from' must be a finite number", naming neither the column nor the package.
The years argument has been checked for NA all along; the column it
defaults from had not, and one blank cell in a CSV is enough to hit it.
complete_years() leaked an internal error on a non-numeric year. Its
years argument was checked carefully but the year column was not, so a
character one surfaced as dplyr's "Can't join x$year with y$year due to
incompatible types" -- naming dplyr's internals rather than the column -- and
a factor got base R's bare "'min' not meaningful for factors". Both come
straight out of a CSV read.
country_factsheet() printed NA as the country name for any code with
no country_meta row. The fallback was first_or_na(row$country) %||% iso,
but %||% only replaces NULL and first_or_na() returns NA_character_,
so it never fired: country_factsheet("Kosovo") headed its output
NA (XKX) while listing four real land neighbours underneath. It now falls
back to the name the caller used.
country_meta had five blank capitals alongside 34 NAs.
WDI_data uses "" for an unknown capital and it passed through
unchanged, so is.na(capital) was wrong for Gibraltar, Hong Kong, Israel,
Macao and the Palestinian Territories, and the factsheet printed
capital: with nothing after it. Blank strings are now NA, in the
dataset and in the script that builds it.
Suggests: testthat understated its minimum. The suite has used
expect_no_error() (testthat 3.1.5) for some time and now uses
expect_no_match() (3.2.0), while DESCRIPTION still asked only for 3.0.0.
Bumped to >= 3.2.0.
rank_countries() and share_of_world() said nothing about a repeated
country-year. interpolate_missing() and complete_years() already report
that shape, but the two verbs that aggregate across rows did not -- and
their output is the harder to reconcile. Given a frame with USA-2020
duplicated, rank_countries() returned the same country holding ranks 1
and 3, and share_of_world() gave it shares of 0.1 and 0.7 against a
world total that counted it twice. Both now report it, each naming its own
consequence rather than the lag family's.
Four numeric-column verbs did not check that the column was numeric.
country_network() validates its weight with check_numeric_col(); the
verbs shaped like it did not. bubble_map() and flow_map() reached
ggplot2's bare Discrete value supplied to a continuous scale, and only at
build time -- so the call returned happily and the failure surfaced when
the plot was printed, naming neither the argument nor the column.
spike_map() blamed the join (No rows with a non-negative <col> joined to a centroid) and convergence_club() blamed the panel (Not enough countries with a complete series), when in both cases the column simply was not a
number. All four now refuse at the call, naming the column.
fetch_indicator() trusted a registered source's key_col claim. When
a source declared key_col = "iso3c" its codes were used verbatim, but the
declaration is the source's claim rather than a guarantee -- and this is the
package's public extension point. Lowercase codes ("usa") passed through
unchanged, a factor stayed a factor, and numeric UN M49 codes (840) sailed
through as numbers, each producing rows that silently joined to nothing and
read as "the provider has no data". The key is now standardised whatever the
source claims (a no-op on codes that are already right), and values that are
not usable as the declared key become NA and are named
(countryatlas_bad_key).
A factor value column was read as its level indices. adapter_reshape()
coerced the provider's value column with as.numeric(), which on a factor
returns level indices: a column of factor("10", "20") became 1, 2.
check_numeric_col() rejects a factor outright with precisely this advice
(as.numeric(as.character(x))), and its comment notes how easily such a
column happens -- but the adapters take theirs from a third party, so they
cannot reject it and must not misread it either.
Every source path misread the provider's time column. ... forwards to the client, so the caller chooses its type:
eurostat's time_format = "num" returns a numeric year and "raw" a
character one, while the default is a Date. Each adapter assumed one shape.
fetch_eurostat() failed on the other two with base R's opaque
invalid 'trim' argument -- format() reading "%Y" as its trim
argument. fetch_oecd() was worse, failing silently: as.integer() on a
Date returned 18262, the day count, as the year, and a quarterly
"2020-Q1" became NA. Both now read the year from a date, a number, or a
string leading with one, and say so when a value is not a year at all.
The same bare as.integer() reached six sites, not two: fetch_comtrade(),
whose period is YYYYMM for monthly data (so "202001" became the year
202001); adapter_reshape() itself, which fetch_owid() reaches without
preprocessing; and fetch_indicator() -- the public extension point, where
the year is whatever a third-party fetch function returned. All six now share
one reader.
Two more sites outside the adapters took a year from the caller and read it
the same wrong way. audit_time_coverage() turned a Date year column into
day counts (1990-01-01 -> 7305) and then flagged both USSR rows as
post-dissolution -- silently wrong output from the one verb whose job is
catching that class of mistake. And deflate() reported a Date base_year
back as `base_year` 11323 is not in year, a number the caller never
supplied; it now accepts a date, a number or a string, and names what it was
actually given when it cannot.
The source adapters returned an empty frame in silence when no entity
resolved. adapter_reshape() resolves the provider's entity column with
suppressWarnings() on purpose -- every Our World in Data or Eurostat
response carries aggregate rows like "World" and "EU27" that are not
countries and never resolve -- but that also swallowed a provider renaming
its entities, or the wrong column being named. The result was zero rows and
no explanation, sending the reader to check their own indicator code, while
the same function takes care to explain an empty input a few lines earlier.
Nothing resolving at all now aborts (countryatlas_no_entities), naming the
entities it could not match. An empty result from the countries/years
filters is unaffected -- those entities did resolve.
bivariate_map() leaked classInt's "single unique value" for a column
with no variation. classInt needs two distinct values per axis to cut
classes from, and a constant column reached it as a bare simpleError from a
third-party package, naming neither the column nor the function nor anything
to do about it. Both axes are now checked, naming the column and its distinct
count.
tile_map() overstated its coverage, the same way bubble_map() and
spike_map() did. The bundled equal-area grid does not cover every code --
Hong Kong and Macao have snapshot data and no tile -- so counting the input's
coded countries as shown claimed 191 where the map could draw 189. Coverage
is now measured against what the grid can actually place, and the
countryatlas_no_centroid warning names the countries it cannot.
per_capita() returned Inf for a zero population, silently.
deflate() and to_ppp() were fixed for precisely this -- there is a test
named "an unusable deflator or PPP factor gives NA, not Inf" whose comment
records that Inf "propagated silently into every scale and summary
downstream" -- but the most used function of the family never got the fix. A
zero or missing population now yields NA and says so, matching its
siblings. A negative population still passes through unchanged and
silently, because that is pinned deliberately elsewhere in the suite --
negative values are the caller's business and the arithmetic stays honest.
country_weights() built an edgeless graph without comment.
country_weights("distance", cutoff_km = 1) returned 239 countries and zero
links, as did an all-zero custom matrix. Every statistic then refused to run
with Not enough connected countries with data -- an error about the data,
raised far from the cutoff_km or matrix that actually caused it. Building
weights that link nothing now warns (countryatlas_empty_weights), naming
the cause specific to the scheme.
smooth_rates() and to_ppp() returned all-NA columns in silence --
the same bug rate_check() already had fixed, kept by its two siblings. With
no finite positive denominator every rate is NA and the smoothed column
with it; with no finite positive conversion factor every converted value is
NA. Correct arithmetic either way, but the output looked like a computation
that had run rather than one with nothing to run on. Both now say so, and
report a partial loss with a count.
country_weights("custom") validated only the row and column names, so
bad input leaked a bare base-R error from deep downstream instead of being
refused where the caller could act. A character matrix reached rowSums() as
'x' must be numeric; an NA entry was accepted, then killed any statistic
built on it with subscript out of bounds; an NA endpoint in a long frame
surfaced as NAs are not allowed in subscripted assignments; and an NA
weight was accepted outright, silently turning every result into NA. All
four are now refused at construction, naming the argument and the count.
Numeric and logical matrices and long frames are unaffected -- and this is the
path vignette("honest-maps") recommends for a non-geographic adjacency
(trade volume, migration, shared language).
One dplyr::filter() referenced its columns bare. The bounding-box
branch of world_geometry(geometry = "polygon") filtered on long/lat
rather than .data$long/.data$lat -- the only such site against twenty
.data$ uses elsewhere. A bare reference falls back to a variable of that
name in the calling scope when the column is absent, so it can filter on the
wrong thing where .data$ errors plainly. With it converted,
utils::globalVariables() no longer needs to declare long and lat;
only animate_world()'s NSE default time = year remains.
spin_globe()'s example was never executed by anything. It sat in
\dontrun{}, which R CMD check skips even under --run-donttest, so the
one example of the GIF pipeline was free to rot unnoticed. It needs no
network -- only bundled data and gifski/magick -- so it is now
\donttest{}, guarded on those packages and cut to six frames, which runs
in about five seconds and is checked from here on.
as_ggsql_source(format = "duckdb") did not say who owns the connection
it returns. duckdb keeps its in-memory database alive until the handle is
released, and neither @return nor @param format told the caller to close
it -- while the "parquet" branch quietly closed the connection it opened.
?as_ggsql_source now spells out all three lifecycles, and a connection the
function opened itself is released if the write throws, so a failure cannot
leave a handle nobody holds.
sigma_convergence() returned an empty or blank series in silence. Its
positive-value filter is documented -- n counts what survived -- but two of
its outcomes were not. A column with no positive values came back as a 0-row
tibble, and a year with a single country got sigma = NA from sd(), so an
empty or blank convergence series was indistinguishable from a real one. Both
now warn (countryatlas_no_positive, countryatlas_thin_year), the latter
naming the years affected. An ordinary panel is unchanged and silent.
gini() and theil() returned NA without saying why. Both carried a
comment stating the package convention -- "NA plus a word about why (as for
zero weights)" -- while the line beneath returned NA in silence for exactly
those cases. An all-zero column and all-zero weights were indistinguishable
from a missing input. Both now warn (countryatlas_undefined_index) naming
which it was. Perfect equality still returns 0, silently, because that is
a value and not an undefined one.
A constant column made the spatial statistics return NaN in silence.
morans_i(), gearys_c(), local_morans() and getis_ord() all divide by
the cross-sectional variance, so a column with no variation between countries
is 0/0 -- and getis_ord()'s z-score came back Inf where the numerator was
non-zero. For a statistic that is worse than an error, because it reads like
a computed result. All four now warn
(countryatlas_zero_variance), explaining that these measures compare
variation between neighbours and there is none, and return NA rather than
NaN. An all-zero column also stops getis_ord() dividing gi_star by its
own zero sum. Real data is unaffected.
map_provenance() gained n_total. n_countries holds the countries
actually drawn with a value -- the numerator -- but the name reads like the
map's country total, which is n_countries + n_missing. Anyone taking it as
the denominator understated their own coverage. The denominator is now a
field of its own, and the three counts are spelled out in ?map_provenance.
The coverage caption did not pluralise. footnote = "auto" is built
with sprintf() and lands on a published map, where a single-country frame
read All 1 countries shown. and an empty one All 0 countries shown. The
same noun appears in coverage_map()'s caption and map_provenance()'s
print block. All three now agree with the count, an empty frame says
No countries to show., and a frame with no coverage to report gets no
caption rather than one full of NA.
spatial_lag() gave every year of a panel the first year's neighbour
average. It is the one spatial verb that returns a column aligned to the
caller's own rows, and it matched on iso3c alone. France's
gdp_per_capita ran 39,683 -> 158,734 -> 277,784 across three years while
gdp_per_capita_lag sat at 63,409 for all of them, so value / lag --
the Moran-scatterplot axis this column exists for -- silently compared 2002
against 2000. A panel now gets a lag computed per year; a cross-section is
unchanged.
The spatial statistics silently computed on an arbitrary year of a
panel. align_weights() -- shared by morans_i(), gearys_c(),
getis_ord(), local_morans(), spatial_lag() and lisa_map() -- reduced
to one row per country with a bare distinct(iso3c, .keep_all = TRUE), so a
panel collapsed to whichever row came first in the frame. Moran's I on the
same data returned 0.47 or 0.29 depending only on row order, and unlike the
map verbs these said nothing at all about having chosen. They now go through
the shared reduction: the earliest year, deterministically, with the
countryatlas_panel warning. A genuine cross-section is unaffected.
"Only the earliest year of each country is used" was not true. The
one-row-per-country reduction behind rate_check(), audit_coverage(),
correlate_indicators(), world_table() and every map verb warned that it
keeps the earliest year, but the code was
distinct(iso3c, .keep_all = TRUE), which keeps whichever row comes first
in the frame. That is the earliest year only for a caller who happened to
sort by year: shuffle the same panel and rate_check() returned a different
numerator for France (20 sorted, 30 shuffled) and the map verbs drew a
different year, while the warning went on promising "earliest" either way.
The earliest year is now selected explicitly, so these verbs are
reproducible for a given frame regardless of row order, and the survivors
keep their original relative order.
standardize_country() silently destroyed columns the caller never asked
about. add defaults to c("iso3c", "iso2c", "continent", "region"), so
the ordinary call -- standardize_country(d, country), to get iso3c --
also replaced any continent, region or iso2c already in the frame. A
user's own regional classification vanished without a word, while the eleven
other column-adding verbs all report this through warn_overwrite(). It now
warns (countryatlas_unasked_overwrite) naming the columns, only when
add was left at its default: passing add yourself still means you asked
for those columns, as documented. add = "iso3c" adds just the code.
country_join() silently multiplied rows when standardising collapsed two
names onto one code. wdj_to_key() maps distinct inputs to the same code --
"France" and "FRANCE ", or "Congo" and "Congo-Kinshasa" -- so a y
whose names all looked distinct could join one country twice. dplyr says
nothing: with unique keys on the other side that is an ordinary one-to-many,
not the many-to-many it flags. Two rows became three, France appeared twice
with different values, and a downstream sum() double-counted it. Both
country_join() and country_join_all() now warn
(countryatlas_key_collapse), naming the code and the inputs that collapsed
onto it. Only a collapse is reported: duplicates already present in the
input under one name, and country-by-year panels, are legitimate one-to-many
joins and stay silent. warn = FALSE silences it.
Subnational maps counted countries instead of regions, and binned on
them too. na_coverage(), classification_table(), apply_binned_fill()
and imputed_count() de-duplicate before counting -- the polygon backend
repeats a country's value down every vertex -- but keyed on iso3c. A
subnational frame carries iso3c as well as a region code, so every NUTS
region of a country collapsed to one row: a 280-region map reported
"27 of 27"; because distinct() keeps the first row per key, blank regions
inside a country whose first region had data were reported as zero
missing, claiming complete coverage of a map with visible holes; and the
quantile breaks were computed from 27 values rather than 280, so the colour
scale itself was wrong. All four now key on the most specific unit column
present (nuts_id, then iso_3166_2, then iso3c, then group).
country_borders() silently omits five whole countries at its default
scale, and country_factsheet() reported the resulting short count as
fact. Adjacency comes from Natural Earth, and the default scale = "small"
(110m) has no polygon at all for Andorra, Liechtenstein, Monaco, San Marino
or the Vatican -- countries for which a land border is the entire geography.
They contributed no rows, so each reported zero neighbours and France
reported 8 instead of 10, under a heading stating the count as fact.
?country_borders now has a Which countries the default leaves out
section naming them and pointing at scale = "medium" (which has all five),
and country_factsheet() names what its count excludes -- only for the
countries actually affected. (The Brazil and Suriname borders it reports for
France are real, via French Guiana.)
Passing something that is not a column to a column argument leaked a raw
rlang error. quo_arg_name() called rlang::as_name() directly, so
world_map(data, 5) failed with Can't convert a double vector to a string.
and world_map(data, gdp_per_capita + 1) -- a natural thing to try -- with
Can't convert a call to a string. Neither named the argument, the function,
or what was expected, and the leak reached every one of the package's ~66
unquoted column arguments. The error now names the argument and the offending
expression, and suggests the fix: compute the column first for an expression,
drop the pronoun for .data$x, or pass a bare name or string for a literal.
world_table(value = NULL) labelled an unsorted slice with a rank
column. With nothing to rank on, the frame keeps whatever order it arrived
in and top_n takes an arbitrary slice -- but the result was still numbered
1..n under a heading that told the reader these were the top n by something.
world_table(world_snapshot$countries, top_n = 5) presented Afghanistan as
"rank 1" next to an empty GDP cell. The column is now added only when a
value was given, and truncating an unsorted table warns.
A failed World Bank download was cached to disk as if it had succeeded.
WDI::WDI() answers a failed request by warning and returning a zero-row
frame; memoise cached that, and the World Bank cache is persistent -- so one
call made while the network was down poisoned every later session until
someone ran clear_wdi_cache(disk = TRUE) by hand. An empty response is now
never cached, and says why (countryatlas_no_data).
register_country_source(cache = ) did nothing. The flag was stored in
the registry and reported by country_sources(), but nothing ever read it:
the documented per-session memoisation never happened, and cache = FALSE
was equally inert. Results are now memoised per source, keyed on the
indicator, countries, years and any extra arguments, and cleared by
clear_country_cache(source = ). Empty results are not memoised.
world_map(engine = "tmap") could not draw in its default style.
tm_scale_intervals() is tmap's interval scale, and "cont"/"cat" are
not interval styles -- they name different constructors. So the default
style = "continuous" failed outright with tmap's Invalid style. Style should be one of "fixed", "sd", "equal", "pretty", ..., and
style = "categorical" warned that an interval scale was being applied to
non-numeric data. Each style now reaches the constructor tmap has for it.
flow_map() drew every trans-Pacific flow across the whole map. A great
circle Tokyo -> Los Angeles has longitudes running ...178, 179, -179, -178..., and geom_path() under coord_quickmap() joined those two points
literally, so the arc streaked back across Africa instead of crossing the
ocean. Arcs are now split at the antimeridian and land exactly on the edge.
flow_map() titled its legends weight whatever the caller's weight
column was called, because the internal arc frame's column is literally named
weight. Both scales now carry the real name -- which also merges what were
two legends of the same variable into one.
geom_country_labels(mapping = ) could only name four columns, and
cancelled flag = TRUE. The centroid reduction returned
iso3c/long/lat/flag and dropped the rest, so
aes(colour = continent) -- the ordinary reason to pass a mapping -- died on
"object 'continent' not found". Separately, the caller's mapping replaced
the defaults instead of adding to them, so supplying one removed the label
aesthetic entirely and silently ignored flag.
animate_world(title = ) was discarded. The frame marker was written
straight into title, overwriting anything passed through ... to
world_map(). The title now stays and the frame label moves to the subtitle.
style = "binned" ignored n_bins. It was passed to ggplot2 as
n.breaks, which is only a hint -- scales::extended_breaks() snaps to round
numbers -- so n_bins = 5, 6 and 7 all drew five bins and 3 drew four.
world_map(), globe_map() and value_by_alpha_map() now compute explicit
equal-interval boundaries, so n_bins means the same thing under "binned",
"quantile" and "jenks" as documented -- and in every verb that takes it,
not just world_map(). globe_map() and value_by_alpha_map() also record
the boundaries in provenance, which only world_map() did before.
classification_report = TRUE fabricated classes for continuous fills.
With no break vector the report fell back to one row per distinct value: a
continuous choropleth of GDP per capita produced a 189-row table of n = 1
that looked like a classification and described nothing. A continuous
colourbar now reports NULL and warns (countryatlas_no_classes); "binned"
gets a real table, because it now has real breaks.
bubble_map() and spike_map() failed on data already carrying
centroid_lon/centroid_lat -- including world_geometry("centroids")'s
own output. dplyr suffixed both sides of the join to .x/.y and the aes()
referring to centroid_lon then found no such column. The incoming columns
are dropped in favour of the bundled ones.
bubble_map() and spike_map() overstated their coverage. Both join the
bundled centroid table, which does not cover every code in the codelist. The
five countries with snapshot data and no centroid -- Hong Kong, Macao,
Gibraltar, the British Virgin Islands and Tuvalu -- were dropped at draw time
(bubble_map() left-joined and let ggplot2 mutter "Removed 5 rows";
spike_map() inner-joined and said nothing), but provenance was computed on
the frame as it arrived. A population map that never drew Hong Kong reported
"215 of 215" with n_missing = 0. Coverage is now measured against what is
actually placed, and a new countryatlas_no_centroid warning names the
countries that could not be.
na_style = "hatched" and disputes = "mark" silently discarded the
projection. ggpattern::geom_sf_pattern() and the geom_sf() inside the
dispute layer each return list(<layer>, <CoordSf>) -- a default
coord_sf(crs = NULL) -- and ggplot2 replaces the plot's coordinate system
unconditionally when a coord is added. Both layers go on after
wdj_coord_sf(), so they threw away the requested CRS and its latitude clip:
world_map(projection = "mercator", na_style = "hatched") and the same call
with projection = "robinson" drew byte-identical maps. Two honesty features
quietly reprojecting the map is the opposite of the point.
clear_wdi_cache(disk = FALSE) deleted the on-disk cache, and any other
file beside it. memoise's cache_filesystem()$reset() is
file.remove(list.files(dir, full.names = TRUE)), so forget() on a
disk-backed memo was never an in-memory operation -- the call the examples
label "forget the in-session memo" wiped the persistent cache and every
unrelated file that happened to share the directory. The disk flag gated
nothing on the way in; it only decided whether the directory was additionally
removed. Dropping the memo reference is what an in-session clear means here,
and the next fetch reads the existing entries straight back.
years meant something different for the World Bank than for every other
source. It is documented as a numeric year vector, and the four adapters
honour that, but the WDI path used only min()/max() as a request range and
never filtered the result -- so years = c(2000, 2020) asked for two years
and got twenty-one.
A mistyped classify silently added no classification columns.
world_data() and country_data() filtered the argument with intersect(),
so classify = "incomes" matched nothing and quietly produced a frame with no
income, continent or region column rather than saying the value was not
recognised. language went straight to WDI, where a length-2 value surfaced
as "the condition has length > 1". Both are now checked before the network
call, so the error does not depend on having a connection.
A mistyped region drew an empty map in silence. region accepts a
continent, a group name, iso3c codes, country names or a bounding box, and
anything matching none of those fell through to name-matching, resolved to
NA, and produced an empty subset -- so region = "Europ" returned a blank
map with no explanation, and region = NA reached the internal tests as base
R's "missing value where TRUE/FALSE needed". Both are now reported. An
explicitly-uppercase unknown code still yields an empty subset, which is
deliberate and documented in the code.
add reported its problems under somebody else's argument.
standardize_country(add = ) and locate_country(add = ) name attributes to
derive from iso3c, and neither validated the argument: an unknown name
surfaced as countrycode's complaint about its destination argument, a bad
one in locate_country() as convert_country()'s to, and add = NA as
base R's bare "missing value where TRUE/FALSE needed". Both now check it, and
the shortcut table they accept is shared with the validator so the two cannot
drift apart.
A malformed custom_match could put a non-code in iso3c. origin was
validated and the override table was not, although every value in it lands in
the iso3c column -- and the iso3c branch whitelists those values as valid
by construction, so nothing downstream rejected them either.
custom_match = c(Freedonia = 1) therefore produced iso3c == "1", and every
join, geometry lookup and group test after it keyed on that. The table must
now be the name-to-code map country_overrides() returns.
wdi_search() searched for whatever it was given. Only field was
checked, so a non-string pattern went straight into the regex and matched
something plausible: wdi_search(1) returned 10,125 indicators and
wdi_search(NA) the entire 29,495-row catalogue, both without complaint,
while an empty one leaked base R's "invalid 'pattern' argument". A cache
that was not a WDIcache() object died on "$ operator is invalid for atomic
vectors". Both are now checked.
spin_globe(file = ) was the one argument its validation block missed,
so a non-string path leaked base R's "invalid 'path' argument" from inside the
writer -- the block's own comment says a bad argument is the caller's bug and
the message should not depend on which optional packages are installed.
audit_coverage()'s group breakdown measured one indicator and did not
say which. by_group$na_rate is computed on the first indicator -- with the
default that is whichever numeric column comes first -- while the printed
heading reads "Coverage by group", so a region's GDP coverage of 0.25 read as
its overall coverage even though population and life expectancy were complete.
The table now carries an indicator column naming what was measured.
compare_sources() could report two providers as agreeing perfectly when
it had never compared them. An unnamed indicator of more than one code was
silently truncated to its first element and broadcast to every source, so the
verb compared a code against itself and found no disagreement. Its own error
message elsewhere already told callers to "pass a single unnamed code";
nothing enforced it. Now rejected, with a pointer to the named form.
deflate() overwrote an existing column in silence. to_ppp() and
smooth_rates() are the same shape -- take a panel, write one derived column
back into it -- and both announce a clash before destroying the caller's
column. deflate() did not, so a frame that already carried gdp_real lost
it without a word.
A repeated country-year made interpolate_missing() rewrite observed
values. stats::approx() collapses tied x-values to their mean, so two rows
sharing a year were both overwritten with the average -- 20 and 999 became
509.5 apiece -- and the _imputed flag reported FALSE for them, since it
compares "was NA" against "is not NA" and neither ever was. The safeguard
the code documents, that a filler changing an observed value shows up as a
flagged cell, cannot catch this case. The malformed input is now reported, and
approx()'s own tie notice -- which reached the caller as an uninformative
"There was 1 warning in dplyr::mutate()" -- no longer leaks.
A repeated country-year corrupted every verb that reads across rows.
lag_by_country(), diff_by_country(), growth_rate(), index_to(),
complete_years(), deflate(), to_ppp(), convergence_club() and
beta_convergence() all read neighbouring rows, so a duplicated
country-year silently shifted them: with France's 2019 present twice as 20
and 999, 2020 was lagged against 999, giving a difference of 979 and 4895%
growth. The malformed input is now reported, and it names the offending key.
The one-row-per-country verbs collapsed a panel silently.
world_table(), rate_check(), audit_coverage(), gridded_cartogram(),
bubble_map(), spike_map(), globe_map(), interactive_map(),
tile_map() and correlate_indicators() reduce to one row per country -- which is for repeated geometry rows, not for time.
Handed a panel they kept whichever row sorted first and presented that year as
the answer: France's 2018 value of 10, out of 10, 20 and 30. They now say so.
tile_map() was worse than silent -- its grid holds exactly one cell per
country, so a panel fanned 239 cells out into 659 overlapping ones.
A panel handed to world_map() was drawn silently. attach_geometry()
joins a panel deliberately -- facet_map() and animate_world() are built on
it -- so a multi-year frame reaching a single static map drew each country
once per year and let whichever row came last win, with a caption that still
counted each country once, so nothing looked wrong. world_map() now says so
and points at the two verbs meant for a panel. Faceting by year and
animate_world() resolve the panel and stay quiet; faceting by anything else
does not -- each panel still stacks every year -- so there it still warns.
Three counts lacked the upper bound their coercion needs.
world_query(n_bins = ), od_map(origins = ) and
convergence_club(min_size = ) validated only a lower bound, then coerced
with as.integer() -- which past 2^31-1 returns NA with R's bare "NAs
introduced by coercion to integer range". The query came out reading
BIN fill INTO NA, od_map() reached seq_len(NA), and every size
comparison in convergence_club() became NA. compute_breaks() has carried
this bound since 2.0.0; these three now do too.
subnational_map() dropped unmatched regions in silence. Its join keeps
the geometry and discards data rows that match none of it, so codes from a
different NUTS vintage simply vanished -- and only a total join failure was
reported, even though that error names the vintage problem exactly. Partial
mismatches are now named too.
check_country_match() suggested a country for a blank cell. The guard
skipped NA and "" but not " ", and Jaro-Winkler finds spurious
similarity between a two-space string and a name containing spaces -- so a
whitespace-only cell, the commonest thing a CSV import produces, came back
confidently suggesting "Congo - Kinshasa" at a distance of 0.29, well inside
the threshold.
audit_coverage()'s n_missing and na_rate are no longer named vectors.
vapply() names its result after indicator, so a$na_rates$na_rate handed
back c(gdp = 0.1) instead of 0.1 -- the same wart country_network()
already calls unname() on.
Three verbs folded every uncoded country into a single row. The
de-duplication that stops a polygon frame counting a country once per vertex
ran distinct() on iso3c, and distinct() treats NA as a value -- so
countries the codelist could not resolve collapsed together.
audit_coverage(), the verb the package points at for "which countries are
missing", named one unmatched country out of four and divided every na_rate
by a short n; rate_check() and world_table() quietly returned three rows
for a five-row input. Coded rows are still collapsed; uncoded ones now
de-duplicate on whatever else identifies them.
country_join() and country_join_all() never reported a failed
reconciliation. Reconciling both sides to a common key before joining is
the entire premise of these two verbs, yet a name that resolved to no country
was dropped in silence -- join_world() has warned about this all along, and
wdj_to_key() only speaks up when a name resolves to iso3c but has no
COW/GW code, which on the default key = "iso3c" is never. Both now name the
unresolved values, per side and per table, and both take warn = FALSE to
opt out.
aggregate_regions(fun = "weighted_mean") returned NaN for a zero total
weight. The unweighted branch turns an empty group into NA precisely
because 0, NaN and +/-Inf all read as real figures for a region there is
no data for; the weighted branch checked only for missing values, so weights
that were all zero -- or that cancelled -- divided by zero and leaked the
NaN the documentation promises never to produce.
add_indicator() suffixed the column it said it was overwriting. When
the fetched indicator shared a name with a column already in data, the
warning read "Overwriting ... rename them first to keep the original values"
while the left_join underneath produced val.x and val.y -- so the caller
got neither the column they asked for nor the one they had, advised by a
message describing a mechanism that was not running. It now overwrites, as
every other warn_overwrite() caller does.
An uncoded geometry row was counted as a country. The bundled sf
basemap carries one row with no iso3c; coverage counted it, so it sat in
the denominator and in n_missing while missing_iso3c -- which sorts, and
so drops NA -- listed one fewer. footnote = "auto" read "159 of 176
countries shown; 17 missing" where provenance could name only 16 of them.
Coverage now counts coded countries only, and the two agree.
gridded_cartogram() did the same, and mentioned only half of it. It
drops countries with no positive value and countries with no bundled
centroid; only the second was ever reported, and provenance counted the
survivors, so a grid covering 94 of 215 countries reported "94 of 94". It now
names both drops and keeps the whole input as the denominator.
A cartogram reported the world it kept, not the one it started from.
cartogram_map() and dorling_map() drop every country without a positive
weight -- necessarily, since a cartogram's area is the weight -- but they
did it silently, and provenance was then computed on the survivors, so
n_total shrank to match. A map showing 69 of 175 countries reported
"69 of 73". Coverage is now measured against the frame as it arrived, and the
countries that could not be sized are named.
bivariate_map() overstated its coverage the same way. It classifies two
variables jointly, so a country holding only one is drawn as no-data -- but
provenance counted the x column alone. On a half-covered second variable it
reported 159 countries shown where 69 were coloured. It now counts both, and
names what it dropped.
A VSUP map overstated its own coverage. world_map(uncertainty = )
colours a country only where it has both a value and an uncertainty, but
coverage was counted from missing fill values alone. On a frame whose
uncertainty column was sparser than its value column -- an uncertainty join
that half missed, say -- footnote = "auto" reported 159 of 175 countries
shown when 69 were actually drawn, and map_provenance() agreed with it. The
footnote that exists to stop a map overstating what it covers was doing
exactly that. Coverage now counts both columns, and the countries dropped for
a missing uncertainty are named.
interpolate_missing() flagged the wrong rows as imputed. The
"was missing" vectors were captured from the input and compared against the
output, but the pipeline between them arranges by iso3c and year -- so the
two lined up only when the caller happened to pass an already-sorted frame.
On anything else the _imputed flags landed on entirely different rows,
marking observed values as imputed and imputed ones as observed. Since
world_map() reads that column to write its caption, an unsorted panel
produced a map that misreported exactly which countries had been filled in.
The flags now travel with their rows.
flow_matrix(fill = ) corrupted the flows it was meant to leave alone.
fill is documented as the value for pairs with no flow, but the matrix was
initialised to it and the observed weights accumulated on top, so fill = -1
turned a flow of 10 into 9 -- and symmetric = TRUE then added the fill to
itself. Flows are now accumulated into zero and fill is applied only to
pairs no flow ever reached. The default fill = 0 is unaffected.
compare_sources() reported disagreement between the wrong sources. Every
column of the pairwise summary is computed for its own pair except
n_disagree, which read the row-wise spread across all sources: comparing
three providers, a pair that agreed exactly was still counted as disagreeing
wherever some third provider was the outlier. A source that reports one value
for every country also leaked cor()'s "standard deviation is zero" warning;
the correlation is NA either way, so the case is now decided explicitly.
fetch_comtrade() fetched only the first indicator. It read
indicator[[1]] and ignored the rest, so a two-commodity call quietly
returned one column -- against the documented "one column per indicator" that
its three sibling adapters honour. It now makes one request per commodity and
joins them.
The source adapters validate indicator before doing anything else.
Called directly with an empty vector, fetch_owid(), fetch_eurostat() and
fetch_oecd() returned a silent NULL, and fetch_comtrade() leaked
comtradr's "subscript out of bounds". All four now give the same message
fetch_indicator() always gave, and give it whether or not the provider's
client package is installed.
A provider that changes shape is named as such. adapter_reshape()
checked the entity column but not the value column, although fetch_eurostat()
hard-codes "values" and fetch_oecd() guesses between two spellings; a
missing one became as.numeric(NULL) and surfaced as a recycling error
several frames away. fetch_comtrade()'s year column had the same gap.
country_sources() no longer writes to your home directory. It reported
which of the five backing packages were installed by calling
requireNamespace() on each, which loads them -- and comtradr creates
~/.cache/R/comtradr in its .onLoad. Merely asking which sources were
available therefore created a directory in the user's file space, which CRAN
policy forbids and R CMD check reports as a new file in another directory.
Availability is now tested with system.file(), which does not load.
Five defects found by auditing 2.0.1; three produced a wrong or unusable picture rather than an error.
cartogram_map(), dorling_map() and tile_map() rejected a categorical
fill. All three hard-wired scale_fill_viridis_c(), so a discrete fill
column -- which their fill argument documents no restriction on -- was
accepted at the call and then died at print time with ggplot2's bare
"Discrete value supplied to a continuous scale". The scale is now chosen from
the column's type, as world_map() has done since 2.0.0.
geom_country_labels(data = ...) was unusable: data was hard-wired in
the layer call while ... was documented as passing to that same call, so the
ordinary idiom for labelling a subset failed on R's formal argument "data" matched by multiple actual arguments.
world_map(projection = "mercator") produced an unusable map. Natural
Earth's Antarctica reaches -90 degrees, where Mercator's y goes to infinity;
PROJ clamps rather than erroring, so the panel came out three times taller
than the world is wide, with the inhabited world a sliver above one grey
rectangle of smeared Antarctica. Mercator is now clipped to +/-85.05113
degrees, as Web Mercator has been since it was defined.
locate_country(points = ) leaked an sf internal ("no applicable method
for 'st_transform'") for anything that was not already sf. A plain lon/lat
frame is the commonest thing to pass, so it now gets a named error saying how
to convert it.
morans_i() could not reveal what it had dropped -- see below.
A wrong coding scheme died inside countrycode. origin is user-facing
on seventeen exported functions -- neighbors(), country_join(),
standardize_country(), flow_map(), country_timeline(), in_group() and
the rest -- and was validated only as "a string". Anything else reached
countrycode::countrycode() and failed there, so origin = "country" raised
a forty-item list of accepted values attributed to an argument the caller had
never passed. Invalid schemes now raise a countryatlas error naming the one
they probably meant: "country" and "name" both suggest "country.name",
"iso3" and "ISO3C" suggest "iso3c".
convert_country()'s own from and to were the two that guard missed.
Every scheme other than "country.name" and "iso3c" skips the iso3c hop,
so from reaches countrycode() directly and was still blamed on origin;
and to was never checked at all, so a typo produced "the destination
argument must be ... one of the column names in the conversion directory" --
an argument, and a directory, the caller never mentioned. Both now name
themselves and suggest the nearest real value, so to = "contnent" offers
"continent". The shortcut destinations (flag, currency, tld,
calling_code, name_fr-style localised names) are unaffected.
register_country_source(key_col = ) was used as a coding scheme.
key_col names the column fetch returns, and is documented and indexed as
such -- but it was also handed to countrycode() as origin, so the only
registrations that worked were those whose column happened to be named after
a scheme. Every bundled source uses the "iso3c" default, which
short-circuits before countrycode() is reached, so nothing in the package
exercised it and the first source registered with key_col = "country" died.
How to read the column is now its own argument, key_type (default
"iso3c"), validated at registration and reported by country_sources().
"5 countries ... has no cowc code." The COW / Gleditsch-Ward key
warning interpolates which table it refers to between the count and the
verb, and cli keys an agreement marker to the most recent interpolated
value -- a length-1 string here -- so the verb stayed singular however many
countries were lost. cli::qty() now re-keys it to the count. (Only
literal markup such as {.field iso3c} is safe to sit between a count and
its agreement.)
world_map(engine = "tmap") ignored most of its arguments. The engine
received all of world_map()'s arguments and used ten of them, dropping the
rest in silence: projection and recenter (so the documented default,
Equal Earth, was dropped just as quietly as an explicit request), plus
na_style, footnote, classification_report, uncertainty and
disputes. projection and recenter are now honoured -- wdj_crs()
resolves both and tm_shape() takes the result, which also means a bad
projection name is finally rejected instead of ignored. The remainder are
genuinely ggplot2-specific, so they are now named
(countryatlas_engine_ignored) rather than quietly not happening.
globe_map(interactive = TRUE) accepted styling it could not use, and
validated nothing. The hand-off to MapLibre returned before
arg_match() and check_label_args() ran, so style = "nonsense" and a
length-3 title were taken without a murmur, and style, palette,
n_bins, borders, title, legend, na_label and backend were all
dropped. The arguments are now validated before the hand-off, and the ones
MapLibre cannot carry are named.
world_map(engine = "tmap") ignored na_label. The argument was
passed into the tmap backend and never used, so a caller who named the
missing-data key got tmap's own default instead, with nothing to indicate
the label had been dropped -- while the ggplot2 engine honoured it. Every
tmap scale takes label.na, so it is now passed through, and both engines
share one normalisation of what the argument means (first element; a
length-1 NA or NULL leaves the engine's own formatter alone).
projection was accepted and silently ignored by the polygon backend.
It is documented for the sf backend, and recenter already warned when it
could not be honoured, but projection did not: the polygon backend returns
unprojected longitude/latitude, so projection = "mollweide" looked
honoured and changed nothing. It now warns
(countryatlas_projection_ignored). world_data(geometry = "none") fetches
no geometry at all and so warns for scale, projection and recenter
alike, where before it took all three and dropped them.
world_data(region = ..., geometry = "none") returned every country in
the world. region is documented as a plain "Optional subset", but it was
only ever applied inside attach_geometry() -- which the "none" branch
skips -- so the argument was dropped in silence and a request for one
continent came back with all of them. It now subsets the table directly. A
bounding-box region has nothing to clip against without geometry, so that
combination is refused rather than guessed at.
A scale of more than one value produced an error from base R. The
sf backend builds its cache key with paste0("scale_", scale) before
validating, so a length-2 value vectorised into a two-element key and [[
on an environment failed with "wrong arguments for subsetting an
environment" -- naming neither the argument nor the package. A typo, a
number, NA and NULL all reached the real check and reported properly;
only the multi-value case escaped it. scale is now checked before it
becomes a key. Affects country_borders(), neighbors(), morans_i(),
world_geometry(geometry = "sf") and everything else routed through
get_world_sf().
scale was accepted and silently ignored by the polygon backend.
It selects a Natural Earth resolution, which only the sf backend fetches;
the bundled polygons come at one resolution. world_geometry(scale = "large") and attach_geometry(scale = "large") therefore returned small
polygons as though the request had been honoured, and scale = 2 was not
even rejected. Both now warn (countryatlas_scale_ignored) and point at
geometry = "sf", matching the existing notice for recenter.
cartogram_diagnostics() leaked the geometry engine's error. One
invalid ring makes s2 refuse st_area() with "Loop 0 is not valid: Edge 0
crosses edge 2", which names neither the country nor the package. It now
reports which geometries are invalid and points at sf::st_make_valid().
Unlike country_borders(), it cannot simply fall back to the planar GEOS
engine: an area is the number this function reports, so switching engines
would quietly change the answer.
complete_years() gave every year it invented an empty geometry. The
function exists so "animations do not flicker on missing years", but on an
sf panel it produced exactly that: tidyr::complete() fills an invented
row's geometry with an empty geometry rather than NA, so the
tidyr::fill() that carries static columns forward saw nothing missing and
skipped it, and the completed years rendered blank. Each country's own shape
is now carried across its invented rows.
complete_years() also dropped the sf class. tidyr::complete()
returns a plain tibble while leaving the geometry column intact, so the
result still held a live sfc column that geom_sf() and st_bbox() would
not accept until the caller re-ran st_as_sf(). Fifteen other
frame-returning verbs preserve sf; this one now does too.
interactive_map() discarded ... for two of its five engines, one of
them against its own documentation. @param ... promised the dots reach
world_map() for the "plotly" and "ggiraph" engines -- but the
"ggiraph" branch assembles its own ggplot instead of calling
world_map(), as a comment in it says, so
interactive_map(engine = "ggiraph", style = "quantile") returned a default
continuous map with no hint that the classification had been dropped. The
"leaflet" engine builds its own map too and was not documented at all,
and "mapgl" forwards to mapgl::maplibre(), which was also undocumented.
Both self-assembling engines now name what they were given, the @param
says where the dots actually go for each of the five, and "plotly" still
forwards them (so an unknown argument is still an error there).
world_table(engine = "tibble") dropped title and subtitle in
silence. A tibble has no header, so neither can be drawn -- but the gt
path already refuses to drop a subtitle quietly ("subtitle needs a
title"), and the same objection applies here: the caller got back a table
they believed was titled. Both are now named, and when gt is simply not
installed the notice points at installing it rather than at an engine the
session cannot reach.
index_to() with no base_year gave base R's error. Omitting the
argument reached check_number() and produced 'argument "base_year" is
missing, with no default'. deflate(), the sibling with the same argument,
has said "base_year is required." since 2.0.0.
disputed_territories$administered_by and $claimed_by mixed ISO codes
with placeholders, and said nothing about it. The columns read as iso3c
and mostly are, but six parties are entities ISO assigns no code to and are
written as ABK, CYP-N, OST, PMR, SAH and SOL -- so treating the
column as ISO-keyed produced silent NAs in a package whose premise is that
ISO codes are the join key. The iso3c column had always documented its own
NA convention; these two now document theirs -- including that five of the
six administer the like-named territory (which has no ISO code either) while
SAH is a claimant only, of a territory ISO does code (ESH) -- and a test
pins the exact set so it cannot drift or absorb a typo.
Three more character column arguments died on base R's "object not
found". aggregate_regions(by = region), world_table(columns = gdp) and
country_codes(codes = iso3c) all take column names as strings while
sitting next to an argument that takes a bare column, which makes the slip
an easy one -- and it failed while the argument was being evaluated, before
any validation could run, so the message named neither the argument nor the
package. complete_years(), interpolate_missing() and audit_coverage()
already caught it; these three now do too. (rank_countries(within = )
deliberately accepts either form and is unchanged.)
A repeated column name was ambiguous everywhere but one verb. What
read.csv(check.names = FALSE) gives you for a sheet with two gdp columns
makes every by-name reference ambiguous. interpolate_missing() refused it;
nothing else did. Ten verbs leaked the tibble error "Column name gdp must
not be duplicated. Use .name_repair to specify repair" -- a message about
internals in tibble, not about the caller's data -- and two, per_capita()
and
to_ppp(), silently succeeded: they computed from whichever column [[
reached first and dropped the other without a word. The guard now lives in
the validators every verb already calls (check_cols(),
check_panel_cols(), check_panel_unique()), and in the two functions that
reach as_tibble() before validating anything
(countryatlas_duplicate_columns).
rank_countries() returned a data.frame where its siblings return a
tibble. It ended in dplyr::ungroup(), which strips a grouping but does
not normalise a class -- and because it deliberately ungroups its input
first, mutate() left a plain frame plain. Four sibling verbs ended the
same way and were safe only because group_by() had already made their
intermediate a tibble; all five now state the contract explicitly.
Four verbs leaked an incoming grouping. to_ppp(), smooth_rates(),
interpolate_missing() and spatial_lag() ended in a bare data, so they
returned whatever class arrived: a grouped frame stayed grouped and the
caller's next mutate() silently computed per group, and a plain
data.frame never became a tibble. Their sibling verbs all normalise on the
way out. The leak also hid on early-return paths of otherwise-fixed verbs --
smooth_rates(method = "none"), interpolate_missing(method = "none") and
both branches of spatial_lag() -- so a mode of the same function returned
a different class from its default.
join_world(region = ..., geometry = "none") returned every row, for
exactly the reason world_data() did: the "none" branch returned before
any of the geometry arguments were applied, though region is documented as
a plain region subset. It now subsets, refuses a bounding box that has
nothing to clip against, and warns for scale, projection and recenter.
standardize_subnational() never consulted the regions crosswalk, and
did not say so. It looks for a geo_name, name or region_name column
in regions::nuts_lau_2019 or regions::all_valid_nuts_codes. As of
regions 0.1.8 the first exposes lau_name_national and lau_name_latin,
and the second has no name column at all -- so neither matched, the lookup
was skipped, and the caller was told only that their regions "did not
resolve", with a hint about European coverage that had never been consulted.
It now reports once per session that the installed regions offers no
crosswalk it can use, so the real reason is visible. The behaviour is
otherwise unchanged, deliberately: the regions datasets that do pair
names with codes (nuts_changes, google_nuts_matchtable) key NUTS codes
such as DE2 rather than ISO 3166-2 codes such as DE-BY, and filling an
iso_3166_2 column from them would break this function's promise that an
unresolved region gets NA, never a guess.
Four messages read wrongly when they described exactly one thing.
subnational_map() said "1 value ... match no geometry and are dropped",
check_dispute_coverage() said "1 have no ISO code" and "1 ... territories
appear", and gridded_cartogram() said "there are 1 countries to place".
Each now agrees with its own count. cli keys a {?...} agreement to the
most recent interpolated number, so the count and every agreement have to sit
together -- an interpolation in between silently re-keys them, which is why
the totals in two of these messages moved to the end of the sentence.
An unreachable GISCO reported itself as an sf method error.
giscoR::gisco_get_nuts() answers a failed download with NULL rather than
an error -- the same shape as owidR's blank result, which fetch_owid()
names explicitly and for exactly this reason. nuts_geometry() had no such
guard, so the NULL reached sf as "no applicable method for st_as_sf
applied to an object of class NULL", which says nothing about GISCO being
unreachable and sends the reader to check their arguments. An empty response
now says so, naming the level and vintage. A response carrying no NUTS_ID
column is diagnosed too: previously the derived iso3c came back
zero-length and base R failed with "replacement has 0 rows, data has 2".
A changed World Bank response shape was reported as a failed download.
countrycode() is handed raw$iso2c directly to derive iso3c, so a
response carrying neither key raised its own "sourcevar must be a character
or numeric vector" -- which the fetch wrapper then relabelled "Could not
fetch indicator … from the World Bank API". That blames the network for a
change in the provider's response, and attaches advice about an argument the
caller never passed. The response shape is now diagnosed by name, listing the
columns that did arrive, and the wrapper passes that diagnosis through
instead of overwriting it.
A non-numeric provider response became a column of NA in silence.
as.numeric() turns text that is not a number into NA without complaint,
so a provider answering with "n/a" or ".." -- or renaming a column so the
value column now holds a label -- handed back pure NA. That reads as "the
provider has no data for these countries", which is a very different claim
from "the response was not numeric". fetch_eurostat() and fetch_oecd()
were exposed because they name the value column outright, where
fetch_owid() auto-detects it and so refuses a non-numeric column up front.
The count is now reported, with the offending values shown. A value the
provider itself reported as missing is already NA and is not counted.
The provider adapters discarded duplicate rows in silence.
fetch_owid(), fetch_eurostat(), fetch_oecd() and fetch_comtrade() all
end by keeping one row per country-year, which is the contract the downstream
joins rely on. But they kept whichever row came first and said nothing, so a
provider answering with two different values for one country-year handed back
an arbitrary one -- order-dependently, and invisibly. The count is now
reported, as it is everywhere else this package drops rows. A genuine panel
is untouched, and a clean response stays silent. country_data()'s own
collapse is deliberately left quiet: there the cause is two iso2c codes
mapping to one iso3c, which is structural to the World Bank country list
and would fire on essentially every call.
A source whose keys collapsed repeated the caller's rows in silence.
Standardisation merges keys as well as failing on them -- "United States" and
"USA" both reach USA -- and add_indicator()'s join then matched twice, so
a two-row frame came back with three rows and one country holding two
different values. dplyr only warns on many-to-many relationships, not
one-to-many, so nothing was said. join_world() has warned about this since
it gained that check; the source adapters now do too. The standardised key is
also checked directly, because the collapse check by construction only sees a
code reached from more than one raw value -- a source that simply returns
USA twice collapses nothing and so said nothing, while the join still
turned two rows into three. A genuine panel is untouched: the check keys on
iso3c and year together wherever a year column exists. The documented
fetch contract now states the uniqueness requirement, which it never did --
it specified the columns and said missing data should arrive as NA rather
than a missing row, but never that the key must not repeat.
A cross-section joined to a multi-year fetch lost the year that
distinguished the rows. Dropping the fetch's year column is right for a
single-year fetch, where it broadcasts the one value across the frame. Done
unconditionally, a two-row cross-section joined to a three-year source came
back as six rows: the same gdp three times, against values whose year had
just been deleted, so nothing recorded which year any of them belonged to.
The column is now kept when it is doing work, and add_indicator() says why
the frame grew.
add_indicator() handed back the source's own key column as though it
were requested data, because the columns to add excluded only "iso3c". A
source keyed on anything else left a stray column -- country next to
iso3c -- in the result.
Re-registering a source kept serving the old one's answers. The
session cache keys on the indicator, countries and years but not on fetch,
so correcting a broken adapter and registering it again returned the broken
result -- which is what writing an adapter looks like. Registering a name now
drops that source's cached answers.
An optional column argument was not validated like a required one.
quo_arg_name() gives every unquoted column argument its "must name a
column, not gdp + 1" error, with the advice to compute the column with
mutate() first -- and for the required arguments it did. Twelve
optional ones called rlang::as_name() directly, so an expression reached
the caller as rlang's own "Can't convert a call to a string", naming neither
the argument nor what it wanted. In several functions the required argument
was checked and the optional one beside it was not: bubble_map() validated
size but not color, flow_map() validated from and to but not
weight, cartogram_map() validated weight but not fill. Now covered:
per_capita(pop), aggregate_regions(weight), rank_countries(within),
flow_matrix(weight), flow_map(weight), bubble_map(color),
cartogram_map(fill), gridded_cartogram(fill),
cartogram_diagnostics(weight), interactive_map(tooltip),
animate_world(time) and join_world(country_col). within's deliberate
character-vector form is unaffected.
Three optional columns were never checked for existence either.
per_capita(pop = ) left the population vector NULL and failed with base
R's "replacement has 0 rows, data has 4"; aggregate_regions(weight = ) and
rank_countries(within = ) failed from inside dplyr. All three now say which
column is missing, as their required arguments already did.
A data frame was accepted where a country vector belongs.
as.character() on a data frame deparses each column into a string, so
neighbors(my_df) came back with the two "countries" c("USA", "FRA") and
c(1, 2) -- silently, because those are just strings that match nothing and
every row then reads as "country not found". Nearly every other verb here
takes data as its first argument, so handing a frame to one that takes a
vector is the natural mistake, and eight of them took it:
neighbors(), convert_country(), country_timeline(),
dissolve_country(), in_group(), distance_between(),
check_country_match() and repair_country_names(). gini() and theil()
already refused a data frame; all of them now do. A data frame is only the
common case -- as.character() deparses any list element that is not a
single value, so list(c("FRA", "DEU")) collapsed to the one string
c("FRA", "DEU") and returned a single NA where two codes were asked for.
A flat list of scalars, a matrix and a factor all coerce correctly and are
left alone.
aggregate_regions() crashed on the package's own sf output. An sf
frame is the one geometry shape this verb can aggregate correctly -- it
carries a single row per country, so the totals are right -- but dplyr's
sf-aware summarise() unions the geometries per group, and the bundled
Natural Earth polygons include two invalid ones (Sudan and Mozambique). So
join_world(geometry = "sf") |> aggregate_regions(population) died with
the raw GEOS error TopologyException: side location conflict, while the
same call
on a plain frame worked. It also drew the "aggregating counts each country
once per geometry row" warning, which is true of a polygon frame and false of
an sf one. The geometry is now dropped up front -- the documented return
was always "a tibble of by plus the aggregated value" -- giving results
identical to dropping it by hand, and the warning is confined to the polygon
case where the hazard is real.
Four verbs reorder your rows and did not say so. growth_rate(),
lag_by_country(), diff_by_country() and interpolate_missing() read each
country's series in time order, so they return rows sorted by iso3c then
year -- but documented only "data with a column added". A caller holding
a vector alongside data and relying on position would have had it silently
stop lining up. The sorting is required and unchanged; the documentation now
states it.
?interpolate_missing described the wrong mechanism. It said the
"countryatlas_imputed" attribute is what "the map verbs read". They do not:
provenance counts the *_imputed columns by name, which is why the
provenance chain survives verbs that drop attributes, as dplyr does. The
page now points at the columns and says the attribute is a convenience
nothing in the package consumes.
An sf frame stopped being sf halfway through a pipeline.
per_capita(), share_of_world() and standardize_country() all document
their result as "data with the requested columns added", and
rank_countries() honours that -- but these three ended with
tibble::as_tibble(), which strips the sf class. The geometry column
survived, so nothing looked wrong until the next verb:
join_world(geometry = "sf") |> share_of_world(population) |> world_map()
died on "data has no map geometry", while the same pipeline through
rank_countries() worked -- which is what gave it away. sf now survives
all three. Every other class is still normalised to a tibble and grouping is
still dropped, both of which as_tibble() had been doing at the same time
and only one of which was wrong.
Three map verbs broke on a frame that already had the columns they join
in. Each attaches coordinates under a fixed name, and when the caller's
frame already carried that name dplyr suffixed both sides to .x/.y,
leaving the code reading the unsuffixed name to fail on a column that had
been renamed out from under it -- always as an internal vctrs or ggplot2
error that says nothing about countries:
gridded_cartogram() on a frame carrying centroid_lon/centroid_lat
-- which country_meta supplies, so joining it for capitals or area
first is ordinary -- failed with "Can't subset rows with
is.na(df$centroid_lon) | ...".tile_map() on a frame with a row or col column, names common enough
to collide by accident, failed with "Problem while computing aesthetics".flow_map() on a frame with x0, y0, x1 or y1 -- exactly what a
caller who geocoded their own endpoints has -- failed with "Can't subset
columns that don't exist".bubble_map() and spike_map() already dropped the caller's centroid
columns before the same join; all three now drop the colliding copy, and the
output is identical either way. Where the clashing column is one the verb
actually reads -- a fill that is also a tile coordinate, a weight that is
also an arc endpoint -- it is refused by name rather than silently dropped.
deflate() and to_ppp() did not guard the join collision per_capita()
does. All three fetch a series into a .wdj_* column and join it on
iso3c/year. A caller whose own frame already had that column made dplyr
suffix both sides to .x/.y, leaving the fetched column unreachable and
the arithmetic operating on nothing. per_capita() has dropped the colliding
column since it hit this; its two siblings now do the same.
audit_coverage() reported perfect coverage of nothing.
tibble::as_tibble() turns a bare vector into a one-column tibble called
value, so a character vector of country codes -- the mirror of the mistake
above -- came back as a coverage object whose three tables were all empty.
That reads as "no missing data" when nothing had been examined at all.
standardize_country() already refused a non-frame; this now does too.
world_table() returned an empty table without saying why. Rows whose
value is NA cannot be ranked, so they are dropped -- but when that removed
every row, a frame that went in with countries in it came back with none, and
nothing distinguished "the column is empty" from "there was nothing to
report". It now says so, while a partial drop (the normal case) and a 0-row
input stay silent.
recenter was silently dropped on the polygon backend. The polygon
backend returns lon/lat vertices, and recentring them means re-splitting
every ring at the new antimeridian -- which is what
sf::st_break_antimeridian() does on the sf backend. recenter was simply
ignored on the polygon side, so join_world(recenter = 150),
attach_geometry(recenter = 150) and world_geometry(recenter = 150)
returned byte-identical coordinates to recenter = NULL and drew an
Atlantic-centred map for someone who had asked for a Pacific-centred one.
They now say so and point at geometry = "sf", in the same shape as the
existing bounding-box warning on that backend. ?world_geometry was also
the one help page that did not already describe recenter as an sf
option.
n_perm = 0 returned undocumented NA p-values. morans_i() documents
the escape hatch ("use 0 to skip the test"), and all three statistics
validate n_perm with a lower bound of zero -- but local_morans() and
gearys_c() described the argument only as "permutations for the
pseudo-p-value", so passing 0 handed back a whole column of NA with
nothing to say the test had been skipped. All three now document it, and say
that p_value comes back NA.
Two bundled datasets did not name every column they have.
?country_meta listed sixteen of its seventeen columns, omitting income --
the World Bank income group that audit_coverage(by = "income") and
rank_countries(within = "income") read -- and ?world_snapshot called
continent, region and income merely "classifications" without naming
them.
interpolate_missing() silently renamed a duplicated column.
complete_years() and audit_coverage() reject a frame with two columns of
the same name; this one filled the first and handed the second back renamed
-- v and v in, v and v.1 out, without a word.
index_to() indexed a whole country to NA depending on row order.
year == base_year is NA for a missing year, and x[c(NA, TRUE)] returns
an NA element before the real match, so taking the first element picked
up the NA. The same three observations gave 100/150/50 with the missing
year last and NA/NA/NA with it first. The base year is now chosen from
the rows that have one.
compare_sources() invented a country from an unparseable year.
read_year() deliberately records NA for a time value it cannot parse, and
subsetting a data frame by a condition containing NA appends a row of all
NA -- a phantom country with no iso3c, which then survived the join into
the comparison table.
A brace in borrowed text replaced the message it was reporting. A cli
bullet is a template, so text taken from somewhere else -- a parallel
worker's error, an on-disk cache read failure, countrycode's own complaint
-- had its braces interpolated. A worker failing with bad json {"a": 1}
reported "Could not evaluate cli {} expression" and the real failure was
gone, which is the worst possible moment to lose it. Four sites now pass the
borrowed text through as a value.
A bare column in a string-taking verb gave object 'v' not found. Nine
of these verbs take an unquoted column through tidy eval; three take strings
-- interpolate_missing(value), complete_years(value) and
audit_coverage(indicator). Writing the bare column that works everywhere
else produced base R's "object not found", naming neither the argument nor
the string it wanted. They now say what to write -- value = "v" -- while
leaving a genuine error in the argument untouched.
morans_i() shipped with one hard-wired weight scheme, land-border contiguity.
An island has no land border, so Japan, the United Kingdom, Australia,
Indonesia, Madagascar, New Zealand, the Philippines, Iceland and every small
island state carried no weight and left the analysis -- 49 of the 191 countries
with data in world_snapshot. The omission is systematic, not random.
country_weights() builds the weights as a first-class object:
"contiguity", "knn" (every country gets neighbours, islands included),
"distance", or "custom" -- which is how a non-geographic neighbourhood,
trade volume or migration flows, goes through the same API.morans_i(weights = ) accepts it, and now reports n_excluded and an
excluded list-column whatever the scheme. The two answers differ and both
are defensible: contiguity gives I = 0.61 on 142 countries, k-nearest gives
0.47 on 189.local_morans() and lisa_map() give the LISA cluster
classification and its map; gearys_c(), getis_ord() and
spatial_lag() complete the set. No spdep required -- at ~200 countries
the dense arithmetic is trivial.A contract, not N bespoke fetchers.
register_country_source() teaches the package a new provider. A registry
rather than more Suggests, so a source with no CRAN package -- V-Dem, the
IMF, a proprietary internal feed -- is a first-class citizen without the
package depending on anything.fetch_indicator(), add_indicator() and country_sources() are
the user-facing verbs; fetch_owid(), fetch_eurostat(),
fetch_oecd() and fetch_comtrade() ship as built-in adapters, each
gated on its own client package.compare_sources() is the one that earns its place. Anyone can call
owidR; what nobody does is tell you that OWID and the World Bank disagree
about GDP per capita for fourteen countries because of different vintages, PPP
bases or territorial definitions. On the ISO spine that comparison is one
join.clear_wdi_cache() generalises to clear_country_cache(source = ); the
old name still works.historical_geometry() draws the world as it was, from CShapes 2.0
(1886-2019) -- states and colonies, which is what makes a pre-decolonisation
map possible at all. world_geometry(year = ) and attach_geometry(year = )
route to it.country_join(key = )
and country_join_all(key = ) gain "cowc", "cown" and "gwn" as
alternate spines, and warn -- naming which table -- about the dependencies
COW/GW cannot carry. historical_geometry() is keyed on
gwcode, with iso3c as a best-effort extra that is NA for every entity
that never had one.country_groups_history (new dataset) dates membership for twelve groups,
so country_groups(as_of = ) and in_group(as_of = ) answer the
question a panel actually asks. The United Kingdom was in the EU in 2016 and
not in 2021; EFTA had eight members in 1965 and has four now. Commonwealth,
G20 and OPEC are deliberately not dated -- their histories involve
suspensions and contested dates, and a fabricated date is worse than an absent
one. The table is validated at build time against country_groups_tbl.country_timeline() reads the crosswalk both ways: what the USSR became,
and what Estonia was part of. audit_time_coverage() catches the rows a
successful join leaves wrong -- South Sudan with 1995 data, Czechoslovakia
with 2001 data.world_map(na_style =) chooses how missing countries are drawn:
"grey" (the default, and ambiguous -- grey reads as "low" to many people),
"hatched" (diagonal hatching via the optional ggpattern, unmistakable and
greyscale-safe), "outline" or "omit".world_map(footnote = "auto") generates the coverage line -- "174 of 195
countries shown; 21 missing" -- so a map cannot quietly overstate what it
covers. A string is used verbatim.coverage_map() maps data availability itself, the cartographic
counterpart to audit_coverage()'s table.world_map(classification_report = TRUE) attaches the breaks, the method
and the count of countries per class to the plot. A map whose top class holds
one country and whose bottom holds ninety is misleading, and the counts say so
at once.classify_compare() draws the same choropleth under several
classifications with that table for each. On world_snapshot's GDP per
capita, equal-interval and pretty breaks put 173 of 189 countries (92%) in a
single class while quantile spreads them 38/38/37/38/38 -- the difference
between a map that says something and one that says nothing.
?world_map now states Brewer & Pickle's (2002) finding that quantiles read
best and Jenks materially worse, with the citation, since that is the reverse
of the common GIS default.value_by_alpha_map() encodes the value in colour and an equalising
variable (usually population) in opacity, over a neutral background. It is the
answer to the small-number problem -- an eleven-thousand-person country's rate
shouting as loudly as a billion-person country's -- and unlike a cartogram it
solves it without distorting geometry (Roth, Woodruff & Johnson 2010).disputed_territories (new dataset) records that a territory is
contested and who the parties are. It does not adjudicate, rank claims or
imply any claim is better founded. dispute_policy() records which map
convention you are using -- and warns that selecting "de_jure" does not
change a single shape, because the package ships no claimed-boundary
geometry. check_dispute_coverage() and world_map(disputes = "mark")
stop a contested area passing unremarked.world_map(uncertainty = ) switches the fill to a value-suppressing
uncertainty palette (Correll, Moritz & Heer 2018): the value range contracts
as uncertainty rises, so an uncertain estimate cannot claim an extreme colour,
and the legend becomes the value x uncertainty grid.rate_check() flags rates computed over denominators too small to trust,
and smooth_rates() shrinks them toward the global rate by
empirical-Bayes. On a five-country example Tuvalu's rate shrinks from 2.7e-4
to 5.9e-5 at a weight of 0.08, while China's is untouched at 0.9999.interpolate_missing() fills panel gaps and flags every value it
invents, non-optionally. Gaps longer than max_gap are left alone, because
interpolating across a decade is not interpolation. world_map() reads the
flag and notes it in the caption.projection_distortion() measures what a projection does, numerically.
It cross-validates: every equal-area projection reads exactly 1.000 areal
distortion, Mercator reads 4.4 mean and 132 max, and Mercator alone reads
~0 degrees angular.projection_info() returns the property table for all thirteen
projections: construction family, equal-area, conformal, the PROJ string, and
a note on what each is good for. subset(projection_info(), equal_area) is
the short answer to "what may I safely use for a choropleth".projection_compare() draws one choropleth under several projections at
once, holding the data and the classification fixed so only the CRS varies.tissot_map() draws Tissot's indicatrix -- circles of equal ground radius
projected with everything else. On Mercator they stay round and grow
enormously; on Equal Earth they hold their area and shear. It makes the
package's honesty claim visible rather than asserted.map_provenance() reports what went into a map -- package version,
snapshot year, geometry backend, projection, classification and its breaks,
fill column, and countries shown versus missing. Every one of those is already
known at plot time; this makes it readable. Every map verb records it, not
just world_map(): a provenance feature that covers some of the verbs is
worse than none, because the gap only shows up when someone relies on it.inst/CITATION credits the package and the sources it reconciles:
countrycode (Arel-Bundock, Enevoldsen & Yetman 2018), the World Bank,
Natural Earth, and the papers behind Equal Earth, the classification guidance,
value-by-alpha and Moran's I. citation("countryatlas") now produces the full
set.flow_matrix(), country_network() and od_map() turn a
bilateral table into a matrix, a network summary, or small multiples -- the
answer to the arc map's problem, which is that past a few dozen flows
flow_map() is a plate of spaghetti.country_factsheet() and world_table() are the last mile for the
audience that wanted a table. world_table() uses gt when it is installed
and a tibble otherwise.standardize_subnational(), nuts_geometry() and
subnational_map() go one level below the country, scoped to where a
maintained code system and free geometry both exist. There is no admin2, no
bundled boundary data, and no attempt to pretend a French departement and a US
state are the same kind of object.gridded_cartogram() allocates one cell per N people by largest
remainder, so the reader can count. cartogram_diagnostics() reports the
residual area error, because cartograms fail quietly -- on the bundled
snapshot the default contiguous cartogram leaves Greenland 371x too large.cartogram_map(type = "flow") routes to cartogramR's Gastner-Seguy-More
algorithm. Each type now gates only on the package it actually needs.interactive_map(engine = "mapgl") renders through MapLibre GL, and
globe_map(interactive = TRUE) turns the static orthographic globe into
one you can spin with the mouse.world_map(engine = "tmap") is an alternative static renderer for people
already working in tmap. The package stays ggplot2-native; this is a door, not
a second front door.world_query() gains layer, facet, size and n_bins, so the
database-side path can express bubbles, binning and small multiples.wdj_overrides() now warns. It has been soft-deprecated since 2.0.0, but
an interactive-only note never reaches the scripts still calling it. Use
country_overrides(); the two return the same table.?wdj_overrides now says it is deprecated. The code started warning in
this release, but the shared help page still described wdj_overrides() as
"a backward-compatible alias" and its examples called it twice -- so the one
place a reader goes to check was describing the previous release's behaviour,
and running the documented examples emitted the deprecation warning. The
examples now use country_overrides().options(countryatlas.gdp_compat = TRUE) now warns. The
gdp_per_capita_2015 alias dates from 1.0.0 and will be removed.New remove_country_source(). register_country_source() had no
counterpart, so registering was permanent for the session: anything that
registered a source -- an example, a test, an exploratory script -- left
country_sources() reporting different rows for the rest of the session with
no way back. Removing a source also drops its memoised answers, so
re-registering the same name cannot serve results from the function that was
just removed. The five built-in sources are protected.
The persistent World Bank cache now expires and is size-capped. It used
memoise::cache_filesystem(), which has neither, so the directory grew
without bound for the life of the installation -- while CRAN policy allows a
cache under tools::R_user_dir() only if "sizes are kept as small as
possible and the contents are actively managed (including removing outdated
material)". It now uses cachem::cache_disk(): entries expire after 30 days
and the least-recently-used go once the directory passes 50 MB, both
adjustable with options(countryatlas.cache_max_age = ) and
options(countryatlas.cache_max_size = ). Expiry also matters on the merits,
since World Bank observations are revised. Entries written by earlier
versions are swept once, and a corrupt entry is now silently re-fetched
rather than warned about and left in place. cachem is an unconditional
dependency of memoise, so this adds nothing to install.
clear_country_cache() can now release the geometry caches. Called with
no source it also drops the Natural Earth sf layer held per scale (tens
of megabytes at scale = "medium") and the memoised map_data("world")
tibble (~99,000 rows per override set). Those are the largest things the
package keeps in memory and there was no API to release them; in a Shiny app
or a plumber process they were held for the life of the process. Naming a
source leaves geometry alone. Relatedly, world_polygons is memoised in
.onLoad() rather than at build time, which is both memoise's own guidance
and what makes the cache reachable.
add_indicator() no longer claims "only what you need is fetched". True
for comtrade, partly true for wdi, and false for owid, eurostat and
oecd, which download the full dataset and filter locally -- so
add_indicator(one_row, "owid", "life-expectancy") transfers every country
and year to keep one value. ?add_indicator now says which is which.
?map_provenance listed 15 of the verbs that carry provenance.
od_map(), subnational_map(), projection_compare() and
world_map(engine = "tmap") also carry it; tissot_map() is the one map
verb that carries none, and now says so. A test loops over every verb and
asserts the provenance names the column the caller asked about.
?historical_geometry did not document owner or capname, which it
returns when the installed cshapes supplies them. owner is what makes
dependencies = TRUE legible, so it is worth naming.
?country_groups_tbl gives the membership date. It pointed at the
package NEWS for the reference date, where two different dates were on
record; the Rd now states 2026-06-01 and documents the as_of attribute
that carries it in code. An Rd should not delegate a fact to a changelog.
?disputed_territories says that nothing in the package reads
administered_by or claimed_by. dispute_layer() and dispute_note()
key on iso3c alone; the party columns are for your own filtering, and are
documented precisely so such a filter is writable.
The README computes the count it quotes. "42 of 215 countries silently
vanish" was hard-coded prose next to a figure built from a live fetch, so it
drifted as the World Bank revised its coverage -- by the time anyone checked
it was 37 of 210. Both now come from one computation.
stats, utils, tools, grDevices and parallel are declared in
Imports. They were called with :: and not declared. Base-priority
packages are exempt from the undeclared-:: check, so this was never a check
finding; declaring them is conventional and removes any dependence on how
that exemption is treated in future R.
Two reads of ggplot internals are isolated. ggplot2 4.0.0 moved
ggplot to S7 with @ accessors and a compatibility layer over $.
animate_world() now reads the title with ggplot2::get_labs() where
available, and cartogram_map() stores its frame in an attribute so
cartogram_diagnostics() need not reach into the plot's data slot at all.
Build-time validation that only reported. ?country_groups_history says
the table "is validated at build time against country_groups_tbl"; the
script compared, message()d a mismatch, and wrote the .rda anyway, so a
disagreement scrolled past in a build log -- and NEWS records that this
drift has already bitten once. It is a hard stop() now, accumulating every
mismatch first, and it reads the snapshot from the working tree rather than
from the installed package (which would compare the new table against the
previous release's snapshot). world_snapshot's assembly gained
relationship = "one-to-one" and a uniqueness assertion, and
disputed_territories now validates administered_by / claimed_by
against its documented six placeholders in both directions.
country_meta$area_km2 is anchored to real areas. The formula that
produced it lived in data-raw/ as a copy of the package's
ring_area_km2() that had silently missed the antimeridian fix -- and
nothing pinned the column numerically, the only assertions being > 0 and
">90% non-NA", both of which a 179x-inflated value passes. The copies are
reunited, the build script self-checks against the analytic area for an
equatorial square, a wrapped square and a polar cap, and the shipped values
are checked against ten known country areas. (The shipped values were
correct; nothing had established that.) data-raw/overrides_snapshot.R's
copy of the override table is likewise pinned to the package's.
The CI matrix gained a _R_CHECK_DEPENDS_ONLY_ leg. All five existing
legs install every Suggests, so none of them ran the configuration CRAN
actually runs -- which is how a broken \link{} to a Suggests-only topic
reached a release. The Linux legs also generate tr_TR.UTF-8, so the
Turkish-dotless-i hardening that broke 2.0.0 on CRAN's Fedora flavours is
actually exercised rather than skipped.
The silence policy covers every offline verb. It enforced "a correct
call to any verb is completely silent" for 39 of 102 exports, and every
warning bug found in the pre-release review sat in the gap. It now covers 32
more, plus the cross-product of world_map()'s na_style / disputes /
style arguments and a zero-row leg for the rate verbs -- which is what
found the deflate() and rate_check() bugs above.
The Honest maps vignette named two countries that contiguity weights do not drop. Its island list included the United Kingdom and Indonesia, but the UK keeps its land border with Ireland and Indonesia keeps its borders with Malaysia, Papua New Guinea and Timor-Leste -- so neither is excluded. A vignette about maps quietly misleading, quietly misleading. Corrected, and it now uses the two as the illustration that you cannot tell from the finished map who dropped out.
Two more vignette claims corrected. Getting started called
world_snapshot$countries$income "an ordered factor"; it is a plain factor
whose levels are in income order, built that way deliberately in data-raw/,
and is.ordered() is FALSE. countryatlas and ggsql said DRAW spatial
arrived at ggsql 0.4.0 while three places in R/ said 0.4.1 -- both are true
of different components, but read together they were a contradiction, so the
engine and the R package are now named separately.
Every vignette's prose claims are pinned by tests. Three were already; the four added or rewritten since then were not, which is how the errors above survived. The README's are pinned too, including that its verb table covers every non-deprecated export.
as_of documents what a bare year means. It resolves to 1 January, so
in_group("Croatia", "EU", as_of = 2013) is FALSE -- Croatia joined that
July. The convention was always this; only the documentation was silent, and
the examples all sat far from a boundary. Pass "YYYY-MM-DD" when the month
matters.
Every figure in the vignettes now carries alt text, so the pkgdown site and the rendered articles are readable with a screen reader.
The geom_country_labels() example labelled all 188 countries at once, which
made ggrepel drop every one of them; it now shows the two idioms that work
-- selecting a subset through data, and zooming in.
Documentation and vignettes zoom with coord_quickmap() rather than
coord_cartesian(). Both replace the map's coordinate system, but only the
former keeps the latitude-dependent aspect ratio, so the Europe inset was
being drawn without any fixed aspect at all.
DESCRIPTION declares Language: en-GB, and inst/WORDLIST records the
package's technical vocabulary, so the CRAN spell check reports real typos
instead of 300 false positives.
common_indicators gains the two price-conversion series deflate() and
to_ppp() reach for by default.
inst/CITATION credits the new methods: Anselin, Geary, Getis-Ord, Correll et
al., Phillips-Sul, Gastner-Seguy-More and Schvitz et al. alongside the
existing set.
The mean Earth radius is one shared constant rather than three literals.
A bad projection or scale now says which argument it means. Both were
validated by match.arg() inside a shared helper, which produces R's
anonymous 'arg' should be one of ... -- naming neither the argument nor the
function the user called. Seventeen exported functions take projection and
nine take scale, so one poor message was reachable a great many ways.
Suggests grows to 41 packages. That is heavy, and it is the price of a light
core: nothing here is needed to install the package or to draw a choropleth.
A patch release. The only behaviour change is to a test and to where an optional DuckDB connection keeps its state; nothing in the package's own API moves.
test-standardize.R's de-accenting test failed on CRAN's two r-devel Fedora
flavours, which run in a latin1 locale. The test asserted that outside a
UTF-8 locale iconv(x, to = "ASCII//TRANSLIT") cannot produce a resolvable
spelling -- generalising from LC_CTYPE=C, which is the case
?country_overrides actually documents. That is not what glibc does: the
\u00e7 escape makes the input UTF-8 marked in every locale, so iconv
reads it as UTF-8 and only the target charmap matters. Latin-1 has
transliteration data and still yields "Curacao"; only C/POSIX, which has
none, degrades to NA or "Cura?ao". The test now asserts the invariant that
holds in every locale -- de-accenting may or may not resolve, but it never
resolves to a different country -- and passes under C, latin1, latin9 and
UTF-8.as_ggsql_source(format = "duckdb") now opens its connection with
duckdb(shared_home = FALSE) where the installed duckdb supports it (1.4 and
later). By default duckdb keeps downloaded extensions and secrets in
~/.duckdb, which a throwaway in-memory table has no business creating in the
user's home; R CMD check also reports a new ~/.duckdb among "new files in
some other directories".girafe() widget -- which interactive_map(engine = "ggiraph") does, and which no 1.0.0 test did -- makes gdtools copy 90
Liberation font files into tools::R_user_dir("gdtools", "data"). R CMD check snapshots that tree and reports anything new, which is the NOTE CRAN
raised against 1.0.0 for our own WDI cache (fixed separately, in
wdj_cache_dir()). A new tests/testthat/setup-user-dirs.R points the R
user directories at the session temp directory before any test runs, so the
widget still renders, just somewhere disposable.?clear_wdi_cache claimed nothing was written until a World Bank fetch
succeeded, so an offline session "never creates" the cache directory. The
directory is in fact created the first time a cached fetch is attempted,
because that is when the location has to be proved writable -- a failed
fetch leaves it behind empty. The help page now describes what happens.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), so 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, such as 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 throughisTRUE(), which never errors but silently turns anything that is notTRUEintoFALSE, 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, andgini(x, na.rm = "yes")kept theNAs and returnedNA. All 23 logical arguments across the package now requireTRUEorFALSEand 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.per_capita() multiplying back to the original value, a deflator equal to
the base year and a PPP factor of one being identities, compounding
growth_rate() walking the series back out, index_to() landing exactly on
to at the base year, ranks forming a permutation, and gini()/theil()
reaching their analytic bounds and staying scale-free.R CMD check cannot report "new files in some other directories" -- the NOTE
CRAN raised against 1.0.0. The redirect now sets R_USER_CACHE_DIR as well
as XDG_CACHE_HOME: tools::R_user_dir(pkg, "cache") reads the former
first, so setting only the fallback left the redirect at the mercy of the
machine, and wherever R_USER_CACHE_DIR happened to be set a suggested
package's cache still escaped to the real user directory.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.