← crimeaisukraine.org

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.

24.9M/wk
downloads in the Natural Earth default family (Crimea → Russia)
6.3M/wk
ship geometry with Crimea → Ukraine (override or pre-2014 freeze)
2.5M/wk
deliberate neutral carve-out (Highcharts: Crimea → no one)
11.1M/wk
data-agnostic (tiles / user-supplied GeoJSON)

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.

Crimea → Russia Crimea → Ukraine Split (npm ✓ / CDN ✗) Neither (carve-out) Depends / untested

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.