Scales and Classification

A choropleth's scale decides which class a value falls into and which color that class gets. It is the single most consequential setting on the map: the same dataset can tell four different stories depending on how it is classed, so ApexMaps always computes the breaks explicitly and prints them in the legend rather than hiding them behind a gradient.

series: [
  {
    name: 'Unemployment rate',
    joinBy: ['iso_a3', 'code'],
    data,
    scale: { type: 'quantile', classes: 5, palette: 'blues' },
  },
]

Which scale type should you use

scale.type picks the classification method. Nine are available, split between classed scales (data sorted into discrete buckets, one color per bucket) and continuous scales (a value maps to a point on a gradient).

TypeKindWhat it does
quantile (default)ClassedBreaks at even population intervals, so every class holds roughly the same number of features
equalIntervalClassedBreaks at even value intervals across the domain
jenks / naturalBreaksClassedFisher-Jenks breaks that minimize within-class variance, finding the groupings the data itself suggests
thresholdClassedBreaks you supply yourself via scale.breaks
quantizeClassedAlias for equalInterval
linearContinuousValue maps directly onto the ramp
logContinuousLogarithmic mapping, for data spanning orders of magnitude
sqrtContinuousSquare-root mapping
ordinalCategoricalOne color per distinct category, for non-numeric data
scale: { type: 'quantile', classes: 5 }
scale: { type: 'jenks', classes: 5 }
scale: { type: 'equalInterval', classes: 5 }
scale: { type: 'threshold', breaks: [5, 25, 100, 400], palette: 'reds' }
scale: { type: 'log' }   // continuous, draws a gradient legend

Quantile is the default because it guarantees every class is populated: with five classes and fifty features, each class gets about ten. That is also its limitation. Quantile flatters skewed data by construction, spreading a long tail of small values across several classes just because the count demands it, which is a trade rather than a free win.

Jenks computes the breaks the data itself suggests, minimizing the variance within each class and maximizing the variance between classes. It costs more to compute (O(n²·k), sampled above 2,000 values, with a warning added to scale.warnings when that happens) but is the honest choice when the data actually clusters.

Equal interval divides the domain into equal-width bands. It is the only method whose legend is arithmetic a reader can do in their head, 0-20, 20-40, 40-60, and it is also the one most likely to leave a class empty on skewed data, since a band's width has nothing to do with how many values fall inside it.

Threshold is for when the breaks are the point: a statutory limit, a target, a zero crossing. You supply scale.breaks directly and nothing is computed. Omitting breaks on a threshold scale falls back to quantile with a warning, since a threshold scale with no thresholds isn't one.

Continuous scales (linear, log, sqrt) skip classing entirely: a value maps to a position on the ramp and the legend renders as a gradient bar instead of discrete swatches. log is for data spanning orders of magnitude (population, GDP); sqrt compresses the top end of a skewed continuous range without discarding precision the way classing does.

Ordinal scales are for categories rather than numbers, one color per distinct value seen in the data, cycling through the palette if there are more categories than colors.

An empty class is a signal, not a style choice

If a classed scale produces a class with zero features in it, ApexMaps adds a warning rather than silently rendering a legend entry nobody's data occupies:

classification "equalInterval" produced an empty class; consider 'quantile', or fewer classes

That most often happens with equalInterval or threshold on skewed data, or with quantile when there are fewer distinct values in the data than classes requested.

Counts versus rates

A choropleth of raw counts is nearly always the map of where the big areas or the big populations are, and almost never what its author meant to show. normalizeBy divides the value by another field before classifying, and the legend retitles itself so a normalized map can't be confused with a raw one:

series: [
  {
    name: 'Cases',
    joinBy: ['iso_a3', 'code'],
    data,                        // [{ code, value, population }]
    normalizeBy: 'population',   // legend becomes "Cases (per population)"
  },
]

Left unnormalized, a large-integer choropleth with no normalizeBy gets a dev-mode advisory, because that shape of data is a raw count nine times out of ten. It's advice, not a block: sometimes a count map is exactly the right one. The bubble series is the other answer to the same problem, since a circle's area is independent of the polygon underneath it.

Automatic diverging scale selection

When no palette is set and the data's domain straddles zero, ApexMaps chooses a diverging ramp (rdbu) instead of a sequential one, anchored at zero:

// domain is [-40, 40], no palette set: diverging chosen automatically
series: [{ name: 'Change', joinBy: 'name', data, scale: { classes: 5 } }]

This exists to prevent the single most common palette mistake: a sequential ramp on signed data. A sequential blue ramp run from -40 to 40 makes -40 and 40 both look like "high blue," and the sign, the entire point of the map, becomes invisible. Naming a palette explicitly always overrides the automatic choice, so scale: { classes: 5, palette: 'blues' } on the same signed data reproduces that failure on purpose if you ever want to see it.

No-data is its own color

An unmatched feature, one with no row in the joined data, never falls into the lowest class. It resolves to scale.nullColor (default a light gray, #eeeeee) with the label scale.nullLabel (default "No data"), and that null-checking happens before any numeric coercion:

scale: { nullColor: '#e5e7eb', nullLabel: 'Not reported' }

This matters because Number(null), Number('') and Number(false) are all 0. Coercing first would file every unmatched feature as a literal zero, so a map with data for 15 of 51 US states would put three of its five quantile breaks at zero and print a legend class reading "0 to 0" for a bucket that has nothing real in it. ApexMaps checks for null explicitly and routes it to the no-data color instead, so a class boundary is never accidentally built out of features that were never measured.

See also

  • Palettes for the ramps scale.palette draws from, and how they're sampled.
  • Legends, Labels & Tooltips for how class breaks and the null swatch render in the legend.
  • Theming for the CSS surface the legend and tooltip use.