@rcpch/imd-map 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -271,6 +271,10 @@ createImdMap({
271
271
  nationLabel: 'Nation',
272
272
  patientLabel: 'Patient',
273
273
  leadCentreLabel: 'Lead centre',
274
+ areaTooltipText:
275
+ '<strong>{{areaName}}</strong><br/>' +
276
+ '<span>{{decileLabel}}: {{imdDecile}}</span><br/>' +
277
+ '<span>{{nationLabel}}: {{nation}}</span>',
274
278
  patientTooltipText: '{{patientLabel}}',
275
279
  leadCentreTooltipText: '{{leadCentreLabel}}: {{label}}',
276
280
  },
@@ -280,7 +284,7 @@ createImdMap({
280
284
 
281
285
  ### Tooltip templates
282
286
 
283
- `patientTooltipText` and `leadCentreTooltipText` support `{{token}}` interpolation.
287
+ `areaTooltipText`, `patientTooltipText`, and `leadCentreTooltipText` support `{{token}}` interpolation.
284
288
 
285
289
  If you are writing inline JavaScript inside a Django template, Django will try to
286
290
  evaluate `{{...}}` first. Use one of these patterns so the map library still
@@ -321,6 +325,29 @@ patientTooltipText: 'Group: {{group}}'
321
325
  | `{{leadCentreLabel}}` | The `leadCentreLabel` style option (default `"Lead centre"`) |
322
326
  | `{{label}}` | The `label` field from `setLeadCentre({ label, lat, lon })` |
323
327
 
328
+ **Area tokens** (`areaTooltipText`):
329
+
330
+ | Token | Value |
331
+ |---|---|
332
+ | `{{areaCode}}` | Area code from tile `code` |
333
+ | `{{areaName}}` | Area name from tile `area_name` |
334
+ | `{{areaType}}` | Area type from tile `area_type` (fallback `LSOA`) |
335
+ | `{{nation}}` | Nation from tile `nation` |
336
+ | `{{imdDecile}}` | IMD decile from tile `imd_decile` |
337
+ | `{{imdYear}}` | IMD publication year from tile `imd_year` |
338
+ | `{{boundaryYear}}` | Boundary year from tile `year` |
339
+ | `{{laCode}}` | Local authority code from tile `la_code` |
340
+ | `{{laName}}` | Local authority name from tile `la_name` |
341
+ | `{{laYear}}` | Local authority year from tile `la_year` |
342
+ | `{{nhserCode}}` | NHS England region code from tile `nhser_code` |
343
+ | `{{nhserName}}` | NHS England region name from tile `nhser_name` |
344
+ | `{{icbCode}}` | ICB code from tile `icb_code` |
345
+ | `{{icbName}}` | ICB name from tile `icb_name` |
346
+ | `{{lhbCode}}` | Local health board code from tile `lhb_code` |
347
+ | `{{lhbName}}` | Local health board name from tile `lhb_name` |
348
+ | `{{decileLabel}}` | The `decileLabel` style option |
349
+ | `{{nationLabel}}` | The `nationLabel` style option |
350
+
324
351
  Style can also be updated at runtime:
325
352
 
326
353
  ```js
@@ -353,11 +380,34 @@ map.setStyle({ tooltip: { areaLabel: 'Local area' } });
353
380
  | `center` | `[lon, lat]` | UK center | Initial map center |
354
381
  | `zoom` | `number` | `5` | Initial zoom level |
355
382
  | `style` | `MapStyleOptions` | RCPCH defaults | Visual style overrides |
383
+ | `areaTooltipMode` | `'default' \| 'template' \| 'none'` | `'default'` | Built-in area tooltip, template tooltip, or no built-in area popup |
356
384
  | `onViewChange` | `function` | — | Called when nation or era changes |
357
- | `onAreaHover` | `function` | — | Called on choropleth feature hover |
385
+ | `onAreaHover` | `function` | — | Called on choropleth feature hover (includes pointer `lngLat`) |
358
386
  | `onAreaClick` | `function` | — | Called on choropleth feature click |
359
387
  | `onWarning` | `function` | — | Called for non-fatal issues |
360
388
 
389
+ ### Area tooltip mode
390
+
391
+ ```js
392
+ createImdMap({
393
+ container: 'map',
394
+ tilesBaseUrl: '...',
395
+ areaTooltipMode: 'template',
396
+ style: {
397
+ tooltip: {
398
+ areaTooltipText:
399
+ '<strong>{{areaName}}</strong><br/>' +
400
+ '<span>{{decileLabel}}: {{imdDecile}}</span><br/>' +
401
+ '<span>{{nationLabel}}: {{nation}}</span>',
402
+ },
403
+ },
404
+ });
405
+ ```
406
+
407
+ - `default`: current built-in area tooltip rows.
408
+ - `template`: render `style.tooltip.areaTooltipText` with token interpolation.
409
+ - `none`: no built-in area popup; `onAreaHover`/`onAreaClick` still fire for fully external tooltips.
410
+
361
411
  ### Instance methods
362
412
 
363
413
  | Method | Description |
package/dist/index.d.ts CHANGED
@@ -58,6 +58,11 @@ interface TooltipStyleOptions {
58
58
  patientLabel?: string;
59
59
  /** Label used in lead-centre hover tooltip. Default: "Lead centre". */
60
60
  leadCentreLabel?: string;
61
+ /**
62
+ * Area tooltip content template.
63
+ * Supports token interpolation, e.g. "{{areaName}}", "{{imdDecile}}", "{{nation}}".
64
+ */
65
+ areaTooltipText?: string;
61
66
  /**
62
67
  * Patient tooltip content template.
63
68
  * Supports token interpolation, e.g. "{{patientLabel}}" or "{{id}}".
@@ -105,10 +110,28 @@ interface ImdMapState {
105
110
  };
106
111
  }
107
112
  interface AreaHoverPayload {
113
+ areaCode: string | undefined;
114
+ areaName: string | undefined;
115
+ areaType: string | undefined;
116
+ nation: string | undefined;
117
+ imdDecile: number | undefined;
118
+ imdYear: number | undefined;
119
+ boundaryYear: number | undefined;
120
+ laCode: string | undefined;
121
+ laName: string | undefined;
122
+ laYear: number | undefined;
123
+ nhserCode: string | undefined;
124
+ nhserName: string | undefined;
125
+ icbCode: string | undefined;
126
+ icbName: string | undefined;
127
+ lhbCode: string | undefined;
128
+ lhbName: string | undefined;
129
+ lngLat: {
130
+ lng: number;
131
+ lat: number;
132
+ };
108
133
  lsoaCode: string | undefined;
109
134
  lsoaName: string | undefined;
110
- imdDecile: number | undefined;
111
- nation: string | undefined;
112
135
  rawProperties: Record<string, unknown>;
113
136
  }
114
137
  type AreaClickPayload = AreaHoverPayload;
@@ -179,6 +202,13 @@ interface CreateImdMapOptions {
179
202
  center?: [number, number];
180
203
  zoom?: number;
181
204
  style?: MapStyleOptions;
205
+ /**
206
+ * Area tooltip rendering mode.
207
+ * - default: built-in tooltip content
208
+ * - template: uses style.tooltip.areaTooltipText token interpolation
209
+ * - none: disables built-in area popup (callbacks still fire)
210
+ */
211
+ areaTooltipMode?: 'default' | 'template' | 'none';
182
212
  onViewChange?: (view: {
183
213
  nation: Nation;
184
214
  era: Era;
package/dist/index.esm.js CHANGED
@@ -23,7 +23,8 @@ function willEraBeOverridden(nation, requestedEra) {
23
23
  var ZOOM_TIERS = [
24
24
  { tier: "z0_4", minzoom: 0, maxzoom: 5 },
25
25
  { tier: "z5_7", minzoom: 5, maxzoom: 8 },
26
- { tier: "z8_10", minzoom: 8, maxzoom: 24 }
26
+ { tier: "z8_10", minzoom: 8, maxzoom: 11 },
27
+ { tier: "z11_14", minzoom: 11, maxzoom: 24 }
27
28
  ];
28
29
  function resolveFullTableName(effectiveEra, tier) {
29
30
  return `public.uk_master_${effectiveEra}_${tier}`;
@@ -272,6 +273,7 @@ var DEFAULT_STYLE = {
272
273
  nationLabel: "Nation",
273
274
  patientLabel: "Patient",
274
275
  leadCentreLabel: "Lead centre",
276
+ areaTooltipText: '<strong style="display:block;margin-bottom:2px;">{{areaName}}</strong><span>LSOA year: {{boundaryYear}}</span><br/><span>{{decileLabel}}: <strong>{{imdDecile}}</strong></span><br/><span>IMD year: {{imdYear}}</span><br/><span>{{nationLabel}}: {{nation}}</span>',
275
277
  patientTooltipText: "{{patientLabel}}",
276
278
  leadCentreTooltipText: "{{leadCentreLabel}}: {{label}}"
277
279
  },
@@ -564,6 +566,61 @@ function buildChoroplethTooltipHtml(properties, style) {
564
566
  <span>${t.nationLabel ?? "Nation"}: ${String(nation)}</span>
565
567
  </div>`;
566
568
  }
569
+ function buildChoroplethTooltipHtmlFromTemplate(properties, style) {
570
+ const t = style.tooltip;
571
+ const bg = t.backgroundColor ?? "#0d0d58";
572
+ const color = t.textColor ?? "#ffffff";
573
+ const radius = t.borderRadius ?? 4;
574
+ const areaCode = getFeatureProperty(properties, "code");
575
+ const areaName = getFeatureProperty(properties, "area_name") ?? "Unknown area";
576
+ const areaType = getFeatureProperty(properties, "area_type") ?? "LSOA";
577
+ const nation = getFeatureProperty(properties, "nation") ?? "\u2013";
578
+ const imdDecile = getFeatureProperty(properties, "imd_decile") ?? "\u2013";
579
+ const imdYear = getFeatureProperty(properties, "imd_year") ?? "\u2013";
580
+ const boundaryYear = getFeatureProperty(properties, "year") ?? "\u2013";
581
+ const laCode = getFeatureProperty(properties, "la_code") ?? "";
582
+ const laName = getFeatureProperty(properties, "la_name") ?? "";
583
+ const laYear = getFeatureProperty(properties, "la_year") ?? "";
584
+ const nhserCode = getFeatureProperty(properties, "nhser_code") ?? "";
585
+ const nhserName = getFeatureProperty(properties, "nhser_name") ?? "";
586
+ const icbCode = getFeatureProperty(properties, "icb_code") ?? "";
587
+ const icbName = getFeatureProperty(properties, "icb_name") ?? "";
588
+ const lhbCode = getFeatureProperty(properties, "lhb_code") ?? "";
589
+ const lhbName = getFeatureProperty(properties, "lhb_name") ?? "";
590
+ const template = t.areaTooltipText ?? "<strong>{{areaName}}</strong><br/><span>{{decileLabel}}: {{imdDecile}}</span>";
591
+ const text = interpolateTemplate(template, {
592
+ ...properties ?? {},
593
+ areaCode,
594
+ areaName,
595
+ areaType,
596
+ nation,
597
+ imdDecile,
598
+ imdYear,
599
+ boundaryYear,
600
+ laCode,
601
+ laName,
602
+ laYear,
603
+ nhserCode,
604
+ nhserName,
605
+ icbCode,
606
+ icbName,
607
+ lhbCode,
608
+ lhbName,
609
+ decileLabel: t.decileLabel ?? "IMD decile",
610
+ nationLabel: t.nationLabel ?? "Nation"
611
+ });
612
+ return `<div style="background:${bg};color:${color};padding:8px 12px;border-radius:${radius}px;font-size:13px;line-height:1.6;font-family:sans-serif;">
613
+ <span>${text}</span>
614
+ </div>`;
615
+ }
616
+ function toOptionalNumber(value) {
617
+ if (typeof value === "number" && Number.isFinite(value)) return value;
618
+ if (typeof value === "string" && value.trim() !== "") {
619
+ const parsed = Number(value);
620
+ if (Number.isFinite(parsed)) return parsed;
621
+ }
622
+ return void 0;
623
+ }
567
624
  function buildPatientTooltipHtml(properties, style) {
568
625
  const t = style.tooltip;
569
626
  const id = getFeatureProperty(properties, "id") ?? "";
@@ -602,14 +659,44 @@ function buildLeadCentreTooltipHtml(properties, style) {
602
659
  <span>${text}</span>
603
660
  </div>`;
604
661
  }
605
- function featureToPayload(feature) {
662
+ function featureToPayload(feature, lngLat) {
606
663
  const props = feature.properties ?? {};
607
- const decileRaw = getFeatureProperty(props, "imd_decile");
664
+ const areaCode = getFeatureProperty(props, "code");
665
+ const areaName = getFeatureProperty(props, "area_name");
666
+ const areaType = getFeatureProperty(props, "area_type");
667
+ const nation = getFeatureProperty(props, "nation");
668
+ const imdDecile = toOptionalNumber(getFeatureProperty(props, "imd_decile"));
669
+ const imdYear = toOptionalNumber(getFeatureProperty(props, "imd_year"));
670
+ const boundaryYear = toOptionalNumber(getFeatureProperty(props, "year"));
671
+ const laCode = getFeatureProperty(props, "la_code");
672
+ const laName = getFeatureProperty(props, "la_name");
673
+ const laYear = toOptionalNumber(getFeatureProperty(props, "la_year"));
674
+ const nhserCode = getFeatureProperty(props, "nhser_code");
675
+ const nhserName = getFeatureProperty(props, "nhser_name");
676
+ const icbCode = getFeatureProperty(props, "icb_code");
677
+ const icbName = getFeatureProperty(props, "icb_name");
678
+ const lhbCode = getFeatureProperty(props, "lhb_code");
679
+ const lhbName = getFeatureProperty(props, "lhb_name");
608
680
  return {
609
- lsoaCode: String(getFeatureProperty(props, "code") ?? ""),
610
- lsoaName: String(getFeatureProperty(props, "area_name") ?? ""),
611
- imdDecile: typeof decileRaw === "number" ? decileRaw : void 0,
612
- nation: String(getFeatureProperty(props, "nation") ?? ""),
681
+ areaCode: areaCode === void 0 || areaCode === null ? void 0 : String(areaCode),
682
+ areaName: areaName === void 0 || areaName === null ? void 0 : String(areaName),
683
+ areaType: areaType === void 0 || areaType === null ? void 0 : String(areaType),
684
+ nation: nation === void 0 || nation === null ? void 0 : String(nation),
685
+ imdDecile,
686
+ imdYear,
687
+ boundaryYear,
688
+ laCode: laCode === void 0 || laCode === null ? void 0 : String(laCode),
689
+ laName: laName === void 0 || laName === null ? void 0 : String(laName),
690
+ laYear,
691
+ nhserCode: nhserCode === void 0 || nhserCode === null ? void 0 : String(nhserCode),
692
+ nhserName: nhserName === void 0 || nhserName === null ? void 0 : String(nhserName),
693
+ icbCode: icbCode === void 0 || icbCode === null ? void 0 : String(icbCode),
694
+ icbName: icbName === void 0 || icbName === null ? void 0 : String(icbName),
695
+ lhbCode: lhbCode === void 0 || lhbCode === null ? void 0 : String(lhbCode),
696
+ lhbName: lhbName === void 0 || lhbName === null ? void 0 : String(lhbName),
697
+ lngLat,
698
+ lsoaCode: areaCode === void 0 || areaCode === null ? void 0 : String(areaCode),
699
+ lsoaName: areaName === void 0 || areaName === null ? void 0 : String(areaName),
613
700
  rawProperties: props
614
701
  };
615
702
  }
@@ -621,8 +708,15 @@ function attachChoroplethInteraction(map, popup, style, options) {
621
708
  if (!features.length) return;
622
709
  map.getCanvas().style.cursor = "pointer";
623
710
  const feature = features[0];
624
- popup.setLngLat(e.lngLat).setHTML(buildChoroplethTooltipHtml(feature.properties, style)).addTo(map);
625
- options.onAreaHover?.(featureToPayload(feature));
711
+ const areaTooltipMode = options.areaTooltipMode ?? "default";
712
+ if (areaTooltipMode !== "none") {
713
+ const html = areaTooltipMode === "template" ? buildChoroplethTooltipHtmlFromTemplate(
714
+ feature.properties,
715
+ style
716
+ ) : buildChoroplethTooltipHtml(feature.properties, style);
717
+ popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
718
+ }
719
+ options.onAreaHover?.(featureToPayload(feature, { lng: e.lngLat.lng, lat: e.lngLat.lat }));
626
720
  });
627
721
  map.on("mouseleave", layerId, () => {
628
722
  map.getCanvas().style.cursor = "";
@@ -631,7 +725,7 @@ function attachChoroplethInteraction(map, popup, style, options) {
631
725
  map.on("click", layerId, (e) => {
632
726
  const features = map.queryRenderedFeatures(e.point, { layers: [layerId] });
633
727
  if (!features.length) return;
634
- options.onAreaClick?.(featureToPayload(features[0]));
728
+ options.onAreaClick?.(featureToPayload(features[0], { lng: e.lngLat.lng, lat: e.lngLat.lat }));
635
729
  });
636
730
  }
637
731
  }