geo-morpher 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +76 -439
  2. package/data/winchester/winchester_lsoa_cartogram.geojson +3245 -0
  3. package/data/winchester/winchester_lsoa_geo.geojson +24263 -0
  4. package/data/winchester/winchester_msoa_cartogram.geojson +635 -0
  5. package/data/winchester/winchester_msoa_geo.geojson +10743 -0
  6. package/data/winchester/winchester_ward_cartogram.geojson +725 -0
  7. package/data/winchester/winchester_ward_geo.geojson +10087 -0
  8. package/examples/README.md +2 -0
  9. package/examples/indonesia/index.html +364 -0
  10. package/examples/indonesia/main.js +770 -0
  11. package/examples/leaflet/canvas-glyphs.html +44 -0
  12. package/examples/leaflet/canvas-main.js +208 -0
  13. package/examples/leaflet/index.html +13 -10
  14. package/examples/leaflet/main.js +83 -20
  15. package/examples/leaflet/zoom-scaling-glyphs.html +1 -0
  16. package/examples/maplibre/index.html +3 -3
  17. package/examples/maplibre/indonesia/main.js +469 -136
  18. package/examples/maplibre/main.js +3 -21
  19. package/examples/winchester/index.html +294 -0
  20. package/examples/winchester/main.js +333 -0
  21. package/examples/winchester/winchester_example.md +145 -0
  22. package/package.json +3 -4
  23. package/src/adapters/leaflet/glyphLayer.js +23 -9
  24. package/src/adapters/leaflet/index.js +3 -1
  25. package/src/adapters/leaflet/utils/collections.js +3 -37
  26. package/src/adapters/leaflet/utils/coordinates.js +1 -5
  27. package/src/adapters/leaflet/utils/glyphNormalizer.js +19 -74
  28. package/src/adapters/maplibre/glyphLayer.js +21 -10
  29. package/src/adapters/maplibre/index.js +2 -0
  30. package/src/adapters/maplibre/morphLayers.js +6 -5
  31. package/src/adapters/maplibre/utils/coordinates.js +2 -26
  32. package/src/adapters/maplibre/utils/customGlyphLayer.js +485 -0
  33. package/src/adapters/maplibre/utils/glyphNormalizer.js +14 -117
  34. package/src/adapters/shared/collections.js +27 -0
  35. package/src/adapters/shared/dom.js +6 -0
  36. package/src/adapters/shared/geometry.js +26 -0
  37. package/src/adapters/shared/glyphNormalizer.js +103 -0
  38. package/src/adapters/shared/markerAdapter.js +64 -0
  39. package/src/core/geomorpher.js +95 -8
  40. package/src/index.js +6 -2
  41. package/src/utils/projections.js +34 -16
  42. package/examples/maplibre/indonesia/index.html +0 -264
  43. package/morphs.js +0 -1
