geomorpher

geo-morpher API Reference

This document expands on the README by describing each public export in depth, outlining the lifecycle needed to build your own morphing experience, and highlighting practical considerations when integrating GeoMorpher into custom projects.


Implementation overview and workflow

  1. Collect aligned geodata: obtain two GeoJSON FeatureCollections representing the same places—one “regular” geography and one cartogram. Ensure every feature contains a unique identifier at feature.properties[geoJSONJoinColumn].
  2. Prepare your metrics: load tabular data keyed by the same identifier. Use the data array for preloaded rows, or supply getData when you need to fetch lazily.
  3. Instantiate GeoMorpher with the appropriate options (projection, aggregations, normalisation). See the detailed section below.
  4. Await morpher.prepare() once during application start-up. This normalises the cartogram, enriches properties, computes centroids, and builds ring interpolators.
  5. Render baseline layers using getRegularFeatureCollection() and getCartogramFeatureCollection() or defer to an adapter to create them on the map for you.
  6. Drive morphing by calling getInterpolatedFeatureCollection(factor) directly or by invoking an adapter controller’s updateMorphFactor method from your UI.
  7. Attach glyphs or annotations with the glyph helpers. They use the same morph factor to remain aligned with geometry.

Core API

class GeoMorpher

import { GeoMorpher } from "geo-morpher";

const morpher = new GeoMorpher({
  regularGeoJSON,
  cartogramGeoJSON,
  data,
  getData,
  joinColumn: "lsoa",
  geoJSONJoinColumn: "code",
  aggregations: { population: "sum" },
  normalize: true,
  projection: WGS84Projection,
  cartogramGridOptions: {},
});

await morpher.prepare();

Constructor options

Lifecycle

Data accessors

Implementation notes

Data enrichment details

The enrichment pipeline inside utils/enrichment.js resolves joins using joinColumngeoJSONJoinColumn. Aggregations run per feature; for example:

aggregations: {
  population: "sum",
  households: "sum",
  density: (values, feature) => values.reduce((acc, val) => acc + val, 0) / feature.area,
}

Custom aggregation functions receive the list of numeric values for a feature and the enriched GeoJSON feature. Use this hook for domain-specific computations. If you disable normalize, raw sums are preserved. When normalisation is enabled, aggregated values are adjusted by feature area—which is useful for density-driven cartograms but may not fit every dataset.

Error handling

Legacy wrapper

geoMorpher(options) is a convenience async function retained for the Observable notebook workflow. It simply instantiates GeoMorpher, awaits prepare(), and resolves to:

{
  morpher,
  keyData,
  regularGeodataLookup,
  regularGeodataWgs84,
  cartogramGeodataLookup,
  cartogramGeodataWgs84,
  tweenLookup,
}

New projects should prefer new GeoMorpher(...) for clearer lifecycle control.


Adapter APIs

Adapters translate GeoMorpher outputs into the mapping primitives expected by MapLibre and Leaflet. Each adapter is asynchronous and will trigger morpher.prepare() if it has not yet run.

MapLibre – createMapLibreMorphLayers(params): Promise<Controller>

Purpose:

Parameters:

Controller surface:

Basemap effect schema:

Keep in mind: initialise this adapter after the MapLibre map emits load so that source/layer creation succeeds.

MapLibre – createMapLibreGlyphLayer(params): Promise<GlyphController>

Purpose:

Tips:

Leaflet – createLeafletMorphLayers(params)

Purpose:

Parameters:

Return value:

{
  group,
  regularLayer,
  cartogramLayer,
  tweenLayer,
  updateMorphFactor,
}

updateMorphFactor(next) replaces features inside the tween layer and reapplies the basemap effect with the new factor.

Leaflet – createLeafletGlyphLayer(params)

Addendum — featureProvider / featureCollection

const glyphController = await createMapLibreGlyphLayer({
  drawGlyph: ({ feature, featureId }) => ({ html: `<div>${featureId}</div>` }),
  featureProvider: ({ geometry, morphFactor }) => someCustomProvider(geometry, morphFactor),
  map,
});

featureCollection is a static GeoJSON FeatureCollection passed directly to the glyph controller at creation time. It allows purely client-side glyph rendering without a morpher.


Utility exports (src/index.js)

Adapter helper exports


Integration patterns

MapLibre with vanilla controls

import maplibregl from "maplibre-gl";
import {
  GeoMorpher,
  createMapLibreMorphLayers,
  createMapLibreGlyphLayer,
} from "geo-morpher";

const morpher = new GeoMorpher({ regularGeoJSON, cartogramGeoJSON, data, aggregations });
await morpher.prepare();

const map = new maplibregl.Map({
  container: "map",
  style: "https://demotiles.maplibre.org/style.json",
  center: [-1.2577, 51.752],
  zoom: 11,
});

await map.once("load");

const morphController = await createMapLibreMorphLayers({
  morpher,
  map,
  basemapEffect: {
    layers: ["basemap"],
    properties: { "raster-opacity": [1, 0.15] },
    easing: (t) => t * t,
  },
});

const glyphController = await createMapLibreGlyphLayer({
  morpher,
  map,
  geometry: "interpolated",
  drawGlyph: ({ data }) => ({
    html: `<div class="metric">${Math.round(data?.population ?? 0)}</div>`,
    className: "geomorpher-glyph",
    iconSize: [48, 48],
    iconAnchor: [24, 24],
  }),
});

const slider = document.querySelector("#morph-slider");
slider.addEventListener("input", (event) => {
  const factor = Number(event.target.value) / 100;
  morphController.updateMorphFactor(factor);
  glyphController.updateGlyphs({ morphFactor: factor });
});

// Decoupled provider example – render glyphs from a provider function instead
const providerController = await createMapLibreGlyphLayer({
  map,
  drawGlyph: ({ feature }) => ({ html: `<div>${feature.properties.code}</div>` }),
  featureProvider: ({ geometry, morphFactor }) => morpher.getInterpolatedFeatureCollection(morphFactor),
});
slider.addEventListener("input", (event) => {
  const factor = Number(event.target.value) / 100;
  morphController.updateMorphFactor(factor);
  providerController.updateGlyphs({ morphFactor: factor });
});

React + Leaflet


Testing and validation


Troubleshooting checklist


Glossary

Consult docs/glyphs.md for a deeper dive into glyph authoring techniques and performance guidance. For roadmap updates and adapter caveats, see the README status section and the project issue tracker.