geo-morpher 0.1.0 → 0.1.1
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 +33 -49
- package/examples/README.md +53 -0
- package/examples/{browser → leaflet}/index.html +3 -2
- package/examples/{browser → leaflet}/main.js +28 -37
- package/examples/leaflet/zoom-scaling-glyphs.html +121 -0
- package/examples/{browser → leaflet}/zoom-scaling-glyphs.js +27 -77
- package/examples/{browser/maplibre → maplibre}/index.html +3 -1
- package/examples/{browser → maplibre}/indonesia/index.html +3 -1
- package/examples/{browser → maplibre}/indonesia/main.js +2 -0
- package/examples/{browser/maplibre → maplibre}/main.js +8 -2
- package/examples/maplibre/projections/index.html +77 -0
- package/examples/maplibre/projections/main.js +151 -0
- package/package.json +22 -46
- package/src/index.js +4 -0
- package/dist/index.cjs +0 -3304
- package/dist/index.cjs.map +0 -1
- package/dist/index.js +0 -3271
- package/dist/index.js.map +0 -1
- package/examples/browser/README.md +0 -189
- package/examples/browser/zoom-scaling-glyphs.html +0 -257
- package/examples/custom-projection.js +0 -236
- package/examples/native.js +0 -52
package/README.md
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
# geo-morpher
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/js/geo-morpher)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
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.
|
|
4
7
|
|
|
5
8
|

