Joins

Roughly nine in ten real-world map failures are join failures: "Ivory Coast" against a geometry file that spells it "Côte d'Ivoire", a FIPS code that lost its leading zero in a spreadsheet, ISO-2 against ISO-3. Most libraries render the unmatched features in silent no-data grey. ApexMaps reports what actually happened.

joinBy

joinBy tells a series which geometry property to match against which data field. It accepts three forms:

joinBy: 'name'                          // same field name on both sides
joinBy: ['iso_a3', 'code']              // [geometryProperty, dataKey]
joinBy: { geo: 'iso_a3', data: 'code' } // equivalent, spelled out

Bubble, marker, and arc series accept the same joinBy shapes to resolve positions from geometry centroids or endpoints when the data itself carries no coordinates.

Automatic join-key detection

Leave joinBy off entirely and ApexMaps guesses both sides.

On the geometry side, each registry pack states its own recommended key (abbr for us/states, nuts_id for the EU packs, iso_3166_2 for admin-1 packs), and that recommendation wins over generic detection. See Geometry Registry for the full list. Generic detection matters for geometry you pass directly: an admin-1 feature carries a country-level code alongside its own, and picking the wrong one would give every region in a country the same key.

On the data side, the first row is scanned for a field matching one of these candidates, in order:

id, key, code, iso, iso_a3, iso3, iso_a2, iso2, fips, geoid, hc-key, region, state, country, name

The first candidate present, with a non-empty value, wins. If none match, ApexMaps falls back to the first string-valued field, which is nearly always the label.

Auto-detection is a starting point, not a guarantee: whichever field it settled on is reported back through map.diagnoseJoin(), so you can see what it picked without guessing.

The join diagnostic

In development, ApexMaps prints what actually happened, rather than leaving a grey map to explain itself:

join: 3/6 data rows matched 3/177 features (geometry key "name", data key "name")
  3 data row(s) did not match geometry:
    "Ivory Coast" -> did you mean "Côte d'Ivoire"?
    "United States" -> did you mean "United States of America"?
    "Democratic Republic of the Congo" -> did you mean "Dem. Rep. Congo"?
  174 feature(s) had no data (rendered as no-data): Fiji, Tanzania, W. Sahara, ...

Suggestions come from a curated alias table for the country-name variants that actually show up in published datasets, plus edit distance as a fallback. This runs automatically when debug.enabled is 'auto' (the default, active on localhost) and debug.joinDiagnostics is true (also the default).

Call it yourself for a structured result instead of console text:

const result = map.diagnoseJoin()
// {
//   matched, totalData, totalFeatures,
//   unmatchedData: [{ key, row, suggestions }],
//   unmatchedFeatures: [{ key, name }],
//   sharedKeys: [{ key, count, names }],
//   applied: string[],
//   geoKeyField, dataKeyField,
//   report: () => string,
// }

result.report() returns the same multi-line text shown above. Pass a series index (map.diagnoseJoin(1)) to inspect a series other than the first.

One key, several features

A key can map to more than one feature, and the diagnostic says so rather than silently keeping only the last match. Natural Earth gives Australia, the Indian Ocean Territories, and Ashmore and Cartier Islands the same iso_a3, and Lord Howe Island carries AU-NSW alongside New South Wales. One data row legitimately colors all of them; result.sharedKeys lists every key held by more than one feature so a count that looks off by a few polygons has an explanation.

FIPS leading-zero repair

US County FIPS codes are five digits and keep a leading zero ("06037" for Alameda County). A numeric key that lost it in a spreadsheet round-trip, 1001 instead of "01001", is repaired automatically: ApexMaps detects when every geometry key in a pack is a fixed-width numeric string with at least one leading zero, then pads a shorter numeric data key out to match.

This repair applies even with fuzzyJoin left off, because it is lossless and unambiguous, unlike a name-similarity guess. Every repair it makes is reported in result.applied.

fuzzyJoin

Matching is exact by default. Silently guessing turns an obvious failure into a plausible wrong answer, which is worse: nobody double-checks a map that looks fine. Pass fuzzyJoin: true on a series to opt in to normalized and alias matching:

series: [
  {
    name: 'Coverage',
    joinBy: 'name',
    fuzzyJoin: true,
    data: [
      { name: 'Ivory Coast', value: 33 },
      { name: 'United States', value: 88 },
    ],
  },
]

With fuzzyJoin: true, a key that fails an exact match is retried against a normalized form (diacritics stripped, lowercased, punctuation removed) and against the same curated alias table the diagnostic's suggestions come from. Every substitution it makes is still reported, in result.applied, so the convenience stays auditable rather than silent.

map.updateSeries([{ name: 'Coverage', joinBy: 'name', fuzzyJoin: true, data: messyData }])
const result = map.diagnoseJoin()
console.log(result.applied)
// ["matched \"Ivory Coast\" to \"Côte d'Ivoire\"", ...]

fuzzyJoin is a free feature in every license tier. Cleaning up someone else's data is not a premium experience, it is the cost of using real data.