choropleth-kit 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +215 -2
- package/dist/chunk-R3XS6XPP.js +1816 -0
- package/dist/core.d.ts +123 -0
- package/dist/core.js +197 -0
- package/dist/drill-chain-DCiuiZKK.d.ts +1247 -0
- package/dist/geo.d.ts +35 -0
- package/dist/geo.js +361 -0
- package/dist/index.d.ts +1186 -0
- package/dist/index.js +2926 -0
- package/dist/styles.css +417 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +138 -3
|
@@ -0,0 +1,1816 @@
|
|
|
1
|
+
// src/core/geography.ts
|
|
2
|
+
import { feature } from "topojson-client";
|
|
3
|
+
var unwrapGeography = (value) => "default" in value ? value.default : value;
|
|
4
|
+
var toFeatureCollection = ({
|
|
5
|
+
geography,
|
|
6
|
+
objectName
|
|
7
|
+
}) => {
|
|
8
|
+
const kind = geography?.type;
|
|
9
|
+
if (kind === "FeatureCollection") {
|
|
10
|
+
return geography;
|
|
11
|
+
}
|
|
12
|
+
if (kind !== "Topology") {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[choropleth] toFeatureCollection: unrecognized format (type = ${JSON.stringify(kind)}). Expected a "Topology" (TopoJSON) or a "FeatureCollection" (GeoJSON).`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const topology = geography;
|
|
18
|
+
const availableNames = Object.keys(topology.objects);
|
|
19
|
+
const name = objectName ?? availableNames[0];
|
|
20
|
+
const object = name === void 0 ? void 0 : topology.objects[name];
|
|
21
|
+
if (name === void 0 || object === void 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`[choropleth] toFeatureCollection: object "${objectName ?? "(none)"}" does not exist in the topology. Available objects: ${availableNames.length > 0 ? availableNames.join(", ") : "(none)"}.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const converted = feature(topology, object);
|
|
27
|
+
if (converted.type === "FeatureCollection") {
|
|
28
|
+
return converted;
|
|
29
|
+
}
|
|
30
|
+
return { type: "FeatureCollection", features: [converted] };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/core/bounds.ts
|
|
34
|
+
import { geoBounds } from "d3-geo";
|
|
35
|
+
import polylabel from "polylabel";
|
|
36
|
+
var polygonsOf = (geometry) => {
|
|
37
|
+
if (geometry.type === "Polygon") return [geometry];
|
|
38
|
+
if (geometry.type === "MultiPolygon") {
|
|
39
|
+
return geometry.coordinates.map((coordinates) => ({ type: "Polygon", coordinates }));
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
var getFocusGeometry = ({
|
|
44
|
+
feature: feature2,
|
|
45
|
+
path,
|
|
46
|
+
near = 0.5
|
|
47
|
+
}) => {
|
|
48
|
+
const { geometry } = feature2;
|
|
49
|
+
if (geometry === null) return null;
|
|
50
|
+
const polygons = polygonsOf(geometry);
|
|
51
|
+
if (polygons === null || polygons.length <= 1) return geometry;
|
|
52
|
+
const measured = polygons.map((polygon) => {
|
|
53
|
+
const [[x0, y0], [x1, y1]] = path.bounds(polygon);
|
|
54
|
+
return {
|
|
55
|
+
polygon,
|
|
56
|
+
area: path.area(polygon),
|
|
57
|
+
cx: (x0 + x1) / 2,
|
|
58
|
+
cy: (y0 + y1) / 2,
|
|
59
|
+
diagonal: Math.hypot(x1 - x0, y1 - y0)
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const usable = measured.filter((m) => Number.isFinite(m.area) && Number.isFinite(m.cx));
|
|
63
|
+
const main = usable.reduce(
|
|
64
|
+
(best, m) => best === void 0 || m.area > best.area ? m : best,
|
|
65
|
+
void 0
|
|
66
|
+
);
|
|
67
|
+
if (main === void 0) return geometry;
|
|
68
|
+
const kept = usable.filter(
|
|
69
|
+
(m) => m === main || Math.hypot(m.cx - main.cx, m.cy - main.cy) <= near * main.diagonal
|
|
70
|
+
);
|
|
71
|
+
const [only] = kept;
|
|
72
|
+
return kept.length === 1 && only !== void 0 ? only.polygon : { type: "MultiPolygon", coordinates: kept.map((m) => m.polygon.coordinates) };
|
|
73
|
+
};
|
|
74
|
+
var ringArea = (ring) => {
|
|
75
|
+
let sum = 0;
|
|
76
|
+
ring.forEach(([x, y], i) => {
|
|
77
|
+
const [nx, ny] = ring[(i + 1) % ring.length] ?? [x, y];
|
|
78
|
+
sum += x * ny - nx * y;
|
|
79
|
+
});
|
|
80
|
+
return Math.abs(sum / 2);
|
|
81
|
+
};
|
|
82
|
+
var getAnchorPoint = ({
|
|
83
|
+
geometry,
|
|
84
|
+
project,
|
|
85
|
+
precision = 1
|
|
86
|
+
}) => {
|
|
87
|
+
if (geometry === null) return null;
|
|
88
|
+
const polygons = polygonsOf(geometry)?.map((p) => p.coordinates);
|
|
89
|
+
if (polygons === void 0) return null;
|
|
90
|
+
const projected = polygons.map(
|
|
91
|
+
(rings) => rings.map(
|
|
92
|
+
(ring) => ring.flatMap(([lon, lat]) => {
|
|
93
|
+
const at = lon === void 0 || lat === void 0 ? null : project([lon, lat]);
|
|
94
|
+
return at === null ? [] : [at];
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
).filter(([outer]) => outer !== void 0 && outer.length >= 3);
|
|
98
|
+
const biggest = projected.reduce((best, rings) => {
|
|
99
|
+
const outer = rings[0] ?? [];
|
|
100
|
+
return best === void 0 || ringArea(outer) > ringArea(best[0] ?? []) ? rings : best;
|
|
101
|
+
}, void 0);
|
|
102
|
+
if (biggest === void 0) return null;
|
|
103
|
+
const [x, y] = polylabel(
|
|
104
|
+
biggest.map((ring) => ring.map(([px2, py]) => [px2, py])),
|
|
105
|
+
precision
|
|
106
|
+
);
|
|
107
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
108
|
+
};
|
|
109
|
+
var DEFAULT_PADDING = 24;
|
|
110
|
+
var normalizePadding = (padding = DEFAULT_PADDING) => typeof padding === "number" ? { top: padding, right: padding, bottom: padding, left: padding } : padding;
|
|
111
|
+
var boundsToTransform = ({
|
|
112
|
+
bounds,
|
|
113
|
+
size,
|
|
114
|
+
padding = DEFAULT_PADDING,
|
|
115
|
+
scaleExtent = [0, Infinity]
|
|
116
|
+
}) => {
|
|
117
|
+
const [[x0, y0], [x1, y1]] = bounds;
|
|
118
|
+
const { top, right, bottom, left } = normalizePadding(padding);
|
|
119
|
+
const innerW = size.width - left - right;
|
|
120
|
+
const innerH = size.height - top - bottom;
|
|
121
|
+
const raw = Math.min(innerW / (x1 - x0), innerH / (y1 - y0));
|
|
122
|
+
const k = Math.min(scaleExtent[1], Math.max(scaleExtent[0], Number.isNaN(raw) ? 1 : raw));
|
|
123
|
+
return {
|
|
124
|
+
k,
|
|
125
|
+
x: left + innerW / 2 - k * ((x0 + x1) / 2),
|
|
126
|
+
y: top + innerH / 2 - k * ((y0 + y1) / 2)
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
var EDGE_SAMPLES = 9;
|
|
130
|
+
var projectBounds = (bounds, project) => {
|
|
131
|
+
const [[west, south], [east, north]] = bounds;
|
|
132
|
+
if (east < west) {
|
|
133
|
+
const eastHalf = [
|
|
134
|
+
[west, south],
|
|
135
|
+
[180, north]
|
|
136
|
+
];
|
|
137
|
+
const westHalf = [
|
|
138
|
+
[-180, south],
|
|
139
|
+
[east, north]
|
|
140
|
+
];
|
|
141
|
+
return projectBounds(180 - west >= east + 180 ? eastHalf : westHalf, project);
|
|
142
|
+
}
|
|
143
|
+
const samples = [];
|
|
144
|
+
for (let i = 0; i < EDGE_SAMPLES; i += 1) {
|
|
145
|
+
const t = i / (EDGE_SAMPLES - 1);
|
|
146
|
+
const lon = west + (east - west) * t;
|
|
147
|
+
const lat = south + (north - south) * t;
|
|
148
|
+
samples.push([lon, south], [lon, north], [west, lat], [east, lat]);
|
|
149
|
+
}
|
|
150
|
+
const projected = samples.flatMap((at) => {
|
|
151
|
+
const p = project(at);
|
|
152
|
+
return p === null ? [] : [p];
|
|
153
|
+
});
|
|
154
|
+
if (projected.length === 0) return null;
|
|
155
|
+
const xs = projected.map(([x]) => x);
|
|
156
|
+
const ys = projected.map(([, y]) => y);
|
|
157
|
+
return [
|
|
158
|
+
[Math.min(...xs), Math.min(...ys)],
|
|
159
|
+
[Math.max(...xs), Math.max(...ys)]
|
|
160
|
+
];
|
|
161
|
+
};
|
|
162
|
+
var getFocusExtent = ({
|
|
163
|
+
feature: feature2,
|
|
164
|
+
path,
|
|
165
|
+
project,
|
|
166
|
+
near
|
|
167
|
+
}) => {
|
|
168
|
+
const geometry = getFocusGeometry({ feature: feature2, path, near });
|
|
169
|
+
if (geometry === null) return null;
|
|
170
|
+
const [[west, south], [east, north]] = geoBounds(geometry);
|
|
171
|
+
if (west > east)
|
|
172
|
+
return projectBounds(
|
|
173
|
+
[
|
|
174
|
+
[west, south],
|
|
175
|
+
[east, north]
|
|
176
|
+
],
|
|
177
|
+
project
|
|
178
|
+
);
|
|
179
|
+
const [[x0, y0], [x1, y1]] = path.bounds(geometry);
|
|
180
|
+
return [x0, y0, x1, y1].every(Number.isFinite) ? [
|
|
181
|
+
[x0, y0],
|
|
182
|
+
[x1, y1]
|
|
183
|
+
] : null;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/core/camera.ts
|
|
187
|
+
import { interpolateZoom } from "d3-interpolate";
|
|
188
|
+
var FIT_TRANSFORM = { k: 1, x: 0, y: 0 };
|
|
189
|
+
var isFitTransform = (t, epsilon = 1e-3) => Math.abs(t.k - 1) < epsilon && Math.abs(t.x) < epsilon && Math.abs(t.y) < epsilon;
|
|
190
|
+
var applyTransform = ({ k, x, y }, [px2, py]) => [
|
|
191
|
+
px2 * k + x,
|
|
192
|
+
py * k + y
|
|
193
|
+
];
|
|
194
|
+
var invertTransform = ({ k, x, y }, [sx, sy]) => [
|
|
195
|
+
(sx - x) / k,
|
|
196
|
+
(sy - y) / k
|
|
197
|
+
];
|
|
198
|
+
var constrainAxis = ({
|
|
199
|
+
t,
|
|
200
|
+
k,
|
|
201
|
+
c0,
|
|
202
|
+
c1,
|
|
203
|
+
v0,
|
|
204
|
+
v1,
|
|
205
|
+
overscroll = 0
|
|
206
|
+
}) => {
|
|
207
|
+
const size = (c1 - c0) * k;
|
|
208
|
+
const view = v1 - v0;
|
|
209
|
+
if (size <= view) return v0 + (view - size) / 2 - c0 * k;
|
|
210
|
+
const slack = view * overscroll;
|
|
211
|
+
const min = v1 - c1 * k - slack;
|
|
212
|
+
const max = v0 - c0 * k + slack;
|
|
213
|
+
return Math.min(max, Math.max(min, t));
|
|
214
|
+
};
|
|
215
|
+
var constrainTransform = ({
|
|
216
|
+
transform,
|
|
217
|
+
content,
|
|
218
|
+
view,
|
|
219
|
+
overscroll
|
|
220
|
+
}) => ({
|
|
221
|
+
k: transform.k,
|
|
222
|
+
x: constrainAxis({
|
|
223
|
+
t: transform.x,
|
|
224
|
+
k: transform.k,
|
|
225
|
+
c0: content[0][0],
|
|
226
|
+
c1: content[1][0],
|
|
227
|
+
v0: view[0][0],
|
|
228
|
+
v1: view[1][0],
|
|
229
|
+
overscroll
|
|
230
|
+
}),
|
|
231
|
+
y: constrainAxis({
|
|
232
|
+
t: transform.y,
|
|
233
|
+
k: transform.k,
|
|
234
|
+
c0: content[0][1],
|
|
235
|
+
c1: content[1][1],
|
|
236
|
+
v0: view[0][1],
|
|
237
|
+
v1: view[1][1],
|
|
238
|
+
overscroll
|
|
239
|
+
})
|
|
240
|
+
});
|
|
241
|
+
var preserveView = ({
|
|
242
|
+
transform,
|
|
243
|
+
oldSize,
|
|
244
|
+
newSize,
|
|
245
|
+
invertOld,
|
|
246
|
+
projectNew
|
|
247
|
+
}) => {
|
|
248
|
+
const center = invertOld(invertTransform(transform, [oldSize.width / 2, oldSize.height / 2]));
|
|
249
|
+
const projected = center === null ? null : projectNew(center);
|
|
250
|
+
if (projected === null) return FIT_TRANSFORM;
|
|
251
|
+
return {
|
|
252
|
+
k: transform.k,
|
|
253
|
+
x: newSize.width / 2 - transform.k * projected[0],
|
|
254
|
+
y: newSize.height / 2 - transform.k * projected[1]
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
var MIN_FLIGHT_MS = 350;
|
|
258
|
+
var MAX_FLIGHT_MS = 1200;
|
|
259
|
+
var DEFAULT_FLIGHT_CURVATURE = 1.25;
|
|
260
|
+
var transformToZoomView = ({ k, x, y }, { width, height }) => [(width / 2 - x) / k, (height / 2 - y) / k, width / k];
|
|
261
|
+
var createZoomInterpolator = (rho) => interpolateZoom.rho(rho);
|
|
262
|
+
var flightDuration = ({
|
|
263
|
+
from,
|
|
264
|
+
to,
|
|
265
|
+
size,
|
|
266
|
+
speed = 1,
|
|
267
|
+
curvature = DEFAULT_FLIGHT_CURVATURE
|
|
268
|
+
}) => {
|
|
269
|
+
const natural = createZoomInterpolator(curvature)(
|
|
270
|
+
transformToZoomView(from, size),
|
|
271
|
+
transformToZoomView(to, size)
|
|
272
|
+
).duration;
|
|
273
|
+
const scaled = natural * speed;
|
|
274
|
+
if (!Number.isFinite(scaled)) return MIN_FLIGHT_MS;
|
|
275
|
+
return Math.min(MAX_FLIGHT_MS, Math.max(MIN_FLIGHT_MS, scaled));
|
|
276
|
+
};
|
|
277
|
+
var INERTIA_TAU_MS = 325;
|
|
278
|
+
var inertiaOffset = ({
|
|
279
|
+
velocity,
|
|
280
|
+
elapsed,
|
|
281
|
+
tau = INERTIA_TAU_MS
|
|
282
|
+
}) => {
|
|
283
|
+
if (!(elapsed > 0) || !Number.isFinite(velocity)) return 0;
|
|
284
|
+
return velocity * tau * (1 - Math.exp(-elapsed / tau));
|
|
285
|
+
};
|
|
286
|
+
var INERTIA_MIN_VELOCITY = 0.3;
|
|
287
|
+
var INERTIA_WINDOW_MS = 100;
|
|
288
|
+
var releaseVelocity = ({
|
|
289
|
+
samples,
|
|
290
|
+
now,
|
|
291
|
+
windowMs = INERTIA_WINDOW_MS,
|
|
292
|
+
minVelocity = INERTIA_MIN_VELOCITY
|
|
293
|
+
}) => {
|
|
294
|
+
const recent = samples.filter((s) => now - s.time <= windowMs);
|
|
295
|
+
if (recent.length < 2) return null;
|
|
296
|
+
const first = recent[0];
|
|
297
|
+
const last = recent[recent.length - 1];
|
|
298
|
+
if (first === void 0 || last === void 0) return null;
|
|
299
|
+
if (recent.some((s) => s.k !== last.k)) return null;
|
|
300
|
+
const dt = last.time - first.time;
|
|
301
|
+
if (!(dt > 0)) return null;
|
|
302
|
+
const vx = (last.x - first.x) / dt;
|
|
303
|
+
const vy = (last.y - first.y) / dt;
|
|
304
|
+
return Math.hypot(vx, vy) >= minVelocity ? [vx, vy] : null;
|
|
305
|
+
};
|
|
306
|
+
var bubbleScale = (k, scaling) => {
|
|
307
|
+
if (!(k > 0) || !Number.isFinite(k)) return 1;
|
|
308
|
+
switch (scaling) {
|
|
309
|
+
case "screen":
|
|
310
|
+
return 1 / k;
|
|
311
|
+
case "geo":
|
|
312
|
+
return 1;
|
|
313
|
+
case "sqrt":
|
|
314
|
+
return 1 / Math.sqrt(k);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var WORLD_BOUNDS = [
|
|
318
|
+
[-180, -90],
|
|
319
|
+
[180, 90]
|
|
320
|
+
];
|
|
321
|
+
var BOUNDS_SAMPLES = 5;
|
|
322
|
+
var toViewState = ({
|
|
323
|
+
transform,
|
|
324
|
+
size,
|
|
325
|
+
invert,
|
|
326
|
+
level = { id: "root", depth: 0 }
|
|
327
|
+
}) => {
|
|
328
|
+
const toCoordinates = (screen) => {
|
|
329
|
+
const at = invert(invertTransform(transform, screen));
|
|
330
|
+
return at !== null && Number.isFinite(at[0]) && Number.isFinite(at[1]) ? at : null;
|
|
331
|
+
};
|
|
332
|
+
const samples = [];
|
|
333
|
+
for (let i = 0; i < BOUNDS_SAMPLES; i += 1) {
|
|
334
|
+
for (let j = 0; j < BOUNDS_SAMPLES; j += 1) {
|
|
335
|
+
const at = toCoordinates([
|
|
336
|
+
size.width * i / (BOUNDS_SAMPLES - 1),
|
|
337
|
+
size.height * j / (BOUNDS_SAMPLES - 1)
|
|
338
|
+
]);
|
|
339
|
+
if (at !== null) samples.push(at);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const lons = samples.map(([lon]) => lon);
|
|
343
|
+
const lats = samples.map(([, lat]) => lat);
|
|
344
|
+
const bounds = samples.length === 0 ? WORLD_BOUNDS : [
|
|
345
|
+
[Math.min(...lons), Math.min(...lats)],
|
|
346
|
+
[Math.max(...lons), Math.max(...lats)]
|
|
347
|
+
];
|
|
348
|
+
const center = toCoordinates([size.width / 2, size.height / 2]) ?? [
|
|
349
|
+
(bounds[0][0] + bounds[1][0]) / 2,
|
|
350
|
+
(bounds[0][1] + bounds[1][1]) / 2
|
|
351
|
+
];
|
|
352
|
+
return {
|
|
353
|
+
k: transform.k,
|
|
354
|
+
x: transform.x,
|
|
355
|
+
y: transform.y,
|
|
356
|
+
zoom: transform.k,
|
|
357
|
+
center,
|
|
358
|
+
bounds,
|
|
359
|
+
level
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/core/theme.ts
|
|
364
|
+
var defineTheme = (theme) => theme;
|
|
365
|
+
var light = {
|
|
366
|
+
background: "transparent",
|
|
367
|
+
empty: { fill: "#d4d4d4" },
|
|
368
|
+
context: { fill: "#e5e5e5", border: "#ffffff" },
|
|
369
|
+
border: { color: "#ffffff", width: 0.5 },
|
|
370
|
+
hover: { stroke: "#000000", width: 1.5 },
|
|
371
|
+
selected: { stroke: "#0b5fff", width: 2 },
|
|
372
|
+
focus: { ring: "#0b5fff", width: 2 },
|
|
373
|
+
dim: { opacity: 0.45 },
|
|
374
|
+
bubble: { fill: "#2563eb", stroke: "#ffffff", opacity: 0.75 },
|
|
375
|
+
font: { color: "#111111" },
|
|
376
|
+
tooltip: {
|
|
377
|
+
background: "#ffffff",
|
|
378
|
+
color: "#111111",
|
|
379
|
+
border: "#cccccc",
|
|
380
|
+
radius: 4,
|
|
381
|
+
shadow: "none"
|
|
382
|
+
},
|
|
383
|
+
controls: { background: "#ffffff", color: "#111111", border: "#cccccc", radius: 6 }
|
|
384
|
+
};
|
|
385
|
+
var dark = {
|
|
386
|
+
background: "#0f172a",
|
|
387
|
+
empty: { fill: "#475569" },
|
|
388
|
+
context: { fill: "#1e293b", border: "#0f172a" },
|
|
389
|
+
border: { color: "#0f172a", width: 0.5 },
|
|
390
|
+
hover: { stroke: "#f8fafc", width: 1.5 },
|
|
391
|
+
selected: { stroke: "#60a5fa", width: 2 },
|
|
392
|
+
focus: { ring: "#93c5fd", width: 2 },
|
|
393
|
+
dim: { opacity: 0.4 },
|
|
394
|
+
bubble: { fill: "#60a5fa", stroke: "#0f172a", opacity: 0.8 },
|
|
395
|
+
font: { color: "#e2e8f0" },
|
|
396
|
+
tooltip: {
|
|
397
|
+
background: "#f8fafc",
|
|
398
|
+
color: "#0f172a",
|
|
399
|
+
border: "#cbd5e1",
|
|
400
|
+
radius: 4,
|
|
401
|
+
shadow: "0 4px 12px rgb(0 0 0 / 0.4)"
|
|
402
|
+
},
|
|
403
|
+
controls: { background: "#1e293b", color: "#e2e8f0", border: "#334155", radius: 6 }
|
|
404
|
+
};
|
|
405
|
+
var minimal = {
|
|
406
|
+
background: "transparent",
|
|
407
|
+
empty: { fill: "#eeeeee" },
|
|
408
|
+
context: { fill: "#f3f3f3", border: "#ffffff" },
|
|
409
|
+
border: { color: "#ffffff", width: 0.25 },
|
|
410
|
+
hover: { stroke: "#444444", width: 1 },
|
|
411
|
+
selected: { stroke: "#111111", width: 1.5 },
|
|
412
|
+
focus: { ring: "#111111", width: 2 },
|
|
413
|
+
dim: { opacity: 0.5 },
|
|
414
|
+
bubble: { fill: "#111111", stroke: "#ffffff", opacity: 0.6 },
|
|
415
|
+
font: { color: "#333333" },
|
|
416
|
+
tooltip: {
|
|
417
|
+
background: "#ffffff",
|
|
418
|
+
color: "#222222",
|
|
419
|
+
border: "transparent",
|
|
420
|
+
radius: 2,
|
|
421
|
+
shadow: "0 1px 4px rgb(0 0 0 / 0.18)"
|
|
422
|
+
},
|
|
423
|
+
controls: { background: "#ffffff", color: "#333333", border: "transparent", radius: 2 }
|
|
424
|
+
};
|
|
425
|
+
var contrast = {
|
|
426
|
+
background: "#ffffff",
|
|
427
|
+
empty: { fill: "#bdbdbd", pattern: "hatch" },
|
|
428
|
+
context: { fill: "#e0e0e0", border: "#000000" },
|
|
429
|
+
border: { color: "#000000", width: 1 },
|
|
430
|
+
hover: { stroke: "#000000", width: 3 },
|
|
431
|
+
selected: { stroke: "#0033cc", width: 4 },
|
|
432
|
+
focus: { ring: "#000000", width: 4 },
|
|
433
|
+
dim: { opacity: 0.35 },
|
|
434
|
+
bubble: { fill: "#003399", stroke: "#000000", opacity: 0.85 },
|
|
435
|
+
font: { color: "#000000", size: 14 },
|
|
436
|
+
tooltip: {
|
|
437
|
+
background: "#000000",
|
|
438
|
+
color: "#ffffff",
|
|
439
|
+
border: "#ffffff",
|
|
440
|
+
radius: 0,
|
|
441
|
+
shadow: "none"
|
|
442
|
+
},
|
|
443
|
+
controls: { background: "#ffffff", color: "#000000", border: "#000000", radius: 0 }
|
|
444
|
+
};
|
|
445
|
+
var THEMES = {
|
|
446
|
+
light,
|
|
447
|
+
dark,
|
|
448
|
+
minimal,
|
|
449
|
+
contrast
|
|
450
|
+
};
|
|
451
|
+
var isThemeName = (value) => typeof value === "string" && Object.hasOwn(THEMES, value);
|
|
452
|
+
var resolveTheme = (theme) => isThemeName(theme) ? THEMES[theme] : theme;
|
|
453
|
+
var extendTheme = (base, overrides) => {
|
|
454
|
+
const from = resolveTheme(base) ?? {};
|
|
455
|
+
const merged = { ...from };
|
|
456
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
457
|
+
if (value === void 0) continue;
|
|
458
|
+
const previous = merged[key];
|
|
459
|
+
merged[key] = typeof value === "object" && value !== null && typeof previous === "object" ? { ...previous, ...value } : value;
|
|
460
|
+
}
|
|
461
|
+
return merged;
|
|
462
|
+
};
|
|
463
|
+
var px = (value) => `${value}px`;
|
|
464
|
+
var themeToCssVars = (theme) => {
|
|
465
|
+
const vars = {};
|
|
466
|
+
if (theme === void 0) return vars;
|
|
467
|
+
const set = (name, value, unit) => {
|
|
468
|
+
if (value === void 0) return;
|
|
469
|
+
vars[`--cm-${name}`] = typeof value === "number" && unit !== void 0 ? unit(value) : `${value}`;
|
|
470
|
+
};
|
|
471
|
+
set("background", theme.background);
|
|
472
|
+
set("empty-fill", theme.empty?.fill);
|
|
473
|
+
set("context-fill", theme.context?.fill);
|
|
474
|
+
set("context-border-color", theme.context?.border);
|
|
475
|
+
set("border-color", theme.border?.color);
|
|
476
|
+
set("border-width", theme.border?.width, px);
|
|
477
|
+
set("hover-stroke", theme.hover?.stroke);
|
|
478
|
+
set("hover-stroke-width", theme.hover?.width, px);
|
|
479
|
+
set("selected-stroke", theme.selected?.stroke);
|
|
480
|
+
set("selected-stroke-width", theme.selected?.width, px);
|
|
481
|
+
set("focus-ring", theme.focus?.ring);
|
|
482
|
+
set("focus-ring-width", theme.focus?.width, px);
|
|
483
|
+
set("dim-opacity", theme.dim?.opacity);
|
|
484
|
+
set("bubble-fill", theme.bubble?.fill);
|
|
485
|
+
set("bubble-stroke", theme.bubble?.stroke);
|
|
486
|
+
set("bubble-opacity", theme.bubble?.opacity);
|
|
487
|
+
set("font-family", theme.font?.family);
|
|
488
|
+
set("font-size", theme.font?.size, px);
|
|
489
|
+
set("label-color", theme.font?.color);
|
|
490
|
+
set("tooltip-bg", theme.tooltip?.background);
|
|
491
|
+
set("tooltip-color", theme.tooltip?.color);
|
|
492
|
+
set("tooltip-border", theme.tooltip?.border);
|
|
493
|
+
set("tooltip-radius", theme.tooltip?.radius, px);
|
|
494
|
+
set("tooltip-shadow", theme.tooltip?.shadow);
|
|
495
|
+
set("control-bg", theme.controls?.background);
|
|
496
|
+
set("control-color", theme.controls?.color);
|
|
497
|
+
set("control-border", theme.controls?.border);
|
|
498
|
+
set("control-radius", theme.controls?.radius, px);
|
|
499
|
+
return vars;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/core/labels.ts
|
|
503
|
+
var en = {
|
|
504
|
+
noData: "No data",
|
|
505
|
+
geographyError: "The map could not be loaded.",
|
|
506
|
+
zoomIn: "Zoom in",
|
|
507
|
+
zoomOut: "Zoom out",
|
|
508
|
+
resetView: "Reset view",
|
|
509
|
+
gestureHintWheel: "Use Ctrl + scroll to zoom",
|
|
510
|
+
gestureHintWheelMac: "Use \u2318 + scroll to zoom",
|
|
511
|
+
gestureHintTouch: "Use two fingers to move the map",
|
|
512
|
+
breadcrumb: "Breadcrumb",
|
|
513
|
+
breadcrumbRoot: "All",
|
|
514
|
+
drillUp: "Up one level",
|
|
515
|
+
drillHint: "Click to see regions",
|
|
516
|
+
tapAgainHint: "Tap again to enter"
|
|
517
|
+
};
|
|
518
|
+
var es = {
|
|
519
|
+
noData: "Sin dato",
|
|
520
|
+
geographyError: "No se pudo cargar el mapa.",
|
|
521
|
+
zoomIn: "Acercar",
|
|
522
|
+
zoomOut: "Alejar",
|
|
523
|
+
resetView: "Restablecer vista",
|
|
524
|
+
gestureHintWheel: "Usa Ctrl + rueda para hacer zoom",
|
|
525
|
+
gestureHintWheelMac: "Usa \u2318 + rueda para hacer zoom",
|
|
526
|
+
gestureHintTouch: "Usa dos dedos para mover el mapa",
|
|
527
|
+
breadcrumb: "Ruta de navegaci\xF3n",
|
|
528
|
+
breadcrumbRoot: "Todo",
|
|
529
|
+
drillUp: "Subir un nivel",
|
|
530
|
+
drillHint: "Clic para ver regiones",
|
|
531
|
+
tapAgainHint: "Toca de nuevo para entrar"
|
|
532
|
+
};
|
|
533
|
+
var resolveLabels = (labels) => ({
|
|
534
|
+
...en,
|
|
535
|
+
...labels
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// src/core/join.ts
|
|
539
|
+
var defaultGetFeatureId = (feature2) => feature2.id === void 0 ? void 0 : String(feature2.id);
|
|
540
|
+
var joinData = ({
|
|
541
|
+
features,
|
|
542
|
+
data,
|
|
543
|
+
getId,
|
|
544
|
+
getFeatureId = defaultGetFeatureId
|
|
545
|
+
}) => {
|
|
546
|
+
const featureIds = /* @__PURE__ */ new Set();
|
|
547
|
+
for (const feature2 of features) {
|
|
548
|
+
const id = getFeatureId(feature2);
|
|
549
|
+
if (id !== void 0) featureIds.add(id);
|
|
550
|
+
}
|
|
551
|
+
const dataById = /* @__PURE__ */ new Map();
|
|
552
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
553
|
+
for (const datum of data) {
|
|
554
|
+
const id = getId(datum);
|
|
555
|
+
if (dataById.has(id)) duplicates.add(id);
|
|
556
|
+
else dataById.set(id, datum);
|
|
557
|
+
}
|
|
558
|
+
const joined = /* @__PURE__ */ new Map();
|
|
559
|
+
const unmatchedData = [];
|
|
560
|
+
for (const [id, datum] of dataById) {
|
|
561
|
+
if (featureIds.has(id)) joined.set(id, datum);
|
|
562
|
+
else unmatchedData.push(id);
|
|
563
|
+
}
|
|
564
|
+
const featuresWithoutData = [...featureIds].filter((id) => !dataById.has(id));
|
|
565
|
+
return { joined, unmatchedData, featuresWithoutData, duplicateDataIds: [...duplicates] };
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/core/classify.ts
|
|
569
|
+
import { scaleQuantile, scaleQuantize } from "d3-scale";
|
|
570
|
+
var DEFAULT_CLASSES = 5;
|
|
571
|
+
var finiteValues = (values) => values.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
572
|
+
var assertValidClasses = (classes) => {
|
|
573
|
+
if (!Number.isInteger(classes) || classes < 1) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`[choropleth] classify: "classes" must be an integer \u2265 1 (received: ${classes}).`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var computeThresholds = ({
|
|
580
|
+
values,
|
|
581
|
+
classification,
|
|
582
|
+
domain
|
|
583
|
+
}) => {
|
|
584
|
+
switch (classification.method) {
|
|
585
|
+
case "linear":
|
|
586
|
+
return [];
|
|
587
|
+
case "threshold": {
|
|
588
|
+
const { thresholds } = classification;
|
|
589
|
+
const ascending = thresholds.every(
|
|
590
|
+
(t, i) => Number.isFinite(t) && (i === 0 || t > (thresholds[i - 1] ?? t))
|
|
591
|
+
);
|
|
592
|
+
if (!ascending) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`[choropleth] classify: "thresholds" must be a list of finite numbers in strictly ascending order (received: [${thresholds.join(", ")}]).`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return [...thresholds];
|
|
598
|
+
}
|
|
599
|
+
case "quantize": {
|
|
600
|
+
const classes = classification.classes ?? DEFAULT_CLASSES;
|
|
601
|
+
assertValidClasses(classes);
|
|
602
|
+
return scaleQuantize().domain([...domain]).range(Array.from({ length: classes }, (_, i) => i)).thresholds();
|
|
603
|
+
}
|
|
604
|
+
case "quantile": {
|
|
605
|
+
const classes = classification.classes ?? DEFAULT_CLASSES;
|
|
606
|
+
assertValidClasses(classes);
|
|
607
|
+
if (values.length === 0) return [];
|
|
608
|
+
return scaleQuantile().domain([...values]).range(Array.from({ length: classes }, (_, i) => i)).quantiles();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/core/scales.ts
|
|
614
|
+
import { scaleDiverging, scaleSequential, scaleThreshold } from "d3-scale";
|
|
615
|
+
|
|
616
|
+
// src/core/palettes.ts
|
|
617
|
+
import {
|
|
618
|
+
interpolateBlues,
|
|
619
|
+
interpolateBrBG,
|
|
620
|
+
interpolateGreens,
|
|
621
|
+
interpolateGreys,
|
|
622
|
+
interpolateOranges,
|
|
623
|
+
interpolatePiYG,
|
|
624
|
+
interpolatePRGn,
|
|
625
|
+
interpolatePuOr,
|
|
626
|
+
interpolatePurples,
|
|
627
|
+
interpolateRdBu,
|
|
628
|
+
interpolateRdYlBu,
|
|
629
|
+
interpolateRdYlGn,
|
|
630
|
+
interpolateReds,
|
|
631
|
+
interpolateSpectral,
|
|
632
|
+
interpolateViridis,
|
|
633
|
+
schemeAccent,
|
|
634
|
+
schemeDark2,
|
|
635
|
+
schemePaired,
|
|
636
|
+
schemePastel1,
|
|
637
|
+
schemeSet2,
|
|
638
|
+
schemeSet3,
|
|
639
|
+
schemeTableau10
|
|
640
|
+
} from "d3-scale-chromatic";
|
|
641
|
+
var SEQUENTIAL_PALETTES = {
|
|
642
|
+
blues: interpolateBlues,
|
|
643
|
+
greens: interpolateGreens,
|
|
644
|
+
greys: interpolateGreys,
|
|
645
|
+
oranges: interpolateOranges,
|
|
646
|
+
purples: interpolatePurples,
|
|
647
|
+
reds: interpolateReds,
|
|
648
|
+
viridis: interpolateViridis
|
|
649
|
+
};
|
|
650
|
+
var DIVERGING_PALETTES = {
|
|
651
|
+
rdbu: interpolateRdBu,
|
|
652
|
+
brbg: interpolateBrBG,
|
|
653
|
+
piyg: interpolatePiYG,
|
|
654
|
+
prgn: interpolatePRGn,
|
|
655
|
+
puor: interpolatePuOr,
|
|
656
|
+
rdylbu: interpolateRdYlBu,
|
|
657
|
+
rdylgn: interpolateRdYlGn,
|
|
658
|
+
spectral: interpolateSpectral
|
|
659
|
+
};
|
|
660
|
+
var DEFAULT_SEQUENTIAL_PALETTE = "blues";
|
|
661
|
+
var DEFAULT_DIVERGING_PALETTE = "rdbu";
|
|
662
|
+
var SEQUENTIAL_START = 0.15;
|
|
663
|
+
var sampleSequentialColors = ({
|
|
664
|
+
interpolate,
|
|
665
|
+
classes
|
|
666
|
+
}) => Array.from(
|
|
667
|
+
{ length: classes },
|
|
668
|
+
(_, i) => (
|
|
669
|
+
// With a single class there is no "from less to more": a mid tone is used, not the light extreme.
|
|
670
|
+
interpolate(
|
|
671
|
+
classes === 1 ? 0.6 : SEQUENTIAL_START + (1 - SEQUENTIAL_START) * i / (classes - 1)
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
);
|
|
675
|
+
var sampleDivergingColors = ({
|
|
676
|
+
interpolate,
|
|
677
|
+
classes
|
|
678
|
+
}) => (
|
|
679
|
+
// Center of each band: with 5 classes t = 0.1, 0.3, 0.5, 0.7, 0.9. With an odd number the middle
|
|
680
|
+
// class falls exactly at t = 0.5, the palette's neutral color, which is the one that must
|
|
681
|
+
// represent the midpoint. That is why the diverging one does not use SEQUENTIAL_START: its light
|
|
682
|
+
// end is in the center, and that is intentional.
|
|
683
|
+
Array.from({ length: classes }, (_, i) => interpolate((i + 0.5) / classes))
|
|
684
|
+
);
|
|
685
|
+
var CATEGORICAL_PALETTES = {
|
|
686
|
+
tableau10: schemeTableau10,
|
|
687
|
+
set2: schemeSet2,
|
|
688
|
+
set3: schemeSet3,
|
|
689
|
+
accent: schemeAccent,
|
|
690
|
+
dark2: schemeDark2,
|
|
691
|
+
paired: schemePaired,
|
|
692
|
+
pastel1: schemePastel1
|
|
693
|
+
};
|
|
694
|
+
var DEFAULT_CATEGORICAL_PALETTE = "tableau10";
|
|
695
|
+
|
|
696
|
+
// src/core/scales.ts
|
|
697
|
+
var DEFAULT_EMPTY_FILL = "#d4d4d4";
|
|
698
|
+
var extent = (values) => {
|
|
699
|
+
let min = Infinity;
|
|
700
|
+
let max = -Infinity;
|
|
701
|
+
for (const v of values) {
|
|
702
|
+
if (v < min) min = v;
|
|
703
|
+
if (v > max) max = v;
|
|
704
|
+
}
|
|
705
|
+
return values.length === 0 ? [0, 1] : [min, max];
|
|
706
|
+
};
|
|
707
|
+
var createColorScale = (options) => {
|
|
708
|
+
const emptyFill = options.emptyFill ?? DEFAULT_EMPTY_FILL;
|
|
709
|
+
const values = finiteValues(options.values);
|
|
710
|
+
const midpoint = options.variant === "diverging" ? options.midpoint ?? 0 : 0;
|
|
711
|
+
const [min, max] = extent(values);
|
|
712
|
+
let domain = [min, max];
|
|
713
|
+
if (options.variant === "diverging") {
|
|
714
|
+
const spread = Math.max(Math.abs(min - midpoint), Math.abs(max - midpoint)) || 1;
|
|
715
|
+
domain = [midpoint - spread, midpoint + spread];
|
|
716
|
+
}
|
|
717
|
+
const classification = options.classification ?? (options.variant === "diverging" ? { method: "quantize", classes: DEFAULT_CLASSES } : { method: "quantile", classes: DEFAULT_CLASSES });
|
|
718
|
+
const thresholds = computeThresholds({ values, classification, domain });
|
|
719
|
+
const interpolate = options.variant === "diverging" ? DIVERGING_PALETTES[options.palette ?? DEFAULT_DIVERGING_PALETTE] : SEQUENTIAL_PALETTES[options.palette ?? DEFAULT_SEQUENTIAL_PALETTE];
|
|
720
|
+
const isUsable2 = (v) => typeof v === "number" && Number.isFinite(v);
|
|
721
|
+
if (classification.method === "linear") {
|
|
722
|
+
const continuous = options.variant === "diverging" ? scaleDiverging(interpolate).domain([domain[0], midpoint, domain[1]]).clamp(true) : scaleSequential(interpolate).domain(domain).clamp(true);
|
|
723
|
+
return {
|
|
724
|
+
getColor: (value) => isUsable2(value) ? continuous(value) : emptyFill,
|
|
725
|
+
kind: "continuous",
|
|
726
|
+
domain,
|
|
727
|
+
thresholds: [],
|
|
728
|
+
colors: [],
|
|
729
|
+
emptyFill
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const classes = thresholds.length + 1;
|
|
733
|
+
const colors = options.variant === "diverging" ? sampleDivergingColors({ interpolate, classes }) : sampleSequentialColors({ interpolate, classes });
|
|
734
|
+
const classed = scaleThreshold().domain(thresholds).range(colors);
|
|
735
|
+
return {
|
|
736
|
+
getColor: (value) => isUsable2(value) ? classed(value) : emptyFill,
|
|
737
|
+
kind: "classed",
|
|
738
|
+
domain,
|
|
739
|
+
thresholds,
|
|
740
|
+
colors,
|
|
741
|
+
emptyFill
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/core/projection.ts
|
|
746
|
+
import {
|
|
747
|
+
geoAlbers,
|
|
748
|
+
geoPath,
|
|
749
|
+
geoEqualEarth,
|
|
750
|
+
geoMercator,
|
|
751
|
+
geoNaturalEarth1,
|
|
752
|
+
geoOrthographic
|
|
753
|
+
} from "d3-geo";
|
|
754
|
+
var PROJECTIONS = {
|
|
755
|
+
// Equal areas: each country takes up the area that belongs to it. Recommended for the world.
|
|
756
|
+
equalEarth: geoEqualEarth,
|
|
757
|
+
// Compromise projection, pleasant to the eye; distorts areas somewhat.
|
|
758
|
+
naturalEarth: geoNaturalEarth1,
|
|
759
|
+
// Inflates polar areas (Greenland ≈ Africa). Useful for street maps, not for world choropleths:
|
|
760
|
+
// the eye associates more painted area with more importance.
|
|
761
|
+
mercator: geoMercator,
|
|
762
|
+
// Conic for the US (with Alaska and Hawaii repositioned). Looks bad over the whole world.
|
|
763
|
+
albers: geoAlbers,
|
|
764
|
+
// Globe view: only one hemisphere is visible.
|
|
765
|
+
orthographic: geoOrthographic
|
|
766
|
+
};
|
|
767
|
+
var projectionFactory = (name = DEFAULT_PROJECTION) => PROJECTIONS[name];
|
|
768
|
+
var DEFAULT_PROJECTION = "equalEarth";
|
|
769
|
+
var createProjection = ({
|
|
770
|
+
name = DEFAULT_PROJECTION,
|
|
771
|
+
width,
|
|
772
|
+
height,
|
|
773
|
+
featureCollection
|
|
774
|
+
}) => {
|
|
775
|
+
if (!(Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0)) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`[choropleth] createProjection: width and height must be finite numbers > 0 (received: ${width} \xD7 ${height}).`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const projection = PROJECTIONS[name]();
|
|
781
|
+
if (featureCollection.features.length === 0) return projection;
|
|
782
|
+
return projection.fitSize([width, height], featureCollection);
|
|
783
|
+
};
|
|
784
|
+
var DEFAULT_ASPECT_RATIO = 960 / 500;
|
|
785
|
+
var MIN_ASPECT_RATIO = 0.2;
|
|
786
|
+
var MAX_ASPECT_RATIO = 5;
|
|
787
|
+
var computeAspectRatio = ({
|
|
788
|
+
name = DEFAULT_PROJECTION,
|
|
789
|
+
featureCollection
|
|
790
|
+
}) => {
|
|
791
|
+
if (featureCollection.features.length === 0) return DEFAULT_ASPECT_RATIO;
|
|
792
|
+
const projection = PROJECTIONS[name]().fitWidth(1e3, featureCollection);
|
|
793
|
+
const [[x0, y0], [x1, y1]] = geoPath(projection).bounds(featureCollection);
|
|
794
|
+
const ratio = (x1 - x0) / (y1 - y0);
|
|
795
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return DEFAULT_ASPECT_RATIO;
|
|
796
|
+
return Math.min(MAX_ASPECT_RATIO, Math.max(MIN_ASPECT_RATIO, ratio));
|
|
797
|
+
};
|
|
798
|
+
var relativeTransform = (from, to) => {
|
|
799
|
+
const k = to.scale() / from.scale();
|
|
800
|
+
const [fx, fy] = from.translate();
|
|
801
|
+
const [tx, ty] = to.translate();
|
|
802
|
+
return { k, x: tx - k * fx, y: ty - k * fy };
|
|
803
|
+
};
|
|
804
|
+
var rebaseProjection = ({
|
|
805
|
+
parent,
|
|
806
|
+
factory,
|
|
807
|
+
focus,
|
|
808
|
+
width,
|
|
809
|
+
height,
|
|
810
|
+
padding
|
|
811
|
+
}) => {
|
|
812
|
+
if (!(Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0)) {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`[choropleth] rebaseProjection: width and height must be finite numbers > 0 (received: ${width} \xD7 ${height}).`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const child = copyShape(factory(), parent);
|
|
818
|
+
child.fitExtent(
|
|
819
|
+
[
|
|
820
|
+
[padding.left, padding.top],
|
|
821
|
+
[width - padding.right, height - padding.bottom]
|
|
822
|
+
],
|
|
823
|
+
focus
|
|
824
|
+
);
|
|
825
|
+
if (!(child.scale() > 0) || !Number.isFinite(child.scale())) {
|
|
826
|
+
throw new Error(
|
|
827
|
+
"[choropleth] rebaseProjection: the focus geometry has no extent (empty or a single point), so it cannot be fitted."
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
return { projection: child, relative: relativeTransform(parent, child) };
|
|
831
|
+
};
|
|
832
|
+
var copyShape = (child, parent) => {
|
|
833
|
+
child.rotate(parent.rotate()).center(parent.center());
|
|
834
|
+
child.angle(parent.angle()).reflectX(parent.reflectX()).reflectY(parent.reflectY());
|
|
835
|
+
child.precision(parent.precision());
|
|
836
|
+
const clipAngle = parent.clipAngle();
|
|
837
|
+
if (clipAngle !== null) child.clipAngle(clipAngle);
|
|
838
|
+
const conic = parent;
|
|
839
|
+
const target = child;
|
|
840
|
+
if (conic.parallels !== void 0 && target.parallels !== void 0) {
|
|
841
|
+
target.parallels(conic.parallels());
|
|
842
|
+
}
|
|
843
|
+
return child;
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// src/core/legend.ts
|
|
847
|
+
var defaultFormatValue = (value) => Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2)));
|
|
848
|
+
var classIndex = (thresholds, value) => {
|
|
849
|
+
let lo = 0;
|
|
850
|
+
let hi = thresholds.length;
|
|
851
|
+
while (lo < hi) {
|
|
852
|
+
const mid = lo + hi >> 1;
|
|
853
|
+
if (thresholds[mid] <= value) lo = mid + 1;
|
|
854
|
+
else hi = mid;
|
|
855
|
+
}
|
|
856
|
+
return lo;
|
|
857
|
+
};
|
|
858
|
+
var buildLegend = (options) => {
|
|
859
|
+
const { scale, values, emptyLabel } = options;
|
|
860
|
+
const format = options.formatValue ?? defaultFormatValue;
|
|
861
|
+
if (scale.kind === "continuous") return [];
|
|
862
|
+
const counts = Array.from({ length: scale.colors.length }, () => 0);
|
|
863
|
+
let empty = 0;
|
|
864
|
+
for (const v of values) {
|
|
865
|
+
if (typeof v !== "number" || !Number.isFinite(v)) empty += 1;
|
|
866
|
+
else
|
|
867
|
+
counts[classIndex(scale.thresholds, v)] = (counts[classIndex(scale.thresholds, v)] ?? 0) + 1;
|
|
868
|
+
}
|
|
869
|
+
const items = scale.colors.map((color, i) => {
|
|
870
|
+
const from = i === 0 ? scale.domain[0] : scale.thresholds[i - 1];
|
|
871
|
+
const to = i === scale.colors.length - 1 ? scale.domain[1] : scale.thresholds[i];
|
|
872
|
+
return { color, from, to, label: `${format(from)} \u2013 ${format(to)}`, count: counts[i] ?? 0 };
|
|
873
|
+
});
|
|
874
|
+
if (emptyLabel !== void 0) {
|
|
875
|
+
items.push({ color: scale.emptyFill, label: emptyLabel, count: empty, empty: true });
|
|
876
|
+
}
|
|
877
|
+
return items;
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/core/borders.ts
|
|
881
|
+
import { feature as toFeature, mesh } from "topojson-client";
|
|
882
|
+
var createBorders = ({
|
|
883
|
+
geography,
|
|
884
|
+
objectName,
|
|
885
|
+
filterFeatures
|
|
886
|
+
}) => {
|
|
887
|
+
if (geography.type !== "Topology") return null;
|
|
888
|
+
const topology = geography;
|
|
889
|
+
const name = objectName ?? Object.keys(topology.objects)[0];
|
|
890
|
+
const object = name === void 0 ? void 0 : topology.objects[name];
|
|
891
|
+
if (object === void 0) return null;
|
|
892
|
+
if (filterFeatures === void 0) {
|
|
893
|
+
return mesh(topology, object, (a, b) => a !== b);
|
|
894
|
+
}
|
|
895
|
+
const kept = /* @__PURE__ */ new Map();
|
|
896
|
+
const keep = (geometry) => {
|
|
897
|
+
let result = kept.get(geometry);
|
|
898
|
+
if (result === void 0) {
|
|
899
|
+
result = filterFeatures(toFeature(topology, geometry));
|
|
900
|
+
kept.set(geometry, result);
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
903
|
+
};
|
|
904
|
+
return mesh(
|
|
905
|
+
topology,
|
|
906
|
+
object,
|
|
907
|
+
(a, b) => a !== b && keep(a) && keep(b)
|
|
908
|
+
);
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// src/core/diagnostics.ts
|
|
912
|
+
var MAX_LISTED = 8;
|
|
913
|
+
var list = (ids) => {
|
|
914
|
+
const shown = ids.slice(0, MAX_LISTED).map((id) => JSON.stringify(id));
|
|
915
|
+
const rest = ids.length - shown.length;
|
|
916
|
+
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
917
|
+
};
|
|
918
|
+
var styleOf = (id) => {
|
|
919
|
+
if (/^\d+$/.test(id)) return "numeric";
|
|
920
|
+
if (/^[A-Za-z]{2}$/.test(id)) return "alpha-2";
|
|
921
|
+
if (/^[A-Za-z]{3}$/.test(id)) return "alpha-3";
|
|
922
|
+
return "other";
|
|
923
|
+
};
|
|
924
|
+
var commonStyle = (ids) => {
|
|
925
|
+
const first = ids[0];
|
|
926
|
+
if (first === void 0) return null;
|
|
927
|
+
const style = styleOf(first);
|
|
928
|
+
return style !== "other" && ids.every((id) => styleOf(id) === style) ? style : null;
|
|
929
|
+
};
|
|
930
|
+
var STYLE_NAMES = {
|
|
931
|
+
numeric: 'ISO 3166-1 numeric (e.g. "724")',
|
|
932
|
+
"alpha-2": 'ISO 3166-1 alpha-2 (e.g. "ES")',
|
|
933
|
+
"alpha-3": 'ISO 3166-1 alpha-3 (e.g. "ESP")',
|
|
934
|
+
other: "something else"
|
|
935
|
+
};
|
|
936
|
+
var hintFor = (unmatched, featuresWithoutData) => {
|
|
937
|
+
const features = new Set(featuresWithoutData);
|
|
938
|
+
const padded = unmatched.find((id) => /^\d{1,2}$/.test(id) && features.has(id.padStart(3, "0")));
|
|
939
|
+
if (padded !== void 0) {
|
|
940
|
+
return `Hint: ${JSON.stringify(padded)} does not match ${JSON.stringify(padded.padStart(3, "0"))}. Leading zeros matter (a spreadsheet may have dropped them).`;
|
|
941
|
+
}
|
|
942
|
+
const upper = new Set(featuresWithoutData.map((id) => id.toUpperCase()));
|
|
943
|
+
const lowered = unmatched.find((id) => id !== id.toUpperCase() && upper.has(id.toUpperCase()));
|
|
944
|
+
if (lowered !== void 0) {
|
|
945
|
+
return `Hint: ids are case-sensitive; ${JSON.stringify(lowered)} would match ${JSON.stringify(lowered.toUpperCase())}.`;
|
|
946
|
+
}
|
|
947
|
+
const dataStyle = commonStyle(unmatched);
|
|
948
|
+
const geometryStyle = commonStyle(featuresWithoutData);
|
|
949
|
+
if (dataStyle !== null && geometryStyle !== null && dataStyle !== geometryStyle) {
|
|
950
|
+
return `Hint: your data ids look like ${STYLE_NAMES[dataStyle]} but the geometry ids look like ${STYLE_NAMES[geometryStyle]}. Convert one of them, or use \`getFeatureId\` to read another property of the geometry.`;
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
};
|
|
954
|
+
var formatJoinDiagnostics = ({
|
|
955
|
+
unmatchedData,
|
|
956
|
+
featuresWithoutData,
|
|
957
|
+
duplicateDataIds,
|
|
958
|
+
dataCount
|
|
959
|
+
}) => {
|
|
960
|
+
if (dataCount === 0) return null;
|
|
961
|
+
const lines = [];
|
|
962
|
+
if (unmatchedData.length > 0) {
|
|
963
|
+
lines.push(
|
|
964
|
+
`${unmatchedData.length} datum(s) have no matching geometry: ${list(unmatchedData)}.`
|
|
965
|
+
);
|
|
966
|
+
const hint = hintFor(unmatchedData, featuresWithoutData);
|
|
967
|
+
if (hint !== null) lines.push(hint);
|
|
968
|
+
}
|
|
969
|
+
if (duplicateDataIds.length > 0) {
|
|
970
|
+
lines.push(
|
|
971
|
+
`${duplicateDataIds.length} id(s) appear more than once in \`data\` (only the first row of each is used): ${list(duplicateDataIds)}.`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (featuresWithoutData.length > 0) {
|
|
975
|
+
lines.push(`${featuresWithoutData.length} region(s) have no data and will use \`emptyFill\`.`);
|
|
976
|
+
}
|
|
977
|
+
return lines.length === 0 ? null : `[choropleth] ${lines.join("\n ")}`;
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/core/categories.ts
|
|
981
|
+
var createCategoricalScale = ({
|
|
982
|
+
values,
|
|
983
|
+
categories,
|
|
984
|
+
palette = DEFAULT_CATEGORICAL_PALETTE,
|
|
985
|
+
emptyFill
|
|
986
|
+
}) => {
|
|
987
|
+
const colors = CATEGORICAL_PALETTES[palette];
|
|
988
|
+
const specs = categories ?? [...new Set(values.filter((v) => typeof v === "string"))].sort((a, b) => a.localeCompare(b)).map((id) => ({ id }));
|
|
989
|
+
const resolved = specs.map((spec, index) => ({
|
|
990
|
+
id: spec.id,
|
|
991
|
+
label: spec.label ?? spec.id,
|
|
992
|
+
// `as string`: the modulo never gives undefined because `colors` has length ≥ 1.
|
|
993
|
+
color: spec.color ?? colors[index % colors.length]
|
|
994
|
+
}));
|
|
995
|
+
const byId = new Map(resolved.map((c) => [c.id, c]));
|
|
996
|
+
return {
|
|
997
|
+
getColor: (category) => category == null ? emptyFill : byId.get(category)?.color ?? emptyFill,
|
|
998
|
+
getLabel: (category) => category == null ? "" : byId.get(category)?.label ?? category,
|
|
999
|
+
categories: resolved,
|
|
1000
|
+
emptyFill
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
var buildCategoricalLegend = ({
|
|
1004
|
+
scale,
|
|
1005
|
+
values,
|
|
1006
|
+
emptyLabel
|
|
1007
|
+
}) => {
|
|
1008
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1009
|
+
let empty = 0;
|
|
1010
|
+
for (const v of values) {
|
|
1011
|
+
if (v == null || !scale.categories.some((c) => c.id === v)) empty += 1;
|
|
1012
|
+
else counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
1013
|
+
}
|
|
1014
|
+
const items = scale.categories.map((c) => ({
|
|
1015
|
+
color: c.color,
|
|
1016
|
+
label: c.label,
|
|
1017
|
+
count: counts.get(c.id) ?? 0
|
|
1018
|
+
}));
|
|
1019
|
+
if (emptyLabel !== void 0)
|
|
1020
|
+
items.push({ color: scale.emptyFill, label: emptyLabel, count: empty, empty: true });
|
|
1021
|
+
return items;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// src/core/bubbles.ts
|
|
1025
|
+
import { scaleSqrt } from "d3-scale";
|
|
1026
|
+
var DEFAULT_MAX_RADIUS = 24;
|
|
1027
|
+
var isPositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
1028
|
+
var createRadiusScale = ({
|
|
1029
|
+
values,
|
|
1030
|
+
maxRadius = DEFAULT_MAX_RADIUS
|
|
1031
|
+
}) => {
|
|
1032
|
+
let max = 0;
|
|
1033
|
+
for (const v of values) if (isPositive(v) && v > max) max = v;
|
|
1034
|
+
const scale = scaleSqrt().domain([0, max || 1]).range([0, maxRadius]).clamp(true);
|
|
1035
|
+
return { getRadius: (value) => max === 0 ? 0 : scale(value), max, maxRadius };
|
|
1036
|
+
};
|
|
1037
|
+
var buildSizeLegend = ({
|
|
1038
|
+
scale,
|
|
1039
|
+
formatValue = defaultFormatValue
|
|
1040
|
+
}) => {
|
|
1041
|
+
if (scale.max === 0) return [];
|
|
1042
|
+
return [0.25, 0.5, 1].map((fraction) => {
|
|
1043
|
+
const value = scale.max * fraction;
|
|
1044
|
+
return { value, radius: scale.getRadius(value), label: formatValue(value) };
|
|
1045
|
+
});
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// src/core/variants.ts
|
|
1049
|
+
var DEFAULT_BUBBLE_FILL = "#2563eb";
|
|
1050
|
+
var isUsable = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1051
|
+
var resolveVariant = (options) => {
|
|
1052
|
+
const { config, entries, emptyFill, formatValue, emptyLabel } = options;
|
|
1053
|
+
const numeric = (get) => entries.map((e) => e.datum === void 0 ? null : get(e.datum));
|
|
1054
|
+
const formatted = (values2) => values2.map((v) => isUsable(v) ? formatValue(v) : "");
|
|
1055
|
+
if (config.variant === "categorical") {
|
|
1056
|
+
const values2 = entries.map((e) => e.datum === void 0 ? null : config.getCategory(e.datum));
|
|
1057
|
+
const scale2 = createCategoricalScale({
|
|
1058
|
+
values: values2,
|
|
1059
|
+
categories: config.categories,
|
|
1060
|
+
palette: config.palette,
|
|
1061
|
+
emptyFill
|
|
1062
|
+
});
|
|
1063
|
+
return {
|
|
1064
|
+
fills: values2.map((v) => scale2.getColor(v)),
|
|
1065
|
+
values: values2,
|
|
1066
|
+
labels: values2.map((v) => scale2.getLabel(v)),
|
|
1067
|
+
scale: null,
|
|
1068
|
+
legend: buildCategoricalLegend({ scale: scale2, values: values2, emptyLabel }),
|
|
1069
|
+
classes: values2.map((v) => {
|
|
1070
|
+
const index = v == null ? -1 : scale2.categories.findIndex((c) => c.id === v);
|
|
1071
|
+
return index < 0 ? null : index;
|
|
1072
|
+
}),
|
|
1073
|
+
bubbles: [],
|
|
1074
|
+
sizeLegend: []
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
if (config.variant === "bubble") return resolveBubbles(options, config);
|
|
1078
|
+
const values = numeric(config.getValue);
|
|
1079
|
+
const scale = createColorScale({
|
|
1080
|
+
values,
|
|
1081
|
+
classification: config.classification,
|
|
1082
|
+
emptyFill,
|
|
1083
|
+
...config.variant === "diverging" ? { variant: "diverging", palette: config.palette, midpoint: config.midpoint } : { variant: "sequential", palette: config.palette }
|
|
1084
|
+
});
|
|
1085
|
+
return {
|
|
1086
|
+
fills: values.map((v) => scale.getColor(v)),
|
|
1087
|
+
values,
|
|
1088
|
+
labels: formatted(values),
|
|
1089
|
+
scale,
|
|
1090
|
+
legend: buildLegend({ scale, values, formatValue, emptyLabel }),
|
|
1091
|
+
classes: values.map(
|
|
1092
|
+
(v) => isUsable(v) && scale.kind !== "continuous" ? classIndex(scale.thresholds, v) : null
|
|
1093
|
+
),
|
|
1094
|
+
bubbles: [],
|
|
1095
|
+
sizeLegend: []
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
var resolveBubbles = (options, config) => {
|
|
1099
|
+
const { entries, data, getId, centroids, project, emptyFill, formatValue, emptyLabel } = options;
|
|
1100
|
+
const values = entries.map((e) => e.datum === void 0 ? null : config.getValue(e.datum));
|
|
1101
|
+
const sources = [];
|
|
1102
|
+
if (config.getCoordinates !== void 0) {
|
|
1103
|
+
for (const datum of data) {
|
|
1104
|
+
const coordinates = config.getCoordinates(datum);
|
|
1105
|
+
const at = coordinates === null ? null : project(coordinates);
|
|
1106
|
+
if (coordinates !== null && at !== null) {
|
|
1107
|
+
sources.push({ id: getId(datum), datum, at, coordinates });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
entries.forEach((entry, index) => {
|
|
1112
|
+
const at = centroids[index];
|
|
1113
|
+
if (entry.id !== void 0 && entry.datum !== void 0 && at != null) {
|
|
1114
|
+
sources.push({ id: entry.id, datum: entry.datum, at, coordinates: null });
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
const sized = sources.map((source) => ({ ...source, value: config.getValue(source.datum) })).filter((s) => isUsable(s.value) && s.value > 0);
|
|
1119
|
+
const radius = createRadiusScale({
|
|
1120
|
+
values: sized.map((s) => s.value),
|
|
1121
|
+
maxRadius: config.maxRadius ?? DEFAULT_MAX_RADIUS
|
|
1122
|
+
});
|
|
1123
|
+
const colorValues = sized.map(
|
|
1124
|
+
(s) => config.getColorValue ? config.getColorValue(s.datum) : null
|
|
1125
|
+
);
|
|
1126
|
+
const colorScale = config.getColorValue === void 0 ? null : createColorScale({
|
|
1127
|
+
variant: "diverging",
|
|
1128
|
+
values: colorValues,
|
|
1129
|
+
classification: config.classification,
|
|
1130
|
+
palette: config.palette,
|
|
1131
|
+
midpoint: config.midpoint,
|
|
1132
|
+
emptyFill
|
|
1133
|
+
});
|
|
1134
|
+
const bubbles = sized.map((s, i) => ({
|
|
1135
|
+
id: s.id,
|
|
1136
|
+
x: s.at[0],
|
|
1137
|
+
y: s.at[1],
|
|
1138
|
+
r: radius.getRadius(s.value),
|
|
1139
|
+
fill: colorScale === null ? config.color ?? DEFAULT_BUBBLE_FILL : colorScale.getColor(colorValues[i]),
|
|
1140
|
+
value: s.value,
|
|
1141
|
+
colorValue: colorValues[i] ?? null,
|
|
1142
|
+
datum: s.datum,
|
|
1143
|
+
coordinates: s.coordinates
|
|
1144
|
+
})).sort((a, b) => b.r - a.r);
|
|
1145
|
+
return {
|
|
1146
|
+
fills: entries.map(() => emptyFill),
|
|
1147
|
+
values,
|
|
1148
|
+
labels: values.map((v) => isUsable(v) ? formatValue(v) : ""),
|
|
1149
|
+
scale: colorScale,
|
|
1150
|
+
legend: colorScale === null ? [] : buildLegend({ scale: colorScale, values: colorValues, formatValue, emptyLabel }),
|
|
1151
|
+
// The bubbles carry the color, not the regions: there is no per-region class here.
|
|
1152
|
+
classes: entries.map(() => null),
|
|
1153
|
+
bubbles,
|
|
1154
|
+
sizeLegend: buildSizeLegend({ scale: radius, formatValue })
|
|
1155
|
+
};
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// src/core/zoom.ts
|
|
1159
|
+
var DEFAULT_FOCUS_BOUNDS = {
|
|
1160
|
+
ES: [
|
|
1161
|
+
[-18.5, 27.5],
|
|
1162
|
+
[4.5, 44]
|
|
1163
|
+
]
|
|
1164
|
+
};
|
|
1165
|
+
var DEFAULT_ZOOM_MIN = 1;
|
|
1166
|
+
var DEFAULT_ZOOM_MAX = 12;
|
|
1167
|
+
var resolveZoomOptions = (zoom, { clickIsBusy }) => {
|
|
1168
|
+
if (zoom === false) return null;
|
|
1169
|
+
const options = zoom === true || zoom === void 0 ? {} : zoom;
|
|
1170
|
+
const { min = DEFAULT_ZOOM_MIN, max = DEFAULT_ZOOM_MAX } = options;
|
|
1171
|
+
if (!(min > 0) || !Number.isFinite(max) || max < min) {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`[choropleth] zoom: "min" must be > 0 and "max" must be finite and \u2265 "min" (received: min = ${min}, max = ${max}).`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const { doubleClick = "auto" } = options;
|
|
1177
|
+
return {
|
|
1178
|
+
min,
|
|
1179
|
+
max,
|
|
1180
|
+
wheel: options.wheel ?? "always",
|
|
1181
|
+
pinch: options.pinch ?? true,
|
|
1182
|
+
pan: options.pan ?? true,
|
|
1183
|
+
doubleClick: doubleClick === "auto" ? !clickIsBusy : doubleClick,
|
|
1184
|
+
constrain: options.constrain ?? "contain",
|
|
1185
|
+
bubbleScaling: options.bubbleScaling ?? "sqrt",
|
|
1186
|
+
inertia: options.inertia ?? true,
|
|
1187
|
+
keyboard: options.keyboard ?? true,
|
|
1188
|
+
padding: options.padding ?? 24,
|
|
1189
|
+
initialView: options.initialView
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
var resolveControls = (zoom) => {
|
|
1193
|
+
if (typeof zoom !== "object" || zoom.controls === void 0 || zoom.controls === false) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
const options = zoom.controls === true ? {} : zoom.controls;
|
|
1197
|
+
return { position: options.position ?? "bottom-right", reset: options.reset ?? true };
|
|
1198
|
+
};
|
|
1199
|
+
var zoomKeyAction = (event) => {
|
|
1200
|
+
if (event.ctrlKey === true || event.metaKey === true || event.altKey === true) return null;
|
|
1201
|
+
switch (event.key) {
|
|
1202
|
+
case "+":
|
|
1203
|
+
case "=":
|
|
1204
|
+
return "in";
|
|
1205
|
+
case "-":
|
|
1206
|
+
case "_":
|
|
1207
|
+
return "out";
|
|
1208
|
+
case "0":
|
|
1209
|
+
return "reset";
|
|
1210
|
+
default:
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
var createZoomFilter = ({ wheel, pinch, pan, doubleClick, onGestureHint }) => (event) => {
|
|
1215
|
+
switch (event.type) {
|
|
1216
|
+
case "wheel": {
|
|
1217
|
+
if (wheel === "never") return false;
|
|
1218
|
+
if (wheel === "always" || event.ctrlKey === true || event.metaKey === true) return true;
|
|
1219
|
+
onGestureHint?.("wheel");
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
case "touchstart": {
|
|
1223
|
+
const multi = (event.touches?.length ?? 0) >= 2;
|
|
1224
|
+
if (multi) return pinch || pan;
|
|
1225
|
+
if (wheel === "cooperative") {
|
|
1226
|
+
onGestureHint?.("touch");
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
return pan;
|
|
1230
|
+
}
|
|
1231
|
+
case "dblclick":
|
|
1232
|
+
return doubleClick;
|
|
1233
|
+
case "mousedown":
|
|
1234
|
+
return pan && !event.button;
|
|
1235
|
+
// primary button only
|
|
1236
|
+
default:
|
|
1237
|
+
return !event.button;
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
var clamp = (value, [min, max]) => Math.min(max, Math.max(min, value));
|
|
1241
|
+
var resolveZoomTarget = ({
|
|
1242
|
+
target,
|
|
1243
|
+
size,
|
|
1244
|
+
padding,
|
|
1245
|
+
scaleExtent,
|
|
1246
|
+
project,
|
|
1247
|
+
getRegionExtent,
|
|
1248
|
+
focusBounds
|
|
1249
|
+
}) => {
|
|
1250
|
+
if (typeof target === "string") {
|
|
1251
|
+
const override = focusBounds?.[target];
|
|
1252
|
+
const extent2 = override ? projectBounds(override, project) : getRegionExtent(target);
|
|
1253
|
+
return extent2 === null ? null : boundsToTransform({ bounds: extent2, size, padding, scaleExtent });
|
|
1254
|
+
}
|
|
1255
|
+
if ("bounds" in target) {
|
|
1256
|
+
const extent2 = projectBounds(target.bounds, project);
|
|
1257
|
+
return extent2 === null ? null : boundsToTransform({ bounds: extent2, size, padding, scaleExtent });
|
|
1258
|
+
}
|
|
1259
|
+
const at = project([target.center[0], target.center[1]]);
|
|
1260
|
+
if (at === null || !Number.isFinite(target.zoom)) return null;
|
|
1261
|
+
const k = clamp(target.zoom, scaleExtent);
|
|
1262
|
+
return { k, x: size.width / 2 - k * at[0], y: size.height / 2 - k * at[1] };
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/core/drill.ts
|
|
1266
|
+
var layerKey = (path, depth = path.length) => JSON.stringify(path.slice(0, depth));
|
|
1267
|
+
var samePath = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
1268
|
+
var startsWith = (path, prefix) => prefix.length <= path.length && prefix.every((id, i) => id === path[i]);
|
|
1269
|
+
var classifyDrill = (from, to) => {
|
|
1270
|
+
if (samePath(from, to)) return "equal";
|
|
1271
|
+
if (startsWith(from, to)) return "up";
|
|
1272
|
+
if (to.length === from.length + 1 && startsWith(to, from)) return "down";
|
|
1273
|
+
if (to.length === from.length && startsWith(to.slice(0, -1), from.slice(0, -1))) return "lateral";
|
|
1274
|
+
return "jump";
|
|
1275
|
+
};
|
|
1276
|
+
var settledLayers = (path) => Array.from({ length: path.length + 1 }, (_, depth) => ({
|
|
1277
|
+
key: layerKey(path, depth),
|
|
1278
|
+
depth,
|
|
1279
|
+
role: depth === path.length ? "active" : "context"
|
|
1280
|
+
}));
|
|
1281
|
+
var createDrillState = (path = []) => ({
|
|
1282
|
+
phase: "idle",
|
|
1283
|
+
path,
|
|
1284
|
+
settledPath: path,
|
|
1285
|
+
direction: null,
|
|
1286
|
+
layers: settledLayers(path),
|
|
1287
|
+
flight: "done",
|
|
1288
|
+
flightId: 0,
|
|
1289
|
+
geometry: null,
|
|
1290
|
+
geometryKey: null,
|
|
1291
|
+
savedCameras: {},
|
|
1292
|
+
returnCamera: null,
|
|
1293
|
+
error: null
|
|
1294
|
+
});
|
|
1295
|
+
var savedCameraFor = (state, path) => state.savedCameras[layerKey(path)] ?? null;
|
|
1296
|
+
var layersFor = (current, to, newRole) => {
|
|
1297
|
+
const existing = new Set(current.map((layer) => layer.key));
|
|
1298
|
+
const wanted = settledLayers(to).map(
|
|
1299
|
+
(layer) => !existing.has(layer.key) && layer.role === "active" ? { ...layer, role: newRole } : layer
|
|
1300
|
+
);
|
|
1301
|
+
const wantedKeys = new Set(wanted.map((layer) => layer.key));
|
|
1302
|
+
const leaving = current.filter((layer) => !wantedKeys.has(layer.key)).map((layer) => ({ ...layer, role: "exiting" }));
|
|
1303
|
+
return [...wanted, ...leaving].sort((a, b) => a.depth - b.depth);
|
|
1304
|
+
};
|
|
1305
|
+
var settleLayers = (layers) => layers.filter((layer) => layer.role !== "exiting").map((layer) => layer.role === "entering" ? { ...layer, role: "active" } : layer);
|
|
1306
|
+
var settle = (state) => state.phase === "revealing" || state.direction === "up" ? {
|
|
1307
|
+
...state,
|
|
1308
|
+
phase: "idle",
|
|
1309
|
+
direction: null,
|
|
1310
|
+
settledPath: state.path,
|
|
1311
|
+
layers: settleLayers(state.layers),
|
|
1312
|
+
flight: "done",
|
|
1313
|
+
geometry: null,
|
|
1314
|
+
geometryKey: null,
|
|
1315
|
+
returnCamera: null
|
|
1316
|
+
} : state;
|
|
1317
|
+
var cancelled = (state) => ({
|
|
1318
|
+
...state,
|
|
1319
|
+
phase: "idle",
|
|
1320
|
+
path: state.settledPath,
|
|
1321
|
+
direction: null,
|
|
1322
|
+
layers: settleLayers(state.layers),
|
|
1323
|
+
flight: "done",
|
|
1324
|
+
geometry: null,
|
|
1325
|
+
geometryKey: null,
|
|
1326
|
+
returnCamera: null
|
|
1327
|
+
});
|
|
1328
|
+
var flyBack = (state) => ({
|
|
1329
|
+
...cancelled(state),
|
|
1330
|
+
phase: "flying",
|
|
1331
|
+
direction: "up",
|
|
1332
|
+
flight: "pending",
|
|
1333
|
+
flightId: state.flightId + 1,
|
|
1334
|
+
returnCamera: state.returnCamera
|
|
1335
|
+
});
|
|
1336
|
+
var pruneCameras = (cameras, path) => {
|
|
1337
|
+
const keep = new Set(Array.from({ length: path.length + 1 }, (_, d) => layerKey(path, d)));
|
|
1338
|
+
return Object.fromEntries(Object.entries(cameras).filter(([key]) => keep.has(key)));
|
|
1339
|
+
};
|
|
1340
|
+
var onDrill = (state, event) => {
|
|
1341
|
+
const start = settle(state);
|
|
1342
|
+
const { to } = event;
|
|
1343
|
+
const from = start.settledPath;
|
|
1344
|
+
const direction = event.direction === "jump" ? "jump" : classifyDrill(from, to);
|
|
1345
|
+
const base = { ...start, error: null };
|
|
1346
|
+
if (start.direction !== null && start.direction !== "up" && samePath(to, start.path))
|
|
1347
|
+
return state;
|
|
1348
|
+
const busy = start.direction !== null;
|
|
1349
|
+
if (direction === "equal") {
|
|
1350
|
+
return busy ? flyBack({ ...base, returnCamera: start.returnCamera }) : base;
|
|
1351
|
+
}
|
|
1352
|
+
if (direction === "up") {
|
|
1353
|
+
return {
|
|
1354
|
+
...base,
|
|
1355
|
+
phase: "flying",
|
|
1356
|
+
path: to,
|
|
1357
|
+
settledPath: to,
|
|
1358
|
+
direction: "up",
|
|
1359
|
+
layers: layersFor(start.layers, to, "active"),
|
|
1360
|
+
flight: "pending",
|
|
1361
|
+
flightId: start.flightId + 1,
|
|
1362
|
+
geometry: null,
|
|
1363
|
+
geometryKey: null,
|
|
1364
|
+
savedCameras: pruneCameras(start.savedCameras, to),
|
|
1365
|
+
returnCamera: null
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
const key = layerKey(to);
|
|
1369
|
+
const cached = event.cached === true;
|
|
1370
|
+
const geometry = cached ? "ready" : "pending";
|
|
1371
|
+
if (direction === "jump") {
|
|
1372
|
+
if (cached) {
|
|
1373
|
+
return {
|
|
1374
|
+
...base,
|
|
1375
|
+
phase: "idle",
|
|
1376
|
+
path: to,
|
|
1377
|
+
settledPath: to,
|
|
1378
|
+
direction: null,
|
|
1379
|
+
layers: settledLayers(to),
|
|
1380
|
+
flight: "done",
|
|
1381
|
+
flightId: start.flightId + 1,
|
|
1382
|
+
geometry: null,
|
|
1383
|
+
geometryKey: null,
|
|
1384
|
+
savedCameras: pruneCameras(start.savedCameras, to),
|
|
1385
|
+
returnCamera: null
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
...base,
|
|
1390
|
+
phase: "loading",
|
|
1391
|
+
path: to,
|
|
1392
|
+
direction: "jump",
|
|
1393
|
+
flight: "done",
|
|
1394
|
+
flightId: start.flightId + 1,
|
|
1395
|
+
geometry: "pending",
|
|
1396
|
+
geometryKey: key,
|
|
1397
|
+
returnCamera: null
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
const fresh = !busy;
|
|
1401
|
+
return {
|
|
1402
|
+
...base,
|
|
1403
|
+
phase: "flying",
|
|
1404
|
+
path: to,
|
|
1405
|
+
direction,
|
|
1406
|
+
flight: "pending",
|
|
1407
|
+
flightId: start.flightId + 1,
|
|
1408
|
+
geometry,
|
|
1409
|
+
geometryKey: key,
|
|
1410
|
+
savedCameras: direction === "down" && fresh && event.camera !== void 0 ? { ...start.savedCameras, [layerKey(from)]: event.camera } : start.savedCameras,
|
|
1411
|
+
returnCamera: fresh ? event.camera ?? null : start.returnCamera
|
|
1412
|
+
};
|
|
1413
|
+
};
|
|
1414
|
+
var startReveal = (state) => ({
|
|
1415
|
+
...state,
|
|
1416
|
+
phase: "revealing",
|
|
1417
|
+
layers: layersFor(state.layers, state.path, "entering"),
|
|
1418
|
+
flight: "done"
|
|
1419
|
+
});
|
|
1420
|
+
var finishJump = (state) => ({
|
|
1421
|
+
...state,
|
|
1422
|
+
phase: "idle",
|
|
1423
|
+
settledPath: state.path,
|
|
1424
|
+
direction: null,
|
|
1425
|
+
layers: settledLayers(state.path),
|
|
1426
|
+
geometry: null,
|
|
1427
|
+
geometryKey: null,
|
|
1428
|
+
savedCameras: pruneCameras(state.savedCameras, state.path),
|
|
1429
|
+
returnCamera: null
|
|
1430
|
+
});
|
|
1431
|
+
var drillReducer = (state, event) => {
|
|
1432
|
+
switch (event.type) {
|
|
1433
|
+
case "DRILL":
|
|
1434
|
+
return onDrill(state, event);
|
|
1435
|
+
case "FLIGHT_END": {
|
|
1436
|
+
if (state.phase !== "flying" || event.id !== state.flightId) return state;
|
|
1437
|
+
if (state.direction === "up") return settle(state);
|
|
1438
|
+
if (!event.completed) return cancelled(state);
|
|
1439
|
+
if (state.geometry === "error") return flyBack(state);
|
|
1440
|
+
return state.geometry === "ready" ? startReveal(state) : { ...state, phase: "loading", flight: "done" };
|
|
1441
|
+
}
|
|
1442
|
+
case "GEOMETRY_READY": {
|
|
1443
|
+
if (event.key !== state.geometryKey || state.geometry !== "pending") return state;
|
|
1444
|
+
const ready = { ...state, geometry: "ready" };
|
|
1445
|
+
if (state.phase !== "loading") return ready;
|
|
1446
|
+
return state.direction === "jump" ? finishJump(ready) : startReveal(ready);
|
|
1447
|
+
}
|
|
1448
|
+
case "GEOMETRY_ERROR": {
|
|
1449
|
+
if (event.key !== state.geometryKey || state.geometry !== "pending") return state;
|
|
1450
|
+
const failed = { ...state, geometry: "error", error: event.key };
|
|
1451
|
+
if (state.phase === "flying") return failed;
|
|
1452
|
+
if (state.phase !== "loading") return state;
|
|
1453
|
+
return state.direction === "jump" ? cancelled(failed) : flyBack(failed);
|
|
1454
|
+
}
|
|
1455
|
+
case "REVEAL_END":
|
|
1456
|
+
return state.phase === "revealing" ? settle(state) : state;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
// src/core/drill-levels.ts
|
|
1461
|
+
var contextOf = (path) => ({
|
|
1462
|
+
path,
|
|
1463
|
+
parentId: path.at(-1) ?? null,
|
|
1464
|
+
depth: path.length
|
|
1465
|
+
});
|
|
1466
|
+
var validateLevels = (levels) => {
|
|
1467
|
+
if (levels.length === 0) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
"[choropleth] drill: at least one level is required (received an empty array)."
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1473
|
+
for (const level of levels) {
|
|
1474
|
+
if (seen.has(level.id)) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`[choropleth] drill: duplicate level id "${level.id}". Each level needs a unique id.`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
seen.add(level.id);
|
|
1480
|
+
}
|
|
1481
|
+
if (levels[0]?.geography === "inherit") {
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`[choropleth] drill: the first level ("${levels[0].id}") cannot use geography: "inherit" because it has no parent. Pass its geometry.`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
var clampPath = (path, levelCount) => {
|
|
1488
|
+
const max = Math.max(0, levelCount - 1);
|
|
1489
|
+
return path.length <= max ? { path, clamped: false } : { path: path.slice(0, max), clamped: true };
|
|
1490
|
+
};
|
|
1491
|
+
var buildBreadcrumbs = ({
|
|
1492
|
+
path,
|
|
1493
|
+
rootLabel,
|
|
1494
|
+
getLabel
|
|
1495
|
+
}) => [
|
|
1496
|
+
{ depth: 0, id: null, label: rootLabel },
|
|
1497
|
+
...path.map((id, index) => ({
|
|
1498
|
+
depth: index + 1,
|
|
1499
|
+
id,
|
|
1500
|
+
label: getLabel?.({ id, depth: index + 1 }) ?? id
|
|
1501
|
+
}))
|
|
1502
|
+
];
|
|
1503
|
+
var geographyKeyFor = (levels, path) => {
|
|
1504
|
+
for (let depth = Math.min(path.length, levels.length - 1); depth >= 0; depth--) {
|
|
1505
|
+
const level = levels[depth];
|
|
1506
|
+
if (level === void 0) continue;
|
|
1507
|
+
const ctx = contextOf(path.slice(0, depth));
|
|
1508
|
+
if (level.geographyKey !== void 0) return level.geographyKey(ctx);
|
|
1509
|
+
if (level.geography === "inherit" && depth > 0) continue;
|
|
1510
|
+
return typeof level.geography === "function" ? `${level.id}:${ctx.parentId}` : level.id;
|
|
1511
|
+
}
|
|
1512
|
+
return "";
|
|
1513
|
+
};
|
|
1514
|
+
var resolveDrillData = (data, ctx) => typeof data === "function" ? data(ctx) : data;
|
|
1515
|
+
var drillDirectionBetween = (from, to) => {
|
|
1516
|
+
const direction = classifyDrill(from, to);
|
|
1517
|
+
return direction === "equal" ? null : direction;
|
|
1518
|
+
};
|
|
1519
|
+
var DEFAULT_LEVEL_CACHE_SIZE = 8;
|
|
1520
|
+
var createLruCache = (capacity = DEFAULT_LEVEL_CACHE_SIZE) => {
|
|
1521
|
+
if (!(Number.isInteger(capacity) && capacity >= 1)) {
|
|
1522
|
+
throw new Error(
|
|
1523
|
+
`[choropleth] createLruCache: capacity must be an integer \u2265 1 (received: ${capacity}).`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1527
|
+
return {
|
|
1528
|
+
get: (key) => {
|
|
1529
|
+
if (!entries.has(key)) return void 0;
|
|
1530
|
+
const value = entries.get(key);
|
|
1531
|
+
entries.delete(key);
|
|
1532
|
+
entries.set(key, value);
|
|
1533
|
+
return value;
|
|
1534
|
+
},
|
|
1535
|
+
has: (key) => entries.has(key),
|
|
1536
|
+
set: (key, value) => {
|
|
1537
|
+
entries.delete(key);
|
|
1538
|
+
entries.set(key, value);
|
|
1539
|
+
if (entries.size > capacity) {
|
|
1540
|
+
const oldest = entries.keys().next().value;
|
|
1541
|
+
if (oldest !== void 0) entries.delete(oldest);
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
delete: (key) => void entries.delete(key),
|
|
1545
|
+
clear: () => entries.clear(),
|
|
1546
|
+
get size() {
|
|
1547
|
+
return entries.size;
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
// src/core/drill-intent.ts
|
|
1553
|
+
var DRILL_INTENT_MS = 150;
|
|
1554
|
+
var createIntentTimer = ({
|
|
1555
|
+
onIntent,
|
|
1556
|
+
delay = DRILL_INTENT_MS,
|
|
1557
|
+
setTimer = (callback, ms) => setTimeout(callback, ms),
|
|
1558
|
+
clearTimer = (handle) => clearTimeout(handle)
|
|
1559
|
+
}) => {
|
|
1560
|
+
let current = null;
|
|
1561
|
+
let handle;
|
|
1562
|
+
let armed = false;
|
|
1563
|
+
const cancel = () => {
|
|
1564
|
+
if (armed) clearTimer(handle);
|
|
1565
|
+
armed = false;
|
|
1566
|
+
};
|
|
1567
|
+
return {
|
|
1568
|
+
hover: (key) => {
|
|
1569
|
+
if (key === current) return;
|
|
1570
|
+
cancel();
|
|
1571
|
+
current = key;
|
|
1572
|
+
if (key === null) return;
|
|
1573
|
+
armed = true;
|
|
1574
|
+
handle = setTimer(() => {
|
|
1575
|
+
armed = false;
|
|
1576
|
+
onIntent(key);
|
|
1577
|
+
}, delay);
|
|
1578
|
+
},
|
|
1579
|
+
dispose: () => {
|
|
1580
|
+
cancel();
|
|
1581
|
+
current = null;
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
1585
|
+
var checkDrillStep = ({
|
|
1586
|
+
id,
|
|
1587
|
+
depth,
|
|
1588
|
+
levelId,
|
|
1589
|
+
knownIds,
|
|
1590
|
+
drillable
|
|
1591
|
+
}) => {
|
|
1592
|
+
if (knownIds !== void 0 && !knownIds.has(id)) {
|
|
1593
|
+
return { kind: "unknown-id", id, depth, levelId };
|
|
1594
|
+
}
|
|
1595
|
+
if (drillable === false) return { kind: "cannot-drill", id, depth, levelId };
|
|
1596
|
+
return null;
|
|
1597
|
+
};
|
|
1598
|
+
var formatDrillRequestProblem = (problem, source) => {
|
|
1599
|
+
const where = `level "${problem.levelId}" (depth ${problem.depth})`;
|
|
1600
|
+
return problem.kind === "unknown-id" ? `[choropleth] ${source}: region ${JSON.stringify(problem.id)} does not exist in ${where}. Hint: ids come from \`getFeatureId\` / \`feature.id\`; navigation was ignored.` : `[choropleth] ${source}: region ${JSON.stringify(problem.id)} cannot be drilled into: \`canDrill\` is false for it in ${where}; navigation was ignored.`;
|
|
1601
|
+
};
|
|
1602
|
+
var formatTooDeepRequest = (path, levelCount, source) => `[choropleth] ${source}: cannot go to ${JSON.stringify(path)}: only ${levelCount} level(s) are defined (max depth ${Math.max(0, levelCount - 1)}); navigation was ignored.`;
|
|
1603
|
+
var formatNoIdsDiagnostic = ({
|
|
1604
|
+
featureCount,
|
|
1605
|
+
idCount,
|
|
1606
|
+
levelId,
|
|
1607
|
+
customGetFeatureId
|
|
1608
|
+
}) => {
|
|
1609
|
+
if (featureCount === 0 || idCount > 0) return null;
|
|
1610
|
+
const level = levelId === null ? "" : ` in level "${levelId}"`;
|
|
1611
|
+
const hint = customGetFeatureId ? "`getFeatureId` returned `undefined` for every one; check the property it reads." : "none has a `feature.id`; pass `getFeatureId` to read another property.";
|
|
1612
|
+
return `[choropleth] none of the ${featureCount} region(s)${level} has an id, so no data can match: ${hint}`;
|
|
1613
|
+
};
|
|
1614
|
+
var formatWrongLevelDataHint = ({
|
|
1615
|
+
drillActive,
|
|
1616
|
+
dataIsSelector,
|
|
1617
|
+
dataCount,
|
|
1618
|
+
unmatchedCount,
|
|
1619
|
+
levelId
|
|
1620
|
+
}) => {
|
|
1621
|
+
if (!drillActive || dataIsSelector || dataCount === 0 || unmatchedCount < dataCount) return null;
|
|
1622
|
+
const level = levelId === null ? "this level" : `level "${levelId}"`;
|
|
1623
|
+
return `Hint: with \`drill\`, a fixed \`data\` array is used for every level and none of its rows match ${level}. Pass \`data\` as a selector to give each level its own rows: \`data={(ctx) => rowsFor(ctx)}\`.`;
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
// src/core/drill-chain.ts
|
|
1627
|
+
import { geoBounds as geoBounds2, geoPath as geoPath2 } from "d3-geo";
|
|
1628
|
+
var boundsToObject = ([[west, south], [east, north]]) => ({
|
|
1629
|
+
type: "MultiPoint",
|
|
1630
|
+
coordinates: [
|
|
1631
|
+
[west, south],
|
|
1632
|
+
[(west + east) / 2, south],
|
|
1633
|
+
[east, south],
|
|
1634
|
+
[east, (south + north) / 2],
|
|
1635
|
+
[east, north],
|
|
1636
|
+
[(west + east) / 2, north],
|
|
1637
|
+
[west, north],
|
|
1638
|
+
[west, (south + north) / 2]
|
|
1639
|
+
]
|
|
1640
|
+
});
|
|
1641
|
+
var widerHalf = ([[west, south], [east, north]]) => {
|
|
1642
|
+
if (west <= east)
|
|
1643
|
+
return [
|
|
1644
|
+
[west, south],
|
|
1645
|
+
[east, north]
|
|
1646
|
+
];
|
|
1647
|
+
return 180 - west >= east + 180 ? [
|
|
1648
|
+
[west, south],
|
|
1649
|
+
[180, north]
|
|
1650
|
+
] : [
|
|
1651
|
+
[-180, south],
|
|
1652
|
+
[east, north]
|
|
1653
|
+
];
|
|
1654
|
+
};
|
|
1655
|
+
var mainBody = (geometry) => {
|
|
1656
|
+
if (geometry === null) return null;
|
|
1657
|
+
const [[west, south], [east, north]] = geoBounds2(geometry);
|
|
1658
|
+
return west > east ? boundsToObject(
|
|
1659
|
+
widerHalf([
|
|
1660
|
+
[west, south],
|
|
1661
|
+
[east, north]
|
|
1662
|
+
])
|
|
1663
|
+
) : geometry;
|
|
1664
|
+
};
|
|
1665
|
+
var buildProjectionChain = ({
|
|
1666
|
+
levels,
|
|
1667
|
+
path,
|
|
1668
|
+
name,
|
|
1669
|
+
width,
|
|
1670
|
+
height,
|
|
1671
|
+
padding,
|
|
1672
|
+
focusBounds
|
|
1673
|
+
}) => {
|
|
1674
|
+
const root = levels[0];
|
|
1675
|
+
const collection = (features) => ({
|
|
1676
|
+
type: "FeatureCollection",
|
|
1677
|
+
features: [...features]
|
|
1678
|
+
});
|
|
1679
|
+
const projections = [
|
|
1680
|
+
createProjection({
|
|
1681
|
+
name,
|
|
1682
|
+
width,
|
|
1683
|
+
height,
|
|
1684
|
+
featureCollection: collection(root?.features ?? [])
|
|
1685
|
+
})
|
|
1686
|
+
];
|
|
1687
|
+
const factory = projectionFactory(name);
|
|
1688
|
+
const pad = normalizePadding(padding);
|
|
1689
|
+
let missing = null;
|
|
1690
|
+
for (let depth = 1; depth <= path.length; depth++) {
|
|
1691
|
+
const parent = projections[depth - 1];
|
|
1692
|
+
const id = path[depth - 1];
|
|
1693
|
+
if (missing !== null) {
|
|
1694
|
+
projections.push(parent);
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
const level = levels[depth - 1];
|
|
1698
|
+
const feature2 = level?.features.find((f) => level.idOf(f) === id);
|
|
1699
|
+
const bounds = focusBounds?.[id];
|
|
1700
|
+
const focus = bounds != null ? boundsToObject(widerHalf(bounds)) : feature2 === void 0 ? null : mainBody(getFocusGeometry({ feature: feature2, path: geoPath2(parent) }));
|
|
1701
|
+
if (focus === null) {
|
|
1702
|
+
missing = id;
|
|
1703
|
+
projections.push(parent);
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
try {
|
|
1707
|
+
projections.push(
|
|
1708
|
+
rebaseProjection({ parent, factory, focus, width, height, padding: pad }).projection
|
|
1709
|
+
);
|
|
1710
|
+
} catch {
|
|
1711
|
+
missing = id;
|
|
1712
|
+
projections.push(parent);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return { projections, missing };
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
export {
|
|
1719
|
+
unwrapGeography,
|
|
1720
|
+
toFeatureCollection,
|
|
1721
|
+
getFocusGeometry,
|
|
1722
|
+
getAnchorPoint,
|
|
1723
|
+
DEFAULT_PADDING,
|
|
1724
|
+
normalizePadding,
|
|
1725
|
+
boundsToTransform,
|
|
1726
|
+
projectBounds,
|
|
1727
|
+
getFocusExtent,
|
|
1728
|
+
FIT_TRANSFORM,
|
|
1729
|
+
isFitTransform,
|
|
1730
|
+
applyTransform,
|
|
1731
|
+
invertTransform,
|
|
1732
|
+
constrainAxis,
|
|
1733
|
+
constrainTransform,
|
|
1734
|
+
preserveView,
|
|
1735
|
+
MIN_FLIGHT_MS,
|
|
1736
|
+
MAX_FLIGHT_MS,
|
|
1737
|
+
DEFAULT_FLIGHT_CURVATURE,
|
|
1738
|
+
transformToZoomView,
|
|
1739
|
+
createZoomInterpolator,
|
|
1740
|
+
flightDuration,
|
|
1741
|
+
INERTIA_TAU_MS,
|
|
1742
|
+
inertiaOffset,
|
|
1743
|
+
INERTIA_MIN_VELOCITY,
|
|
1744
|
+
INERTIA_WINDOW_MS,
|
|
1745
|
+
releaseVelocity,
|
|
1746
|
+
bubbleScale,
|
|
1747
|
+
toViewState,
|
|
1748
|
+
defineTheme,
|
|
1749
|
+
THEMES,
|
|
1750
|
+
isThemeName,
|
|
1751
|
+
resolveTheme,
|
|
1752
|
+
extendTheme,
|
|
1753
|
+
themeToCssVars,
|
|
1754
|
+
en,
|
|
1755
|
+
es,
|
|
1756
|
+
resolveLabels,
|
|
1757
|
+
joinData,
|
|
1758
|
+
DEFAULT_CLASSES,
|
|
1759
|
+
finiteValues,
|
|
1760
|
+
computeThresholds,
|
|
1761
|
+
SEQUENTIAL_PALETTES,
|
|
1762
|
+
DIVERGING_PALETTES,
|
|
1763
|
+
CATEGORICAL_PALETTES,
|
|
1764
|
+
DEFAULT_EMPTY_FILL,
|
|
1765
|
+
createColorScale,
|
|
1766
|
+
projectionFactory,
|
|
1767
|
+
DEFAULT_PROJECTION,
|
|
1768
|
+
createProjection,
|
|
1769
|
+
DEFAULT_ASPECT_RATIO,
|
|
1770
|
+
computeAspectRatio,
|
|
1771
|
+
relativeTransform,
|
|
1772
|
+
rebaseProjection,
|
|
1773
|
+
defaultFormatValue,
|
|
1774
|
+
classIndex,
|
|
1775
|
+
buildLegend,
|
|
1776
|
+
createBorders,
|
|
1777
|
+
formatJoinDiagnostics,
|
|
1778
|
+
createCategoricalScale,
|
|
1779
|
+
buildCategoricalLegend,
|
|
1780
|
+
DEFAULT_MAX_RADIUS,
|
|
1781
|
+
createRadiusScale,
|
|
1782
|
+
buildSizeLegend,
|
|
1783
|
+
DEFAULT_BUBBLE_FILL,
|
|
1784
|
+
resolveVariant,
|
|
1785
|
+
DEFAULT_FOCUS_BOUNDS,
|
|
1786
|
+
DEFAULT_ZOOM_MIN,
|
|
1787
|
+
DEFAULT_ZOOM_MAX,
|
|
1788
|
+
resolveZoomOptions,
|
|
1789
|
+
resolveControls,
|
|
1790
|
+
zoomKeyAction,
|
|
1791
|
+
createZoomFilter,
|
|
1792
|
+
resolveZoomTarget,
|
|
1793
|
+
layerKey,
|
|
1794
|
+
classifyDrill,
|
|
1795
|
+
createDrillState,
|
|
1796
|
+
savedCameraFor,
|
|
1797
|
+
drillReducer,
|
|
1798
|
+
contextOf,
|
|
1799
|
+
validateLevels,
|
|
1800
|
+
clampPath,
|
|
1801
|
+
buildBreadcrumbs,
|
|
1802
|
+
geographyKeyFor,
|
|
1803
|
+
resolveDrillData,
|
|
1804
|
+
drillDirectionBetween,
|
|
1805
|
+
DEFAULT_LEVEL_CACHE_SIZE,
|
|
1806
|
+
createLruCache,
|
|
1807
|
+
DRILL_INTENT_MS,
|
|
1808
|
+
createIntentTimer,
|
|
1809
|
+
checkDrillStep,
|
|
1810
|
+
formatDrillRequestProblem,
|
|
1811
|
+
formatTooDeepRequest,
|
|
1812
|
+
formatNoIdsDiagnostic,
|
|
1813
|
+
formatWrongLevelDataHint,
|
|
1814
|
+
buildProjectionChain
|
|
1815
|
+
};
|
|
1816
|
+
//# sourceMappingURL=chunk-R3XS6XPP.js.map
|