@reuters-graphics/graphics-components 3.7.0 → 3.9.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/dist/components/Geocoder/Geocoder.svelte +27 -3
- package/dist/components/Geocoder/Geocoder.svelte.d.ts +11 -0
- package/dist/components/Geocoder/normalize.d.ts +19 -0
- package/dist/components/Geocoder/normalize.js +25 -0
- package/dist/components/TileMap/TileMap.svelte +31 -0
- package/dist/components/TileMap/TileMap.svelte.d.ts +7 -0
- package/dist/components/TileMap/TileMapLayer.svelte +13 -1
- package/dist/components/TileMap/TileMapLayer.svelte.d.ts +6 -0
- package/dist/components/TileMap/labels.d.ts +34 -0
- package/dist/components/TileMap/labels.js +55 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { onDestroy } from 'svelte';
|
|
4
4
|
import { geocode, type GeocodeFeature, type GeocodeOptions } from './geocode';
|
|
5
|
+
import { normalizeMinLength, normalizeDebounceMs } from './normalize';
|
|
5
6
|
import MagnifyingGlass from '../SearchInput/components/MagnifyingGlass.svelte';
|
|
6
7
|
import X from '../SearchInput/components/X.svelte';
|
|
7
8
|
|
|
@@ -12,12 +13,25 @@
|
|
|
12
13
|
searchPlaceholder?: string;
|
|
13
14
|
/** Callback fired when a location is selected from the results. */
|
|
14
15
|
onselect?: (location: { lng: number; lat: number; name: string }) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Minimum number of characters before a request is made. Raising this
|
|
18
|
+
* cuts the number of paid geocoding requests per search. Defaults to 2.
|
|
19
|
+
*/
|
|
20
|
+
minLength?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Debounce window in milliseconds between the last keystroke and the
|
|
23
|
+
* request. Raising this fires fewer requests while the user is still
|
|
24
|
+
* typing (each request is billed). Defaults to 300.
|
|
25
|
+
*/
|
|
26
|
+
debounceMs?: number;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
let {
|
|
18
30
|
accessToken,
|
|
19
31
|
searchPlaceholder = 'Search for a location',
|
|
20
32
|
onselect,
|
|
33
|
+
minLength = 2,
|
|
34
|
+
debounceMs = 300,
|
|
21
35
|
autocomplete = true,
|
|
22
36
|
bbox,
|
|
23
37
|
country,
|
|
@@ -44,12 +58,22 @@
|
|
|
44
58
|
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
45
59
|
let abortController: AbortController | null = null;
|
|
46
60
|
|
|
61
|
+
// Normalize the throttling props so a non-numeric value passed via markup
|
|
62
|
+
// (e.g. `minLength="two"`) can't coerce to NaN and disable the gate — which
|
|
63
|
+
// would bill a request on every keystroke. Fall back to the documented
|
|
64
|
+
// defaults, clamped to sane floors.
|
|
65
|
+
let effectiveMinLength = $derived(normalizeMinLength(minLength));
|
|
66
|
+
let effectiveDebounceMs = $derived(normalizeDebounceMs(debounceMs));
|
|
67
|
+
|
|
47
68
|
function handleInput() {
|
|
48
69
|
selectedIndex = -1;
|
|
49
70
|
clearTimeout(debounceTimer);
|
|
50
71
|
abortController?.abort();
|
|
51
72
|
|
|
52
|
-
|
|
73
|
+
// Trim first so whitespace-only input (e.g. " ") doesn't clear the gate
|
|
74
|
+
// and bill a request for an empty search.
|
|
75
|
+
const trimmed = query.trim();
|
|
76
|
+
if (trimmed.length < effectiveMinLength) {
|
|
53
77
|
suggestions = [];
|
|
54
78
|
return;
|
|
55
79
|
}
|
|
@@ -58,7 +82,7 @@
|
|
|
58
82
|
abortController = new AbortController();
|
|
59
83
|
try {
|
|
60
84
|
suggestions = await geocode(
|
|
61
|
-
|
|
85
|
+
trimmed,
|
|
62
86
|
{
|
|
63
87
|
accessToken,
|
|
64
88
|
autocomplete,
|
|
@@ -78,7 +102,7 @@
|
|
|
78
102
|
if (e instanceof DOMException && e.name === 'AbortError') return;
|
|
79
103
|
console.error('Geocoder error:', e);
|
|
80
104
|
}
|
|
81
|
-
},
|
|
105
|
+
}, effectiveDebounceMs);
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
onDestroy(() => {
|
|
@@ -10,6 +10,17 @@ interface Props extends Omit<GeocodeOptions, 'accessToken'> {
|
|
|
10
10
|
lat: number;
|
|
11
11
|
name: string;
|
|
12
12
|
}) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Minimum number of characters before a request is made. Raising this
|
|
15
|
+
* cuts the number of paid geocoding requests per search. Defaults to 2.
|
|
16
|
+
*/
|
|
17
|
+
minLength?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Debounce window in milliseconds between the last keystroke and the
|
|
20
|
+
* request. Raising this fires fewer requests while the user is still
|
|
21
|
+
* typing (each request is billed). Defaults to 300.
|
|
22
|
+
*/
|
|
23
|
+
debounceMs?: number;
|
|
13
24
|
}
|
|
14
25
|
/** `Geocoder` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-controls-geocoder--docs) */
|
|
15
26
|
declare const Geocoder: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coercion helpers for the Geocoder's request-throttling props.
|
|
3
|
+
*
|
|
4
|
+
* These props can arrive as strings when set via markup (e.g.
|
|
5
|
+
* `minLength="two"`), so a non-numeric value must not be trusted directly: a
|
|
6
|
+
* `NaN` would disable the length gate or the debounce and bill a Mapbox request
|
|
7
|
+
* on every keystroke. Both helpers fall back to the documented defaults and
|
|
8
|
+
* clamp to sane floors.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Coerce a consumer-supplied `minLength` to a safe integer of at least 1.
|
|
12
|
+
* Invalid or sub-1 values fall back to `fallback` (default 2).
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeMinLength(value: unknown, fallback?: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Coerce a consumer-supplied `debounceMs` to a finite, non-negative number.
|
|
17
|
+
* Invalid or negative values fall back to `fallback` (default 300).
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeDebounceMs(value: unknown, fallback?: number): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coercion helpers for the Geocoder's request-throttling props.
|
|
3
|
+
*
|
|
4
|
+
* These props can arrive as strings when set via markup (e.g.
|
|
5
|
+
* `minLength="two"`), so a non-numeric value must not be trusted directly: a
|
|
6
|
+
* `NaN` would disable the length gate or the debounce and bill a Mapbox request
|
|
7
|
+
* on every keystroke. Both helpers fall back to the documented defaults and
|
|
8
|
+
* clamp to sane floors.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Coerce a consumer-supplied `minLength` to a safe integer of at least 1.
|
|
12
|
+
* Invalid or sub-1 values fall back to `fallback` (default 2).
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeMinLength(value, fallback = 2) {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : fallback;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Coerce a consumer-supplied `debounceMs` to a finite, non-negative number.
|
|
20
|
+
* Invalid or negative values fall back to `fallback` (default 300).
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeDebounceMs(value, fallback = 300) {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
25
|
+
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|