@rcpch/imd-map 0.3.0 → 0.4.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 CHANGED
@@ -8,6 +8,38 @@ This project adheres to [Semantic Versioning](https://semver.org/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.4.0] — 2026-05-16
12
+
13
+ ### Added
14
+
15
+ - Added `channel_islands` as a supported nation value across the public API (`initialNation`, `setNation`, and nation-aware styling).
16
+ - Added resolver and filter support for `channel_islands` in nation filtering and era resolution.
17
+ - Added automated smoke tests for standalone example pages to verify they are served and include expected map initialization and patient plotting hooks.
18
+
19
+ ### Changed
20
+
21
+ - Updated Channel Islands era behavior to honour the requested era (`2011` or `2021`) while rendering 2024 boundaries in both table families.
22
+ - Included Channel Islands in all-UK nation color matching so all-UK choropleth rendering handles `nation: channel_islands` explicitly.
23
+ - Added default neutral base color wiring for Channel Islands to support no-IMD-data rendering.
24
+
25
+ ### Documentation
26
+
27
+ - Updated README and AGENTS guidance for Channel Islands nation support and era semantics.
28
+
29
+ ## [0.3.1] — 2026-04-28
30
+
31
+ ### Added
32
+
33
+ - Optional tile API key support via `tilesApiKey` and `tilesApiKeyParam` options in `CreateImdMapOptions`.
34
+ - Tile API key query parameters are appended to all choropleth and boundary overlay tile requests.
35
+ - Customizable query parameter name for tile auth (defaults to `api_key`).
36
+ - Unit test coverage for tile URL builder with optional auth and auth propagation across overlays.
37
+
38
+ ### Documentation
39
+
40
+ - Updated README runtime tile configuration section with tile auth options and example usage.
41
+ - Added note that browser-delivered keys are non-secret and primarily useful for operational traffic control (rate limiting, revocation, origin restrictions).
42
+
11
43
  ## [0.3.0] — 2026-04-27
12
44
 
13
45
  ### Changed
package/README.md CHANGED
@@ -201,6 +201,24 @@ Tile URL resolution precedence:
201
201
 
202
202
  The library source contains **no hardcoded tile URLs**.
203
203
 
204
+ Optional tile auth query options:
205
+
206
+ - `tilesApiKey`: appends a query value to all choropleth and overlay tile requests
207
+ - `tilesApiKeyParam`: query parameter name for `tilesApiKey` (default: `api_key`)
208
+
209
+ Example:
210
+
211
+ ```js
212
+ createImdMap({
213
+ container: 'map',
214
+ tilesBaseUrl: 'https://your-tile-server.example.com',
215
+ tilesApiKey: 'switchable-browser-token',
216
+ tilesApiKeyParam: 'key',
217
+ });
218
+ ```
219
+
220
+ This is useful for revoking abusive traffic quickly, but browser-delivered keys must still be treated as non-secret.
221
+
204
222
  ### Overlay boundary tile contract
205
223
 
206
224
  Boundary overlays (local authority, NHSER, ICB, LHB) are requested from schema-qualified table ids and rendered with the same schema-qualified `source-layer` name. Example:
@@ -218,22 +236,23 @@ Bring your own overlay configuration (custom overlay table/layer names via libra
218
236
 
219
237
  The `era` option refers to the boundary year used for the LSOA geography, not the IMD publication year.
220
238
 
221
- For England, the supported pairings are:
239
+ For England and Channel Islands, the supported pairings are:
222
240
 
223
- - `2011` era = 2011 LSOA boundaries + 2019 IMD data
224
- - `2021` era = 2021 LSOA boundaries + 2025 IMD data
241
+ - `2011` era = 2011 LSOA boundaries + 2019 IMD data (England only)
242
+ - `2021` era = 2021 LSOA boundaries + 2025 IMD data (England only); Channel Islands on 2024 boundaries
225
243
 
226
244
  | Nation | Requested era | Effective era |
227
245
  |---|---|---|
228
246
  | `all` | `2011` or `2021` | as requested |
229
247
  | `england` | `2011` or `2021` | as requested |
248
+ | `channel_islands` | `2011` or `2021` | as requested (2024 boundaries in both) |
230
249
  | `wales` | any | always `2011` |
231
250
  | `scotland` | any | always `2011` |
232
251
  | `northern_ireland` | any | always `2011` |
233
252
 
234
253
  When the effective era differs from the requested era, `onWarning` is called with code `ERA_OVERRIDE`.
235
254
 
236
- For all-UK maps, `initialEra: '2021'` now uses the mixed-vintage `uk_master_2021_*` tables: England renders with 2021 LSOA boundaries and 2025 IMD data, while Wales, Scotland, and Northern Ireland continue to render from their existing older datasets within the same UK tile family. Use `initialEra: '2011'` when you want the older England 2011 LSOA + 2019 IMD view alongside the existing Welsh and other nation data.
255
+ For all-UK maps, `initialEra: '2021'` now uses the mixed-vintage `uk_master_2021_*` tables: England renders with 2021 LSOA boundaries and 2025 IMD data, Channel Islands renders with 2024 boundaries, while Wales, Scotland, and Northern Ireland continue to render from their existing older datasets within the same UK tile family. Use `initialEra: '2011'` when you want the older England 2011 LSOA + 2019 IMD view alongside the existing Welsh and other nation data.
237
256
 
238
257
  This means you can instantiate two separate UK maps in the same application, choosing the England boundary/IMD pairing by era:
239
258
 
@@ -276,6 +295,7 @@ createImdMap({
276
295
  wales: '#1a9641',
277
296
  scotland: '#2b83ba',
278
297
  northern_ireland: '#7f7f7f',
298
+ channel_islands: '#d1d5db',
279
299
  },
280
300
  // 10 hex colors, index 0 = decile 1 (most deprived)
281
301
  fallbackDecileColors: ['#7a0036', ...],
@@ -407,6 +427,8 @@ map.setStyle({ tooltip: { areaLabel: 'Local area' } });
407
427
  |---|---|---|---|
408
428
  | `container` | `string \| HTMLElement` | — | DOM element ID or element reference |
409
429
  | `tilesBaseUrl` | `string` | — | Base URL of the tile server |
430
+ | `tilesApiKey` | `string` | — | Optional API key appended to tile URLs as a query parameter |
431
+ | `tilesApiKeyParam` | `string` | `'api_key'` | Query parameter name used for `tilesApiKey` |
410
432
  | `initialNation` | `Nation` | `'all'` | Starting nation filter |
411
433
  | `initialEra` | `Era` | `'2021'` | Requested era (may be overridden) |
412
434
  | `enableLocalAuthorityOverlay` | `boolean` | `false` | Show local authority boundary overlay at startup |
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FeatureCollection, Point, Feature } from 'geojson';
2
2
 
3
- type Nation = 'all' | 'england' | 'wales' | 'scotland' | 'northern_ireland';
3
+ type Nation = 'all' | 'england' | 'wales' | 'scotland' | 'northern_ireland' | 'channel_islands';
4
4
  type Era = '2011' | '2021';
5
5
  interface ChoroplethStyleOptions {
6
6
  /** Per-nation arrays of 10 hex colors, index 0 = decile 1 (most deprived). */
@@ -183,6 +183,10 @@ interface CreateImdMapOptions {
183
183
  container: string | HTMLElement;
184
184
  /** Base URL of the tile server. Required for choropleth rendering. */
185
185
  tilesBaseUrl?: string;
186
+ /** Optional API key appended to tile URLs as a query string parameter. */
187
+ tilesApiKey?: string;
188
+ /** Query parameter name used for tilesApiKey. Default: api_key. */
189
+ tilesApiKeyParam?: string;
186
190
  initialNation?: Nation;
187
191
  initialEra?: Era;
188
192
  showDefaultControls?: boolean;
package/dist/index.esm.js CHANGED
@@ -9,6 +9,8 @@ function resolveEffectiveEra(nation, requestedEra) {
9
9
  return requestedEra;
10
10
  case "england":
11
11
  return requestedEra;
12
+ case "channel_islands":
13
+ return requestedEra;
12
14
  case "wales":
13
15
  case "scotland":
14
16
  case "northern_ireland":
@@ -29,9 +31,13 @@ var ZOOM_TIERS = [
29
31
  function resolveFullTableName(effectiveEra, tier) {
30
32
  return `public.uk_master_${effectiveEra}_${tier}`;
31
33
  }
32
- function buildTileUrl(tilesBaseUrl, fullTableName) {
34
+ function buildTileUrl(tilesBaseUrl, fullTableName, auth) {
33
35
  const base = tilesBaseUrl.replace(/\/$/, "");
34
- return `${base}/${fullTableName}/{z}/{x}/{y}.pbf`;
36
+ const tileUrl = `${base}/${fullTableName}/{z}/{x}/{y}.pbf`;
37
+ if (!auth?.apiKey) return tileUrl;
38
+ const paramName = auth.apiKeyParam?.trim() || "api_key";
39
+ const separator = tileUrl.includes("?") ? "&" : "?";
40
+ return `${tileUrl}${separator}${encodeURIComponent(paramName)}=${encodeURIComponent(auth.apiKey)}`;
35
41
  }
36
42
  function resolveNationFilter(nation) {
37
43
  if (nation === "all") return null;
@@ -60,11 +66,11 @@ function choroplethSourceId(tier) {
60
66
  ZOOM_TIERS.map((t) => choroplethSourceId(t.tier));
61
67
  var PATIENTS_SOURCE_ID = "rcpch-imd-patients";
62
68
  var LEAD_CENTRE_SOURCE_ID = "rcpch-imd-lead-centre";
63
- function addOrUpdateChoroplethSources(map, tilesBaseUrl, effectiveEra) {
69
+ function addOrUpdateChoroplethSources(map, tilesBaseUrl, effectiveEra, tileAuth) {
64
70
  for (const { tier } of ZOOM_TIERS) {
65
71
  const sourceId = choroplethSourceId(tier);
66
72
  const fullTableName = resolveFullTableName(effectiveEra, tier);
67
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
73
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
68
74
  const existing = map.getSource(sourceId);
69
75
  if (existing instanceof VectorTileSource) {
70
76
  existing.setTiles([tileUrl]);
@@ -113,6 +119,7 @@ var DEFAULT_BASE_COLORS_BY_NATION = {
113
119
  wales: "#1a9641",
114
120
  scotland: "#2b83ba",
115
121
  northern_ireland: "#7f7f7f",
122
+ channel_islands: "#d1d5db",
116
123
  all: "#d7191c"
117
124
  };
118
125
  function clamp(value, min, max) {
@@ -230,7 +237,8 @@ var DEFAULT_STYLE = {
230
237
  england: DEFAULT_BASE_COLORS_BY_NATION.england,
231
238
  wales: DEFAULT_BASE_COLORS_BY_NATION.wales,
232
239
  scotland: DEFAULT_BASE_COLORS_BY_NATION.scotland,
233
- northern_ireland: DEFAULT_BASE_COLORS_BY_NATION.northern_ireland
240
+ northern_ireland: DEFAULT_BASE_COLORS_BY_NATION.northern_ireland,
241
+ channel_islands: DEFAULT_BASE_COLORS_BY_NATION.channel_islands
234
242
  },
235
243
  fallbackDecileColors: generateDecileRampFromBaseColor(DEFAULT_BASE_COLORS_BY_NATION.england),
236
244
  fillOpacity: 0.7,
@@ -355,7 +363,7 @@ function buildColorExpression(nation, style) {
355
363
  if (nation !== "all") {
356
364
  return buildDecileColorExpression(getDecileColors(nation, style));
357
365
  }
358
- const perNation = ["england", "wales", "scotland", "northern_ireland"].flatMap((n) => [n, buildDecileColorExpression(getDecileColors(n, style))]);
366
+ const perNation = ["england", "wales", "scotland", "northern_ireland", "channel_islands"].flatMap((n) => [n, buildDecileColorExpression(getDecileColors(n, style))]);
359
367
  return [
360
368
  "match",
361
369
  ["get", "nation"],
@@ -899,13 +907,13 @@ function localAuthoritySourceId(tier) {
899
907
  function localAuthorityLayerId(tier) {
900
908
  return `${LOCAL_AUTHORITY_LAYER_ID}-${tier}`;
901
909
  }
902
- function addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, style) {
910
+ function addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, style, tileAuth) {
903
911
  for (const { tier, minzoom, maxzoom } of ZOOM_TIERS) {
904
912
  const sourceId = localAuthoritySourceId(tier);
905
913
  const layerId = localAuthorityLayerId(tier);
906
914
  const fullTableName = buildMvtLayerName(LOCAL_AUTHORITY_TABLE_PREFIX, tier);
907
915
  const sourceLayer = fullTableName;
908
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
916
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
909
917
  const existing = map.getSource(sourceId);
910
918
  if (existing instanceof VectorTileSource) {
911
919
  existing.setTiles([tileUrl]);
@@ -972,13 +980,13 @@ function overlaySourceId(baseSourceId, tier) {
972
980
  function overlayLayerId(baseLayerId, tier) {
973
981
  return `${baseLayerId}-${tier}`;
974
982
  }
975
- function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, input) {
983
+ function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, input) {
976
984
  for (const { tier, minzoom, maxzoom } of ZOOM_TIERS) {
977
985
  const sourceId = overlaySourceId(input.sourceId, tier);
978
986
  const layerId = overlayLayerId(input.layerId, tier);
979
987
  const fullTableName = buildMvtLayerName(input.tablePrefix, tier);
980
988
  const sourceLayer = fullTableName;
981
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
989
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
982
990
  const existing = map.getSource(sourceId);
983
991
  if (existing instanceof VectorTileSource) {
984
992
  existing.setTiles([tileUrl]);
@@ -1014,8 +1022,8 @@ function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, input) {
1014
1022
  });
1015
1023
  }
1016
1024
  }
1017
- function addOrUpdateNhserOverlay(map, tilesBaseUrl, style) {
1018
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1025
+ function addOrUpdateNhserOverlay(map, tilesBaseUrl, style, tileAuth) {
1026
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1019
1027
  sourceId: NHSER_SOURCE_ID,
1020
1028
  layerId: NHSER_LAYER_ID,
1021
1029
  tablePrefix: NHSER_TABLE_PREFIX,
@@ -1023,8 +1031,8 @@ function addOrUpdateNhserOverlay(map, tilesBaseUrl, style) {
1023
1031
  lineWidth: style.boundaries.nhserWidth ?? 1.5
1024
1032
  });
1025
1033
  }
1026
- function addOrUpdateIcbOverlay(map, tilesBaseUrl, style) {
1027
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1034
+ function addOrUpdateIcbOverlay(map, tilesBaseUrl, style, tileAuth) {
1035
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1028
1036
  sourceId: ICB_SOURCE_ID,
1029
1037
  layerId: ICB_LAYER_ID,
1030
1038
  tablePrefix: ICB_TABLE_PREFIX,
@@ -1032,8 +1040,8 @@ function addOrUpdateIcbOverlay(map, tilesBaseUrl, style) {
1032
1040
  lineWidth: style.boundaries.icbWidth ?? 1
1033
1041
  });
1034
1042
  }
1035
- function addOrUpdateLhbOverlay(map, tilesBaseUrl, style) {
1036
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1043
+ function addOrUpdateLhbOverlay(map, tilesBaseUrl, style, tileAuth) {
1044
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1037
1045
  sourceId: LHB_SOURCE_ID,
1038
1046
  layerId: LHB_LAYER_ID,
1039
1047
  tablePrefix: LHB_TABLE_PREFIX,
@@ -1377,6 +1385,10 @@ function createImdMap(options) {
1377
1385
  if (!tilesBaseUrl) {
1378
1386
  logger.warn("No tilesBaseUrl provided. Choropleth tiles will not load.");
1379
1387
  }
1388
+ const tileAuth = {
1389
+ apiKey: options.tilesApiKey,
1390
+ apiKeyParam: options.tilesApiKeyParam
1391
+ };
1380
1392
  let resolvedStyle = mergeStyle(DEFAULT_STYLE, options.style);
1381
1393
  let state = createInitialState(options.initialNation ?? "all", options.initialEra ?? "2021");
1382
1394
  if (options.enableLocalAuthorityOverlay) {
@@ -1473,22 +1485,22 @@ function createImdMap(options) {
1473
1485
  const canShowIcb = nation === "all" || nation === "england";
1474
1486
  const canShowLhb = nation === "all" || nation === "wales";
1475
1487
  if (state.overlays.localAuthority) {
1476
- addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, resolvedStyle);
1488
+ addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1477
1489
  } else {
1478
1490
  hideLocalAuthorityOverlay(map);
1479
1491
  }
1480
1492
  if (state.overlays.nhser && canShowNhser) {
1481
- addOrUpdateNhserOverlay(map, tilesBaseUrl, resolvedStyle);
1493
+ addOrUpdateNhserOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1482
1494
  } else {
1483
1495
  hideNhserOverlay(map);
1484
1496
  }
1485
1497
  if (state.overlays.icb && canShowIcb) {
1486
- addOrUpdateIcbOverlay(map, tilesBaseUrl, resolvedStyle);
1498
+ addOrUpdateIcbOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1487
1499
  } else {
1488
1500
  hideIcbOverlay(map);
1489
1501
  }
1490
1502
  if (state.overlays.lhb && canShowLhb) {
1491
- addOrUpdateLhbOverlay(map, tilesBaseUrl, resolvedStyle);
1503
+ addOrUpdateLhbOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1492
1504
  } else {
1493
1505
  hideLhbOverlay(map);
1494
1506
  }
@@ -1522,10 +1534,13 @@ function createImdMap(options) {
1522
1534
  });
1523
1535
  }
1524
1536
  logger.debug(`tilesBaseUrl resolved to: "${tilesBaseUrl}"`);
1537
+ if (tileAuth.apiKey) {
1538
+ logger.debug("Tile API key is configured for tile URL requests.");
1539
+ }
1525
1540
  map.on("load", () => {
1526
1541
  mapLoaded = true;
1527
1542
  if (tilesBaseUrl) {
1528
- addOrUpdateChoroplethSources(map, tilesBaseUrl, state.effectiveEra);
1543
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, state.effectiveEra, tileAuth);
1529
1544
  addChoroplethLayers(map, state.nation, state.effectiveEra, resolvedStyle);
1530
1545
  }
1531
1546
  applyOverlayVisibility();
@@ -1576,7 +1591,7 @@ function createImdMap(options) {
1576
1591
  if (mapLoaded && tilesBaseUrl) {
1577
1592
  if (eraChanged) {
1578
1593
  removeChoroplethLayers(map);
1579
- addOrUpdateChoroplethSources(map, tilesBaseUrl, newEffectiveEra);
1594
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, newEffectiveEra, tileAuth);
1580
1595
  addChoroplethLayers(map, newNation, newEffectiveEra, resolvedStyle);
1581
1596
  } else if (nationChanged) {
1582
1597
  updateChoroplethNationFilter(map, newNation);