Who Draws Crimea Russian? A Census of Every JS Map Library
We point-in-polygon tested the world geometry actually shipped by every JavaScript web mapping and charting library (Simferopol + Sevastopol, with Kyiv/Moscow calibration controls), and traced each back to its data source. Downloads are npm weekly figures as of 2026-08-05.
The propagation chart: data source → library → weekly reach
Ribbon width = npm weekly downloads. Color = what Crimea renders as. Dashed ribbons = the library bundles no geometry, but its canonical examples fetch that source ("recipe-level"). Hover for evidence.
The census table
| Library | npm / week | Crimea renders as | Evidence |
|---|---|---|---|
| d3 + d3-geo(recipe) | 17.4M | Crimea → Russia (via canonical recipes) | No bundled geometry, but the canonical D3 choropleth recipe fetches world-atlas. d3-geo alone: 26.0M/wk. |
| leaflet + react-leaflet | 10.3M | Depends on data the developer supplies | No bundled polygons. Choropleth overlays pull whatever GeoJSON the tutorial uses: datahub geo-countries (Russia) or johan (Ukraine) — a coin flip. |
| topojson-client(recipe) | 5.4M | Crimea → Russia (via canonical recipes) | world-atlas companion decoder in every D3 map tutorial. |
| echarts | 4.5M | Crimea → Ukraine | The de-facto standard world.json (echarts@4.9) predates 2014 — verified Ukraine. v5 unbundled maps. |
| highcharts | 2.5M | Crimea → no one (carve-out) | Deliberate policy: world map contains no polygon for Crimea; paired ru-all / ru-all-disputed variants push the choice to the developer. |
| vega + vega-lite(recipe) | 1.5M | Crimea → Ukraine (via canonical recipes) | vega-datasets world-110m is a pre-2014 NE snapshot — verified Ukraine. |
| plotly.js | 1.1M | npm → Ukraine · CDN default → Russia | v3.1 (2025) switched bundled maps to UN geodata — verified Ukraine. BUT cdn.plot.ly default still serves the legacy Natural Earth map — verified Russia. Migration issue #7505 open since Aug 2025. Count = plotly.js + plotly.js-dist-min. |
| react-simple-maps(recipe) | 921K | Crimea → Russia (via canonical recipes) | Official docs point at world-atlas CDN TopoJSON. |
| ol (OpenLayers) | 790K | Depends on data the developer supplies | No bundled polygons. |
| @observablehq/plot(recipe) | 602K | Crimea → Russia (via canonical recipes) | Geo mark examples use world-atlas. |
| world-atlas | 546K | Crimea → Russia | Repackaged NE. countries-110m and countries-50m both verified: Crimean cities inside Russia polygon. |
| @amcharts/amcharts5 | 336K | Crimea → Ukraine | worldLow verified Ukraine; no Russia-worldview variant exists. |
| datamaps | 262K | Crimea → Ukraine | Embedded world topology frozen ~2013 — verified Ukraine (calibrated). Unmaintained. |
| jsvectormap | 67K | Crimea → Ukraine | jvectormap-lineage Miller paths — Simferopol verified in UA path. |
| fusioncharts | 49K | Not tested (proprietary format) | Proprietary webpack bundle; geometry not extracted — flagged, not guessed. |
| chartjs-chart-geo(recipe) | 46K | Crimea → Russia (via canonical recipes) | Docs fetch world-atlas from unpkg. Host library chart.js: 12.8M/wk. |
| anychart | 9K | Crimea → Ukraine | CDN geodata world.json verified Ukraine (calibrated mill+affine). |
| datahub geo-countries (dataset) | — | Crimea → Russia | NE-derived GeoJSON used across Leaflet/folium tutorials — verified Russia. |
| johan/world.geo.json (dataset) | — | Crimea → Ukraine | Pre-2014 tutorial staple — verified Ukraine. |
Methodology: point-in-polygon of Simferopol (34.10°E, 44.95°N) and Sevastopol (33.52°E, 44.60°N) against each library's shipped or CDN-served world geometry, decoded from TopoJSON, GeoJSON, Miller-projected SVG paths, or proj4-transformed coordinates; results accepted only when Kyiv→Ukraine and Moscow→Russia calibration passed. FusionCharts flagged untested (proprietary bundle), not guessed.
The plotly split brain
plotly switched its bundled maps to UN geodata in 2025 (v3.1) — the only vendor that moved to a de jure
source. But cdn.plot.ly/world_110m.json, the CDN default, still serves the legacy Natural
Earth map with Crimea in Russia (verified 2026-08-05). The migration issue
(#7505)
has been open since August 2025. Fixes that don't reach defaults don't fix anything.
Correct by accident
≈5.1M weekly downloads (echarts, datamaps, jsvectormap, vega) ship Crimea-in-Ukraine geometry only because their world maps predate 2014 and nobody updated them. Correctness in the JS map ecosystem is mostly inertia — in both directions.
The recipe-level giants
d3 (17.4M/wk), topojson-client (5.4M/wk), react-simple-maps and Observable Plot ship no geometry — but their canonical examples all fetch world-atlas, the NE repackage. This is exactly the pathway code-generating AI reproduces: the model writes the tutorial it was trained on.
Every strategy exists; the root refuses all of them
UN source (plotly), neutral carve-out (Highcharts), corrected custom data (amCharts, anychart) — the downstream ecosystem has demonstrated three viable fixes. Natural Earth, the root feeding D3, world-atlas and the AI training corpora, ships the pro-Russia default and keeps the alternatives in opt-in files. See the Natural Earth consistency audit.