Legends, Labels & Tooltips

A choropleth is decoded through its legend, not guessed at. ApexMaps renders the legend from the same computed scale that colored the map, so what a reader sees is always the real breaks, never a decorative gradient standing in for them.

legend: { position: 'bottom', interactive: true },
tooltip: { formatter: ({ name, value }) => `${name}: ${value}` },

Legend types

legend.style picks how a section renders: 'classes', 'gradient', or 'auto' (the default, which chooses gradient for a continuous scale and classes for everything else). 'gradient' is available for any classed scale that is not ordinal, drawing a bar of hard bands, where a smooth ramp would imply an order categories do not have.

StyleUsed forWhat it shows
classesClassed scales (quantile, jenks, equalInterval, threshold, ordinal)One swatch per class, with its computed range as the label
gradientContinuous scales (linear, log, sqrt)A gradient bar sampled from the ramp, with the domain's low and high labeled
Nested circlesBubble seriesThree reference circles at round values, sharing a baseline

Bubble series get the nested-circle legend automatically whenever size is set, it isn't something you opt into with legend.style. Three concentric circles, drawn to scale and sharing a bottom edge so their diameters line up, is the only bubble legend a reader can actually decode by eye: a single reference circle or a swatch tells you nothing about area.

legend: {
  show: true,
  position: 'bottom',       // 'bottom' | 'top' | 'left' | 'right'
  align: 'center',          // 'start' | 'center' | 'end'
  title: 'Unemployment rate',
  showNull: true,           // include the no-data swatch
}

The no-data swatch, when any feature in the series is unmatched, is always the last item in a classed legend, so its position never shifts as classes change. Set legend.showNull: false to drop it.

Interactive legend: muting a class

legend.interactive (default true) turns each classed legend swatch into a button. Clicking one mutes every feature in that class, dimming it to states.muted.opacity, which is the cheapest useful exploration a reader gets on a choropleth: isolate the top class, or hide the no-data features, with no code.

legend: { interactive: true },
states: { muted: { opacity: 0.25 } },

Muting is per class per series (legendToggle fires with { classIndex, instance }), and it's independent of feature selection: a legend mute and a box-selected set can dim the map at the same time without fighting each other.

A legend.formatter(item, index) overrides the label text for a class, receiving the same LegendItem (color, label, from, to, count, isNull) the default renderer uses:

legend: {
  formatter: (item, i) => (item.isNull ? 'Not reported' : `${item.from} to ${item.to}%`),
}

A side column, a gradient bar, and a hover marker

legend: {
  position: 'right',        // 'left' | 'right' make it a column
  width: 180,               // column width, default 180
  style: 'gradient',
  tickFormatter: (v) => `${v}%`,
  marker: true,             // or { show, label }
}
  • position: 'left' | 'right' measures the plot against what is left of the container, turns the gradient bar vertical, and flips the hover marker to point sideways into it. width (default 180) is the column width.
  • Hovering a feature runs an arrow along the gradient bar to where that feature falls on the scale, so the reader gets the value-to-color mapping read forwards instead of matching colors by eye. A feature with no value parks the arrow rather than pointing at zero, which would be a lie about missing data. marker: { label: false } keeps the arrow and drops the value riding above it.
  • Gradient tick numbers sit at the break they describe rather than inside a band, and tickFormatter formats them.

Tooltips

Tooltips render as an HTML overlay rather than SVG text, which gets you real text wrapping and real CSS instead of whatever a <text> element can do. The default content is the feature or mark's name, its value (or the scale's nullLabel when unmatched), and up to three of the datum's other numeric or string fields, useful for catching a bad join without opening the console.

tooltip: {
  enabled: true,
  followCursor: true,
  formatter: ({ name, value, datum, series }) => `<strong>${name}</strong>: ${value ?? 'no data'}`,
  valueFormatter: (value) => `${value.toFixed(1)}%`,
  offset: [12, 12],
}

tooltip.formatter returns raw HTML and is responsible for its own escaping; the default renderer escapes everything it inserts. It receives a TooltipContext: key, name, value, datum, properties, and series.

Edge flipping

A tooltip positioned at a fixed offset from the cursor will slide off the map when the cursor is near an edge, at which point it either gets clipped or (worse) covers the exact feature the reader is pointing at. ApexMaps flips the tooltip to the opposite side of the cursor instead of clamping it to the container edge, so it's never covering what it describes: the same feature, the same cursor position, but the tooltip renders left-and-above of the point instead of right-and-below once its normal position would overflow the container's right or bottom edge.

Data labels and collision avoidance

dataLabels prints text directly on features, sized and positioned by the same world-space label anchor the accessibility layer uses:

dataLabels: {
  enabled: true,
  collision: 'hide',      // 'hide' | 'none'
  minFeatureArea: 240,     // skip labels smaller than this many square pixels
  style: { fontSize: 11, fontWeight: 500, halo: true, haloColor: 'rgba(255,255,255,0.85)' },
}

Placement follows a fixed order of priorities:

  1. Priority ordering. Larger features and higher-magnitude values are placed first, so when labels have to be dropped, the drop is graceful rather than arbitrary.
  2. Collision avoidance. collision: 'hide' (the default) tests each candidate's screen-space box against every label already placed and drops it on overlap; collision: 'none' draws every label regardless of overlap.
  3. Area gating. minFeatureArea culls labels for features too small on screen to hold text at all, scaled by the current zoom (), so a country too small to label at the initial fit can pick up its label once the reader zooms in.
  4. Halos by default. style.halo (default true) draws the label with a stroke behind its fill, so text stays legible against whatever class color happens to be underneath it, light or dark, without a fixed text color ever being wrong for one end of a ramp.

Labels live in the screen-space overlay, not the projected world group, specifically so text never scales with the camera: zooming in enlarges the map without turning every label into oversized type.

Editorial annotations (a licensed feature) take priority over generated labels rather than the reverse: a label is produced by a rule and yields, an annotation was placed on purpose and doesn't.

See also

  • Scales and Classification for how the breaks a classed legend displays are actually computed.
  • Palettes for the color ramps the legend renders as swatches or a gradient.
  • Theming for the CSS custom properties that style the legend, tooltip and labels.