geo-morpher 0.1.2 → 0.1.4
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.
package/README.md
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/geo-morpher)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
GeoJSON morphing utilities for animating between regular geography and cartograms,
|
|
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
|

|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
> [!TIP]
|
|
11
|
+
> To quickly create a grid cartogram, check out gridmapper (https://danylaksono.is-a.dev/gridmapper/).
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- **MapLibre
|
|
16
|
-
- **
|
|
17
|
-
- **Multivariate Glyphs**:
|
|
18
|
-
- **Basemap Effects**: Synchronized fading
|
|
19
|
-
- **Projection Agnostic**:
|
|
20
|
-
|
|
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.
|
|
21
20
|
|
|
22
21
|
## Installation
|
|
23
22
|
|
|
@@ -25,475 +24,111 @@ To quickly create a grid cartogram, checkout my other library: 
|
|
33
|
-
|
|
34
|
-
- `createMapLibreMorphLayers` provisions GeoJSON sources and fill layers for regular, cartogram, and interpolated geometries, exposing an `updateMorphFactor` helper to drive tweening from UI controls.
|
|
35
|
-
- `createMapLibreGlyphLayer` renders glyphs with `maplibregl.Marker` instances; enable `scaleWithZoom` to regenerate glyph markup as users zoom.
|
|
36
|
-
- Pass your MapLibre namespace explicitly (`maplibreNamespace: maplibregl`) when calling glyph helpers in module-bundled builds where `maplibregl` is not attached to `globalThis`.
|
|
37
|
-
- 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.
|
|
38
|
-
|
|
39
|
-
#### MapLibre basemap effects
|
|
40
|
-
|
|
41
|
-
`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.
|
|
42
|
-
|
|
43
|
-
```js
|
|
44
|
-
const morph = await createMapLibreMorphLayers({
|
|
45
|
-
morpher,
|
|
46
|
-
map,
|
|
47
|
-
basemapEffect: {
|
|
48
|
-
layers: ["basemap", "basemap-labels"],
|
|
49
|
-
properties: {
|
|
50
|
-
"raster-opacity": [1, 0.15],
|
|
51
|
-
"raster-brightness-max": { from: 1, to: 1.4 },
|
|
52
|
-
},
|
|
53
|
-
propertyClamp: {
|
|
54
|
-
"raster-brightness-max": [0, 2],
|
|
55
|
-
},
|
|
56
|
-
easing: (t) => t * t, // optional easing curve
|
|
57
|
-
},
|
|
58
|
-
});
|
|
27
|
+
## Quick Start (MapLibre)
|
|
59
28
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Apply effect manually (e.g., when animating via requestAnimationFrame)
|
|
64
|
-
morph.applyBasemapEffect(0.5);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
- Provide a `layers` string/array or resolver function to target paint properties across multiple layers.
|
|
68
|
-
- Supply ranges (`[from, to]` or `{ from, to }`) for numeric properties such as `raster-opacity`, `fill-opacity`, or `line-opacity`.
|
|
69
|
-
- Use functions in `properties[layerId]` for full control or to manipulate non-numeric paint values.
|
|
70
|
-
- Capture-and-reset logic ensures properties revert to their original values when the effect is disabled.
|
|
71
|
-
- Canvas-style blur is not built-in; use a custom MapLibre layer if a true blur shader is required.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### 1. Prepare morphing data
|
|
75
|
-
|
|
76
|
-
```js
|
|
77
|
-
import { GeoMorpher } from "geo-morpher";
|
|
78
|
-
import regularGeoJSON from "./data/oxford_lsoas_regular.json" assert { type: "json" };
|
|
79
|
-
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";
|
|
80
32
|
|
|
33
|
+
// 1. Prepare data (regular vs cartogram geography)
|
|
81
34
|
const morpher = new GeoMorpher({
|
|
82
|
-
regularGeoJSON,
|
|
83
|
-
cartogramGeoJSON,
|
|
84
|
-
data: await fetchModelData(),
|
|
85
|
-
aggregations: {
|
|
86
|
-
population: "sum",
|
|
87
|
-
households: "sum",
|
|
88
|
-
},
|
|
35
|
+
regularGeoJSON: await (await fetch('regular_lsoa.json')).json(),
|
|
36
|
+
cartogramGeoJSON: await (await fetch('cartogram_lsoa.json')).json(),
|
|
89
37
|
});
|
|
90
|
-
|
|
91
38
|
await morpher.prepare();
|
|
92
39
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const tween = morpher.getInterpolatedFeatureCollection(0.5);
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
#### Projections & Coordinate Systems
|
|
99
|
-
|
|
100
|
-
While `geo-morpher` was born out of UK-centric cartography, it is fully generic. It internally projects all data to WGS84 (latitude/longitude) for mapping compatibility.
|
|
101
|
-
|
|
102
|
-
- **Auto-detection**: If no projection is provided, `GeoMorpher` inspects your coordinates. If they look like WGS84, it uses them as-is.
|
|
103
|
-
- **OSGB Default**: If coordinates fall outside the geographic range, it assumes OSGB (EPSG:27700) and transforms them to WGS84.
|
|
104
|
-
- **Manual Override**: Pass a helper like `WebMercatorProjection` or a custom `proj4` wrapper for other systems.
|
|
105
|
-
|
|
106
|
-
```js
|
|
107
|
-
import { GeoMorpher, WGS84Projection, createProj4Projection } from "geo-morpher";
|
|
108
|
-
import proj4 from "proj4";
|
|
109
|
-
|
|
110
|
-
// 1. Auto-detected (usually works for WGS84 or OSGB)
|
|
111
|
-
const morpher = new GeoMorpher({ regularGeoJSON, cartogramGeoJSON });
|
|
112
|
-
|
|
113
|
-
// 2. Explicit WGS84 (Identity)
|
|
114
|
-
const morpher = new GeoMorpher({ ..., projection: WGS84Projection });
|
|
115
|
-
|
|
116
|
-
// 3. Custom Projection (e.g., UTM Zone 33N)
|
|
117
|
-
const projection = createProj4Projection("+proj=utm +zone=33 +datum=WGS84", proj4);
|
|
118
|
-
const morpher = new GeoMorpher({ ..., projection });
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
See `examples/maplibre/projections/index.html` for a browser-based custom projection demo.
|
|
40
|
+
// 2. Initialize MapLibre
|
|
41
|
+
const map = new maplibregl.Map({ ... });
|
|
122
42
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
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
|
+
});
|
|
134
52
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
regularLayer,
|
|
138
|
-
cartogramLayer,
|
|
139
|
-
tweenLayer,
|
|
140
|
-
updateMorphFactor,
|
|
141
|
-
} = await createLeafletMorphLayers({
|
|
142
|
-
morpher,
|
|
143
|
-
L,
|
|
144
|
-
morphFactor: 0.25,
|
|
145
|
-
regularStyle: () => ({ color: "#1f77b4", weight: 1 }),
|
|
146
|
-
cartogramStyle: () => ({ color: "#ff7f0e", weight: 1 }),
|
|
147
|
-
tweenStyle: () => ({ color: "#2ca02c", weight: 2 }),
|
|
148
|
-
onEachFeature: (feature, layer) => {
|
|
149
|
-
layer.bindTooltip(`${feature.properties.code}`);
|
|
150
|
-
},
|
|
151
|
-
basemapLayer,
|
|
152
|
-
basemapEffect: {
|
|
153
|
-
blurRange: [0, 12],
|
|
154
|
-
opacityRange: [1, 0.05],
|
|
155
|
-
grayscaleRange: [0, 1],
|
|
156
|
-
isEnabled: () => blurEnabled,
|
|
157
|
-
},
|
|
53
|
+
// 4. Drive the morph (0 = regular, 1 = cartogram)
|
|
54
|
+
morph.updateMorphFactor(0.5);
|
|
158
55
|
});
|
|
159
|
-
|
|
160
|
-
group.addTo(map);
|
|
161
|
-
|
|
162
|
-
// Update the tween geometry whenever you like
|
|
163
|
-
updateMorphFactor(0.75);
|
|
164
56
|
```
|
|
165
57
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
### 3. Overlay multivariate glyphs
|
|
169
|
-
|
|
170
|
-
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.
|
|
171
|
-
|
|
172
|
-
See the full glyphs guide: `docs/glyphs.md`
|
|
173
|
-
|
|
174
|
-
**Example with pie charts:**
|
|
175
|
-
|
|
176
|
-
```js
|
|
177
|
-
import {
|
|
178
|
-
GeoMorpher,
|
|
179
|
-
createLeafletMorphLayers,
|
|
180
|
-
createLeafletGlyphLayer,
|
|
181
|
-
} from "geo-morpher";
|
|
58
|
+
## Multivariate Glyphs
|
|
182
59
|
|
|
183
|
-
|
|
184
|
-
{ key: "population", color: "#4e79a7" },
|
|
185
|
-
{ key: "households", color: "#f28e2c" },
|
|
186
|
-
];
|
|
60
|
+
Overlay custom visualizations (SVG, Canvas, or HTML) that stay synced with the morphing polygons.
|
|
187
61
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const slices = categories
|
|
191
|
-
.map(({ key, color }) => ({
|
|
192
|
-
value: Number(properties[key] ?? 0),
|
|
193
|
-
color,
|
|
194
|
-
}))
|
|
195
|
-
.filter((slice) => slice.value > 0);
|
|
62
|
+
```javascript
|
|
63
|
+
import { createMapLibreGlyphLayer } from "geo-morpher";
|
|
196
64
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const svg = buildPieSVG(slices); // your own renderer (D3, Canvas, vanilla SVG...)
|
|
200
|
-
return {
|
|
201
|
-
html: svg,
|
|
202
|
-
className: "pie-chart-marker",
|
|
203
|
-
iconSize: [52, 52],
|
|
204
|
-
iconAnchor: [26, 26],
|
|
205
|
-
};
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const glyphLayer = await createLeafletGlyphLayer({
|
|
65
|
+
const glyphLayer = await createMapLibreGlyphLayer({
|
|
209
66
|
morpher,
|
|
210
|
-
L,
|
|
211
67
|
map,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// Keep glyphs synced with the tweened geometry
|
|
219
|
-
slider.addEventListener("input", (event) => {
|
|
220
|
-
const value = Number(event.target.value);
|
|
221
|
-
updateMorphFactor(value);
|
|
222
|
-
glyphLayer.updateGlyphs({ morphFactor: value });
|
|
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
|
|
223
74
|
});
|
|
224
75
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
// const glyph = await createLeafletGlyphLayer({
|
|
229
|
-
// drawGlyph,
|
|
230
|
-
// L,
|
|
231
|
-
// featureCollection: morpher.getRegularFeatureCollection(), // static set
|
|
232
|
-
// });
|
|
233
|
-
//
|
|
234
|
-
// const glyphProvider = await createLeafletGlyphLayer({
|
|
235
|
-
// drawGlyph,
|
|
236
|
-
// L,
|
|
237
|
-
// featureProvider: ({ geometry, morphFactor }) => morpher.getInterpolatedFeatureCollection(morphFactor),
|
|
238
|
-
// });
|
|
239
|
-
//
|
|
240
|
-
// The adapters also export small helper functions to convert normalized glyph values
|
|
241
|
-
// into platform-specific objects: `createLeafletIcon` and `createMapLibreMarkerData`.
|
|
76
|
+
// Update glyphs during morphing
|
|
77
|
+
glyphLayer.updateGlyphs({ morphFactor: 0.5 });
|
|
242
78
|
```
|
|
243
79
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
- `null`/`undefined` to skip the feature
|
|
247
|
-
- A plain HTML string or DOM element
|
|
248
|
-
- An object with `html`, `iconSize`, `iconAnchor`, `className`, `pane`, and optional `markerOptions`
|
|
249
|
-
- Or an object containing a pre-built `icon` (any Leaflet `Icon`), if you need full control
|
|
250
|
-
|
|
251
|
-
**Configuration object properties:**
|
|
80
|
+
## Basemap Effects
|
|
252
81
|
|
|
253
|
-
|
|
254
|
-
|----------|------|---------|-------------|
|
|
255
|
-
| `html` | string \| HTMLElement | - | Your custom HTML/SVG string or DOM element |
|
|
256
|
-
| `className` | string | `"geomorpher-glyph"` | CSS class for the marker |
|
|
257
|
-
| `iconSize` | [number, number] | `[48, 48]` | Width and height in pixels |
|
|
258
|
-
| `iconAnchor` | [number, number] | `[24, 24]` | Anchor point in pixels (center by default) |
|
|
259
|
-
| `pane` | string | - | Leaflet pane name for z-index control |
|
|
260
|
-
| `markerOptions` | object | `{}` | Additional Leaflet marker options |
|
|
261
|
-
| `divIconOptions` | object | `{}` | Additional Leaflet divIcon options |
|
|
262
|
-
| `icon` | L.Icon | - | Pre-built Leaflet icon (overrides all other options) |
|
|
82
|
+
Automatically adjust basemap styles as you morph to focus attention on the data.
|
|
263
83
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
#### Data contract for glyphs
|
|
267
|
-
|
|
268
|
-
By default `createLeafletGlyphLayer` will surface whatever the core `GeoMorpher` knows about the current feature via `morpher.getKeyData()`:
|
|
269
|
-
|
|
270
|
-
| field | type | description |
|
|
271
|
-
|--------------|----------|-------------|
|
|
272
|
-
| `feature` | GeoJSON Feature | The rendered feature taken from the requested geography (`regular`, `cartogram`, or tweened). Includes `feature.properties` and a `centroid` array. |
|
|
273
|
-
| `featureId` | string | Resolved via `getFeatureId(feature)` (defaults to `feature.properties.code ?? feature.properties.id`). |
|
|
274
|
-
| `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. |
|
|
275
|
-
| `morpher` | `GeoMorpher` | The instance you passed in, allowing on-demand queries (`getInterpolatedLookup`, etc.). |
|
|
276
|
-
| `geometry` | string \| function | The geometry source currently in play (`regular`, `cartogram`, or `interpolated`). |
|
|
277
|
-
| `morphFactor`| number | The morph factor used for the last update (only meaningful when geometry is `interpolated`). |
|
|
278
|
-
|
|
279
|
-
If you want a different data shape, supply `getGlyphData`:
|
|
280
|
-
|
|
281
|
-
```js
|
|
282
|
-
const glyphLayer = await createLeafletGlyphLayer({
|
|
283
|
-
morpher,
|
|
284
|
-
L,
|
|
285
|
-
drawGlyph,
|
|
286
|
-
getGlyphData: ({ featureId }) => externalStatsById[featureId],
|
|
287
|
-
});
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
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.
|
|
291
|
-
|
|
292
|
-
#### Alternative chart types and rendering approaches
|
|
293
|
-
|
|
294
|
-
The glyph system accepts any HTML/SVG content. Here are examples with different visualization types:
|
|
295
|
-
|
|
296
|
-
**Bar chart:**
|
|
297
|
-
```js
|
|
298
|
-
drawGlyph: ({ data, feature }) => {
|
|
299
|
-
const values = [data.value1, data.value2, data.value3];
|
|
300
|
-
const bars = values.map((v, i) =>
|
|
301
|
-
`<rect x="${i*20}" y="${60-v}" width="15" height="${v}" fill="steelblue"/>`
|
|
302
|
-
).join('');
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
html: `<svg width="60" height="60">${bars}</svg>`,
|
|
306
|
-
iconSize: [60, 60],
|
|
307
|
-
iconAnchor: [30, 30],
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**Using D3.js:**
|
|
313
|
-
```js
|
|
314
|
-
import * as d3 from "d3";
|
|
315
|
-
|
|
316
|
-
drawGlyph: ({ data }) => {
|
|
317
|
-
const div = document.createElement('div');
|
|
318
|
-
div.style.width = '80px';
|
|
319
|
-
div.style.height = '80px';
|
|
320
|
-
|
|
321
|
-
const svg = d3.select(div).append('svg')
|
|
322
|
-
.attr('width', 80)
|
|
323
|
-
.attr('height', 80);
|
|
324
|
-
|
|
325
|
-
// Use D3 to create any visualization
|
|
326
|
-
svg.selectAll('circle')
|
|
327
|
-
.data(data.values)
|
|
328
|
-
.enter().append('circle')
|
|
329
|
-
.attr('cx', (d, i) => i * 20 + 10)
|
|
330
|
-
.attr('cy', 40)
|
|
331
|
-
.attr('r', d => d.radius)
|
|
332
|
-
.attr('fill', d => d.color);
|
|
333
|
-
|
|
334
|
-
return div; // Return DOM element directly
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**Custom icons or images:**
|
|
339
|
-
```js
|
|
340
|
-
drawGlyph: ({ data }) => {
|
|
341
|
-
return {
|
|
342
|
-
html: `<img src="/icons/${data.category}.png" width="32" height="32"/>`,
|
|
343
|
-
iconSize: [32, 32],
|
|
344
|
-
iconAnchor: [16, 16],
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
**Pre-built Leaflet icons:**
|
|
350
|
-
```js
|
|
351
|
-
drawGlyph: ({ data }) => {
|
|
352
|
-
const icon = L.icon({
|
|
353
|
-
iconUrl: `/markers/${data.type}.png`,
|
|
354
|
-
iconSize: [32, 32],
|
|
355
|
-
iconAnchor: [16, 32],
|
|
356
|
-
popupAnchor: [0, -32],
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
return { icon }; // Full control over Leaflet icon
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**Sparkline with HTML Canvas:**
|
|
364
|
-
```js
|
|
365
|
-
drawGlyph: ({ data }) => {
|
|
366
|
-
const canvas = document.createElement('canvas');
|
|
367
|
-
canvas.width = 80;
|
|
368
|
-
canvas.height = 40;
|
|
369
|
-
const ctx = canvas.getContext('2d');
|
|
370
|
-
|
|
371
|
-
// Draw sparkline
|
|
372
|
-
ctx.strokeStyle = '#4e79a7';
|
|
373
|
-
ctx.lineWidth = 2;
|
|
374
|
-
ctx.beginPath();
|
|
375
|
-
data.timeSeries.forEach((value, i) => {
|
|
376
|
-
const x = (i / (data.timeSeries.length - 1)) * 80;
|
|
377
|
-
const y = 40 - (value * 40);
|
|
378
|
-
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
379
|
-
});
|
|
380
|
-
ctx.stroke();
|
|
381
|
-
|
|
382
|
-
return canvas.toDataURL(); // Return as data URL
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
#### Zoom-scaling glyphs
|
|
387
|
-
|
|
388
|
-
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.
|
|
389
|
-
|
|
390
|
-
```js
|
|
391
|
-
const glyphLayer = await createLeafletGlyphLayer({
|
|
84
|
+
```javascript
|
|
85
|
+
const morph = await createMapLibreMorphLayers({
|
|
392
86
|
morpher,
|
|
393
|
-
L,
|
|
394
87
|
map,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// Create waffle chart that fills the cartogram polygon
|
|
402
|
-
const gridSize = 10;
|
|
403
|
-
const cellSize = Math.min(width, height) / gridSize;
|
|
404
|
-
const fillRatio = data.value / data.max;
|
|
405
|
-
const filledCells = Math.floor(gridSize * gridSize * fillRatio);
|
|
406
|
-
|
|
407
|
-
const cells = [];
|
|
408
|
-
for (let i = 0; i < gridSize; i++) {
|
|
409
|
-
for (let j = 0; j < gridSize; j++) {
|
|
410
|
-
const index = i * gridSize + j;
|
|
411
|
-
const filled = index < filledCells;
|
|
412
|
-
cells.push(
|
|
413
|
-
`<rect x="${j * cellSize}" y="${i * cellSize}"
|
|
414
|
-
width="${cellSize}" height="${cellSize}"
|
|
415
|
-
fill="${filled ? '#4e79a7' : '#e0e0e0'}"/>`
|
|
416
|
-
);
|
|
417
|
-
}
|
|
88
|
+
basemapEffect: {
|
|
89
|
+
layers: ["osm-tiles"],
|
|
90
|
+
properties: {
|
|
91
|
+
"raster-opacity": [1, 0.25],
|
|
92
|
+
"raster-saturation": [0, -1]
|
|
418
93
|
}
|
|
419
|
-
|
|
420
|
-
return {
|
|
421
|
-
html: `<svg width="${width}" height="${height}">${cells.join('')}</svg>`,
|
|
422
|
-
iconSize: [width, height],
|
|
423
|
-
iconAnchor: [width / 2, height / 2],
|
|
424
|
-
};
|
|
425
|
-
},
|
|
94
|
+
}
|
|
426
95
|
});
|
|
427
96
|
```
|
|
428
97
|
|
|
429
|
-
|
|
430
|
-
- `featureBounds` provides `{ width, height, center, bounds }` in pixels at the current zoom level
|
|
431
|
-
- `zoom` provides the current map zoom level
|
|
432
|
-
- Glyphs automatically update when users zoom in/out
|
|
433
|
-
- Call `glyphLayer.destroy()` to clean up zoom listeners when removing the layer
|
|
434
|
-
|
|
435
|
-
A complete example is available at `examples/leaflet/zoom-scaling-glyphs.html`.
|
|
98
|
+
## Core API
|
|
436
99
|
|
|
437
|
-
###
|
|
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.
|
|
438
105
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
```
|
|
442
|
-
import {
|
|
443
|
-
|
|
444
|
-
const result = await geoMorpher({
|
|
445
|
-
regularGeoJSON,
|
|
446
|
-
cartogramGeoJSON,
|
|
447
|
-
data,
|
|
448
|
-
aggregations,
|
|
449
|
-
morphFactor: 0.5,
|
|
450
|
-
});
|
|
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";
|
|
451
111
|
|
|
452
|
-
|
|
112
|
+
const projection = createProj4Projection("+proj=utm +zone=33 +datum=WGS84", proj4);
|
|
113
|
+
const morpher = new GeoMorpher({ ..., projection });
|
|
453
114
|
```
|
|
454
115
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
The previous Node-only example has been removed in favor of browser-based demos under `examples/maplibre` and `examples/leaflet`.
|
|
458
|
-
|
|
459
|
-
### Native browser examples (MapLibre & Leaflet)
|
|
460
|
-
|
|
461
|
-
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
|
|
462
117
|
|
|
118
|
+
Run the local server to see demos:
|
|
463
119
|
```bash
|
|
464
120
|
npm run examples:browser
|
|
465
121
|
```
|
|
122
|
+
- **MapLibre Demo**: Basic morphing and glyphs.
|
|
123
|
+
- **Indonesia**: Large-scale, multipolygon geometry morphing.
|
|
124
|
+
- **Projections**: Custom coordinate systems.
|
|
466
125
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
- Indonesia (MapLibre): <http://localhost:4173/examples/maplibre/indonesia/index.html>
|
|
470
|
-
- MapLibre (Projections): <http://localhost:4173/examples/maplibre/projections/index.html>
|
|
471
|
-
- Leaflet demo: <http://localhost:4173/examples/leaflet/index.html>
|
|
472
|
-
- Leaflet zoom-scaling: <http://localhost:4173/examples/leaflet/zoom-scaling-glyphs.html>
|
|
473
|
-
|
|
474
|
-
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.)
|
|
475
|
-
|
|
476
|
-
**Additional examples:**
|
|
477
|
-
- `examples/maplibre/index.html` - MapLibre adaptation with basemap paint-property effects and layer toggles
|
|
478
|
-
- `examples/leaflet/zoom-scaling-glyphs.html` - Demonstrates zoom-responsive waffle charts that resize to fill cartogram polygons as you zoom in/out
|
|
479
|
-
|
|
480
|
-
## Testing
|
|
481
|
-
|
|
482
|
-
Run the bundled smoke tests with:
|
|
483
|
-
|
|
484
|
-
```bash
|
|
485
|
-
npm test
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
## Author
|
|
489
|
-
|
|
490
|
-
Dany Laksono
|
|
491
|
-
|
|
492
|
-
## License
|
|
493
|
-
|
|
494
|
-
MIT
|
|
126
|
+
## Legacy Support
|
|
127
|
+
Leaflet is still supported via `createLeafletMorphLayers` and `createLeafletGlyphLayer`. See [API Reference](docs/api.md) for details.
|
|
495
128
|
|
|
496
129
|
## Documentation
|
|
130
|
+
- [API Reference](docs/api.md)
|
|
131
|
+
- [Glyphs Guide](docs/glyphs.md)
|
|
497
132
|
|
|
498
|
-
|
|
499
|
-
|
|
133
|
+
## License
|
|
134
|
+
MIT © [Dany Laksono](https://github.com/danylaksono)
|