@reuters-graphics/graphics-components 3.7.0 → 3.8.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.
@@ -21,6 +21,7 @@
21
21
  import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
22
22
  import type { ContainerWidth } from '../@types/global';
23
23
  import type { ProjectionSpecification } from 'maplibre-gl';
24
+ import { emphasizePlaceLabels } from './labels';
24
25
  import 'maplibre-gl/dist/maplibre-gl.css';
25
26
 
26
27
  interface Props {
@@ -65,6 +66,13 @@
65
66
  * Map style URL
66
67
  */
67
68
  styleUrl?: string;
69
+ /**
70
+ * Darken the basemap's place labels and give them a strong white halo, so
71
+ * city/region names stay readable over colored data layers. Applied once on
72
+ * map load; a no-op on styles without `place` labels (e.g. non-default
73
+ * `styleUrl`s), so it degrades gracefully.
74
+ */
75
+ emphasizeLabels?: boolean;
68
76
  /**
69
77
  * Map height (default: '500px')
70
78
  */
@@ -101,6 +109,7 @@
101
109
  projection,
102
110
  interactive = true,
103
111
  styleUrl = 'https://graphics.thomsonreuters.com/reuters-protomaps/style.json',
112
+ emphasizeLabels = false,
104
113
  height = '500px',
105
114
  width = 'normal',
106
115
  textWidth = 'normal',
@@ -155,6 +164,28 @@
155
164
  );
156
165
  }
157
166
 
167
+ // Darken the basemap's place labels so they read over data layers. Some
168
+ // styles keep applying their own label paint for a moment after the style
169
+ // loads, so a single call at `load` can be overridden and leave the
170
+ // labels their default (lighter) color. Apply on the first `styledata`
171
+ // where the place labels are present, then stop listening.
172
+ if (emphasizeLabels) {
173
+ const applyLabelEmphasis = () => {
174
+ if (!map) return;
175
+ const hasPlaceLabels = map
176
+ .getStyle()
177
+ ?.layers?.some(
178
+ (l) =>
179
+ l.type === 'symbol' &&
180
+ (l as { 'source-layer'?: string })['source-layer'] === 'place'
181
+ );
182
+ if (!hasPlaceLabels) return;
183
+ emphasizePlaceLabels(map);
184
+ map.off('styledata', applyLabelEmphasis);
185
+ };
186
+ map.on('styledata', applyLabelEmphasis);
187
+ }
188
+
158
189
  // Call the callback when map is ready
