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.
- package/README.md +76 -439
- package/data/winchester/winchester_lsoa_cartogram.geojson +3245 -0
- package/data/winchester/winchester_lsoa_geo.geojson +24263 -0
- package/data/winchester/winchester_msoa_cartogram.geojson +635 -0
- package/data/winchester/winchester_msoa_geo.geojson +10743 -0
- package/data/winchester/winchester_ward_cartogram.geojson +725 -0
- package/data/winchester/winchester_ward_geo.geojson +10087 -0
- package/examples/README.md +2 -0
- package/examples/indonesia/index.html +364 -0
- package/examples/indonesia/main.js +770 -0
- package/examples/leaflet/canvas-glyphs.html +44 -0
- package/examples/leaflet/canvas-main.js +208 -0
- package/examples/leaflet/index.html +13 -10
- package/examples/leaflet/main.js +83 -20
- package/examples/leaflet/zoom-scaling-glyphs.html +1 -0
- package/examples/maplibre/index.html +3 -3
- package/examples/maplibre/indonesia/main.js +469 -136
- package/examples/maplibre/main.js +3 -21
- package/examples/winchester/index.html +294 -0
- package/examples/winchester/main.js +333 -0
- package/examples/winchester/winchester_example.md +145 -0
- package/package.json +3 -4
- package/src/adapters/leaflet/glyphLayer.js +23 -9
- package/src/adapters/leaflet/index.js +3 -1
- package/src/adapters/leaflet/utils/collections.js +3 -37
- package/src/adapters/leaflet/utils/coordinates.js +1 -5
- package/src/adapters/leaflet/utils/glyphNormalizer.js +19 -74
- package/src/adapters/maplibre/glyphLayer.js +21 -10
- package/src/adapters/maplibre/index.js +2 -0
- package/src/adapters/maplibre/morphLayers.js +6 -5
- package/src/adapters/maplibre/utils/coordinates.js +2 -26
- package/src/adapters/maplibre/utils/customGlyphLayer.js +485 -0
- package/src/adapters/maplibre/utils/glyphNormalizer.js +14 -117
- package/src/adapters/shared/collections.js +27 -0
- package/src/adapters/shared/dom.js +6 -0
- package/src/adapters/shared/geometry.js +26 -0
- package/src/adapters/shared/glyphNormalizer.js +103 -0
- package/src/adapters/shared/markerAdapter.js +64 -0
- package/src/core/geomorpher.js +95 -8
- package/src/index.js +6 -2
- package/src/utils/projections.js +34 -16
- package/examples/maplibre/indonesia/index.html +0 -264
- package/morphs.js +0 -1
package/README.md
CHANGED
|
@@ -3,15 +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
|
+
> [!TIP]
|
|
11
|
+
> To quickly create a grid cartogram, check out .
|
|
10
12
|
|
|
11
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
102
|
-
const
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
```javascript
|
|
63
|
+
import { createMapLibreGlyphLayer } from "geo-morpher";
|
|
218
64
|
|
|
219
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
//
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
Automatically adjust basemap styles as you morph to focus attention on the data.
|
|
293
83
|
|
|
294
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
497
|
-
|
|
133
|
+
## License
|
|
134
|
+
MIT © [Dany Laksono](https://github.com/danylaksono)
|