package/README.md CHANGED
@@ -3,15 +3,20 @@
3
3
  [![npm version](https://badge.fury.io/js/geo-morpher.svg)](https://badge.fury.io/js/geo-morpher)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- GeoJSON morphing utilities for animating between regular geography and cartograms, packaged as a native JavaScript library with a MapLibre-first adapter and Leaflet compatibility helpers.
6
+ GeoJSON morphing utilities for animating between regular geography and cartograms, with first-class **MapLibre GL JS** support. Smoothly interpolate between any two aligned GeoJSON geometries and overlay multivariate glyphs that stay in sync.
7
7
 
8
8
  ![](demo.gif)
9
9
 
10
+ > [!TIP]
11
+ > To quickly create a grid cartogram, check out ![gridmapper](https://danylaksono.is-a.dev/gridmapper/).
10
12
 
11
- ## Status
12
-
13
- This library is currently in early-stage development (v0.1.0) and has recently completed a migration to a MapLibre-first adapter. The core morphing engine is stable, but the MapLibre adapter is new and under active development. Leaflet compatibility is maintained. Community feedback and contributions are welcome.
13
+ ## Features
14
14
 
15
+ - **MapLibre-first**: High-performance adapter for modern vector maps.
16
+ - **Smooth Morphing**: Seamlessly interpolate between any two aligned GeoJSON geometries (using `flubber`).
17
+ - **Multivariate Glyphs**: Position-synced DOM overlays for charts, icons, and sparklines.
18
+ - **Basemap Effects**: Synchronized fading and styling of basemaps during transitions.
19
+ - **Projection Agnostic**: Automatic support for WGS84, OSGB, and custom projections.
15
20
 
16
21
  ## Installation
17
22
 
@@ -19,479 +24,111 @@ This library is currently in early-stage development (v0.1.0) and has recently c
19
24
  npm install geo-morpher
20
25
  ```
21
26
 
22
- Bring your own MapLibre or Leaflet instance (both listed as peer dependencies). MapLibre is the default adapter; Leaflet remains supported for backward compatibility.
23
-
24
- ## Usage
25
-
26
- Project structure highlights:
27
-
28
- ```text
29
- src/
30
- core/ # GeoMorpher core engine
31
- adapters/ # Integration helpers (Leaflet, etc.)
32
- lib/ # Shared runtime utilities (OSGB projection)
33
- utils/ # Data enrichment and projection helpers
34
- data/ # Sample Oxford LSOA datasets
35
- examples/ # Runnable browser demos (MapLibre & Leaflet)
36
- test/ # node:test coverage for core behaviours
37
- ```
38
-
39
- ### MapLibre adapter (default)
40
-
41
- - `createMapLibreMorphLayers` provisions GeoJSON sources and fill layers for regular, cartogram, and interpolated geometries, exposing an `updateMorphFactor` helper to drive tweening from UI controls.
42
- - `createMapLibreGlyphLayer` renders glyphs with `maplibregl.Marker` instances; enable `scaleWithZoom` to regenerate glyph markup as users zoom.
43
- - Pass your MapLibre namespace explicitly (`maplibreNamespace: maplibregl`) when calling glyph helpers in module-bundled builds where `maplibregl` is not attached to `globalThis`.
44
- - For heavy glyph scenes, consider upgrading to a [CustomLayerInterface](https://www.maplibre.org/maplibre-gl-js/docs/API/interfaces/CustomLayerInterface/) implementation that batches drawing on the GPU. The marker pipeline keeps the API simple while offering a documented migration path.
45
- - Track ongoing enhancements and open items in `docs/maplibre-migration-plan.md` before relying on the adapter in production.
27
+ ## Quick Start (MapLibre)
46
28
 
47
- #### MapLibre basemap effects
48
-
49
- `createMapLibreMorphLayers` accepts a `basemapEffect` configuration that interpolates paint properties on existing style layers as the morph factor changes. This mirrors the Leaflet DOM-based blur/fade behaviour while staying inside the MapLibre style system.
50
-
51
- ```js
52
- const morph = await createMapLibreMorphLayers({
53
- morpher,
54
- map,
55
- basemapEffect: {
56
- layers: ["basemap", "basemap-labels"],
57
- properties: {
58
- "raster-opacity": [1, 0.15],
59
- "raster-brightness-max": { from: 1, to: 1.4 },
60
- },
61
- propertyClamp: {
62
- "raster-brightness-max": [0, 2],
63
- },
64
- easing: (t) => t * t, // optional easing curve
65
- },
66
- });
67
-
68
- // Update morph factor, basemap effect adjusts automatically
69
- morph.updateMorphFactor(0.75);
70
-
71
- // Apply effect manually (e.g., when animating via requestAnimationFrame)
72
- morph.applyBasemapEffect(0.5);
73
- ```
74
-
75
- - Provide a `layers` string/array or resolver function to target paint properties across multiple layers.
76
- - Supply ranges (`[from, to]` or `{ from, to }`) for numeric properties such as `raster-opacity`, `fill-opacity`, or `line-opacity`.
77
- - Use functions in `properties[layerId]` for full control or to manipulate non-numeric paint values.
78
- - Capture-and-reset logic ensures properties revert to their original values when the effect is disabled.
79
- - Canvas-style blur is not built-in; use a custom MapLibre layer if a true blur shader is required.
80
-
81
-
82
- ### 1. Prepare morphing data
83
-
84
- ```js
85
- import { GeoMorpher } from "geo-morpher";
86
- import regularGeoJSON from "./data/oxford_lsoas_regular.json" assert { type: "json" };
87
- import cartogramGeoJSON from "./data/oxford_lsoas_cartogram.json" assert { type: "json" };
29
+ ```javascript
30
+ import maplibregl from "maplibre-gl";
31
+ import { GeoMorpher, createMapLibreMorphLayers } from "geo-morpher";
88
32
 
33
+ // 1. Prepare data (regular vs cartogram geography)
89
34
  const morpher = new GeoMorpher({
90
- regularGeoJSON,
91
- cartogramGeoJSON,
92
- data: await fetchModelData(),
93
- aggregations: {
94
- population: "sum",
95
- households: "sum",
96
- },
35
+ regularGeoJSON: await (await fetch('regular_lsoa.json')).json(),
36
+ cartogramGeoJSON: await (await fetch('cartogram_lsoa.json')).json(),
97
37
  });
98
-
99
38
  await morpher.prepare();
100
39
 
101
- const regular = morpher.getRegularFeatureCollection();
102
- const cartogram = morpher.getCartogramFeatureCollection();
103
- const tween = morpher.getInterpolatedFeatureCollection(0.5);
104
- ```
105
-
106
- #### Using custom projections
107
-
108
- By default, `GeoMorpher` assumes input data is in **OSGB** (British National Grid) and converts to WGS84 for Leaflet. If your data is in a different coordinate system, pass a custom projection:
109
-
110
- ```js
111
- import { GeoMorpher, WGS84Projection, isLikelyWGS84 } from "geo-morpher";
40
+ // 2. Initialize MapLibre
41
+ const map = new maplibregl.Map({ ... });
112
42
 
113
- // Auto-detect coordinate system
114
- const detectedProjection = isLikelyWGS84(regularGeoJSON);
115
- console.log("Detected:", detectedProjection); // 'WGS84', 'OSGB', or 'UNKNOWN'
116
-
117
- // For data already in WGS84 (lat/lng)
118
- const morpher = new GeoMorpher({
119
- regularGeoJSON,
120
- cartogramGeoJSON,
121
- projection: WGS84Projection, // No transformation needed
122
- });
123
-
124
- // For Web Mercator data
125
- import { WebMercatorProjection } from "geo-morpher";
126
- const morpher = new GeoMorpher({
127
- regularGeoJSON,
128
- cartogramGeoJSON,
129
- projection: WebMercatorProjection,
130
- });
131
-
132
- // Custom projection (e.g., using proj4)
133
- const customProjection = {
134
- toGeo: ([x, y]) => {
135
- // Transform [x, y] to [lng, lat]
136
- return [lng, lat];
137
- }
138
- };
139
- ```
140
-
141
- See `examples/maplibre/projections/index.html` for a browser-based custom projection demo.
142
-
143
- ### 2. Drop the morph straight into Leaflet (compat)
144
-
145
- ```js
146
- import L from "leaflet";
147
- import { createLeafletMorphLayers } from "geo-morpher";
148
-
149
- const basemapLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
150
- attribution: "© OpenStreetMap contributors",
151
- }).addTo(map);
152
-
153
- let blurEnabled = true;
43
+ map.on('load', async () => {
44
+ // 3. Create morphing layers
45
+ const morph = await createMapLibreMorphLayers({
46
+ morpher,
47
+ map,
48
+ interpolatedStyle: {
49
+ paint: { "fill-color": "#22c55e", "fill-opacity": 0.4 }
50
+ }
51
+ });
154
52
 
155
- const {
156
- group,
157
- regularLayer,
158
- cartogramLayer,
159
- tweenLayer,
160
- updateMorphFactor,
161
- } = await createLeafletMorphLayers({
162
- morpher,
163
- L,
164
- morphFactor: 0.25,
165
- regularStyle: () => ({ color: "#1f77b4", weight: 1 }),
166
- cartogramStyle: () => ({ color: "#ff7f0e", weight: 1 }),
167
- tweenStyle: () => ({ color: "#2ca02c", weight: 2 }),
168
- onEachFeature: (feature, layer) => {
169
- layer.bindTooltip(`${feature.properties.code}`);
170
- },
171
- basemapLayer,
172
- basemapEffect: {
173
- blurRange: [0, 12],
174
- opacityRange: [1, 0.05],
175
- grayscaleRange: [0, 1],
176
- isEnabled: () => blurEnabled,
177
- },
53
+ // 4. Drive the morph (0 = regular, 1 = cartogram)
54
+ morph.updateMorphFactor(0.5);
178
55
  });
179
-
180
- group.addTo(map);
181
-
182
- // Update the tween geometry whenever you like
183
- updateMorphFactor(0.75);
184
56
  ```
185
57
 
186
- Provide either `basemapLayer` (any Leaflet layer with a container) or `basemapEffect.target` to tell the helper which element to manipulate. By default the basemap will progressively blur and fade as the morph factor approaches 1, but you can adjust the ranges—or add brightness/grayscale tweaks—to match your design. You can also wire up UI to toggle the behaviour at runtime by returning `false` from `basemapEffect.isEnabled`.
187
-
188
- ### 3. Overlay multivariate glyphs
189
-
190
- The glyph system is **completely customizable** with no hardcoded chart types. You provide a rendering function that can return any visualization you can create with HTML, SVG, Canvas, or third-party libraries like D3.js or Chart.js. The helper automatically keeps markers positioned and synchronized with the morphing geometry.
191
-
192
- See the full glyphs guide: `docs/glyphs.md`
193
-
194
- **Example with pie charts:**
195
-
196
- ```js
197
- import {
198
- GeoMorpher,
199
- createLeafletMorphLayers,
200
- createLeafletGlyphLayer,
201
- } from "geo-morpher";
202
-
203
- const categories = [
204
- { key: "population", color: "#4e79a7" },
205
- { key: "households", color: "#f28e2c" },
206
- ];
58
+ ## Multivariate Glyphs
207
59
 
208
- const drawPie = ({ data, feature }) => {
209
- const properties = data?.data?.properties ?? feature.properties ?? {};
210
- const slices = categories
211
- .map(({ key, color }) => ({
212
- value: Number(properties[key] ?? 0),
213
- color,
214
- }))
215
- .filter((slice) => slice.value > 0);
60
+ Overlay custom visualizations (SVG, Canvas, or HTML) that stay synced with the morphing polygons.
216
61
 
217
- if (slices.length === 0) return null;
62
+ ```javascript
63
+ import { createMapLibreGlyphLayer } from "geo-morpher";
218
64
 
219
- const svg = buildPieSVG(slices); // your own renderer (D3, Canvas, vanilla SVG...)
220
- return {
221
- html: svg,
222
- className: "pie-chart-marker",
223
- iconSize: [52, 52],
224
- iconAnchor: [26, 26],
225
- };
226
- };
227
-
228
- const glyphLayer = await createLeafletGlyphLayer({
65
+ const glyphLayer = await createMapLibreGlyphLayer({
229
66
  morpher,
230
- L,
231
67
  map,
232
- geometry: "interpolated",
233
- morphFactor: 0.25,
234
- pane: "glyphs",
235
- drawGlyph: drawPie,
68
+ drawGlyph: ({ data, feature }) => ({
69
+ html: `<div class="glyph">${feature.properties.value}</div>`,
70
+ iconSize: [40, 40],
71
+ iconAnchor: [20, 20]
72
+ }),
73
+ maplibreNamespace: maplibregl
236
74
  });
237
75
 
238
- // Keep glyphs synced with the tweened geometry
239
- slider.addEventListener("input", (event) => {
240
- const value = Number(event.target.value);
241
- updateMorphFactor(value);
242
- glyphLayer.updateGlyphs({ morphFactor: value });
243
- });
76
+ // Update glyphs during morphing
77
+ glyphLayer.updateGlyphs({ morphFactor: 0.5 });
244
78
  ```
245
79
 
246
- `drawGlyph` receives `{ feature, featureId, data, morpher, geometry, morphFactor }` and can return:
247
-
248
- - `null`/`undefined` to skip the feature
249
- - A plain HTML string or DOM element
250
- - An object with `html`, `iconSize`, `iconAnchor`, `className`, `pane`, and optional `markerOptions`
251
- - Or an object containing a pre-built `icon` (any Leaflet `Icon`), if you need full control
252
-
253
- **Configuration object properties:**
254
-
255
- | Property | Type | Default | Description |
256
- |----------|------|---------|-------------|
257
- | `html` | string \| HTMLElement | - | Your custom HTML/SVG string or DOM element |
258
- | `className` | string | `"geomorpher-glyph"` | CSS class for the marker |
259
- | `iconSize` | [number, number] | `[48, 48]` | Width and height in pixels |
260
- | `iconAnchor` | [number, number] | `[24, 24]` | Anchor point in pixels (center by default) |
261
- | `pane` | string | - | Leaflet pane name for z-index control |
262
- | `markerOptions` | object | `{}` | Additional Leaflet marker options |
263
- | `divIconOptions` | object | `{}` | Additional Leaflet divIcon options |
264
- | `icon` | L.Icon | - | Pre-built Leaflet icon (overrides all other options) |
265
-
266
- Optionally provide `getGlyphData` or `filterFeature` callbacks to customise how data/visibility is resolved. When you call `glyphLayer.clear()` all markers are removed; `glyphLayer.getState()` exposes the current geometry, morph factor, and marker count.
267
-
268
- #### Data contract for glyphs
269
-
270
- By default `createLeafletGlyphLayer` will surface whatever the core `GeoMorpher` knows about the current feature via `morpher.getKeyData()`:
271
-
272
- | field | type | description |
273
- |--------------|----------|-------------|
274
- | `feature` | GeoJSON Feature | The rendered feature taken from the requested geography (`regular`, `cartogram`, or tweened). Includes `feature.properties` and a `centroid` array. |
275
- | `featureId` | string | Resolved via `getFeatureId(feature)` (defaults to `feature.properties.code ?? feature.properties.id`). |
276
- | `data` | object \| null | When using the built-in lookup this is the morpher key entry: `{ code, population, data }`. The `data` property holds the *enriched* GeoJSON feature returned from `GeoMorpher.prepare()`—handy when you stored additional indicators during enrichment. |
277
- | `morpher` | `GeoMorpher` | The instance you passed in, allowing on-demand queries (`getInterpolatedLookup`, etc.). |
278
- | `geometry` | string \| function | The geometry source currently in play (`regular`, `cartogram`, or `interpolated`). |
279
- | `morphFactor`| number | The morph factor used for the last update (only meaningful when geometry is `interpolated`). |
280
-
281
- If you want a different data shape, supply `getGlyphData`:
282
-
283
- ```js
284
- const glyphLayer = await createLeafletGlyphLayer({
285
- morpher,
286
- L,
287
- drawGlyph,
288
- getGlyphData: ({ featureId }) => externalStatsById[featureId],
289
- });
290
- ```
80
+ ## Basemap Effects
291
81
 
292
- The callback receives the same context object (minus the final `data` field) and should return whatever payload your renderer expects. `filterFeature(context)` lets you drop glyphs entirely (return `false`) for a given feature.
82
+ Automatically adjust basemap styles as you morph to focus attention on the data.
293
83
 
294
- #### Alternative chart types and rendering approaches
295
-
296
- The glyph system accepts any HTML/SVG content. Here are examples with different visualization types:
297
-
298
- **Bar chart:**
299
- ```js
300
- drawGlyph: ({ data, feature }) => {
301
- const values = [data.value1, data.value2, data.value3];
302
- const bars = values.map((v, i) =>
303
- `<rect x="${i*20}" y="${60-v}" width="15" height="${v}" fill="steelblue"/>`
304
- ).join('');
305
-
306
- return {
307
- html: `<svg width="60" height="60">${bars}</svg>`,
308
- iconSize: [60, 60],
309
- iconAnchor: [30, 30],
310
- };
311
- }
312
- ```
313
-
314
- **Using D3.js:**
315
- ```js
316
- import * as d3 from "d3";
317
-
318
- drawGlyph: ({ data }) => {
319
- const div = document.createElement('div');
320
- div.style.width = '80px';
321
- div.style.height = '80px';
322
-
323
- const svg = d3.select(div).append('svg')
324
- .attr('width', 80)
325
- .attr('height', 80);
326
-
327
- // Use D3 to create any visualization
328
- svg.selectAll('circle')
329
- .data(data.values)
330
- .enter().append('circle')
331
- .attr('cx', (d, i) => i * 20 + 10)
332
- .attr('cy', 40)
333
- .attr('r', d => d.radius)
334
- .attr('fill', d => d.color);
335
-
336
- return div; // Return DOM element directly
337
- }
338
- ```
339
-
340
- **Custom icons or images:**
341
- ```js
342
- drawGlyph: ({ data }) => {
343
- return {
344
- html: `<img src="/icons/${data.category}.png" width="32" height="32"/>`,
345
- iconSize: [32, 32],
346
- iconAnchor: [16, 16],
347
- };
348
- }
349
- ```
350
-
351
- **Pre-built Leaflet icons:**
352
- ```js
353
- drawGlyph: ({ data }) => {
354
- const icon = L.icon({
355
- iconUrl: `/markers/${data.type}.png`,
356
- iconSize: [32, 32],
357
- iconAnchor: [16, 32],
358
- popupAnchor: [0, -32],
359
- });
360
-
361
- return { icon }; // Full control over Leaflet icon
362
- }
363
- ```
364
-
365
- **Sparkline with HTML Canvas:**
366
- ```js
367
- drawGlyph: ({ data }) => {
368
- const canvas = document.createElement('canvas');
369
- canvas.width = 80;
370
- canvas.height = 40;
371
- const ctx = canvas.getContext('2d');
372
-
373
- // Draw sparkline
374
- ctx.strokeStyle = '#4e79a7';
375
- ctx.lineWidth = 2;
376
- ctx.beginPath();
377
- data.timeSeries.forEach((value, i) => {
378
- const x = (i / (data.timeSeries.length - 1)) * 80;
379
- const y = 40 - (value * 40);
380
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
381
- });
382
- ctx.stroke();
383
-
384
- return canvas.toDataURL(); // Return as data URL
385
- }
386
- ```
387
-
388
- #### Zoom-scaling glyphs
389
-
390
- By default, glyphs maintain a fixed pixel size regardless of map zoom level (standard Leaflet marker behavior). However, you can enable `scaleWithZoom` to make glyphs resize proportionally with the underlying map features—ideal for waffle charts, heatmap cells, or other visualizations that should fill polygon bounds.
391
-
392
- ```js
393
- const glyphLayer = await createLeafletGlyphLayer({
84
+ ```javascript
85
+ const morph = await createMapLibreMorphLayers({
394
86
  morpher,
395
- L,
396
87
  map,
397
- scaleWithZoom: true, // Enable zoom-responsive sizing
398
- drawGlyph: ({ data, feature, featureBounds, zoom }) => {
399
- if (!featureBounds) return null;
400
-
401
- const { width, height } = featureBounds; // Pixel dimensions at current zoom
402
-
403
- // Create waffle chart that fills the cartogram polygon
404
- const gridSize = 10;
405
- const cellSize = Math.min(width, height) / gridSize;
406
- const fillRatio = data.value / data.max;
407
- const filledCells = Math.floor(gridSize * gridSize * fillRatio);
408
-
409
- const cells = [];
410
- for (let i = 0; i < gridSize; i++) {
411
- for (let j = 0; j < gridSize; j++) {
412
- const index = i * gridSize + j;
413
- const filled = index < filledCells;
414
- cells.push(
415
- `<rect x="${j * cellSize}" y="${i * cellSize}"
416
- width="${cellSize}" height="${cellSize}"
417
- fill="${filled ? '#4e79a7' : '#e0e0e0'}"/>`
418
- );
419
- }
88
+ basemapEffect: {
89
+ layers: ["osm-tiles"],
90
+ properties: {
91
+ "raster-opacity": [1, 0.25],
92
+ "raster-saturation": [0, -1]
420
93
  }
421
-
422
- return {
423
- html: `<svg width="${width}" height="${height}">${cells.join('')}</svg>`,
424
- iconSize: [width, height],
425
- iconAnchor: [width / 2, height / 2],
426
- };
427
- },
94
+ }
428
95
  });
429
96
  ```
430
97
 
431
- When `scaleWithZoom` is enabled:
432
- - `featureBounds` provides `{ width, height, center, bounds }` in pixels at the current zoom level
433
- - `zoom` provides the current map zoom level
434
- - Glyphs automatically update when users zoom in/out
435
- - Call `glyphLayer.destroy()` to clean up zoom listeners when removing the layer
436
-
437
- A complete example is available at `examples/leaflet/zoom-scaling-glyphs.html`.
438
-
439
- ### Legacy wrapper
98
+ ## Core API
440
99
 
441
- If you previously relied on the `geoMorpher` factory from the Observable notebook, it is still available:
100
+ ### GeoMorpher
101
+ The core engine for geometry interpolation.
102
+ - `new GeoMorpher({ regularGeoJSON, cartogramGeoJSON, data, aggregations })`
103
+ - `prepare()`: Run initialization (projection, enrichment).
104
+ - `getInterpolatedFeatureCollection(factor)`: Get the geometry at a specific state.
442
105
 
443
- ```js
444
- import { geoMorpher } from "geo-morpher";
106
+ ### Projections
107
+ Auto-detects WGS84 or OSGB. For UTM or others:
108
+ ```javascript
109
+ import { createProj4Projection } from "geo-morpher";
110
+ import proj4 from "proj4";
445
111
 
446
- const result = await geoMorpher({
447
- regularGeoJSON,
448
- cartogramGeoJSON,
449
- data,
450
- aggregations,
451
- morphFactor: 0.5,
452
- });
453
-
454
- console.log(result.tweenLookup);
112
+ const projection = createProj4Projection("+proj=utm +zone=33 +datum=WGS84", proj4);
113
+ const morpher = new GeoMorpher({ ..., projection });
455
114
  ```
456
115
 
457
- ### Node script (removed)
458
-
459
- The previous Node-only example has been removed in favor of browser-based demos under `examples/maplibre` and `examples/leaflet`.
460
-
461
- ### Native browser examples (MapLibre & Leaflet)
462
-
463
- Serve the browser demos to see geo-morpher running on top of either Leaflet or MapLibre without a build step. Dependencies are resolved via import maps to CDN-hosted ES modules.
116
+ ## Examples
464
117
 
118
+ Run the local server to see demos:
465
119
  ```bash
466
120
  npm run examples:browser
467
121
  ```
122
+ - **MapLibre Demo**: Basic morphing and glyphs.
123
+ - **Indonesia**: Large-scale, multipolygon geometry morphing.
124
+ - **Projections**: Custom coordinate systems.
468
125
 
469
- Then open:
470
- - MapLibre demo: <http://localhost:4173/examples/maplibre/index.html>
471
- - Indonesia (MapLibre): <http://localhost:4173/examples/maplibre/indonesia/index.html>
472
- - MapLibre (Projections): <http://localhost:4173/examples/maplibre/projections/index.html>
473
- - Leaflet demo: <http://localhost:4173/examples/leaflet/index.html>
474
- - Leaflet zoom-scaling: <http://localhost:4173/examples/leaflet/zoom-scaling-glyphs.html>
475
-
476
- Each demo provides a morph slider and glyph overlays; the MapLibre version showcases GPU-driven rendering, paint-property basemap fading, and DOM marker glyphs running through the new adapter. (An internet connection is required to fetch CDN-hosted modules and map tiles.)
477
-
478
- **Additional examples:**
479
- - `examples/maplibre/index.html` - MapLibre adaptation with basemap paint-property effects and layer toggles
480
- - `examples/leaflet/zoom-scaling-glyphs.html` - Demonstrates zoom-responsive waffle charts that resize to fill cartogram polygons as you zoom in/out
481
-
482
- ## Testing
483
-
484
- Run the bundled smoke tests with:
485
-
486
- ```bash
487
- npm test
488
- ```
489
-
490
- ## License
491
-
492
- MIT
126
+ ## Legacy Support
127
+ Leaflet is still supported via `createLeafletMorphLayers` and `createLeafletGlyphLayer`. See [API Reference](docs/api.md) for details.
493
128
 
494
129
  ## Documentation
130
+ - [API Reference](docs/api.md)
131
+ - [Glyphs Guide](docs/glyphs.md)
495
132
 
496
- - API Reference: `docs/api.md`
497
- - Glyphs Guide: `docs/glyphs.md`
133
+ ## License
134
+ MIT © [Dany Laksono](https://github.com/danylaksono)