159
190
  map.on('load', () => {
160
191
  if (!map) return;
@@ -45,6 +45,13 @@ interface Props {
45
45
  * Map style URL
46
46
  */
47
47
  styleUrl?: string;
48
+ /**
49
+ * Darken the basemap's place labels and give them a strong white halo, so
50
+ * city/region names stay readable over colored data layers. Applied once on
51
+ * map load; a no-op on styles without `place` labels (e.g. non-default
52
+ * `styleUrl`s), so it degrades gracefully.
53
+ */
54
+ emphasizeLabels?: boolean;
48
55
  /**
49
56
  * Map height (default: '500px')
50
57
  */
@@ -4,6 +4,7 @@
4
4
  import type { Map as MaplibreMap, GeoJSONSource } from 'maplibre-gl';
5
5
  import type { Writable } from 'svelte/store';
6
6
  import type { GeoJSON } from 'geojson';
7
+ import { findFirstSymbolLayerId } from './labels';
7
8
 
8
9
  interface Props {
9
10
  /**
@@ -39,6 +40,12 @@
39
40
  * Layer to insert before (for layer ordering)
40
41
  */
41
42
  beforeId?: string;
43
+ /**
44
+ * Insert this layer beneath the basemap's labels, so place names and other
45
+ * symbol layers stay readable on top of it. Ignored when `beforeId` is set
46
+ * (that takes precedence). A no-op on styles with no symbol layers.
47
+ */
48
+ beneathLabels?: boolean;
42
49
  /**
43
50
  * Minimum zoom level to display layer
44
51
  */
@@ -60,6 +67,7 @@
60
67
  paint = {},
61
68
  layout = {},
62
69
  beforeId,
70
+ beneathLabels = false,
63
71
  minZoom,
64
72
  maxZoom,
65
73
  filter,
@@ -146,7 +154,11 @@
146
154
  if (maxZoom !== undefined) layerConfig.maxzoom = maxZoom;
147
155
  if (filter) layerConfig.filter = filter;
148
156
 
149
- map.addLayer(layerConfig as never, beforeId);
157
+ // An explicit `beforeId` wins; otherwise `beneathLabels` inserts the
158
+ // layer just below the first symbol (label) layer so labels stay on top.
159
+ const insertBefore =
160
+ beforeId ?? (beneathLabels ? findFirstSymbolLayerId(map) : undefined);
161
+ map.addLayer(layerConfig as never, insertBefore);
150
162
  }
151
163
 
152
164
  isInitialized = true;
@@ -24,6 +24,12 @@ interface Props {
24
24
  * Layer to insert before (for layer ordering)
25
25
  */
26
26
  beforeId?: string;
27
+ /**
28
+ * Insert this layer beneath the basemap's labels, so place names and other
29
+ * symbol layers stay readable on top of it. Ignored when `beforeId` is set
30
+ * (that takes precedence). A no-op on styles with no symbol layers.
31
+ */
32
+ beneathLabels?: boolean;
27
33
  /**
28
34
  * Minimum zoom level to display layer
29
35
  */
@@ -0,0 +1,34 @@
1
+ import type { Map as MaplibreMap } from 'maplibre-gl';
2
+ /**
3
+ * Find the first symbol (label) layer in the map's current style, or undefined.
4
+ * Symbol layers are the basemap's place names, road labels, etc. Inserting a
5
+ * data layer *before* this id keeps every label above the data.
6
+ *
7
+ * @see https://docs.mapbox.com/mapbox-gl-js/example/geojson-layer-in-stack/
8
+ */
9
+ export declare function findFirstSymbolLayerId(map: MaplibreMap): string | undefined;
10
+ /** Options for {@link emphasizePlaceLabels}. */
11
+ export interface EmphasizeLabelsOptions {
12
+ /** Text color for place labels. Default `#1a1a1a` (near-black). */
13
+ textColor?: string;
14
+ /** Halo color. Default solid white. */
15
+ haloColor?: string;
16
+ /** Halo width, in px. Default `1.6`. */
17
+ haloWidth?: number;
18
+ }
19
+ /**
20
+ * Darken the basemap's populated-place labels (cities, towns, capitals, …) and
21
+ * give them a strong white halo so they stay legible on top of colored data
22
+ * overlays.
23
+ *
24
+ * The default protomaps style fades place labels with a zoom-driven
25
+ * `text-opacity` (city labels only reach full strength around zoom 9), so this
26
+ * also pins `text-opacity` to 1 — otherwise the near-black color reads as gray
27
+ * at the low-to-mid zooms typical of a choropleth.
28
+ *
29
+ * Targets the `place` source-layer symbol layers of the default Reuters
30
+ * protomaps style. On a style that doesn't have them it's a **no-op** (the
31
+ * per-layer checks and `try/catch` make it degrade gracefully), so it's safe to
32
+ * call regardless of `styleUrl`.
33
+ */
34
+ export declare function emphasizePlaceLabels(map: MaplibreMap, opts?: EmphasizeLabelsOptions): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Find the first symbol (label) layer in the map's current style, or undefined.
3
+ * Symbol layers are the basemap's place names, road labels, etc. Inserting a
4
+ * data layer *before* this id keeps every label above the data.
5
+ *
6
+ * @see https://docs.mapbox.com/mapbox-gl-js/example/geojson-layer-in-stack/
7
+ */
8
+ export function findFirstSymbolLayerId(map) {
9
+ const layers = map.getStyle()?.layers;
10
+ if (!layers)
11
+ return undefined;
12
+ for (const layer of layers) {
13
+ if (layer.type === 'symbol')
14
+ return layer.id;
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Darken the basemap's populated-place labels (cities, towns, capitals, …) and
20
+ * give them a strong white halo so they stay legible on top of colored data
21
+ * overlays.
22
+ *
23
+ * The default protomaps style fades place labels with a zoom-driven
24
+ * `text-opacity` (city labels only reach full strength around zoom 9), so this
25
+ * also pins `text-opacity` to 1 — otherwise the near-black color reads as gray
26
+ * at the low-to-mid zooms typical of a choropleth.
27
+ *
28
+ * Targets the `place` source-layer symbol layers of the default Reuters
29
+ * protomaps style. On a style that doesn't have them it's a **no-op** (the
30
+ * per-layer checks and `try/catch` make it degrade gracefully), so it's safe to
31
+ * call regardless of `styleUrl`.
32
+ */
33
+ export function emphasizePlaceLabels(map, opts = {}) {
34
+ const { textColor = '#1a1a1a', haloColor = 'rgba(255, 255, 255, 1)', haloWidth = 1.6, } = opts;
35
+ const layers = map.getStyle()?.layers;
36
+ if (!layers)
37
+ return;
38
+ for (const layer of layers) {
39
+ if (layer.type !== 'symbol')
40
+ continue;
41
+ const sourceLayer = layer['source-layer'];
42
+ if (sourceLayer !== 'place')
43
+ continue;
44
+ try {
45
+ map.setPaintProperty(layer.id, 'text-color', textColor);
46
+ map.setPaintProperty(layer.id, 'text-halo-color', haloColor);
47
+ map.setPaintProperty(layer.id, 'text-halo-width', haloWidth);
48
+ map.setPaintProperty(layer.id, 'text-halo-blur', 0);
49
+ map.setPaintProperty(layer.id, 'text-opacity', 1);
50
+ }
51
+ catch {
52
+ /* layer isn't paintable in this style; skip it */
53
+ }
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reuters-graphics/graphics-components",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "homepage": "https://reuters-graphics.github.io/graphics-components",