|
|
6
9
|
|
|
7
10
|
|
|
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.
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
## Installation
|
|
9
17
|
|
|
10
18
|
```bash
|
|
11
19
|
npm install geo-morpher
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
Bring your own MapLibre or Leaflet instance (both listed as peer dependencies). MapLibre is the default
|
|
15
|
-
|
|
16
|
-
### Choosing an adapter
|
|
17
|
-
|
|
18
|
-
- MapLibre adapter (`createMapLibreMorphLayers`, `createMapLibreGlyphLayer`) is the default. Use it for performance, style expression control, and WebGL-based effects.
|
|
19
|
-
- Leaflet adapter (`createLeafletMorphLayers`, `createLeafletGlyphLayer`) remains supported for teams who want to manipulate markers, tooltips, or UI directly in the DOM.
|
|
20
|
-
- Both adapters share the same `GeoMorpher` core, so you can swap adapters without recalculating morph data.
|
|
21
|
-
- Examples under `examples/browser/` demonstrate both integrations; switch adapters by loading the corresponding entry point.
|
|
22
|
+
Bring your own MapLibre or Leaflet instance (both listed as peer dependencies). MapLibre is the default adapter; Leaflet remains supported for backward compatibility.
|
|
22
23
|
|
|
23
24
|
## Usage
|
|
24
25
|
|
|
@@ -31,24 +32,17 @@ src/
|
|
|
31
32
|
lib/ # Shared runtime utilities (OSGB projection)
|
|
32
33
|
utils/ # Data enrichment and projection helpers
|
|
33
34
|
data/ # Sample Oxford LSOA datasets
|
|
34
|
-
examples/ # Runnable
|
|
35
|
+
examples/ # Runnable browser demos (MapLibre & Leaflet)
|
|
35
36
|
test/ # node:test coverage for core behaviours
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
### MapLibre adapter
|
|
39
|
+
### MapLibre adapter (default)
|
|
39
40
|
|
|
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.
|
|
41
42
|
- `createMapLibreGlyphLayer` renders glyphs with `maplibregl.Marker` instances; enable `scaleWithZoom` to regenerate glyph markup as users zoom.
|
|
42
43
|
- Pass your MapLibre namespace explicitly (`maplibreNamespace: maplibregl`) when calling glyph helpers in module-bundled builds where `maplibregl` is not attached to `globalThis`.
|
|
43
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.
|
|
44
|
-
- Track ongoing enhancements and open items in `docs/maplibre-migration-plan.md` before
|
|
45
|
-
|
|
46
|
-
### Leaflet adapter
|
|
47
|
-
|
|
48
|
-
- `createLeafletMorphLayers` keeps the existing DOM-centric workflow for basemap blur/fade effects and marker overlays.
|
|
49
|
-
- `createLeafletGlyphLayer` accepts any HTML, SVG, Canvas, or Leaflet icon—handy when you need tooltips, popups, and other DOM components under full control.
|
|
50
|
-
- The basemap effect still uses DOM filters (`blur`, `opacity`, `grayscale`) and can target any Leaflet layer container.
|
|
51
|
-
- Stick with Leaflet when you require deep integration with existing Leaflet plugins or server-rendered markup; migrate to MapLibre later without touching your data preparation code.
|
|
45
|
+
- Track ongoing enhancements and open items in `docs/maplibre-migration-plan.md` before relying on the adapter in production.
|
|
52
46
|
|
|
53
47
|
#### MapLibre basemap effects
|
|
54
48
|
|
|
@@ -144,9 +138,9 @@ const customProjection = {
|
|
|
144
138
|
};
|
|
145
139
|
```
|
|
146
140
|
|
|
147
|
-
See `examples/
|
|
141
|
+
See `examples/maplibre/projections/index.html` for a browser-based custom projection demo.
|
|
148
142
|
|
|
149
|
-
### 2. Drop the morph straight into Leaflet
|
|
143
|
+
### 2. Drop the morph straight into Leaflet (compat)
|
|
150
144
|
|
|
151
145
|
```js
|
|
152
146
|
import L from "leaflet";
|
|
@@ -195,6 +189,8 @@ Provide either `basemapLayer` (any Leaflet layer with a container) or `basemapEf
|
|
|
195
189
|
|
|
196
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.
|
|
197
191
|
|
|
192
|
+
See the full glyphs guide: `docs/glyphs.md`
|
|
193
|
+
|
|
198
194
|
**Example with pie charts:**
|
|
199
195
|
|
|
200
196
|
```js
|
|
@@ -276,7 +272,6 @@ By default `createLeafletGlyphLayer` will surface whatever the core `GeoMorpher`
|
|
|
276
272
|
| field | type | description |
|
|
277
273
|
|--------------|----------|-------------|
|
|
278
274
|
| `feature` | GeoJSON Feature | The rendered feature taken from the requested geography (`regular`, `cartogram`, or tweened). Includes `feature.properties` and a `centroid` array. |
|
|
279
|
-
enum{} | Resolved via `getFeatureId(feature)` (defaults to `feature.properties.code ?? feature.properties.id`). |
|
|
280
275
|
| `featureId` | string | Resolved via `getFeatureId(feature)` (defaults to `feature.properties.code ?? feature.properties.id`). |
|
|
281
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. |
|
|
282
277
|
| `morpher` | `GeoMorpher` | The instance you passed in, allowing on-demand queries (`getInterpolatedLookup`, etc.). |
|
|
@@ -439,7 +434,7 @@ When `scaleWithZoom` is enabled:
|
|
|
439
434
|
- Glyphs automatically update when users zoom in/out
|
|
440
435
|
- Call `glyphLayer.destroy()` to clean up zoom listeners when removing the layer
|
|
441
436
|
|
|
442
|
-
A complete example is available at `examples/
|
|
437
|
+
A complete example is available at `examples/leaflet/zoom-scaling-glyphs.html`.
|
|
443
438
|
|
|
444
439
|
### Legacy wrapper
|
|
445
440
|
|
|
@@ -459,17 +454,11 @@ const result = await geoMorpher({
|
|
|
459
454
|
console.log(result.tweenLookup);
|
|
460
455
|
```
|
|
461
456
|
|
|
462
|
-
###
|
|
457
|
+
### Node script (removed)
|
|
463
458
|
|
|
464
|
-
|
|
459
|
+
The previous Node-only example has been removed in favor of browser-based demos under `examples/maplibre` and `examples/leaflet`.
|
|
465
460
|
|
|
466
|
-
|
|
467
|
-
node examples/native.js
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
It loads `data/oxford_lsoas_regular.json` and `data/oxford_lsoas_cartogram.json`, mirrors their population/household properties into a basic dataset, and prints counts plus a sample tweened feature—all without any bundlers or UI frameworks.
|
|
471
|
-
|
|
472
|
-
### Native browser examples (Leaflet & MapLibre)
|
|
461
|
+
### Native browser examples (MapLibre & Leaflet)
|
|
473
462
|
|
|
474
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.
|
|
475
464
|
|
|
@@ -478,14 +467,17 @@ npm run examples:browser
|
|
|
478
467
|
```
|
|
479
468
|
|
|
480
469
|
Then open:
|
|
481
|
-
-
|
|
482
|
-
- MapLibre
|
|
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>
|
|
483
475
|
|
|
484
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.)
|
|
485
477
|
|
|
486
478
|
**Additional examples:**
|
|
487
|
-
- `examples/
|
|
488
|
-
- `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
|
|
489
481
|
|
|
490
482
|
## Testing
|
|
491
483
|
|
|
@@ -495,19 +487,11 @@ Run the bundled smoke tests with:
|
|
|
495
487
|
npm test
|
|
496
488
|
```
|
|
497
489
|
|
|
498
|
-
## Build & publish
|
|
499
|
-
|
|
500
|
-
- `npm run build` compiles ESM and CJS bundles to `dist/` via Rollup.
|
|
501
|
-
- `npm run clean` removes the generated `dist/` folder prior to fresh builds.
|
|
502
|
-
- Publishing runs `npm run build` automatically through the `prepare` hook; verify outputs before executing `npm publish`.
|
|
503
|
-
|
|
504
|
-
## Roadmap & pending updates
|
|
505
|
-
|
|
506
|
-
- GPU-batched glyph rendering via MapLibre `CustomLayerInterface` remains on the roadmap for high-volume scenes—follow progress in `docs/maplibre-migration-plan.md`.
|
|
507
|
-
- Adapter-level automated tests (Jest + MapLibre mocks) are planned to complement the current core-only test suite.
|
|
508
|
-
- Post-processing basemap effects (true blur, grayscale for vector tiles) are being explored as custom layers; today, stick to opacity/brightness tweaks via `basemapEffect`.
|
|
509
|
-
- Globe projection support will land after the MapLibre adapter stabilizes on the current release line; expect a follow-up example once the API settles.
|
|
510
|
-
|
|
511
490
|
## License
|
|
512
491
|
|
|
513
492
|
MIT
|
|
493
|
+
|
|
494
|
+
## Documentation
|
|
495
|
+
|
|
496
|
+
- API Reference: `docs/api.md`
|
|
497
|
+
- Glyphs Guide: `docs/glyphs.md`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
These examples are split by adapter. MapLibre is the default; Leaflet is provided for compatibility.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `examples/maplibre/` – MapLibre GL JS demos (default)
|
|
8
|
+
- `examples/leaflet/` – Leaflet demos (compatibility)
|
|
9
|
+
|
|
10
|
+
## Run
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm run examples:browser
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open in your browser:
|
|
17
|
+
|
|
18
|
+
### MapLibre
|
|
19
|
+
- `http://localhost:4173/examples/maplibre/index.html`
|
|
20
|
+
- `http://localhost:4173/examples/maplibre/indonesia/index.html`
|
|
21
|
+
- `http://localhost:4173/examples/maplibre/projections/index.html`
|
|
22
|
+
|
|
23
|
+
### Leaflet
|
|
24
|
+
- `http://localhost:4173/examples/leaflet/index.html`
|
|
25
|
+
- `http://localhost:4173/examples/leaflet/zoom-scaling-glyphs.html`
|
|
26
|
+
|
|
27
|
+
## Import maps
|
|
28
|
+
|
|
29
|
+
All examples use import maps. Ensure the following mappings (adjust as needed):
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<script type="importmap">
|
|
33
|
+
{
|
|
34
|
+
"imports": {
|
|
35
|
+
"leaflet": "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/+esm",
|
|
36
|
+
"npm:leaflet": "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/+esm",
|
|
37
|
+
"maplibre-gl": "https://esm.sh/maplibre-gl@5.8.0?bundle",
|
|
38
|
+
"@turf/turf": "https://esm.sh/@turf/turf@6.5.0?bundle",
|
|
39
|
+
"@turf/helpers": "https://esm.sh/@turf/helpers@6.5.0?bundle",
|
|
40
|
+
"flubber": "https://esm.sh/flubber@0.4.2?bundle",
|
|
41
|
+
"lodash/isEmpty.js": "https://esm.sh/lodash@4.17.21/isEmpty?bundle",
|
|
42
|
+
"lodash/cloneDeep.js": "https://esm.sh/lodash@4.17.21/cloneDeep?bundle",
|
|
43
|
+
"lodash/keyBy.js": "https://esm.sh/lodash@4.17.21/keyBy?bundle",
|
|
44
|
+
"lodash/mapValues.js": "https://esm.sh/lodash@4.17.21/mapValues?bundle"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
- `@turf/turf` and `flubber` must use esm.sh with `?bundle`.
|
|
52
|
+
- Leaflet works with jsDelivr `+esm`.
|
|
53
|
+
- Raw Node examples have been converted; all demos are browser-based.
|
|
@@ -183,7 +183,6 @@
|
|
|
183
183
|
"imports": {
|
|
184
184
|
"npm:leaflet": "https://esm.sh/leaflet@1.9.4",
|
|
185
185
|
"@turf/turf": "https://esm.sh/@turf/turf@6.5.0?bundle",
|
|
186
|
-
"@turf/helpers": "https://esm.sh/@turf/helpers@6.5.0?bundle",
|
|
187
186
|
"flubber": "https://esm.sh/flubber@0.4.2?bundle",
|
|
188
187
|
"lodash/cloneDeep.js": "https://esm.sh/lodash@4.17.21/cloneDeep?bundle",
|
|
189
188
|
"lodash/keyBy.js": "https://esm.sh/lodash@4.17.21/keyBy?bundle",
|
|
@@ -257,4 +256,6 @@
|
|
|
257
256
|
<script type="module" src="./main.js"></script>
|
|
258
257
|
</body>
|
|
259
258
|
|
|
260
|
-
</html>
|
|
259
|
+
</html>
|
|
260
|
+
|
|
261
|
+
|
|
@@ -91,40 +91,6 @@ async function bootstrap() {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const createPieChartSVG = (slices, { size = 56, stroke = "white" } = {}) => {
|
|
95
|
-
const radius = size / 2;
|
|
96
|
-
const center = radius;
|
|
97
|
-
|
|
98
|
-
const total = slices.reduce((sum, slice) => sum + Math.max(0, slice.value), 0);
|
|
99
|
-
if (!Number.isFinite(total) || total <= 0) {
|
|
100
|
-
return `<svg width="${size}" height="${size}"></svg>`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let currentAngle = -Math.PI / 2;
|
|
104
|
-
const segments = slices
|
|
105
|
-
.filter((slice) => Number.isFinite(slice.value) && slice.value > 0)
|
|
106
|
-
.map((slice) => {
|
|
107
|
-
const angle = (slice.value / total) * Math.PI * 2;
|
|
108
|
-
const endAngle = currentAngle + angle;
|
|
109
|
-
const largeArc = angle > Math.PI ? 1 : 0;
|
|
110
|
-
const startX = center + radius * Math.cos(currentAngle);
|
|
111
|
-
const startY = center + radius * Math.sin(currentAngle);
|
|
112
|
-
const endX = center + radius * Math.cos(endAngle);
|
|
113
|
-
const endY = center + radius * Math.sin(endAngle);
|
|
114
|
-
const path = [
|
|
115
|
-
`M ${center} ${center}`,
|
|
116
|
-
`L ${startX.toFixed(2)} ${startY.toFixed(2)}`,
|
|
117
|
-
`A ${radius} ${radius} 0 ${largeArc} 1 ${endX.toFixed(2)} ${endY.toFixed(2)}`,
|
|
118
|
-
"Z",
|
|
119
|
-
].join(" ");
|
|
120
|
-
currentAngle = endAngle;
|
|
121
|
-
return `<path d="${path}" fill="${slice.color}" stroke="${stroke}" stroke-width="1"></path>`;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const content = segments.join("");
|
|
125
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">${content}</svg>`;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
94
|
const { group, regularLayer, tweenLayer, cartogramLayer, updateMorphFactor } =
|
|
129
95
|
await createLeafletMorphLayers({
|
|
130
96
|
morpher,
|
|
@@ -168,11 +134,34 @@ async function bootstrap() {
|
|
|
168
134
|
return null;
|
|
169
135
|
}
|
|
170
136
|
|
|
137
|
+
const radius = 26;
|
|
138
|
+
const size = 52;
|
|
139
|
+
const center = radius;
|
|
140
|
+
let currentAngle = -Math.PI / 2;
|
|
141
|
+
const total = slices.reduce((sum, s) => sum + s.value, 0);
|
|
142
|
+
const segments = slices.map((slice) => {
|
|
143
|
+
const angle = (slice.value / total) * Math.PI * 2;
|
|
144
|
+
const endAngle = currentAngle + angle;
|
|
145
|
+
const largeArc = angle > Math.PI ? 1 : 0;
|
|
146
|
+
const startX = center + radius * Math.cos(currentAngle);
|
|
147
|
+
const startY = center + radius * Math.sin(currentAngle);
|
|
148
|
+
const endX = center + radius * Math.cos(endAngle);
|
|
149
|
+
const endY = center + radius * Math.sin(endAngle);
|
|
150
|
+
const path = [
|
|
151
|
+
`M ${center} ${center}`,
|
|
152
|
+
`L ${startX.toFixed(2)} ${startY.toFixed(2)}`,
|
|
153
|
+
`A ${radius} ${radius} 0 ${largeArc} 1 ${endX.toFixed(2)} ${endY.toFixed(2)}`,
|
|
154
|
+
"Z",
|
|
155
|
+
].join(" ");
|
|
156
|
+
currentAngle = endAngle;
|
|
157
|
+
return `<path d="${path}" fill="${slice.color}" stroke="white" stroke-width="1"></path>`;
|
|
158
|
+
});
|
|
159
|
+
|
|
171
160
|
return {
|
|
172
|
-
html:
|
|
161
|
+
html: `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">${segments.join("")}</svg>`,
|
|
173
162
|
className: "pie-chart-marker",
|
|
174
|
-
iconSize: [
|
|
175
|
-
iconAnchor: [
|
|
163
|
+
iconSize: [size, size],
|
|
164
|
+
iconAnchor: [radius, radius],
|
|
176
165
|
};
|
|
177
166
|
},
|
|
178
167
|
});
|
|
@@ -223,3 +212,5 @@ async function bootstrap() {
|
|
|
223
212
|
}
|
|
224
213
|
|
|
225
214
|
bootstrap();
|
|
215
|
+
|
|
216
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Zoom-Scaling Glyphs Demo - geo-morpher</title>
|
|
8
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
9
|
+
<style>
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
17
|
+
background: #0f172a;
|
|
18
|
+
color: #e2e8f0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
#map { width: 100vw; height: 100vh; }
|
|
22
|
+
.controls {
|
|
23
|
+
position: absolute; top: 20px; right: 20px; background: rgba(15, 23, 42, 0.95);
|
|
24
|
+
padding: 20px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
|
25
|
+
z-index: 1000; max-width: 320px; backdrop-filter: blur(10px); border: 1px solid rgba(148, 163, 184, 0.1);
|
|
26
|
+
}
|
|
27
|
+
h1 { font-size: 1.25rem; font-weight: 700; margin-bottom: 8px; color: #f1f5f9; }
|
|
28
|
+
.subtitle { font-size: 0.875rem; color: #94a3b8; margin-bottom: 20px; line-height: 1.4; }
|
|
29
|
+
.control-group { margin-bottom: 20px; }
|
|
30
|
+
.control-group:last-child { margin-bottom: 0; }
|
|
31
|
+
label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 8px; color: #cbd5e1; }
|
|
32
|
+
input[type="range"] {
|
|
33
|
+
width: 100%; height: 6px; background: #334155; border-radius: 3px; outline: none; -webkit-appearance: none;
|
|
34
|
+
}
|
|
35
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
36
|
+
-webkit-appearance: none; appearance: none; width: 18px; height: 18px; background: #22c55e;
|
|
37
|
+
border-radius: 50%; cursor: pointer; box-shadow: 0 2px 8px rgba(34, 197, 94, 0.4);
|
|
38
|
+
}
|
|
39
|
+
input[type="range"]::-moz-range-thumb {
|
|
40
|
+
width: 18px; height: 18px; background: #22c55e; border-radius: 50%; cursor: pointer; border: none;
|
|
41
|
+
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.4);
|
|
42
|
+
}
|
|
43
|
+
.value-display { display: inline-block; margin-left: 8px; font-weight: 700; color: #22c55e; font-variant-numeric: tabular-nums; }
|
|
44
|
+
.checkbox-group { display: flex; align-items: center; margin-top: 12px; }
|
|
45
|
+
.checkbox-group input[type="checkbox"] { margin-right: 8px; width: 18px; height: 18px; cursor: pointer; }
|
|
46
|
+
.checkbox-group label { margin: 0; cursor: pointer; font-weight: 500; }
|
|
47
|
+
.info-box { background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 8px; padding: 12px; font-size: 0.875rem; line-height: 1.5; color: #a7f3d0; }
|
|
48
|
+
.status { position: absolute; bottom: 20px; left: 20px; background: rgba(15, 23, 42, 0.95);
|
|
49
|
+
padding: 12px 16px; border-radius: 8px; font-size: 0.875rem; z-index: 1000; backdrop-filter: blur(10px);
|
|
50
|
+
border: 1px solid rgba(148, 163, 184, 0.1); }
|
|
51
|
+
.waffle-glyph { pointer-events: none; }
|
|
52
|
+
.waffle-glyph rect { transition: fill 0.2s ease; }
|
|
53
|
+
.legend { position: absolute; bottom: 20px; right: 20px; background: rgba(15, 23, 42, 0.95); padding: 16px; border-radius: 8px; z-index: 1000; backdrop-filter: blur(10px); border: 1px solid rgba(148, 163, 184, 0.1); }
|
|
54
|
+
.legend h3 { font-size: 0.875rem; font-weight: 600; margin-bottom: 12px; color: #f1f5f9; }
|
|
55
|
+
.legend-item { display: flex; align-items: center; margin-bottom: 8px; font-size: 0.8rem; }
|
|
56
|
+
.legend-item:last-child { margin-bottom: 0; }
|
|
57
|
+
.legend-square { width: 16px; height: 16px; margin-right: 8px; border: 1px solid rgba(255, 255, 255, 0.2); }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
|
|
61
|
+
<body>
|
|
62
|
+
<div id="map"></div>
|
|
63
|
+
|
|
64
|
+
<div class="controls">
|
|
65
|
+
<h1>Zoom-Scaling Glyphs</h1>
|
|
66
|
+
<p class="subtitle">Waffle charts that resize with map zoom level</p>
|
|
67
|
+
|
|
68
|
+
<div class="control-group">
|
|
69
|
+
<label>
|
|
70
|
+
Morph Factor: <span class="value-display" id="factorValue">0.00</span>
|
|
71
|
+
</label>
|
|
72
|
+
<input type="range" id="morphFactor" min="0" max="1" step="0.01" value="0">
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="control-group">
|
|
76
|
+
<div class="checkbox-group">
|
|
77
|
+
<input type="checkbox" id="scaleToggle" checked>
|
|
78
|
+
<label for="scaleToggle">Scale glyphs with zoom</label>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="info-box">
|
|
83
|
+
<strong>Try it:</strong> Zoom in/out to see waffle charts resize to match cartogram polygons. Toggle scaling
|
|
84
|
+
to compare behaviors.
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="status" id="status">Initializing...</div>
|
|
89
|
+
|
|
90
|
+
<div class="legend">
|
|
91
|
+
<h3>Waffle Chart Legend</h3>
|
|
92
|
+
<div class="legend-item">
|
|
93
|
+
<div class="legend-square" style="background: #4e79a7;"></div>
|
|
94
|
+
<span>Filled (population ratio)</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="legend-item">
|
|
97
|
+
<div class="legend-square" style="background: #e0e0e0;"></div>
|
|
98
|
+
<span>Empty</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<script type="importmap">
|
|
103
|
+
{
|
|
104
|
+
"imports": {
|
|
105
|
+
"leaflet": "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/+esm",
|
|
106
|
+
"npm:leaflet": "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/+esm",
|
|
107
|
+
"@turf/turf": "https://esm.sh/@turf/turf@6.5.0?bundle",
|
|
108
|
+
"flubber": "https://esm.sh/flubber@0.4.2?bundle",
|
|
109
|
+
"lodash/isEmpty.js": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/isEmpty.js",
|
|
110
|
+
"lodash/cloneDeep.js": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/cloneDeep.js",
|
|
111
|
+
"lodash/keyBy.js": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/keyBy.js",
|
|
112
|
+
"lodash/mapValues.js": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/mapValues.js"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
<script type="module" src="./zoom-scaling-glyphs.js"></script>
|
|
117
|
+
</body>
|
|
118
|
+
|
|
119
|
+
</html>
|
|
120
|
+
|
|
121
|
+
|