@rcpch/imd-map 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@rcpch/imd-map` will be documented in this file.
4
+
5
+ This project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ---
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] — 2026-04-20
12
+
13
+ ### Added
14
+
15
+ - `createImdMap(options)` — initializes a MapLibre GL choropleth map with IMD vector tiles.
16
+ - Nation and era selection via `setNation`, `setEra`, and `setView`.
17
+ - Era resolution rules: all-UK always uses 2011 era; England supports 2011 and 2021; Wales, Scotland, and Northern Ireland are fixed to 2011.
18
+ - Runtime style overrides for choropleth colors, fill opacity, border colors, patient circle styling, lead-centre styling, and tooltip labels/colors.
19
+ - `setPatients` — accepts plain record arrays, GeoJSON FeatureCollection, or Feature arrays.
20
+ - `setLeadCentre` — accepts a plain coordinate object or GeoJSON point feature.
21
+ - Local authority and health boundary overlays (NHSER, ICB, LHB) via `setOverlayVisibility` and startup flags.
22
+ - Collapsible corner legend with clickable overlay toggles, compact key, and per-row visibility controls.
23
+ - Patient group color mapping support via `style.patients.colorByGroup`.
24
+ - MIT `LICENSE` file.
25
+ - GitHub Actions CI workflow running `npm test` and `npm run build` on push/PR.
26
+ - Built-in hover tooltip showing area name, IMD decile, and nation.
27
+ - `onAreaHover`, `onAreaClick`, `onViewChange`, and `onWarning` event hooks.
28
+ - ESM build (`dist/index.esm.js`) — maplibre-gl as peer dependency.
29
+ - Self-contained IIFE build (`dist/umd/rcpch-imd-map.min.js`) — maplibre-gl bundled.
30
+ - TypeScript declarations (`dist/index.d.ts`).
31
+ - Unit tests for era resolver, coordinate validation, property alias lookup, and patient input normalization.
32
+ - Standalone HTML examples covering basic usage and patient overlay.
33
+
34
+ ### Changed
35
+
36
+ - `fitToData` now fits to all plotted points (patients + lead centre), with optional padding for multi-point bounds.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RCPCH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,417 @@
1
+ # @rcpch/imd-map
2
+
3
+ A browser-first UK deprivation map library. Renders IMD choropleth tiles using [MapLibre GL JS](https://maplibre.org) with optional patient scatter and lead-centre overlays.
4
+
5
+ Authored in TypeScript. Consuming applications only need plain JavaScript.
6
+
7
+ Maintainers and coding agents: see `AGENTS.md` for project structure, test locations, tile contracts, and release workflow.
8
+
9
+ Upstream tile/data source: [`rcpch/rcpch-census-platform`](https://github.com/rcpch/rcpch-census-platform)
10
+
11
+ Live demo: [rcpch.github.io/rcpch-census-platform/](https://rcpch.github.io/rcpch-census-platform/)
12
+
13
+ Release integrity: run `npm pack --json` for npm `shasum` / `integrity`, or `npm run release:checksums` after packing to generate `release-checksums.json` with SHA hashes for the tarball and built bundles.
14
+
15
+ ## CI and release automation
16
+
17
+ - Pushes to `main` and all pull requests run automated validation via `.github/workflows/ci.yml`:
18
+ - `npm ci`
19
+ - `npm test`
20
+ - `npm run build`
21
+ - Publishing is handled by `.github/workflows/release.yml` when a GitHub release is published:
22
+ - `npm ci`
23
+ - `npm test`
24
+ - `npm run build`
25
+ - `npm publish --provenance --access public`
26
+
27
+ For npm publish to succeed, configure npm Trusted Publishing for this GitHub repository/package pair.
28
+
29
+ ---
30
+
31
+ ## What problem does this solve?
32
+
33
+ Standard server-side mapping tools (Plotly, Folium) render finished map HTML on the server. For a vector tile choropleth—where the browser streams tiles from a tile server and renders them using WebGL—this is the wrong boundary. This library moves all map internals to the browser and lets the backend focus on preparing plain data.
34
+
35
+ ---
36
+
37
+ ## Quick start — npm + bundler
38
+
39
+ ```bash
40
+ npm install @rcpch/imd-map
41
+ # maplibre-gl is a peer dependency
42
+ npm install maplibre-gl
43
+ ```
44
+
45
+ ```js
46
+ import 'maplibre-gl/dist/maplibre-gl.css';
47
+ import { createImdMap } from '@rcpch/imd-map';
48
+
49
+ const map = createImdMap({
50
+ container: 'map',
51
+ tilesBaseUrl: 'https://your-tile-server.example.com',
52
+ initialNation: 'all',
53
+ });
54
+
55
+ map.setPatients([
56
+ { id: 'p1', lat: 51.5074, lon: -0.1278 },
57
+ { id: 'p2', lat: 53.4808, lon: -2.2426 },
58
+ ]);
59
+
60
+ map.setLeadCentre({ lat: 51.5202, lon: -0.1049, label: 'Lead Centre' });
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick start — static HTML (CDN)
66
+
67
+ The UMD bundle includes MapLibre GL. No separate script tag required.
68
+
69
+ ```html
70
+ <div id="map" style="height: 600px"></div>
71
+
72
+ <script src="https://cdn.jsdelivr.net/npm/@rcpch/imd-map@0.1.0/dist/umd/rcpch-imd-map.min.js"></script>
73
+ <script>
74
+ const map = RcpchImdMap.createImdMap({
75
+ container: 'map',
76
+ tilesBaseUrl: 'https://your-tile-server.example.com',
77
+ initialNation: 'all',
78
+ style: {
79
+ tooltip: { areaLabel: 'Area', decileLabel: 'IMD decile' },
80
+ },
81
+ });
82
+ </script>
83
+ ```
84
+
85
+ If you need release-grade checksum verification for the published bundle or packed tarball, generate hashes locally with `npm pack --json` and `npm run release:checksums` before publishing.
86
+
87
+ ---
88
+
89
+ ## Quick start — Django / HTMX template
90
+
91
+ The backend prepares plain data. The template embeds it with `json_script`. A small script initializes the map.
92
+
93
+ **Django view:**
94
+
95
+ ```python
96
+ context = {
97
+ "map_payload": {
98
+ "patients": [
99
+ {"id": "p1", "lat": 51.5074, "lon": -0.1278},
100
+ {"id": "p2", "lat": 53.4808, "lon": -2.2426},
101
+ ],
102
+ "leadCentre": {"lat": 51.5202, "lon": -0.1049, "label": "Lead Centre"},
103
+ "style": {
104
+ "decileColors": ["#7a0036","#a3004b","#c92e6f","#de5f92","#eba0ba",
105
+ "#f3bfd0","#f8d7e2","#fce8ef","#fff0f6","#fff5f8"],
106
+ "boundaryColor": "#0d0d58",
107
+ },
108
+ }
109
+ }
110
+ ```
111
+
112
+ **Django template (partial):**
113
+
114
+ ```html
115
+ {% load static %}
116
+
117
+ {% if error %}
118
+ <div class="alert alert-danger">{{ error }}</div>
119
+ {% elif info %}
120
+ <div class="alert alert-info">{{ info }}</div>
121
+ {% else %}
122
+ <div id="organisation-cases-map" style="width:100%;height:32rem;"></div>
123
+ {{ map_payload|json_script:"organisation-cases-map-payload" }}
124
+ {% endif %}
125
+
126
+ <script src="{% static 'vendor/rcpch-imd-map.min.js' %}"></script>
127
+ <script>
128
+ (function () {
129
+ var el = document.getElementById('organisation-cases-map');
130
+ if (!el) return;
131
+
132
+ // Destroy any previous instance to prevent leaks on HTMX swaps
133
+ if (window._npdaMapInstance) {
134
+ window._npdaMapInstance.destroy();
135
+ window._npdaMapInstance = null;
136
+ }
137
+
138
+ var payload = JSON.parse(
139
+ document.getElementById('organisation-cases-map-payload').textContent
140
+ );
141
+
142
+ // Django-safe literal tokens for the map library's tooltip interpolation
143
+ var token = {
144
+ patientLabel: '{% templatetag openvariable %}patientLabel{% templatetag closevariable %}',
145
+ id: '{% templatetag openvariable %}id{% templatetag closevariable %}',
146
+ leadCentreLabel: '{% templatetag openvariable %}leadCentreLabel{% templatetag closevariable %}',
147
+ label: '{% templatetag openvariable %}label{% templatetag closevariable %}'
148
+ };
149
+
150
+ var map = RcpchImdMap.createImdMap({
151
+ container: 'organisation-cases-map',
152
+ tilesBaseUrl: window.RCPCH_DEPRIVATION_TILES_URL,
153
+ initialNation: 'all',
154
+ style: {
155
+ choropleth: { fallbackDecileColors: payload.style.decileColors },
156
+ boundaries: { localAuthorityColor: payload.style.boundaryColor },
157
+ tooltip: {
158
+ areaLabel: 'Local area',
159
+ patientLabel: 'Child',
160
+ patientTooltipText: token.patientLabel + ': ' + token.id,
161
+ leadCentreTooltipText: token.leadCentreLabel + ': ' + token.label,
162
+ backgroundColor: '#0d0d58',
163
+ textColor: '#ffffff',
164
+ },
165
+ },
166
+ onWarning: function (w) {
167
+ console.warn('[rcpch-imd-map]', w.code, w.message);
168
+ },
169
+ });
170
+
171
+ map.setPatients(payload.patients);
172
+ map.setLeadCentre(payload.leadCentre);
173
+
174
+ window._npdaMapInstance = map;
175
+ })();
176
+ </script>
177
+ ```
178
+
179
+ **Copy the built UMD bundle into your Django static directory:**
180
+
181
+ ```bash
182
+ # From the library repo (after npm run build)
183
+ cp dist/umd/rcpch-imd-map.min.js /path/to/npda/project/static/vendor/
184
+ ```
185
+
186
+ Set `window.RCPCH_DEPRIVATION_TILES_URL` before the script runs, for example in your base template:
187
+
188
+ ```html
189
+ <script>window.RCPCH_DEPRIVATION_TILES_URL = "{{ TILES_BASE_URL }}";</script>
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Runtime tile configuration
195
+
196
+ Tile URL resolution precedence:
197
+
198
+ 1. `tilesBaseUrl` option passed to `createImdMap`.
199
+ 2. `window.RCPCH_DEPRIVATION_TILES_URL` global (for static/script-tag use).
200
+ 3. Nothing — a warning is logged and choropleth tiles will not load.
201
+
202
+ The library source contains **no hardcoded tile URLs**.
203
+
204
+ ---
205
+
206
+ ## Nation and era rules
207
+
208
+ | Nation | Requested era | Effective era |
209
+ |---|---|---|
210
+ | `all` | any | always `2011` |
211
+ | `england` | `2011` or `2021` | as requested |
212
+ | `wales` | any | always `2011` |
213
+ | `scotland` | any | always `2011` |
214
+ | `northern_ireland` | any | always `2011` |
215
+
216
+ When the effective era differs from the requested era, `onWarning` is called with code `ERA_OVERRIDE`.
217
+
218
+ ---
219
+
220
+ ## Styling
221
+
222
+ All style options are optional and merge on top of built-in RCPCH defaults.
223
+
224
+ ```js
225
+ createImdMap({
226
+ container: 'map',
227
+ tilesBaseUrl: '...',
228
+ style: {
229
+ choropleth: {
230
+ // Auto-generate a 10-step ramp from one base color per nation
231
+ baseColorByNation: {
232
+ england: '#d7191c',
233
+ wales: '#1a9641',
234
+ scotland: '#2b83ba',
235
+ northern_ireland: '#7f7f7f',
236
+ },
237
+ // 10 hex colors, index 0 = decile 1 (most deprived)
238
+ fallbackDecileColors: ['#7a0036', ...],
239
+ fillOpacity: 0.7,
240
+ borderColor: '#ffffff',
241
+ borderWidth: 0.5,
242
+ },
243
+ boundaries: {
244
+ localAuthorityColor: '#0d0d58',
245
+ icbColor: '#3d3d3d',
246
+ localAuthorityWidth: 1,
247
+ },
248
+ patients: {
249
+ circleColor: '#0d0d58',
250
+ circleRadius: 5,
251
+ circleOpacity: 0.8,
252
+ },
253
+ leadCentre: {
254
+ color: '#e00087',
255
+ radius: 10,
256
+ },
257
+ legend: {
258
+ backgroundColor: '#ffffff',
259
+ textColor: '#0d0d58',
260
+ borderColor: '#d8dde6',
261
+ borderRadius: 8,
262
+ width: 220,
263
+ toggleOnColor: '#0d0d58',
264
+ toggleOffColor: '#6b7280',
265
+ },
266
+ tooltip: {
267
+ backgroundColor: '#0d0d58',
268
+ textColor: '#ffffff',
269
+ areaLabel: 'Area',
270
+ decileLabel: 'IMD decile',
271
+ nationLabel: 'Nation',
272
+ patientLabel: 'Patient',
273
+ leadCentreLabel: 'Lead centre',
274
+ patientTooltipText: '{{patientLabel}}',
275
+ leadCentreTooltipText: '{{leadCentreLabel}}: {{label}}',
276
+ },
277
+ },
278
+ });
279
+ ```
280
+
281
+ ### Tooltip templates
282
+
283
+ `patientTooltipText` and `leadCentreTooltipText` support `{{token}}` interpolation.
284
+
285
+ If you are writing inline JavaScript inside a Django template, Django will try to
286
+ evaluate `{{...}}` first. Use one of these patterns so the map library still
287
+ receives literal tokens:
288
+
289
+ 1. Use `{% templatetag openvariable %}` and `{% templatetag closevariable %}`
290
+ to emit literal `{{` and `}}` (shown in the Django example above).
291
+ 2. Wrap only the relevant JavaScript block in `{% verbatim %}...{% endverbatim %}`
292
+ when you do not need Django variable interpolation inside that block.
293
+ 3. Build the token string server-side (for example in your view context) and pass
294
+ it in your JSON payload.
295
+
296
+ **Patient tokens** (`patientTooltipText`):
297
+
298
+ | Token | Value |
299
+ |---|---|
300
+ | `{{patientLabel}}` | The `patientLabel` style option (default `"Patient"`) |
301
+ | `{{id}}` | The `id` field from `setPatients([{ id, lat, lon }])` |
302
+ | `{{group}}` | The `group` field from `setPatients([{ id, lat, lon, group }])` |
303
+
304
+ Examples:
305
+
306
+ ```js
307
+ // Show the patient id
308
+ patientTooltipText: 'Patient ID: {{id}}'
309
+
310
+ // Show a custom label with id
311
+ patientTooltipText: '{{patientLabel}} — ref: {{id}}'
312
+
313
+ // Show group
314
+ patientTooltipText: 'Group: {{group}}'
315
+ ```
316
+
317
+ **Lead-centre tokens** (`leadCentreTooltipText`):
318
+
319
+ | Token | Value |
320
+ |---|---|
321
+ | `{{leadCentreLabel}}` | The `leadCentreLabel` style option (default `"Lead centre"`) |
322
+ | `{{label}}` | The `label` field from `setLeadCentre({ label, lat, lon })` |
323
+
324
+ Style can also be updated at runtime:
325
+
326
+ ```js
327
+ map.setStyle({ tooltip: { areaLabel: 'Local area' } });
328
+ ```
329
+
330
+ ---
331
+
332
+ ## API reference
333
+
334
+ ### `createImdMap(options)` → `ImdMapInstance`
335
+
336
+ | Option | Type | Default | Description |
337
+ |---|---|---|---|
338
+ | `container` | `string \| HTMLElement` | — | DOM element ID or element reference |
339
+ | `tilesBaseUrl` | `string` | — | Base URL of the tile server |
340
+ | `initialNation` | `Nation` | `'all'` | Starting nation filter |
341
+ | `initialEra` | `Era` | `'2021'` | Requested era (may be overridden) |
342
+ | `enableLocalAuthorityOverlay` | `boolean` | `false` | Show local authority boundary overlay at startup |
343
+ | `enableHealthOverlays` | `boolean` | `false` | Show NHSER, ICB, and LHB boundary overlays at startup |
344
+ | `showLegend` | `boolean` | `true` | Show the collapsible legend control |
345
+ | `legendPosition` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'top-right'` | Legend control position inside map container |
346
+ | `legendCollapsed` | `boolean` | `false` | Start with legend content collapsed |
347
+ | `legendTitle` | `string` | `'Map layers'` | Legend header title text |
348
+ | `showLegendLocalAuthority` | `boolean` | `true` | Show/hide local authority legend toggle row |
349
+ | `showLegendNhser` | `boolean` | `true` | Show/hide NHS England regions legend toggle row |
350
+ | `showLegendIcb` | `boolean` | `true` | Show/hide ICB legend toggle row |
351
+ | `showLegendLhb` | `boolean` | `true` | Show/hide local health boards legend toggle row |
352
+ | `mapStyleUrl` | `string` | Carto Positron | MapLibre base style URL |
353
+ | `center` | `[lon, lat]` | UK center | Initial map center |
354
+ | `zoom` | `number` | `5` | Initial zoom level |
355
+ | `style` | `MapStyleOptions` | RCPCH defaults | Visual style overrides |
356
+ | `onViewChange` | `function` | — | Called when nation or era changes |
357
+ | `onAreaHover` | `function` | — | Called on choropleth feature hover |
358
+ | `onAreaClick` | `function` | — | Called on choropleth feature click |
359
+ | `onWarning` | `function` | — | Called for non-fatal issues |
360
+
361
+ ### Instance methods
362
+
363
+ | Method | Description |
364
+ |---|---|
365
+ | `setView({ nation?, era? })` | Update nation and/or era |
366
+ | `setNation(nation)` | Change the nation filter |
367
+ | `setEra(era)` | Change the requested era |
368
+ | `setStyle(style)` | Update visual style at runtime |
369
+ | `setOverlayVisibility({...})` | Show/hide boundary overlays (`localAuthority`, `nhser`, `icb`, `lhb`) |
370
+ | `setPatients(data, options?)` | Set patient scatter data |
371
+ | `clearPatients()` | Remove patient overlay |
372
+ | `setLeadCentre(data, options?)` | Set lead-centre marker |
373
+ | `clearLeadCentre()` | Remove lead-centre marker |
374
+ | `getState()` | Return current map state snapshot |
375
+ | `resize()` | Trigger MapLibre resize (use after container resize) |
376
+ | `fitToData(options?)` | Fit to lead centre and/or patient points. Uses bounds with default 50px padding for multi-point data; single-point fallback uses zoom 6 unless overridden. |
377
+ | `destroy()` | Remove all layers, sources, listeners, and map instance |
378
+
379
+ Legend notes:
380
+
381
+ - The legend is collapsible and includes clickable rows to toggle overlays.
382
+ - A compact key is shown below toggles with boundary line swatches and an IMD decile color ramp.
383
+ - Rows can be hidden per overlay type using `showLegendLocalAuthority`, `showLegendNhser`, `showLegendIcb`, and `showLegendLhb`.
384
+ - Nation-specific rows stay visible but are disabled when not applicable (for example, `England only` or `Wales only`).
385
+
386
+ ---
387
+
388
+ ## HTMX / partial swap cleanup
389
+
390
+ When a map container is replaced by an HTMX swap, call `destroy()` first to prevent memory leaks:
391
+
392
+ ```js
393
+ document.addEventListener('htmx:beforeSwap', function (e) {
394
+ if (window._npdaMapInstance && e.detail.target.contains(document.getElementById('organisation-cases-map'))) {
395
+ window._npdaMapInstance.destroy();
396
+ window._npdaMapInstance = null;
397
+ }
398
+ });
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Patient data format
404
+
405
+ Accepted as:
406
+
407
+ - Array of plain objects: `[{ id, lat, lon, group?, ...extraProps }]`
408
+ - GeoJSON `FeatureCollection<Point>`
409
+ - Array of GeoJSON `Feature<Point>`
410
+
411
+ Invalid records are skipped and surfaced via `onWarning`. Pass `{ strict: true }` as the second argument to `setPatients` to throw on the first invalid record instead.
412
+
413
+ ---
414
+
415
+ ## License
416
+
417
+ MIT
@@ -0,0 +1,225 @@
1
+ import { FeatureCollection, Point, Feature } from 'geojson';
2
+
3
+ type Nation = 'all' | 'england' | 'wales' | 'scotland' | 'northern_ireland';
4
+ type Era = '2011' | '2021';
5
+ interface ChoroplethStyleOptions {
6
+ /** Per-nation arrays of 10 hex colors, index 0 = decile 1 (most deprived). */
7
+ decileColorsByNation?: Partial<Record<Nation, string[]>>;
8
+ /**
9
+ * Per-nation base colors used to auto-generate a 10-step decile ramp.
10
+ * If both this and decileColorsByNation are supplied, decileColorsByNation wins.
11
+ */
12
+ baseColorByNation?: Partial<Record<Nation, string>>;
13
+ /** Fallback color array used when no nation-specific ramp is provided. */
14
+ fallbackDecileColors?: string[];
15
+ fillOpacity?: number;
16
+ borderColor?: string;
17
+ borderWidth?: number;
18
+ hoverBorderColor?: string;
19
+ hoverBorderWidth?: number;
20
+ }
21
+ interface BoundaryStyleOptions {
22
+ localAuthorityColor?: string;
23
+ localAuthorityWidth?: number;
24
+ nhserColor?: string;
25
+ nhserWidth?: number;
26
+ icbColor?: string;
27
+ icbWidth?: number;
28
+ lhbColor?: string;
29
+ lhbWidth?: number;
30
+ }
31
+ interface PatientStyleOptions {
32
+ circleColor?: string;
33
+ circleRadius?: number;
34
+ circleStrokeColor?: string;
35
+ circleStrokeWidth?: number;
36
+ circleOpacity?: number;
37
+ /** Map of group string → color hex for per-group coloring. */
38
+ colorByGroup?: Record<string, string>;
39
+ }
40
+ interface LeadCentreStyleOptions {
41
+ color?: string;
42
+ radius?: number;
43
+ strokeColor?: string;
44
+ strokeWidth?: number;
45
+ }
46
+ interface TooltipStyleOptions {
47
+ backgroundColor?: string;
48
+ textColor?: string;
49
+ borderColor?: string;
50
+ borderRadius?: number;
51
+ /** Label for the area name row. Default: "Area". */
52
+ areaLabel?: string;
53
+ /** Label for the IMD decile row. Default: "IMD decile". */
54
+ decileLabel?: string;
55
+ /** Label for the nation row. Default: "Nation". */
56
+ nationLabel?: string;
57
+ /** Label used in patient hover tooltips. Default: "Patient". */
58
+ patientLabel?: string;
59
+ /** Label used in lead-centre hover tooltip. Default: "Lead centre". */
60
+ leadCentreLabel?: string;
61
+ /**
62
+ * Patient tooltip content template.
63
+ * Supports token interpolation, e.g. "{{patientLabel}}" or "{{id}}".
64
+ */
65
+ patientTooltipText?: string;
66
+ /**
67
+ * Lead-centre tooltip content template.
68
+ * Supports token interpolation, e.g. "{{leadCentreLabel}}: {{label}}".
69
+ */
70
+ leadCentreTooltipText?: string;
71
+ }
72
+ type LegendPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
73
+ interface LegendStyleOptions {
74
+ backgroundColor?: string;
75
+ textColor?: string;
76
+ borderColor?: string;
77
+ borderRadius?: number;
78
+ fontSize?: number;
79
+ fontFamily?: string;
80
+ width?: number;
81
+ boxShadow?: string;
82
+ toggleOnColor?: string;
83
+ toggleOffColor?: string;
84
+ }
85
+ interface MapStyleOptions {
86
+ choropleth?: ChoroplethStyleOptions;
87
+ boundaries?: BoundaryStyleOptions;
88
+ patients?: PatientStyleOptions;
89
+ leadCentre?: LeadCentreStyleOptions;
90
+ tooltip?: TooltipStyleOptions;
91
+ legend?: LegendStyleOptions;
92
+ }
93
+ interface ImdMapState {
94
+ nation: Nation;
95
+ era: Era;
96
+ /** The era actually used for tile fetching — may differ from requested era. */
97
+ effectiveEra: Era;
98
+ hasPatients: boolean;
99
+ hasLeadCentre: boolean;
100
+ overlays: {
101
+ localAuthority: boolean;
102
+ nhser: boolean;
103
+ icb: boolean;
104
+ lhb: boolean;
105
+ };
106
+ }
107
+ interface AreaHoverPayload {
108
+ lsoaCode: string | undefined;
109
+ lsoaName: string | undefined;
110
+ imdDecile: number | undefined;
111
+ nation: string | undefined;
112
+ rawProperties: Record<string, unknown>;
113
+ }
114
+ type AreaClickPayload = AreaHoverPayload;
115
+ interface NormalizedPatientPoint {
116
+ id?: string;
117
+ lon: number;
118
+ lat: number;
119
+ weight?: number;
120
+ group?: string;
121
+ properties?: Record<string, unknown>;
122
+ }
123
+ /** A plain record coming from a Django view or similar server-rendered source. */
124
+ interface PatientRecord {
125
+ id?: string | number;
126
+ lat?: number;
127
+ latitude?: number;
128
+ lon?: number;
129
+ lng?: number;
130
+ longitude?: number;
131
+ weight?: number;
132
+ group?: string;
133
+ [key: string]: unknown;
134
+ }
135
+ type PatientInput = FeatureCollection<Point> | PatientRecord[] | Feature<Point>[];
136
+ interface PatientLayerOptions {
137
+ /** When true, throws on the first invalid record instead of emitting a warning. */
138
+ strict?: boolean;
139
+ }
140
+ interface LeadCentreInput {
141
+ lat?: number;
142
+ latitude?: number;
143
+ lon?: number;
144
+ lng?: number;
145
+ longitude?: number;
146
+ label?: string;
147
+ /** GeoJSON point feature is also accepted. */
148
+ type?: string;
149
+ geometry?: {
150
+ type: 'Point';
151
+ coordinates: [number, number];
152
+ };
153
+ properties?: Record<string, unknown>;
154
+ }
155
+ interface LeadCentreOptions {
156
+ label?: string;
157
+ }
158
+ interface CreateImdMapOptions {
159
+ /** DOM element ID string or an HTMLElement reference. */
160
+ container: string | HTMLElement;
161
+ /** Base URL of the tile server. Required for choropleth rendering. */
162
+ tilesBaseUrl?: string;
163
+ initialNation?: Nation;
164
+ initialEra?: Era;
165
+ showDefaultControls?: boolean;
166
+ enableLocalAuthorityOverlay?: boolean;
167
+ enableHealthOverlays?: boolean;
168
+ showLegend?: boolean;
169
+ legendPosition?: LegendPosition;
170
+ legendCollapsed?: boolean;
171
+ showLegendLocalAuthority?: boolean;
172
+ showLegendNhser?: boolean;
173
+ showLegendIcb?: boolean;
174
+ showLegendLhb?: boolean;
175
+ legendTitle?: string;
176
+ /** MapLibre GL style URL. Defaults to Carto Positron. */
177
+ mapStyleUrl?: string;
178
+ /** Initial map center as [longitude, latitude]. */
179
+ center?: [number, number];
180
+ zoom?: number;
181
+ style?: MapStyleOptions;
182
+ onViewChange?: (view: {
183
+ nation: Nation;
184
+ era: Era;
185
+ effectiveEra: Era;
186
+ }) => void;
187
+ onAreaHover?: (payload: AreaHoverPayload) => void;
188
+ onAreaClick?: (payload: AreaClickPayload) => void;
189
+ onWarning?: (warning: {
190
+ code: string;
191
+ message: string;
192
+ details?: unknown;
193
+ }) => void;
194
+ }
195
+ interface ImdMapInstance {
196
+ setView(input: {
197
+ nation?: Nation;
198
+ era?: Era;
199
+ }): void;
200
+ setNation(nation: Nation): void;
201
+ setEra(era: Era): void;
202
+ setStyle(style: MapStyleOptions): void;
203
+ setOverlayVisibility(input: {
204
+ localAuthority?: boolean;
205
+ nhser?: boolean;
206
+ icb?: boolean;
207
+ lhb?: boolean;
208
+ }): void;
209
+ setPatients(data: PatientInput, options?: PatientLayerOptions): void;
210
+ clearPatients(): void;
211
+ setLeadCentre(data: LeadCentreInput, options?: LeadCentreOptions): void;
212
+ clearLeadCentre(): void;
213
+ getState(): ImdMapState;
214
+ resize(): void;
215
+ /** Fit map to plotted patients and/or lead centre. Uses bounds + optional padding. */
216
+ fitToData(options?: {
217
+ zoom?: number;
218
+ padding?: number;
219
+ }): void;
220
+ destroy(): void;
221
+ }
222
+
223
+ declare function createImdMap(options: CreateImdMapOptions): ImdMapInstance;
224
+
225
+ export { type AreaClickPayload, type AreaHoverPayload, type BoundaryStyleOptions, type ChoroplethStyleOptions, type CreateImdMapOptions, type Era, type ImdMapInstance, type ImdMapState, type LeadCentreInput, type LeadCentreOptions, type LeadCentreStyleOptions, type LegendPosition, type LegendStyleOptions, type MapStyleOptions, type Nation, type NormalizedPatientPoint, type PatientInput, type PatientLayerOptions, type PatientRecord, type PatientStyleOptions, type TooltipStyleOptions, createImdMap };