Choropleth Maps
A choropleth colors regions by value. ApexMaps needs exactly two things to draw one: which geometry to draw, and which data joins to it.
import ApexMaps from 'apexmaps'
const map = new ApexMaps(document.querySelector('#map'), {
geo: { map: 'world/countries@110m' },
series: [
{
name: 'Unemployment rate',
joinBy: ['iso_a3', 'code'],
data: [
{ code: 'FRA', value: 7.3 },
{ code: 'DEU', value: 5.7 },
],
},
],
})
await map.render()
That's the whole configuration: no geometry to source, host or parse, and no projection, palette, classification, legend or tooltip to set up. Everything below is a default chosen to be publishable on its own, not just functional, so you only need to touch it when your data actually calls for something different.
What the defaults decide
| Decision | Default | Why |
|---|---|---|
| Projection | equalEarth | Web Mercator inflates high-latitude area by an order of magnitude, which is exactly wrong on a map where area sits next to a value-colored region |
| Classification | quantile, 5 classes | Every class ends up populated, and the legend shows the actual break values, because classification silently changes a choropleth's conclusion |
| Ramp | OkLab-sampled | Interpolating through sRGB turns ramp midpoints muddy grey; sampling in OkLab keeps perceived lightness monotonic, which is how a reader infers magnitude |
| No-data color | its own swatch | An unmatched feature never falls into the lowest class, which would understate it |
| Stroke | thin white line | Separates adjacent same-class regions without competing with the fill |
Override any of them through scale:
series: [
{
name: 'Unemployment rate',
joinBy: ['iso_a3', 'code'],
data,
scale: {
type: 'quantile', // quantile, quantize, equalInterval, jenks, naturalBreaks, threshold, linear, log, sqrt, ordinal
classes: 7,
palette: 'blues', // a registered palette name, or a literal array of colors
nullColor: '#eeeeee',
nullLabel: 'No data',
},
},
]
type: 'threshold' takes an explicit breaks array instead of a computed classes count, and domain fixes the input range yourself rather than letting it come from the data.
Joining data to geometry
joinBy tells ApexMaps which geometry property to match against which field on your rows. It accepts three equivalent shapes:
joinBy: 'name' // same property name on both sides
joinBy: ['iso_a3', 'code'] // [geometryProperty, dataKey]
joinBy: { geo: 'iso_a3', data: 'code' } // the same thing, spelled out
Matching stays exact by default, because silently guessing turns an obvious join failure into a plausible wrong answer. A data row that matches nothing, or a feature nothing matches, shows up in the dev-mode join diagnostics rather than rendering as silent grey, down to a "did you mean" suggestion for near-misses like "Ivory Coast" against a pack that spells it "Côte d'Ivoire". Set fuzzyJoin: true on the series to apply those suggestions automatically instead of fixing the data by hand.
Counts versus rates: normalizeBy
A choropleth of raw counts mostly redraws the map of where people already live: whichever country or county has the largest population wins nearly every category, and the map ends up saying nothing else. normalizeBy divides value by another field on the same row before classifying, which is what turns a population map back into an actual finding:
series: [
{
name: 'Cases',
joinBy: ['iso_a3', 'code'],
data, // [{ code, value, population }, ...]
normalizeBy: 'population', // legend retitles to "Cases (per population)"
},
]
The legend retitles itself whenever normalizeBy is set, so a rate map and a count map can never be mistaken for one another at a glance. If your values are large integers with no normalizeBy at all, dev-mode diagnostics note it, not as an error since a count map is sometimes exactly the right chart, but because that shape of data is a raw count nine times out of ten. When magnitude itself is what you want to be legible, rather than a rate, a bubble series is usually the better fit: a circle's area doesn't inherit the size of the polygon underneath it.
Stroke styling
Feature borders are styled with stroke on the series:
series: [
{
name: 'Unemployment rate',
joinBy: ['iso_a3', 'code'],
data,
stroke: {
color: '#ffffff',
width: 0.5,
opacity: 1,
dashArray: '4 2', // any SVG dash pattern
},
},
]
width and opacity matter more than they look: a stroke drawn too heavy at the world scale reads as its own layer of information once the classes get close in color, especially with a sequential ramp on a low classes count.