countryatlas 2.0.0
A major release that wires countryatlas into the database-rendering world via ‘ggsql’, widens the map vocabulary, and fixes several correctness issues found by auditing 1.0.0. The version is bumped to 2.0.0 because the bug fixes change the output of world_map() (quantile binning), bubble_map() / flow_map() (de-duplicated symbols), geom_country_labels() (label placement) and convert_country() (override-only entities) — code that depended on the old behaviour may see different maps or values.
New: database-side rendering with ggsql
-
as_ggsql_source()exports a curated, ISO-reconciled, WDI-joined table (withsfgeometry 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 aggsqlspatial 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,DBIandnanoarroware optionalSuggests. See the new countryatlas and ggsql vignette.
New: maps, projections and helpers
-
globe_map()— an orthographic globe choropleth, withbackend = "sf"(smoothest limb) orbackend = "polygon"(needs onlymaps+mapproj). -
spin_globe()— a rotating-globe animated GIF (oneglobe_map()frame per central longitude, assembled withgifskiormagick). -
facet_map()— small-multiple choropleths (the static counterpart toanimate_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 tagginglon/latwithiso3c. -
repair_country_names()— the “act on it” companion tocheck_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 forwdj_overrides()(kept as an alias) after the rename to countryatlas. -
country_groups_tblgainsMercosur,GCC,NordicandVisegrad. -
country_borders()— a tidy adjacency edge list built from polygon topology (sf::st_touches()), withneighbors()for a vectorised per-country lookup. -
distance_between()— great-circle (haversine) distance between two countries’ centroids; needs neithersfnor the network. -
dorling_map()— the Dorling cartogram promoted to a first-class verb, withk/itermaxtuning;cartogram_map()itself gains...passthrough to the underlyingcartogram::cartogram_*()call.
New: historical entities, inequality and spatial statistics
-
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 successoriso3crows (one-to-many, dated); modern names pass through as single rows, so a whole messy column pipes in unchanged. -
check_country_match()gains ahistoricalcolumn. It flags dissolved entities even when countrycode “matches” them — the headline case is"USSR", which countrycode silently resolves to Russia’sRUS, so Soviet-era data becomes Russian data with no warning. -
correlate_indicators()— pairwise indicator correlations on the spine (pearson/spearman, pairwise-complete, per-pairn), 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()andtheil()— 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 byiso3cand ordered byyear, completing the panel toolkit aroundgrowth_rate()/index_to()/complete_years(). -
morans_i()— global Moran’s I with a permutation pseudo-p-value, computed on the row-standardisedcountry_borders()adjacency. Nospdepdependency: the weights come from the package’s own curated topology. -
spike_map()— triangular spikes at country centroids (height ∝ value), the overplotting-resistant cousin ofbubble_map(); needs onlymaps. -
convert_country()acceptsto = "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, not4e+06) whenscalesis installed; the continuous scale uses the same formatter.
Bug fixes
-
bivariate_map()errored on every call (“the condition has length > 1”, pre-dating 2.0.0). The two fill columns were injected intobiscale::bi_class()with!!rlang::sym(), butbi_class()reads them withas.character(substitute(...))rather than tidy eval, so the injection deparsed into a multi-element vector insidebiscale. The happy path is now covered by a test (the old one only checked that the function errors cleanly whensfis absent). -
as_ggsql_source()andinteractive_map(engine = "ggsql")errored on anysfinput – the whole point of the ggsql bridge.sf::st_as_binary()returns a classedWKBobject, whichtibblerejects (“all columns must be vectors”); the geometry column is now the plain list of raw vectors thatnanoarrowencodes as binary andDBIwrites as aBLOB. -
projection = "winkel_tripel"errored on every render – one of the eight projections this release adds. The CRS built fine and the geometry projected fine, butggplot2::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()] blankspanel.gridanyway, so nothing visible changes. All 13 projections are now covered by a full-render test. -
world_geometry("coastline", geometry = "sf")errored with a GEOSTopologyExceptionin every projection except"plate_carree": a couple of Natural Earth rings are self-intersecting andsf::st_union()(unlike the spatial predicates) refuses them. The geometry is repaired before the union. -
world_geometry(region = c(xmin, ymin, xmax, ymax))– andworld_data()/attach_geometry()with a bounding-boxregion– errored on thesfbackend (“Loop 0 is not valid”), becausesf::st_crop()runs under the strict S2 engine on unprojected geometry. It now clips with the GEOS planar predicate, ascountry_borders()/locate_country()already did. -
convert_country()’swarnargument was documented but silently ignored (every internalcountrycode()call is wrapped insuppressWarnings(), because countrycode also warns on intermediate hops thatconvert_country()goes on to recover). It now reports inputs that match no country, likestandardize_country()does. A recognised country whose destination value is genuinely missing still returnsNAquietly. -
world_map()/globe_map()’sna_labelwas accepted and silently ignored. The"quantile","jenks"and"categorical"legends now label their missing-data key with it (the continuous and binned colourbars have noNAkey to name, which the documentation now says). - Kosovo’s
XKXresolves forcountryandflagfromfrom = "iso3c", not just from its name. It has no row at all incountrycode::codelist, so everything derived from the code wasNA– which surfaced ascountry_borders()/neighbors()returningNAnames for Kosovo’s four land borders,locate_country(add = "country")returningNAfor points inside it, andstandardize_country(add = c("country", "flag"))doing the same. The curated fallback table now carries the name and flag too. -
per_capita()without an explicitpopcolumn died with an opaquevctrserror (“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 thepopargument. -
theil()returnsNAshares (notNaN) for a perfectly equal distribution, where the total is0and the shares are undefined – matching howgini()andshare_of_world()treat a zero denominator. -
country_join()/country_join_all()no longer cross-join rows whoseiso3cisNA: unmatched countries used to collapse to a singleNAkey and fan out into a Cartesian product. The joins now passna_matches = "never"(#4). -
country_join_all()validates the length oforigin(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 aregioncolumn no longer shadows a realcountrycolumn (#6). -
standardize_country(add = ...)accepts any rawcountrycodedestination (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 becomeNA(and are flagged bywarn) 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-NAvalue: the fetch window is widened so an earlier observation can actually be found (#7). -
fetch_wdi()keepsiso2c/countryfor a country that appears only in a non-first indicator (they are coalesced across indicators) instead of leaving themNA(#8). -
world_map()/globe_map()withstyle = "quantile"/"jenks"no longer error on a constant, single-country, or all-NAvalue 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")returnsNAfor an all-NAgroup instead ofInf/-Inf(#11). -
share_of_world()returnsNA(notNaN/Inf) when the (per-year) total is zero or non-finite (#13). -
gini()returnsNAwith a warning for negative input rather than a value outside the documented[0, 1]range (#14). -
spike_map()no longer producesNaNspike coordinates when every height is zero (#15). -
world_query()honourstransformeven whenpalette = NULL, emitting a standaloneSCALE 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 viacoord_sf(). - Polygon centroids returned more than one row for ten
iso3ccodes (overrides map several names — Azores/Madeira → PRT — to one code), fanning out joins inbubble_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 appliedwdj_overrides()forto = "iso3c", so override-only entities (e.g. “Canary Islands”, “Azores”, “Bonaire”) returnedNAfor every other destination (continent, region, iso2c, flag, currency, country name, …). It now resolves the override-correctediso3cfirst and derives every other destination from that. - Kosovo’s
XKXneeded extra care: it has no row at all incountrycode::codelist, so deriving destinations purely via theiso3cround-trip above isNAfor everything — which would have regressedflag/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 theiso3cround-trip comes back empty, and fillsiso2c/continent(which neither path classifies) from the same curated fallbackstandardize_country()uses. Net effect versus 1.0.0: zero regressions, plus newly-workingcontinent/iso2cfor Kosovo — which also fixeslocate_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 suppliedtooltipcolumn, defaulting tofillas before when omitted. -
world_data(overrides = )(andattach_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 defaultwdj_overrides(). The override set now flows through to both the polygon andsfmatchers, 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. - A mistyped column name is now reported by countryatlas rather than leaking out of
ggplot2as a bare “object ‘x’ not found” from inside a layer, or out ofvctrsas a subscript error.world_map(),globe_map(),facet_map(),tile_map(),bubble_map()(bothsizeandcolor),spike_map(),flow_map()(from,toandweight),interactive_map()(fillandtooltip) andmorans_i()all validate up front, matching the messageper_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 reportedn_missing = 0andna_rate = NaNfor a column name that isn’t indata; it now errors. -
world_map()/globe_map()errored (“‘length = 2’ in coercion to ‘logical(1)’”) whenna_labelwas longer than one element. The first element is used to label the singleNAkey, and aNULL/NAlabel still leaves the default formatter alone. -
per_capita()sentstart = Infto the World Bank whendatahad ayearcolumn that was entirelyNA; it now falls back to last year, as it already did for a frame with noyearcolumn 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 rawvctrssubscript error. -
theil()returnedNaNwhen every weight was zero; it now returnsNA, matchinggini(). -
world_map()/globe_map()withstyle = "categorical"and a numericfillcolumn letggplot2raise “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 leaksbiscale’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 otherbiscalewarning still passes through. - A
regiongiven as lowercaseiso3ccodes 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), soregion = c("usa", "can")subset to the USA alone. Codes are now recognised in any case, matching whatstandardize_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. - Every
sf-backed call printed three or more lines ofsfinternals 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 wassf::st_break_antimeridian(), which toggles the s2 engine and runs an intersection internally, and which sits on the path of everysfcall: a plainattach_geometry(geometry = "sf")emitted them, as didworld_map(),world_geometry(),country_borders(),neighbors(),morans_i(),locate_country()andsimplify_geometry(). It is now wrapped in the samequietly_sf()helper the othersfcalls already used, so those paths are silent. (The notices bypass R’s condition system, sosuppressMessages()could not have caught them.) -
cartogram_map()/dorling_map()never validated theirweightorfillcolumn, the one place the rest of the package’s existence checks were missed. A badweightreachedcartogramas"missing value where TRUE/FALSE needed"(or, for the Dorling variant, a warning aboutmax()and then a wrong picture), and a badfillwas not caught at all. -
index_to()likewise never checked its value column, so a typo produced adplyrerror from insidemutate();base_yearandtoare validated too. - Scalar arguments are validated, so a typo or an
NAnames the argument instead of surfacing as"missing value where TRUE/FALSE needed",classInt’s"n less than 2", or aPROJcomplaint aboutlat_0. Coversn_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 tocoord_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()), andn_frames/fps/width/height(spin_globe()). - Two of those scalar arguments were not merely reported badly – they drew the wrong thing in silence. A negative
max_heightdrewspike_map()’s spikes upside down, andglobe_map(lat = )beyond +/-90 built a CRSPROJrejects, which only surfaced later ascoord_sf()’s"crs not found: is it missing?". -
geom_country_labels(repel = TRUE)silently drew plain labels whenggrepelwas not installed – the one degraded optional backend the package did not announce, whereclassInt,gganimateandrmapshaperall report theirs. It now says so once per session (the argument defaults toTRUE, so reporting on every call would be noise), and stays quiet whenrepel = FALSEwas asked for. - The number of bins no longer depends on whether
classIntis installed. For a fractionaln_bins,classInttruncated internally while the base-quantile fallback passed the fraction toseq(length.out = )and produced one break more, so the same call binned differently in different environments.n_binsis now truncated to a whole number of bins before either backend sees it. -
simplify_geometry(keep = 0)errored underrmapshaperbut was silently accepted by thesf::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. - Count arguments are bounded above as well as below. The scalar checks required a finite number, but several call sites then coerce with
as.integer(), which returnsNApast2^31-1– son_perm = 1e10orn_bins = 1e10produced “NAs introduced by coercion” or, worse, “missing value where TRUE/FALSE needed”.n_bins,n(flow_map(),lag_by_country(),diff_by_country()),n_permandn_framesnow name the range.morans_i()also dropped amax(0L, ...)clamp that the validation had made unreachable. -
lag_by_country()/diff_by_country()clampedn <= 0up to1, so a lag of0quietly returned a lag of1; it now errors. -
complete_years(value = )silently ignored a column name that wasn’t indataunder the defaultmethod = "none", while erroring fromall_of()for"locf"/"linear"; it now errors consistently. -
interactive_map(engine = "ggsql")now gates onggsql>= 0.4.1 rather than mere presence.DRAW spatial– the clauseworld_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_queryrecords that the clause has shipped in the engine but not yet in the R bindings, and thatPROJECT TOadditionally needs a spatial backend (for DuckDB, itsspatialextension);world_query()itself remains a dependency-free string builder. -
R CMD checkno 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 undertools::R_user_dir()during a check; under check it now lives in the session temp directory instead. Normal use is unchanged, andoptions(countryatlas.cache_dir = )still overrides both.?clear_wdi_cachenow documents where the cache lives and how to disable it. - An unmatched country in your data could be drawn as a real country. dplyr joins default to
na_matches = "na", so anNAISO code matched anotherNAISO 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. Affectedattach_geometry(geometry = "sf")(and sojoin_world()andworld_map()downstream of it) andbubble_map(backend = "sf"). All country-keyed joins in the package now passna_matches = "never", whichcountry_join()andcountry_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, soprojection = c("a", "b")produced twoPROJECT TOclauses,source = character(0)deleted theFROMline entirely, andtitle = NAbecame the literal text'NA'– each of which surfaced only later, as a parse error inside ggsql’s SQL front end.world_query()andas_ggsql_source()now validate their string arguments up front and name the offending one.NULLstill omits an optional clause, and an emptytitleis still allowed. -
as_ggsql_source(format = "parquet")built itsCOPY ... 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 withDBI::dbQuoteString(), matching thedbQuoteIdentifier()treatment the table name already had. -
suffix = character(0)made the whole computation vanish.suffixispaste0()-ed onto the value column’s name, and dplyr’s"{character(0)}" :=is a silent no-op – sogrowth_rate(x, g, suffix = character(0))returnedxunchanged, with no growth column and no error.suffix = NAproduced a column namedgNA, andsuffix = ""overwrote the source column in place.per_capita(),growth_rate(),index_to(),share_of_world(),lag_by_country()anddiff_by_country()now require a single non-empty string (lag_by_country()/diff_by_country()still acceptNULLfor the default suffix). - Several arguments produced an error that named nothing rather than the argument at fault:
-
convert_country(to = c("country", "continent"))– a plausible attempt at two destinations – raised “the condition has length > 1”, and a zero-lengthtoorfromraised “argument is of length zero”. -
origindid the same across every function that resolves country names. It is now validated once in the shared internal, sostandardize_country(),country_join(),country_join_all(),join_world(),flow_map(),neighbors(),distance_between(),in_group(),repair_country_names()andcheck_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.byremains documented as plural, so multiple grouping columns still work.
-
- Logical arguments are validated too, closing the same gap in two forms. The
bordersargument ofworld_map()andglobe_map()fed a bareif (), 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 namingborders. 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()andtheil()returned a wrong number for a wrong-lengthweightsvector. Both recycled it withrep_len(), which accepts any length silently, sogini(1:10, weights = c(1, 2))returned0.2902– computed from an alternating 1,2 pattern – where the correctly-weighted answer is0.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(andtheil()’sgroups) must now be length 1 or the length ofx, andweightsmust be numeric.complete_years()likewise rejects ayearsvector that is non-numeric, empty, or containsNA, all of which it previously coerced toNAbehind base R’s warning. - Omitting a required argument now names it. Every affected function resolved its column argument with
rlang::as_name(), which raisesargument "x" is missing, with no defaultfor a missing value – naming rlang’s own parameter, and none of these functions has an argument calledx.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()andflow_map()now report e.g.`fill` is required.Optional tidy-eval arguments are unaffected. -
distance_between()paired the wrong countries whenaandbhad mismatched lengths. It combined them through vectorised arithmetic, so R’s recycling applied: 2 countries against 3 returneda[1]-b[1],a[2]-b[2]anda[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 rulelocate_country()has always enforced forlon/lat. - Three documented contracts did not match their code:
?locate_countrysaidlon/latwere “recycled together” when the function has always required equal lengths, and?gini/?theilpromised thatweightswas “recycled againstxthe 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 anpointssf object”. - Offline safety is now covered by tests rather than assumed. Checks run
\donttest{}examples and rebuild vignettes, and CRAN policy does not allow either to fail for want of a network connection.world_data()andcountry_data()degrade a failed fetch to a warning and a metadata-only frame, andwdi_search()readsWDI’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. - An unwritable cache directory silently cost you your data.
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)reportedCould not fetch indicator "..." from the World Bank APIand returned the country spine with every indicatorNA– blaming the API for a local permission problem, while the same call withcache = FALSEreturned the data perfectly. (ThetryCatch()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. - Projected maps failed outright under
options(OutDec = ",")– the ordinary setting in comma-decimal locales. The PROJ strings are built by pasting numbers, sorecenter = 48.9became+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. - Two more of the same kind, triggered by
options(scipen = -10), which formats a double in scientific notation:sf::st_crs(4326)becameEPSG:4.326e+03and yielded anNACRS (surfacing later asst_crs(x) == st_crs(y) is not TRUEfromlocate_country()), and Natural Earth’s scale110became1.1e+02, soworld_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, whichscipendoes not affect. -
simplify_geometry()andworld_geometry("graticule")are insulated from two upstream bugs of the same family, both reproducible without this package:rmapshaperserialiseskeepfor V8, which rejects the0,1thatoptions(OutDec = ",")produces, andsf::st_graticule()overflows the node stack underoptions(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. Itsmutate()honoured the caller’sgroup_by(), so the same data ranked4, 1, 3, 2ungrouped and2, 1, 2, 1after an incidentalgroup_by(region)upstream in the pipe – withwithin = NULLin both cases, which documents a global ranking.rank,percentileandz_scorewere all affected.withinis 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. - A non-numeric value column now errors by name instead of producing nonsense. A factor column is easy to acquire –
read.csv()on a column with one stray non-numeric entry gives you one – and arithmetic on it failed four different ways:growth_rate()andper_capita()returned a column ofNAs behind base R’s “‘/’ not meaningful for factors”;share_of_world(),rank_countries()andaggregate_regions()raised an opaque error from insidedplyr::mutate();gini()managed “missing value where TRUE/FALSE needed”; andmorans_i()quietly returned a plausible-looking statistic. These, plusindex_to(),diff_by_country(),beta_convergence(),sigma_convergence()andtheil(), 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"returned0,"mean"and"weighted_mean"NaN, and"min"/"max"-Inf/Infplus 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 returnNA, 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_regionsdocuments this. -
complete_years()failed on a zero-row panel, where every other panel helper returns zero rows. With noyearsit reachedseq(min(numeric(0)), max(numeric(0)))and died on base R’s “‘from’ must be a finite number”; withyearssupplied it died on tidyr’s “Can’t recycleyear(size 3) to size 0”. Neither message names anything the caller did. It now returns the empty frame, columns intact, for all threemethodvalues – while still reporting a badyearsorvalueargument. -
bivariate_map()andcartogram_map()(and sodorling_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 ofNA.biscalethen indexedsVar[1:(length(sVar) - 1)], which becomes1:-1, and reported “only 0’s may be mixed with negative subscripts”;cartogramcomparedNAinif (meanSizeError < maxSizeError)and reported “missing value where TRUE/FALSE needed”. Neither mentions the data. Both now say which columns are empty, asspike_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 withouter(), 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 offworld_data(geometry = "polygon"). It cannot de-duplicate oniso3c, sinceby = 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_coveragedescribed the raw list it returns without mentioning that the object is classed and has aprint()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 becausest_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 – withlon_0 = 0it cut the outline at +/-180, its own edges, taking it down to two thirds of the globe and, under Mollweide, to nothing. - Where an ocean background cannot be drawn,
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")andworld_geometry("ocean")returned a baresfcrather than thesfobject?world_geometrypromises (and that the other fourwhatvalues deliver), so dplyr verbs failed on exactly those two. Both are nowsf; the geometry is unchanged. -
?world_geometry’s@returnwas a single line naming no columns. It now lists what eachwhatreturns, and warns that the sf backend’scentroid_lon/centroid_latare in the object’s own CRS – projected metres, not degrees, socentroid_lonfor France is174097, not2.1. Usecountry_meta$centroid_lonfor degrees. -
sf::st_coordinates()failed onworld_geometry("countries", geometry = "sf"), in every projection including the default. Natural Earth supplies 177 uniformMULTIPOLYGONs, butst_break_antimeridian()runs anst_intersection()internally that collapses a single-partMULTIPOLYGONto aPOLYGON, leaving 148POLYGON+ 29MULTIPOLYGON– ansfc_GEOMETRYcolumn, whichst_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 toMULTIPOLYGON, which is a type change only: row count, codes and land area are unchanged.?world_geometryalso now notes that a hemispheric projection returns empty geometries for the far side, where the samest_coordinates()limitation applies for a different and correct reason. -
world_map()accepted a frame with no geometry, returned aggplotobject 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 thefillcolumn but never thelong/lat/groupcolumns the polygon path needs. Forgettingattach_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()andglobe_map()all take a country-level frame, soworld_map(snap, gdp)looks like it should work too. It now says so at the call, and names the fix.facet_map()delegates toworld_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 ownggplotrather than callingworld_map(), so it bypassed that check and failed at render time on.data$long, whileengine = "plotly"named the problem properly. Both now give the same message.?interactive_mapalso documents that the"leaflet"engine attaches geometry itself if handed a country-level table, which the others do not. - The numeric fill styles now require a numeric column.
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()andfacet_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 whatanimate_world()andfacet_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_namesnow says which way thestringdistfallback errs. Thethresholdargument already noted that the metric changes whenstringdistis 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 withstringdist, 77 without, none repaired thatstringdistdid 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-partMULTIPOLYGONto aPOLYGON, so a homogeneous input came back as a mixedsfc_GEOMETRYcolumn andsf::st_coordinates()failed on the result – the same defect asworld_geometry("countries"), restored one step downstream. The output is cast back; row count and geometry are unchanged. -
simplify_geometry()’skeepargument now means roughly the same thing with and withoutrmapshaper. Thesf::st_simplify()fallback was given a fixeddToleranceof(1 - keep) * 10000, i.e. metres whatever the coordinate system: 9 km on a projected frame, which barely simplified anything (79% of vertices kept atkeep = 0.1), and 9000 degrees on a lon/lat frame, which is meaningless and only survivable becausepreserveTopologykeeps 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 tokeep.?simplify_geometrysays that onlyrmapshaperhonourskeepas a true proportion. -
gini()’s negative-value warning saidReturning "NA", which reads as the two-character string rather than the missing value. It now renders asNA, matching the same correction already made elsewhere. - The test suite no longer fails on R >= 4.6. Several tests set
options(scipen = -10)to exercise the scientific-notation bugs fixed above, but R 4.6 clampsscipento a minimum of -9 and warns (“invalid ‘scipen’ -10, used -9”), so an exact round-trip assertion failed and three warnings were raised – anERRORunderR CMD checkon current R, even though the package code itself was correct. The tests now use -9 and compare against the value R actually stored. - The
wdj_overrides()soft-deprecation notice told the wrong people. It lived in the shared function body, so in an interactive session it fired forcountry_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 towdj_overrides(); the table itself is unchanged. The documented default is nowcountry_overrides(), so?attach_geometryand 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 theregion = "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 withiconv(x, to = "ASCII//TRANSLIT")as an alternative to running under UTF-8, but//TRANSLITis itself locale-dependent: underLC_CTYPE=Cit returnsNA, or replaces each accent with?when given an explicitfrom = "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. - A bad
options(countryatlas.workers)reachedmclapply(). The option is advertised in this file, so a stray value is reachable:"abc",NAandInfall becameNAworkers and surfaced as “missing value where TRUE/FALSE needed” from deep inside a parallel fetch, whilec(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. - A bad
options(countryatlas.cache_dir)did the same thing one layer down. The option is documented in?clear_wdi_cache, and a stray value reacheddir.exists()/dir.create():NA, a number andTRUEeach 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. - An empty cache directory failed on R 4.6 but not on R 4.4.
dir.create("")warns and returnsFALSEon 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 surroundingsuppressWarnings()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. -
?theilnow says that a row with a missing group is dropped along with rows whose value is missing, so the decomposition’stotalis computed over the grouped subset and can differ from the ungroupedtheil(x). On the bundled snapshot that difference is entirely Puerto Rico, which has noregion.theil()also gains numeric anchors on the bundled data, whichgini()already had. - Every
sf-backed verb leakedsf’s internal chatter asmessage()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, sopurrr::quietly(),testthat::expect_silent()or a plainwithCallingHandlers()aroundattach_geometry(),neighbors(),country_borders(),locate_country()ormorans_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 ordinarymessage()s, and only a few GDAL diagnostics need the stream redirect.) -
?world_geometrycalled 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 atregionfor a genuine polar view, and the error"ocean"raises in those projections no longer gives “it shows one hemisphere” as the reason. -
?world_geometrynow documents the Natural Earth features that have no ISO code and so come back withiso3cNA– Somaliland at every scale, plus the Indian Ocean Territories and Ashmore and Cartier Islands from"medium"on. -
?theme_world_mapsaid the theme is used by all the package’s plotting functions.bivariate_map()is the exception – it appliesbiscale::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_geometrydocumentedkeepas a proportion “(0-1)”, butkeep = 0is 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 ofvaluewas classified as a static attribute and carry-filled, so naming fewer columns invented more figures – and evenmethod = "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 staysNAin the rowscomplete()adds.value = NULLbehaves exactly as before. -
aggregate_regions()silently ignoredweightfor everyfunexcept"weighted_mean", returning the unweighted figure – on European GDP per capita, 38,323 where the population-weighted answer is 29,896. Passingweightwith any otherfunnow errors, mirroring the existing abort whenfun = "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 atorigin– feeding itiso3ccodes whileoriginstill defaults to"country.name"is the usual cause, and it drew a blank map. -
?in_groupnow says that a valueorigincannot resolve answersFALSE, indistinguishable from a country that is genuinely outside the group, and points atcheck_country_match()for telling the two apart. -
world_map()/globe_map()passedpalette,title,legendandna_labeltoviridisLiteandggplot2unchecked, so a mistake came back in their vocabulary rather than the package’s:palette = c("magma", "viridis")reached a bareswitch()and reported “EXPR must be a length 1 vector”, and a numericpalettewas accepted without a word. A length-2titleorlegendwas accepted too, andggplot2then drew both strings on top of each other. All four are now checked;world_query()already validated the two of them it takes (paletteandtitle).na_labelkeeps its documented tolerance – there is oneNAkey, so the first element wins and a length-1NAstill 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 betweenlatestand the shape arguments silently, and with opposite precedence depending on the year: a multi-yearyearoverrodelatest = TRUE, while a single year hadlatest = TRUEoverridepanel = 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 comparesdkm <= tolerance_kmas strings when the tolerance is character, and"2650" <= "a"isTRUE, 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 staysNA. Now checked, before thesfgate. - Three more scalars the validation sweep had missed now name themselves instead of failing in a dependency’s vocabulary:
correlate_indicators(min_n = )(anNAgave “missing value where TRUE/FALSE needed”, a length-2 value “the condition has length > 1”), anddorling_map(k = )/dorling_map(itermax = )(which reportedcartogram’s “all sizes are missing and/or non-positive” and an assertion naming its internalmaxiter). -
interactive_map(engine = "ggsql")reported a missingggsql>= 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. Thesfcheck now runs ahead of the package gates.spin_globe()had the same inversion for itsfillcolumn: 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 forgifski. -
join_world()could not read a column of ISO codes without being told to. Its fallback detector tried each character column withorigin = "country.name", which does not match most alpha-3 codes, so a column of them was rejected outright – and a column namediso3cwas found by name but still read as country names, sojoin_world(tibble(iso3c = c("FRA", "JPN")))warned that nothing matched and returned allNA. Detection now tries the code schemes as well and carries the one that worked through to the conversion, soiso3c,iso_a3,iso2cand an unrecognised name likecodeall resolve. An explicitoriginstill wins, and a column whose name implies a scheme is only read that way if the scheme actually resolves it. - A bounding-box
regionon the polygon backend only filters vertices; it cannot clip a polygon, so a country crossing the edge keeps a truncated ring thatgeom_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 atgeometry = "sf", where the clip is a realsf::st_crop();?world_geometryand 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 declaresrelationship = "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 noyearcolumn returned a share of the group, not of the world:sum()insidemutate()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 byyearand that replaces the caller’s groups. It now ignores the grouping in both cases, and says so where it would have mattered. - The verbs that add a column now say when they replace one the caller already had.
rank_countries()overwroterank,percentileandz_score, andper_capita(),share_of_world(),growth_rate(),index_to(),lag_by_country()anddiff_by_country()overwrote their target column, all in silence – a user’s ownrankcolumn 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:addnames the columns literally, so replacing an existingcontinentis what was asked for. -
geom_country_labels()on ansfmap failed withrlang’s internal “Columnlongnot found in.data”. The layer reads the polygon backend’slong/latcolumns, and its ownaes()was evaluated against the sf frame before the guard inside could run. It now errors with its own message and points atggplot2::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 nogroupcolumn.groupis 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 whilegroupis present.
Housekeeping
gini()andtheil()returned a silentNaNwhen the input contained an infinity.Infis notNA, so it passedna.rmand (for Theil) the non-positive filter, then made the mean infinite and every shareInf/Inf. Every other verb propagates an infinity visibly –Infin,Infout, which the caller can see – but an inequality index has no such value to report, so both now warn and returnNA, as they already did for a zero total weight. Infiniteweightsare caught too.NaNis still treated asNAand dropped.?theilpromised a tibble whenevergroupsis supplied, but every degenerate path – nothing left afterna.rm, a zero total weight, an infinity – returns a singleNAinstead. The help page now says so.dorling_map(k = 0)passed validation and then failed insidecartogramwith “all sizes are missing and/or non-positive”.check_number()’s bounds are inclusive, solo = 0admitted 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 namingk; anything above zero still works.?countryatlasgains an Options section. Two of the three options the package reads –countryatlas.workersandcountryatlas.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 askedneighbors()for each country in turn and then again for each of that country’s neighbours, to confirm the reverse edge – andneighbors()recomputes the whole world’ssf::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.?neighborsnow 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 namescountry_borders()as where the cost comes from. A test pins the fact the advice rests on – onecountry_borders()call perneighbors()call, whatever the length ofx.?attach_geometryclaimed 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 thesfbackend atscale = "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 nopathwas 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 undertempdir(), and since the function returns the path the workflow is unchanged; an explicitpathstill writes exactly where you point it.spin_globe()already defaulted totempfile(), and no other export writes at all.?country_metaand?world_snapshotnow say that theircountrycolumns disagree, and why.country_metacarriescountrycode’s English names andworld_snapshotthe 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 oniso3cleaves you holding twocountrycolumns with nothing to explain the difference – the very reconciliationcountry_join()advertises, using the same example. The count is pinned by a test, along with the referential consistency of all five code columns againstcountry_meta.?world_dataand?country_datanow say where thecountrylabel 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 thecountrycodenames (“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 atiso3cas the stable key and atconvert_country(iso3c, to = "country")for one consistent set of labels.utils::globalVariables()declared 29 names where 7 are needed. Emptying it and reading whatR CMD checkactually reports showed the rest were covered by the.data$xidiom 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 inR/.Six exported functions failed when the package was loaded but not attached –
countryatlas::dissolve_country(),distance_between(),country_groups(),in_group(),tile_map()andworld_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: undercountryatlas::fn()in a script with nolibrary()call, the lazy-data objects are not reachable. They are nowcountryatlas::-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 backNULLand 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 – passingpopexplicitly never touched it.Two verbs leaked someone else’s message on an empty frame.
facet_map()gaveggplot2’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, whererange()warns twice anddplyradds 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_geometrynow 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 theEN.ATM.CO2E.*carbon series with the AR5 greenhouse-gas series, and the bundledcommon_indicatorstable had already been updated, but the example still asked forEN.ATM.CO2E.KT– so anyone copying it got a warning and an all-NAcolumn. It now usesEN.GHG.CO2.MT.CE.AR5, which returns data.R CMD checkreports examples “OK” without failing on the warning, so nothing surfaced this; a test now checks every indicator code quoted inR/,man/or the vignettes against the bundled table.country_groups_tblwas out of date by two years in four places, while carrying anas_ofstamp 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")returnedFALSEbefore 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_cacheoffers that option as the way to relocate the cache without saying it has to be set first. It only ever took effect becauseclear_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, andreadRDS()’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 untilclear_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 insideparallel::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, andparallel = TRUEis 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
NAin Turkish, Azeri and Crimean Tatar locales.toupper()andtolower()followLC_CTYPE, and in those localesiandIare not a case pair:toupper("idn")returns a dotted capital I, not"IDN", andtolower("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 ani(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; andjoin_world()on a frame withISO3Candgeocolumns pickedgeo, 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")isNAundertr_TR). So a user withLC_COLLATE=tr_TRstill sees name-keyed gaps – notably the polygon backend, which labels its geometry by joining region names to codes, soworld_geometry(region = "IND")comes back with 21 rows of Siachen Glacier instead of India. Working around that would mean forcing the C locale around everycountrycodecall, 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-crandoes 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 forkeddetectCores() - 1workers 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 calledworld_data(2020)unconditionally, and the defaultgeometry = "polygon"backend comes from the suggestedmapspackage.R CMD checkruns\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 withrequireNamespace("maps"). The second call in the block passesgeometry = "none"and needs nothing beyond the hard dependencies, so it is left to run unconditionally.The
beyond-the-choroplethvignette failed to build whereverrnaturalearthwas absent. Its chunk guard washas_sf <- requireNamespace("sf"), but the sf geometry backend gates on three packages –sf,rnaturalearthandrnaturalearthdata– so on a machine with sf but without the Natural Earth data the guarded chunk evaluated toTRUE, ran, and stoppedR CMD buildwith “The packages "rnaturalearth" and "rnaturalearthdata" are required for the sf geometry backend”. The other two vignettes already tested forrnaturalearth; 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 askip_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 ofworld_snapshotGDP per capita on Natural Earth geometry joined byattach_geometry(), withspike_map()-style population spikes rising off the horizon and the binned-legend swatches under the wordmark.The
gdp_per_capita_2015compatibility alias (a one-cycle deprecation shim from 1.0.0) is now opt-in: setoptions(countryatlas.gdp_compat = TRUE)to restore it. The default isFALSE, soworld_data()no longer emits a duplicate column.world_snapshotrefreshed to year 2024 (was 2022) and rebuilt with the latest WDI data and curated overrides.country_groups_tblmembership date bumped to 2026-06-01 (was 2024-01-01).?world_snapshotwas 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.
.Rbuildignorenow excludes the session-local.claude/directory, whichgitignores butR CMD builddoes not, so it was shipping in the tarball and trippingR CMD check’s “hidden files and directories” NOTE.Comments in
R/overrides.Rare ASCII-only, so no source file carries non-ASCII characters outside a deliberate\Uescape.?world_snapshotno longer splits a code span across two source lines, which had left the checked-in.Rddisagreeing with whatroxygen2regenerates.beta_convergence()failed with a bare"subscript out of bounds"when the initial levels had no spread across countries. A constant predictor makeslm()return anNAcoefficient, whichsummary()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, sowdj_lapply()’smclapplybranch runs on one of the package’s busiest code paths, yet no test reached it – every other test used a single indicator or passedparallel = FALSE. Confirmed: a parallel fetch is identical to the serial one, forking preserves order,...reaches the workers,wdj_workers()honoursoptions(countryatlas.workers)and CRAN’s two-core limit, and an error inside a fork is surfaced rather than left as atry-errorfor 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 twoiso2ccodes map to oneiso3c, and thatcache = TRUEreally short-circuits a repeated fetch. No defects were found; the tests pin the behaviour.spin_globe()validatesn_frames/fps/width/height/latbefore gating ongifski/magick, matching howglobe_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-iso3cerror 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-backendglobe_map()– having been unrunnable only because their examples made a live World Bank fetch. Driven by the bundledworld_snapshotinstead, and guarded withrequireNamespace(), they are\donttest{}examples that execute in under half a second each. The safe form ofclear_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()isTRUEforNaNas well, so the checks ontheil()’s zero-weight result and its shares at perfect equality passed whether the value was the fixedNAor theNaNthe bug produced. Both now assert the exact value. - Nothing verified that quantile breaks are computed on one value per country rather than per polygon vertex – the fix that forced this major version bump. Removing the de-duplication, or flipping the flag that controls it on either
world_map()orglobe_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()return0, notNaN), zero-variance columns (NaNz-scores andNAcorrelations rather than errors), duplicate(iso3c, year)panel rows, poles and antipodal great circles, collinear rings, all-NAand all-zero fill columns, and single-country frames.?distance_betweenand?country_metanow state which countries have no bundled centroid.country_metais assembled fromcountrycode::codelist, which has no Kosovo row, sodistance_between("Kosovo", "Serbia")isNAeven thoughneighbors("Kosovo")andcountry_borders()know about it – and ten small or dependent territories have a row but no centroid. The@returnalready saidNAwas 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
groupcolumn – which polygon frames have andsfframes do not:-
correlate_indicators()reportednandrover 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 inflatedndefeated the point. -
audit_coverage()reported the wrong country count and a wrongNArate 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 aniso3ccolumn is present.
-
Quantile and jenks breaks on the
sfbackend double-counted divided countries. The de-duplication added in this release skipped thesfpath on the assumption that Natural Earth is one row per country, but it is not: Cyprus occupies two rows sharing oneiso3cat 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 atn_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"where33was meant. The shortcut was mapped tocountrycode’sgenc3ccolumn, which is an ISO-style three-letter code, not a dialling prefix; it now maps to thetelephonecolumn, so France gives33, the USA1and Japan81. A source comment claimed the limitation was “documented as best-effort”, but?convert_countrynever mentionedcalling_codeat all; the shortcut is now listed there.The
Descriptionfield – the text CRAN renders on the package page – advertised nine map idioms while the package ships eleven:spike_map()andfacet_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.?countryatlaslistedmorans_i()under “Core data assembly” while_pkgdown.ymllisted 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 withgini(),theil()and the convergence measures on both surfaces.The
@seealsocross-references are reciprocal. All three the package had pointed one way only: a reader of?giniwas sent totheil()but a reader of?theilwas sent nowhere, and likewise forbeta_convergence()/sigma_convergence()anddissolve_country()/check_country_match().?theildid not link togini()at all – it named Gini in prose without a cross-reference. The four related diagnostics (check_country_match(),repair_country_names(),dissolve_country(), plushistorical_codes) now all reference each other.Every page taking a
projectionargument now says where the valid values are. Onlyworld_map()enumerated the 13 projections and onlyworld_geometry()pointed at it; the other eight entries said no more than “Projection.” (bivariate_map()) or “Projection options for thesfbackend”, leaving a reader with nothing to go on. Relatedly, the three pages that documentprojectionandrecentertogether described only the projection, sorecenter’s meaning – a central meridian – was missing fromworld_data(),join_world()andattach_geometry().The
rnaturalearthhiresrequirement is now documented on every page that takes ascaleargument, not just?world_geometry. Seven topics –world_data(),join_world(),attach_geometry(),locate_country(),country_borders(),neighbors()andmorans_i()– describedscalewithout 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 thernaturalearthhirespackage, which is not on CRAN and is not inSuggests. Left ungated,rnaturalearthresponded 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 atscale = "medium"(50m) as the option that needs nothing extra.?world_geometrydocuments the requirement, and the sf & projections vignette no longer demonstrates the scale most readers cannot run.?country_bordersrecommended a graph recipe that produced nonsense.igraph::graph_from_data_frame()treats the first two columns as the edge endpoints, andcountry_borders()returnsiso3c_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 runningFRAto"France"). The documented call now passes only the two code columns.?neighborsand?country_bordersnow warn thatigraphalso exports aneighbors()– taking a graph and a vertex rather than country names – so whichever package is attached later wins.?country_bordersrecommendsigraphfor turning the adjacency into a graph, which walks users straight into the clash, so both pages now say to qualify the call ascountryatlas::neighbors(). It is the only collision between this package’s exports and any ofdplyr,ggplot2,tidyr,tibble,sf,maps,WDI,countrycode,scales,leaflet,plotly,igraph,raster,terra,purrr,stringr,forcats,readror the base packages.?country_overridesnow documents why every name in the override table is plain ASCII: ASCII spellings match in any locale, whereas accented spellings rely oncountrycode’s own matching and resolve toNAunder a non-UTF-8 locale (LC_CTYPE=C). The note points aticonv(x, to = "ASCII//TRANSLIT")for input that may carry accents.The test suite is now green under CRAN’s
noSuggestsconfiguration (_R_CHECK_DEPENDS_ONLY_=true), which runs with every optional package absent. Four tests calledglobe_map(backend = "polygon")ormorans_i()without guarding onmapproj/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 bothglobe_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 xn_binsx backend combination before and after (50 fingerprints, ~2.6M values, all identical).Documented
\valueclaims are now asserted as executable contracts, so Rd prose cannot drift from the code in silence. The 36 exports whose\valuemakes a specific structural promise – named columns, a single row, an attached"model"object, a length matching the input – are covered; the rest return aggplot, 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()andcartogram_map()– are covered, includingcartogram_map(type = "contiguous")(the default type, previously never exercised) andwdi_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 onlymaps+mapprojwhen it also hard-requiresgifskiormagick; theggsqlrow overstated the requirements ofas_ggsql_source()(which never needsggsql) and understated the versioninteractive_map(engine = "ggsql")needs; andlocate_country(),flow_map()andbubble_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 missingsimplify_geometry()andclear_wdi_cache()from its section list, though_pkgdown.ymlhad 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 perworld_tilescell,historical_codesin step with its alias table, andcountry_metacentroids still agreeing withpolygon_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()andfacet_map()gained worked examples, and the vignettes prefercountry_overrides()over the soft-deprecatedwdj_overrides(). The README’s rendered output and figures were stale (pre-dating the quantile-breaks fix and thegdp_per_capita_2015opt-in) and have been re-rendered from the 2.0.0 code.geofacetis dropped fromSuggests: no code ever used it, and?tile_map/ the README claimed ageofacet-backed small-multiples feature that did not exist. Facet atile_map()like any otherggplot, or usefacet_map()for choropleth small multiples.The README’s figures are shipped in the tarball again, so the images on the CRAN package page resolve.
.Rbuildignoreexcluded the generated.pngs but not the (much larger).gif, which left six of the seven images broken.?world_snapshot’s@formatsaid “two elements” while listing three, and advertised ansfelement that isNULLin the released package. It now documents what actually ships and points atattach_geometry()for geometry.?attach_geometrydocuments which countries each geometry backend actually carries. Rows with no matching geometry are dropped silently, and thesfbackend’sscalechanges which countries exist rather than only how detailed they are: of the 215 countries inworld_snapshot, the defaultscale = "small"(110m) maps 169 andscale = "medium"maps 214. Five territories (Gibraltar, Hong Kong, Macao, Tuvalu, the British Virgin Islands) are in no backend at any scale.?world_tilesand?tile_mapsaid “one square per country” without saying how many. The grid is the 239country_metarows that have a bundled centroid, so the 10 without one have no tile andtile_map()silently dropsdatarows keyed on them. Both are now documented, and a test pins the grid to that definition.
countryatlas 1.0.0
CRAN release: 2026-06-24
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.
Breaking-ish changes
-
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 namegdp_per_capita_2015→gdp_per_capita. A one-cycle deprecation shim keepsgdp_per_capita_2015available as an alias (toggle withoptions(countryatlas.gdp_compat = FALSE)). - The 16 regions the previous version silently dropped (Kosovo, Micronesia, the Virgin Islands, Saint Martin, Bonaire/Saba/Sint Eustatius, the Canary Islands, Madeira/Azores, …) are now matched via [
wdj_overrides()] instead of deleted, so they appear on maps. Diffs of map output will show increased coverage.
New: core data assembly
-
world_data()gainsindicator(one or many WDI codes; named vectors drive clean column names), multi-year panels, ansfbackend (geometry = "sf"),regionsubsetting,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).
New: diagnostics
-
check_country_match(),wdj_overrides(),audit_coverage()— never lose a country silently.
New: reference data & translation
-
convert_country()(flags, currency, tld, research codes),country_codes(),country_groups()/in_group(),wdi_search(). - Bundled datasets:
world_snapshot,country_meta,common_indicators,country_groups_tbl,world_tiles.
New: visualization
-
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().
Performance & offline
- WDI fetches are memoised with an optional on-disk cache; multiple indicators are fetched in parallel (
parallel::mclapply) where supported. Seeclear_wdi_cache(). - The bundled
world_snapshotlets every example, test and vignette run offline and deterministically.
Engineering
- Namespace hygiene (targeted
@importFrominstead of blanket@import). - Input validation with friendly
cli/rlangerrors. - A
testthat(3e) suite; network calls are skipped offline and on CRAN. - Vignettes and a
pkgdownsite. - Refreshed CI: R-CMD-check, test-coverage and pkgdown workflows.
- Heavy spatial dependencies (
sf,rnaturalearth,cartogram,biscale,geofacet,gganimate,leaflet, …) are all inSuggestsand gated byrlang::check_installed(), so the base install stays light.
Group memberships in country_groups_tbl are point-in-time as of 2024-01-01.
