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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2926 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_EMPTY_FILL,
|
|
3
|
+
DEFAULT_FLIGHT_CURVATURE,
|
|
4
|
+
DEFAULT_FOCUS_BOUNDS,
|
|
5
|
+
FIT_TRANSFORM,
|
|
6
|
+
INERTIA_TAU_MS,
|
|
7
|
+
THEMES,
|
|
8
|
+
bubbleScale,
|
|
9
|
+
buildBreadcrumbs,
|
|
10
|
+
buildProjectionChain,
|
|
11
|
+
checkDrillStep,
|
|
12
|
+
clampPath,
|
|
13
|
+
classifyDrill,
|
|
14
|
+
computeAspectRatio,
|
|
15
|
+
constrainTransform,
|
|
16
|
+
contextOf,
|
|
17
|
+
createBorders,
|
|
18
|
+
createDrillState,
|
|
19
|
+
createIntentTimer,
|
|
20
|
+
createLruCache,
|
|
21
|
+
createProjection,
|
|
22
|
+
createZoomFilter,
|
|
23
|
+
createZoomInterpolator,
|
|
24
|
+
defaultFormatValue,
|
|
25
|
+
defineTheme,
|
|
26
|
+
drillDirectionBetween,
|
|
27
|
+
drillReducer,
|
|
28
|
+
extendTheme,
|
|
29
|
+
flightDuration,
|
|
30
|
+
formatDrillRequestProblem,
|
|
31
|
+
formatJoinDiagnostics,
|
|
32
|
+
formatNoIdsDiagnostic,
|
|
33
|
+
formatTooDeepRequest,
|
|
34
|
+
formatWrongLevelDataHint,
|
|
35
|
+
geographyKeyFor,
|
|
36
|
+
getAnchorPoint,
|
|
37
|
+
getFocusExtent,
|
|
38
|
+
getFocusGeometry,
|
|
39
|
+
inertiaOffset,
|
|
40
|
+
isFitTransform,
|
|
41
|
+
isThemeName,
|
|
42
|
+
joinData,
|
|
43
|
+
preserveView,
|
|
44
|
+
relativeTransform,
|
|
45
|
+
releaseVelocity,
|
|
46
|
+
resolveControls,
|
|
47
|
+
resolveDrillData,
|
|
48
|
+
resolveLabels,
|
|
49
|
+
resolveTheme,
|
|
50
|
+
resolveVariant,
|
|
51
|
+
resolveZoomOptions,
|
|
52
|
+
resolveZoomTarget,
|
|
53
|
+
savedCameraFor,
|
|
54
|
+
themeToCssVars,
|
|
55
|
+
toFeatureCollection,
|
|
56
|
+
toViewState,
|
|
57
|
+
unwrapGeography,
|
|
58
|
+
validateLevels,
|
|
59
|
+
zoomKeyAction
|
|
60
|
+
} from "./chunk-R3XS6XPP.js";
|
|
61
|
+
|
|
62
|
+
// src/react/use-choropleth.ts
|
|
63
|
+
import {
|
|
64
|
+
useEffect as useEffect6,
|
|
65
|
+
useLayoutEffect,
|
|
66
|
+
useMemo as useMemo3,
|
|
67
|
+
useRef as useRef5,
|
|
68
|
+
useState as useState5
|
|
69
|
+
} from "react";
|
|
70
|
+
import { geoPath } from "d3-geo";
|
|
71
|
+
|
|
72
|
+
// src/react/drill-levels.ts
|
|
73
|
+
var defineDrillLevels = (levels) => {
|
|
74
|
+
validateLevels(levels);
|
|
75
|
+
return levels;
|
|
76
|
+
};
|
|
77
|
+
var buildDrillEvent = ({
|
|
78
|
+
from,
|
|
79
|
+
to,
|
|
80
|
+
item
|
|
81
|
+
}) => {
|
|
82
|
+
const direction = drillDirectionBetween(from, to);
|
|
83
|
+
if (direction === null) return null;
|
|
84
|
+
return {
|
|
85
|
+
direction,
|
|
86
|
+
from: contextOf(from),
|
|
87
|
+
to: contextOf(to),
|
|
88
|
+
...item === void 0 ? {} : { item }
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
var isDrillable = (levels, depth, item) => {
|
|
92
|
+
const level = levels[depth];
|
|
93
|
+
if (level === void 0 || depth >= levels.length - 1) return false;
|
|
94
|
+
const { canDrill } = level;
|
|
95
|
+
if (canDrill === void 0) return true;
|
|
96
|
+
return typeof canDrill === "function" ? canDrill(item) : canDrill;
|
|
97
|
+
};
|
|
98
|
+
var sourceDepth = (levels, depth) => {
|
|
99
|
+
let source = Math.min(depth, levels.length - 1);
|
|
100
|
+
while (source > 0 && levels[source]?.geography === "inherit") source--;
|
|
101
|
+
return source;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/react/use-drill-choreography.ts
|
|
105
|
+
import { useEffect, useRef } from "react";
|
|
106
|
+
|
|
107
|
+
// src/react/env.ts
|
|
108
|
+
var isProduction = () => {
|
|
109
|
+
try {
|
|
110
|
+
return process.env.NODE_ENV === "production";
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
116
|
+
|
|
117
|
+
// src/react/use-drill-choreography.ts
|
|
118
|
+
var REVEAL_MS = 450;
|
|
119
|
+
var lastOf = (items) => items[items.length - 1];
|
|
120
|
+
var useDrillChoreography = ({
|
|
121
|
+
engine,
|
|
122
|
+
store,
|
|
123
|
+
shownChain,
|
|
124
|
+
chainFor
|
|
125
|
+
}) => {
|
|
126
|
+
const { active, machine, dispatch } = engine;
|
|
127
|
+
const latest = useRef({ engine, shownChain, chainFor });
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
latest.current = { engine, shownChain, chainFor };
|
|
130
|
+
});
|
|
131
|
+
const flying = machine.phase === "flying";
|
|
132
|
+
const flightId = machine.flightId;
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!active || !flying) return;
|
|
135
|
+
const { engine: current, shownChain: shown, chainFor: build } = latest.current;
|
|
136
|
+
const state = current.machine;
|
|
137
|
+
const finish = (completed) => dispatch({ type: "FLIGHT_END", id: flightId, completed });
|
|
138
|
+
let destination;
|
|
139
|
+
if (state.direction === "up") {
|
|
140
|
+
destination = state.returnCamera ?? savedCameraFor(state, state.path) ?? FIT_TRANSFORM;
|
|
141
|
+
} else {
|
|
142
|
+
const levels = current.chainLevelsFor(state.path);
|
|
143
|
+
const next = levels === null ? null : build(levels, state.path);
|
|
144
|
+
const from = lastOf(shown?.projections ?? []);
|
|
145
|
+
const to = lastOf(next?.projections ?? []);
|
|
146
|
+
destination = from === void 0 || to === void 0 || next?.missing != null ? null : relativeTransform(from, to);
|
|
147
|
+
}
|
|
148
|
+
const api = store.getZoom();
|
|
149
|
+
if (api === null || destination === null) {
|
|
150
|
+
finish(true);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const flight = { cancelled: false };
|
|
154
|
+
void api.flyTo(destination).then(({ completed }) => {
|
|
155
|
+
if (!flight.cancelled) finish(completed);
|
|
156
|
+
});
|
|
157
|
+
return () => {
|
|
158
|
+
flight.cancelled = true;
|
|
159
|
+
};
|
|
160
|
+
}, [active, flying, flightId, store, dispatch]);
|
|
161
|
+
const revealing = machine.phase === "revealing";
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!active || !revealing) return;
|
|
164
|
+
const timer = setTimeout(
|
|
165
|
+
() => dispatch({ type: "REVEAL_END" }),
|
|
166
|
+
prefersReducedMotion() ? 0 : REVEAL_MS
|
|
167
|
+
);
|
|
168
|
+
return () => clearTimeout(timer);
|
|
169
|
+
}, [active, revealing, dispatch]);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/react/map-item.ts
|
|
173
|
+
var ROOT_LEVEL = { id: "root", depth: 0 };
|
|
174
|
+
var isSameItem = (a, b) => a === b || a !== null && b !== null && a.kind === b.kind && a.id === b.id;
|
|
175
|
+
|
|
176
|
+
// src/react/map-store.ts
|
|
177
|
+
import { useSyncExternalStore } from "react";
|
|
178
|
+
var GESTURE_HINT_MS = 1500;
|
|
179
|
+
var INITIAL_STATE = {
|
|
180
|
+
hovered: null,
|
|
181
|
+
pointer: null,
|
|
182
|
+
pointerType: "mouse",
|
|
183
|
+
interacting: false,
|
|
184
|
+
camera: FIT_TRANSFORM,
|
|
185
|
+
gestureHint: null
|
|
186
|
+
};
|
|
187
|
+
var createMapStore = () => {
|
|
188
|
+
let state = INITIAL_STATE;
|
|
189
|
+
let hintTimer;
|
|
190
|
+
let zoomApi = null;
|
|
191
|
+
let drillApi = null;
|
|
192
|
+
let fallbackView = null;
|
|
193
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
194
|
+
const store = {
|
|
195
|
+
subscribe: (listener) => {
|
|
196
|
+
listeners.add(listener);
|
|
197
|
+
return () => listeners.delete(listener);
|
|
198
|
+
},
|
|
199
|
+
getState: () => state,
|
|
200
|
+
setState: (patch) => {
|
|
201
|
+
const changed = Object.keys(patch).some(
|
|
202
|
+
(key) => !Object.is(state[key], patch[key])
|
|
203
|
+
);
|
|
204
|
+
if (!changed) return;
|
|
205
|
+
state = { ...state, ...patch };
|
|
206
|
+
for (const listener of listeners) listener();
|
|
207
|
+
},
|
|
208
|
+
showGestureHint: (kind) => {
|
|
209
|
+
clearTimeout(hintTimer);
|
|
210
|
+
store.setState({ gestureHint: kind });
|
|
211
|
+
hintTimer = setTimeout(() => store.setState({ gestureHint: null }), GESTURE_HINT_MS);
|
|
212
|
+
},
|
|
213
|
+
dispose: () => clearTimeout(hintTimer),
|
|
214
|
+
registerZoom: (api) => {
|
|
215
|
+
zoomApi = api;
|
|
216
|
+
},
|
|
217
|
+
getZoom: () => zoomApi,
|
|
218
|
+
registerDrill: (api) => {
|
|
219
|
+
drillApi = api;
|
|
220
|
+
},
|
|
221
|
+
getDrill: () => drillApi,
|
|
222
|
+
registerFallbackView: (read) => {
|
|
223
|
+
fallbackView = read;
|
|
224
|
+
},
|
|
225
|
+
getFallbackView: () => fallbackView?.() ?? null
|
|
226
|
+
};
|
|
227
|
+
return store;
|
|
228
|
+
};
|
|
229
|
+
var useMapStore = (store, selector) => useSyncExternalStore(
|
|
230
|
+
store.subscribe,
|
|
231
|
+
() => selector(store.getState()),
|
|
232
|
+
() => selector(INITIAL_STATE)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// src/react/use-container-size.ts
|
|
236
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
237
|
+
var DEFAULT_WIDTH = 960;
|
|
238
|
+
var DEFAULT_ASPECT_RATIO = 960 / 500;
|
|
239
|
+
var useContainerSize = ({
|
|
240
|
+
initial,
|
|
241
|
+
aspectRatio = DEFAULT_ASPECT_RATIO,
|
|
242
|
+
useContainerHeight = false
|
|
243
|
+
} = {}) => {
|
|
244
|
+
const ref = useRef2(null);
|
|
245
|
+
const [measured, setMeasured] = useState(null);
|
|
246
|
+
useEffect2(() => {
|
|
247
|
+
const element = ref.current;
|
|
248
|
+
if (element === null || typeof ResizeObserver === "undefined") return;
|
|
249
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
250
|
+
if (entry === void 0) return;
|
|
251
|
+
const { width, height: height2 } = entry.contentRect;
|
|
252
|
+
if (!(width > 0)) return;
|
|
253
|
+
setMeasured(
|
|
254
|
+
(prev) => prev?.width === width && prev.height === height2 ? prev : { width, height: height2 }
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
observer.observe(element);
|
|
258
|
+
return () => observer.disconnect();
|
|
259
|
+
}, []);
|
|
260
|
+
if (measured === null) {
|
|
261
|
+
return { ref, ...initial ?? { width: DEFAULT_WIDTH, height: DEFAULT_WIDTH / aspectRatio } };
|
|
262
|
+
}
|
|
263
|
+
const height = useContainerHeight && measured.height > 0 ? measured.height : measured.width / aspectRatio;
|
|
264
|
+
return { ref, width: measured.width, height };
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/react/use-drill-engine.ts
|
|
268
|
+
import {
|
|
269
|
+
useCallback as useCallback2,
|
|
270
|
+
useEffect as useEffect4,
|
|
271
|
+
useMemo as useMemo2,
|
|
272
|
+
useReducer,
|
|
273
|
+
useRef as useRef4,
|
|
274
|
+
useState as useState3
|
|
275
|
+
} from "react";
|
|
276
|
+
|
|
277
|
+
// src/react/use-drill-down.ts
|
|
278
|
+
import { useCallback, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
279
|
+
var createDrillBridge = () => {
|
|
280
|
+
let status = "idle";
|
|
281
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
282
|
+
return {
|
|
283
|
+
getStatus: () => status,
|
|
284
|
+
setStatus: (next) => {
|
|
285
|
+
if (next === status) return;
|
|
286
|
+
status = next;
|
|
287
|
+
listeners.forEach((listener) => listener());
|
|
288
|
+
},
|
|
289
|
+
subscribe: (listener) => {
|
|
290
|
+
listeners.add(listener);
|
|
291
|
+
return () => void listeners.delete(listener);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
var DRILL_BRIDGE = /* @__PURE__ */ Symbol("choropleth.drillBridge");
|
|
296
|
+
var samePath = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
297
|
+
var warn = (message) => {
|
|
298
|
+
if (!isProduction()) console.warn(`[choropleth] useDrillDown: ${message}`);
|
|
299
|
+
};
|
|
300
|
+
var useDrillDown = ({
|
|
301
|
+
levels,
|
|
302
|
+
path: controlledPath,
|
|
303
|
+
defaultPath,
|
|
304
|
+
onPathChange,
|
|
305
|
+
getLabel
|
|
306
|
+
}) => {
|
|
307
|
+
useMemo(() => validateLevels(levels), [levels]);
|
|
308
|
+
const controlled = controlledPath !== void 0;
|
|
309
|
+
const [innerPath, setInnerPath] = useState2(
|
|
310
|
+
() => clampPath(defaultPath ?? [], levels.length).path
|
|
311
|
+
);
|
|
312
|
+
const [bridge] = useState2(createDrillBridge);
|
|
313
|
+
const requested = controlled ? controlledPath : innerPath;
|
|
314
|
+
const { path, clamped } = clampPath(requested, levels.length);
|
|
315
|
+
const pathKey = JSON.stringify(requested);
|
|
316
|
+
useEffect3(() => {
|
|
317
|
+
if (clamped) {
|
|
318
|
+
warn(
|
|
319
|
+
`path ${pathKey} is deeper than the ${levels.length} level(s) allow; it was truncated to ${JSON.stringify(path)}.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}, [clamped, pathKey]);
|
|
323
|
+
const latest = useRef3({ path, levels: levels.length, controlled, onPathChange });
|
|
324
|
+
useEffect3(() => {
|
|
325
|
+
latest.current = { path, levels: levels.length, controlled, onPathChange };
|
|
326
|
+
});
|
|
327
|
+
const commit = useCallback((next) => {
|
|
328
|
+
const current = latest.current;
|
|
329
|
+
const { path: valid, clamped: tooDeep } = clampPath(next, current.levels);
|
|
330
|
+
if (tooDeep) {
|
|
331
|
+
warn(
|
|
332
|
+
`cannot drill to ${JSON.stringify(next)}: only ${current.levels} level(s) are defined (max depth ${current.levels - 1}).`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (samePath(valid, current.path)) return valid;
|
|
336
|
+
latest.current = { ...current, path: valid };
|
|
337
|
+
if (!current.controlled) setInnerPath(valid);
|
|
338
|
+
current.onPathChange?.(valid);
|
|
339
|
+
return valid;
|
|
340
|
+
}, []);
|
|
341
|
+
const drillDown = useCallback(
|
|
342
|
+
(id) => commit([...latest.current.path, id]),
|
|
343
|
+
[commit]
|
|
344
|
+
);
|
|
345
|
+
const drillUp = useCallback(
|
|
346
|
+
(steps = 1) => {
|
|
347
|
+
const up = Number.isFinite(steps) ? Math.max(0, Math.floor(steps)) : 0;
|
|
348
|
+
const { path: current } = latest.current;
|
|
349
|
+
return commit(current.slice(0, Math.max(0, current.length - up)));
|
|
350
|
+
},
|
|
351
|
+
[commit]
|
|
352
|
+
);
|
|
353
|
+
const drillTo = commit;
|
|
354
|
+
const reset = useCallback(() => commit([]), [commit]);
|
|
355
|
+
const status = useSyncExternalStore2(bridge.subscribe, bridge.getStatus, bridge.getStatus);
|
|
356
|
+
const rootLabel = levels[0]?.label ?? levels[0]?.id ?? "";
|
|
357
|
+
const breadcrumbs = useMemo(
|
|
358
|
+
() => buildBreadcrumbs({
|
|
359
|
+
path,
|
|
360
|
+
rootLabel,
|
|
361
|
+
getLabel: getLabel === void 0 ? void 0 : ({ id, depth }) => getLabel({ id, depth })
|
|
362
|
+
}),
|
|
363
|
+
// `getLabel` is usually an inline function: its identity must not recompute the breadcrumbs on every render.
|
|
364
|
+
// The route (`pathKey`) and the root do.
|
|
365
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
366
|
+
[pathKey, rootLabel]
|
|
367
|
+
);
|
|
368
|
+
const level = levels[path.length] ?? levels[levels.length - 1];
|
|
369
|
+
const context = useMemo(() => contextOf(path), [path]);
|
|
370
|
+
return useMemo(
|
|
371
|
+
() => ({
|
|
372
|
+
levels,
|
|
373
|
+
path,
|
|
374
|
+
context,
|
|
375
|
+
level,
|
|
376
|
+
breadcrumbs,
|
|
377
|
+
status,
|
|
378
|
+
canDrillUp: path.length > 0,
|
|
379
|
+
drillDown,
|
|
380
|
+
drillUp,
|
|
381
|
+
drillTo,
|
|
382
|
+
reset,
|
|
383
|
+
[DRILL_BRIDGE]: bridge
|
|
384
|
+
}),
|
|
385
|
+
[levels, path, context, level, breadcrumbs, status, drillDown, drillUp, drillTo, reset, bridge]
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
var isDrillController = (drill) => !Array.isArray(drill);
|
|
389
|
+
|
|
390
|
+
// src/react/use-drill-engine.ts
|
|
391
|
+
var REJECTED_AFTER_MS = 1e3;
|
|
392
|
+
var defaultFeatureId = (feature) => feature.id === void 0 ? void 0 : String(feature.id);
|
|
393
|
+
var collectionIds = /* @__PURE__ */ new WeakMap();
|
|
394
|
+
var nextCollectionId = 1;
|
|
395
|
+
var idOfCollection = (collection) => {
|
|
396
|
+
if (collection === void 0) return "-";
|
|
397
|
+
let id = collectionIds.get(collection);
|
|
398
|
+
if (id === void 0) {
|
|
399
|
+
id = nextCollectionId++;
|
|
400
|
+
collectionIds.set(collection, id);
|
|
401
|
+
}
|
|
402
|
+
return String(id);
|
|
403
|
+
};
|
|
404
|
+
var samePath2 = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
|
|
405
|
+
var collections = /* @__PURE__ */ new WeakMap();
|
|
406
|
+
var filtered = /* @__PURE__ */ new WeakMap();
|
|
407
|
+
var featureCollectionOf = (geography, objectName) => {
|
|
408
|
+
const byName = collections.get(geography) ?? /* @__PURE__ */ new Map();
|
|
409
|
+
collections.set(geography, byName);
|
|
410
|
+
const name = objectName ?? "";
|
|
411
|
+
const found = byName.get(name);
|
|
412
|
+
if (found !== void 0) return found;
|
|
413
|
+
const created = toFeatureCollection({ geography, objectName });
|
|
414
|
+
byName.set(name, created);
|
|
415
|
+
return created;
|
|
416
|
+
};
|
|
417
|
+
var stableFilter = (raw, keep) => {
|
|
418
|
+
const kept = raw.features.flatMap((feature, index) => keep(feature) ? [index] : []);
|
|
419
|
+
if (kept.length === raw.features.length) return raw;
|
|
420
|
+
const signature = kept.join(",");
|
|
421
|
+
const bySignature = filtered.get(raw) ?? /* @__PURE__ */ new Map();
|
|
422
|
+
filtered.set(raw, bySignature);
|
|
423
|
+
const found = bySignature.get(signature);
|
|
424
|
+
if (found !== void 0) return found;
|
|
425
|
+
const created = {
|
|
426
|
+
...raw,
|
|
427
|
+
features: kept.flatMap((index) => {
|
|
428
|
+
const feature = raw.features[index];
|
|
429
|
+
return feature === void 0 ? [] : [feature];
|
|
430
|
+
})
|
|
431
|
+
};
|
|
432
|
+
bySignature.set(signature, created);
|
|
433
|
+
return created;
|
|
434
|
+
};
|
|
435
|
+
var isSync = (input) => typeof input !== "function" && !("then" in input);
|
|
436
|
+
var resolveInput = (input) => isSync(input) ? Promise.resolve(input) : (typeof input === "function" ? input() : input).then(unwrapGeography);
|
|
437
|
+
var fixedGeography = (level) => {
|
|
438
|
+
const { geography } = level;
|
|
439
|
+
return typeof geography === "function" || geography === "inherit" || !isSync(geography) ? void 0 : geography;
|
|
440
|
+
};
|
|
441
|
+
var NO_LEVELS = [
|
|
442
|
+
{ id: "root", geography: { type: "FeatureCollection", features: [] } }
|
|
443
|
+
];
|
|
444
|
+
var EMPTY_PATH = [];
|
|
445
|
+
var EMPTY_COLLECTION = { type: "FeatureCollection", features: [] };
|
|
446
|
+
var useDrillEngine = ({
|
|
447
|
+
drill,
|
|
448
|
+
data,
|
|
449
|
+
filterFeatures,
|
|
450
|
+
store,
|
|
451
|
+
onDrill,
|
|
452
|
+
onDrillEnd,
|
|
453
|
+
onDrillError
|
|
454
|
+
}) => {
|
|
455
|
+
const active = drill !== void 0;
|
|
456
|
+
const internal = useDrillDown({
|
|
457
|
+
levels: drill === void 0 || isDrillController(drill) ? NO_LEVELS : drill
|
|
458
|
+
});
|
|
459
|
+
const controller = drill === void 0 ? null : isDrillController(drill) ? drill : internal;
|
|
460
|
+
const levels = controller?.levels ?? NO_LEVELS;
|
|
461
|
+
const target = controller?.path ?? EMPTY_PATH;
|
|
462
|
+
const targetKey = JSON.stringify(target);
|
|
463
|
+
const [machine, dispatch] = useReducer(drillReducer, target, createDrillState);
|
|
464
|
+
const [cache] = useState3(() => createLruCache());
|
|
465
|
+
const [version, setVersion] = useState3(0);
|
|
466
|
+
const [failed, setFailed] = useState3(/* @__PURE__ */ new Map());
|
|
467
|
+
const pending = useRef4(/* @__PURE__ */ new Set());
|
|
468
|
+
const sourceOf = useCallback2((depth2) => sourceDepth(levels, depth2), [levels]);
|
|
469
|
+
const keyAt = (depth2, path) => geographyKeyFor(levels, path.slice(0, sourceOf(depth2)));
|
|
470
|
+
const geographyAt = (depth2, path) => {
|
|
471
|
+
const level2 = levels[sourceOf(depth2)];
|
|
472
|
+
if (level2 === void 0) return void 0;
|
|
473
|
+
return fixedGeography(level2) ?? cache.get(keyAt(depth2, path));
|
|
474
|
+
};
|
|
475
|
+
const mounted = useRef4(false);
|
|
476
|
+
useEffect4(() => {
|
|
477
|
+
mounted.current = true;
|
|
478
|
+
return () => {
|
|
479
|
+
mounted.current = false;
|
|
480
|
+
};
|
|
481
|
+
}, []);
|
|
482
|
+
const request = (path, prefetching, retryFailed = false) => {
|
|
483
|
+
for (let depth2 = 0; depth2 <= path.length; depth2++) {
|
|
484
|
+
const source = sourceOf(depth2);
|
|
485
|
+
const level2 = levels[source];
|
|
486
|
+
if (level2 === void 0 || fixedGeography(level2) !== void 0) continue;
|
|
487
|
+
const key = keyAt(depth2, path);
|
|
488
|
+
if (cache.has(key) || pending.current.has(key)) continue;
|
|
489
|
+
if (failed.has(key) && !retryFailed) continue;
|
|
490
|
+
const input = typeof level2.geography === "function" ? level2.geography(contextOf(path.slice(0, source))) : level2.geography;
|
|
491
|
+
if (input === "inherit") continue;
|
|
492
|
+
pending.current.add(key);
|
|
493
|
+
resolveInput(input).then(
|
|
494
|
+
(loaded) => {
|
|
495
|
+
pending.current.delete(key);
|
|
496
|
+
cache.set(key, loaded);
|
|
497
|
+
if (mounted.current) setVersion((v) => v + 1);
|
|
498
|
+
},
|
|
499
|
+
(error) => {
|
|
500
|
+
pending.current.delete(key);
|
|
501
|
+
if (!mounted.current) return;
|
|
502
|
+
if (prefetching) setVersion((v) => v + 1);
|
|
503
|
+
else setFailed((previous2) => new Map(previous2).set(key, error));
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const latestRequest = useRef4(request);
|
|
509
|
+
useEffect4(() => {
|
|
510
|
+
latestRequest.current = request;
|
|
511
|
+
});
|
|
512
|
+
const prefetch = useCallback2(
|
|
513
|
+
(path) => {
|
|
514
|
+
if (active) latestRequest.current(path, true);
|
|
515
|
+
},
|
|
516
|
+
[active]
|
|
517
|
+
);
|
|
518
|
+
const requestedTarget = useRef4("");
|
|
519
|
+
useEffect4(() => {
|
|
520
|
+
const retryFailed = requestedTarget.current !== targetKey;
|
|
521
|
+
requestedTarget.current = targetKey;
|
|
522
|
+
if (active) request(target, false, retryFailed);
|
|
523
|
+
}, [active, targetKey, levels, version]);
|
|
524
|
+
const keepFor = (depth2, path) => {
|
|
525
|
+
const source = sourceOf(depth2);
|
|
526
|
+
return (feature) => {
|
|
527
|
+
for (let k = source; k <= depth2; k++) {
|
|
528
|
+
const level2 = levels[k];
|
|
529
|
+
if (level2?.filterFeatures?.(feature, contextOf(path.slice(0, k))) === false) return false;
|
|
530
|
+
}
|
|
531
|
+
return source !== 0 || filterFeatures === void 0 || filterFeatures(feature);
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
const featuresAt = (depth2, path) => {
|
|
535
|
+
const geography2 = geographyAt(depth2, path);
|
|
536
|
+
if (geography2 === void 0) return void 0;
|
|
537
|
+
const raw = featureCollectionOf(geography2, levels[sourceOf(depth2)]?.objectName);
|
|
538
|
+
return stableFilter(raw, keepFor(depth2, path));
|
|
539
|
+
};
|
|
540
|
+
const featuresFor = (path) => Array.from({ length: path.length + 1 }, (_, d) => featuresAt(d, path));
|
|
541
|
+
const chainLevelOf = (depth2, fc) => ({
|
|
542
|
+
features: fc.features,
|
|
543
|
+
idOf: levels[depth2]?.getFeatureId ?? levels[sourceOf(depth2)]?.getFeatureId ?? defaultFeatureId
|
|
544
|
+
});
|
|
545
|
+
const chainLevelsFor = (path) => {
|
|
546
|
+
const collectionsByDepth = featuresFor(path);
|
|
547
|
+
const parents = collectionsByDepth.slice(0, path.length);
|
|
548
|
+
if (parents.some((fc) => fc === void 0)) return null;
|
|
549
|
+
return collectionsByDepth.map((fc, depth2) => chainLevelOf(depth2, fc ?? EMPTY_COLLECTION));
|
|
550
|
+
};
|
|
551
|
+
const targetFeatures = active ? featuresFor(target) : [];
|
|
552
|
+
const targetGeometry = active && targetFeatures.every((f) => f !== void 0);
|
|
553
|
+
const targetEmpty = targetGeometry && (targetFeatures[target.length]?.features.length ?? 0) === 0;
|
|
554
|
+
const liveData = resolveDrillData(data, contextOf(target));
|
|
555
|
+
const targetReady = targetGeometry && !targetEmpty && liveData !== void 0;
|
|
556
|
+
const shownPath = machine.phase === "revealing" ? machine.path : machine.settledPath;
|
|
557
|
+
const shownKey = JSON.stringify(shownPath);
|
|
558
|
+
const shownIsTarget = samePath2(shownPath, target);
|
|
559
|
+
const shownFeatures = active ? shownIsTarget ? targetFeatures : featuresFor(shownPath) : [];
|
|
560
|
+
const hasShown = active && shownFeatures.every((f) => f !== void 0);
|
|
561
|
+
const [latest] = useState3(() => ({
|
|
562
|
+
data: void 0,
|
|
563
|
+
path: shownPath,
|
|
564
|
+
chain: [],
|
|
565
|
+
fills: /* @__PURE__ */ new Map()
|
|
566
|
+
}));
|
|
567
|
+
const rememberFills = useCallback2(
|
|
568
|
+
(fills) => {
|
|
569
|
+
latest.fills = fills;
|
|
570
|
+
},
|
|
571
|
+
[latest]
|
|
572
|
+
);
|
|
573
|
+
const chainSig = `${shownKey}|${shownFeatures.map(idOfCollection).join(",")}`;
|
|
574
|
+
const chain = useMemo2(
|
|
575
|
+
() => shownFeatures.flatMap((fc, depth2) => fc === void 0 ? [] : [chainLevelOf(depth2, fc)]),
|
|
576
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
577
|
+
[chainSig, levels]
|
|
578
|
+
);
|
|
579
|
+
const [seen, setSeen] = useState3(shownPath);
|
|
580
|
+
const [previous, setPrevious] = useState3(null);
|
|
581
|
+
if (!samePath2(seen, shownPath)) {
|
|
582
|
+
setSeen(shownPath);
|
|
583
|
+
setPrevious({ path: latest.path, chain: latest.chain, fills: latest.fills });
|
|
584
|
+
}
|
|
585
|
+
useEffect4(() => {
|
|
586
|
+
latest.path = shownPath;
|
|
587
|
+
latest.chain = chain;
|
|
588
|
+
if (shownIsTarget && liveData !== void 0) latest.data = liveData;
|
|
589
|
+
});
|
|
590
|
+
const depth = shownPath.length;
|
|
591
|
+
const level = active ? levels[depth] ?? null : null;
|
|
592
|
+
const sourceLevel = levels[sourceOf(depth)];
|
|
593
|
+
const features = hasShown ? shownFeatures[depth] : void 0;
|
|
594
|
+
const geography = hasShown ? geographyAt(depth, shownPath) : void 0;
|
|
595
|
+
const emptyError = useMemo2(() => {
|
|
596
|
+
if (!targetEmpty) return null;
|
|
597
|
+
const parent = target.at(-1) ?? null;
|
|
598
|
+
const id = levels[target.length]?.id ?? "?";
|
|
599
|
+
return new Error(
|
|
600
|
+
`[choropleth] drill level "${id}" has 0 features for "${parent}". Hint: return false from canDrill for it.`
|
|
601
|
+
);
|
|
602
|
+
}, [targetEmpty, targetKey, levels]);
|
|
603
|
+
const failure = (() => {
|
|
604
|
+
if (!active) return null;
|
|
605
|
+
for (let d = 0; d <= target.length; d++) {
|
|
606
|
+
const error = failed.get(keyAt(d, target));
|
|
607
|
+
if (error !== void 0) return { error };
|
|
608
|
+
}
|
|
609
|
+
return emptyError === null ? null : { error: emptyError };
|
|
610
|
+
})();
|
|
611
|
+
const latestProps = useRef4({ onDrill, onDrillEnd, onDrillError, controller, target, machine });
|
|
612
|
+
useEffect4(() => {
|
|
613
|
+
latestProps.current = { onDrill, onDrillEnd, onDrillError, controller, target, machine };
|
|
614
|
+
});
|
|
615
|
+
const waiters = useRef4([]);
|
|
616
|
+
const finish = (waiter, completed) => {
|
|
617
|
+
clearTimeout(waiter.timer);
|
|
618
|
+
waiters.current = waiters.current.filter((w) => w !== waiter);
|
|
619
|
+
waiter.resolve({ completed });
|
|
620
|
+
};
|
|
621
|
+
const arrived = (waiter) => {
|
|
622
|
+
const { target: wanted, machine: current } = latestProps.current;
|
|
623
|
+
if (!samePath2(wanted, waiter.target)) return false;
|
|
624
|
+
waiter.seen = true;
|
|
625
|
+
return current.phase === "idle" && samePath2(current.settledPath, waiter.target);
|
|
626
|
+
};
|
|
627
|
+
const navigate = useCallback2((run) => {
|
|
628
|
+
const ctl = latestProps.current.controller;
|
|
629
|
+
if (ctl === null) return Promise.resolve({ completed: false });
|
|
630
|
+
const wanted = run(ctl);
|
|
631
|
+
return new Promise((resolve) => {
|
|
632
|
+
const waiter = { target: wanted, resolve, seen: false, timer: void 0 };
|
|
633
|
+
waiter.timer = setTimeout(() => {
|
|
634
|
+
if (!waiter.seen) finish(waiter, false);
|
|
635
|
+
}, REJECTED_AFTER_MS);
|
|
636
|
+
for (const other of waiters.current) {
|
|
637
|
+
if (!samePath2(other.target, wanted)) finish(other, false);
|
|
638
|
+
}
|
|
639
|
+
waiters.current.push(waiter);
|
|
640
|
+
if (arrived(waiter)) finish(waiter, true);
|
|
641
|
+
});
|
|
642
|
+
}, []);
|
|
643
|
+
useEffect4(() => {
|
|
644
|
+
for (const waiter of waiters.current) {
|
|
645
|
+
if (arrived(waiter)) finish(waiter, true);
|
|
646
|
+
else if (waiter.seen && !samePath2(target, waiter.target)) finish(waiter, false);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
useEffect4(
|
|
650
|
+
() => () => {
|
|
651
|
+
for (const waiter of waiters.current) finish(waiter, false);
|
|
652
|
+
},
|
|
653
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
654
|
+
[]
|
|
655
|
+
);
|
|
656
|
+
const dispatched = useRef4(target);
|
|
657
|
+
const machineAtDispatch = useRef4(machine);
|
|
658
|
+
const clicked = useRef4(void 0);
|
|
659
|
+
const noteItem = useCallback2((item) => {
|
|
660
|
+
clicked.current = item;
|
|
661
|
+
}, []);
|
|
662
|
+
const settledKey = JSON.stringify(machine.settledPath);
|
|
663
|
+
useEffect4(() => {
|
|
664
|
+
if (!active || controller === null) return;
|
|
665
|
+
if (samePath2(dispatched.current, target)) return;
|
|
666
|
+
const direction = classifyDrill(machine.settledPath, target);
|
|
667
|
+
const eager = direction === "down" || direction === "lateral" || direction === "equal";
|
|
668
|
+
if (!eager && !targetReady) return;
|
|
669
|
+
dispatched.current = target;
|
|
670
|
+
machineAtDispatch.current = machine;
|
|
671
|
+
const view = store.getZoom()?.getView();
|
|
672
|
+
dispatch({
|
|
673
|
+
type: "DRILL",
|
|
674
|
+
to: target,
|
|
675
|
+
cached: targetReady,
|
|
676
|
+
...view === void 0 ? {} : { camera: { k: view.k, x: view.x, y: view.y } }
|
|
677
|
+
});
|
|
678
|
+
}, [active, targetKey, targetReady, settledKey]);
|
|
679
|
+
useEffect4(() => {
|
|
680
|
+
if (!active || machine.geometry !== "pending" || machine.geometryKey === null) return;
|
|
681
|
+
if (targetReady && samePath2(machine.path, target)) {
|
|
682
|
+
dispatch({ type: "GEOMETRY_READY", key: machine.geometryKey });
|
|
683
|
+
}
|
|
684
|
+
}, [active, targetReady, machine.geometry, machine.geometryKey]);
|
|
685
|
+
const reported = useRef4(null);
|
|
686
|
+
useEffect4(() => {
|
|
687
|
+
if (!active || failure === null || reported.current === failure.error) return;
|
|
688
|
+
reported.current = failure.error;
|
|
689
|
+
const { onDrillError: report, controller: ctl, machine: current } = latestProps.current;
|
|
690
|
+
const wanted = latestProps.current.target;
|
|
691
|
+
if (report !== void 0) report({ error: failure.error, ctx: contextOf(wanted) });
|
|
692
|
+
else if (!isProduction()) {
|
|
693
|
+
const reason = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
694
|
+
console.error(
|
|
695
|
+
reason.startsWith("[choropleth]") ? reason : `[choropleth] drill: failed to load the geometry for ${JSON.stringify(wanted)}: ${reason}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (samePath2(dispatched.current, wanted) && machine.geometryKey !== null) {
|
|
699
|
+
dispatch({ type: "GEOMETRY_ERROR", key: machine.geometryKey });
|
|
700
|
+
} else if (ctl !== null) {
|
|
701
|
+
dispatched.current = current.settledPath;
|
|
702
|
+
ctl.drillTo(current.settledPath);
|
|
703
|
+
}
|
|
704
|
+
}, [active, failure]);
|
|
705
|
+
const machinePathKey = JSON.stringify(machine.path);
|
|
706
|
+
useEffect4(() => {
|
|
707
|
+
if (!active || controller === null) return;
|
|
708
|
+
if (machine === machineAtDispatch.current) return;
|
|
709
|
+
if (!samePath2(dispatched.current, target) || samePath2(machine.path, target)) return;
|
|
710
|
+
dispatched.current = machine.path;
|
|
711
|
+
machineAtDispatch.current = machine;
|
|
712
|
+
controller.drillTo(machine.path);
|
|
713
|
+
}, [active, machinePathKey, targetKey]);
|
|
714
|
+
const lastTarget = useRef4(target);
|
|
715
|
+
useEffect4(() => {
|
|
716
|
+
if (!active || samePath2(lastTarget.current, target)) return;
|
|
717
|
+
const event = buildDrillEvent({
|
|
718
|
+
from: lastTarget.current,
|
|
719
|
+
to: target,
|
|
720
|
+
item: clicked.current
|
|
721
|
+
});
|
|
722
|
+
lastTarget.current = target;
|
|
723
|
+
clicked.current = void 0;
|
|
724
|
+
if (event !== null) latestProps.current.onDrill?.(event);
|
|
725
|
+
}, [active, targetKey]);
|
|
726
|
+
const lastEnd = useRef4(machine.settledPath);
|
|
727
|
+
useEffect4(() => {
|
|
728
|
+
if (!active || machine.phase !== "idle") return;
|
|
729
|
+
const previousEnd = lastEnd.current;
|
|
730
|
+
if (samePath2(previousEnd, machine.settledPath)) return;
|
|
731
|
+
lastEnd.current = machine.settledPath;
|
|
732
|
+
const event = buildDrillEvent({ from: previousEnd, to: machine.settledPath });
|
|
733
|
+
if (event !== null) latestProps.current.onDrillEnd?.(event);
|
|
734
|
+
}, [active, machine.phase, settledKey]);
|
|
735
|
+
const status = !active ? "loading" : hasShown ? "ready" : failure !== null ? "error" : "loading";
|
|
736
|
+
const bridge = controller?.[DRILL_BRIDGE];
|
|
737
|
+
const phase = machine.phase;
|
|
738
|
+
useEffect4(() => {
|
|
739
|
+
bridge?.setStatus(phase);
|
|
740
|
+
}, [bridge, phase]);
|
|
741
|
+
return {
|
|
742
|
+
active,
|
|
743
|
+
controller,
|
|
744
|
+
levels,
|
|
745
|
+
shownPath,
|
|
746
|
+
shownKey,
|
|
747
|
+
depth,
|
|
748
|
+
level,
|
|
749
|
+
geography,
|
|
750
|
+
status,
|
|
751
|
+
objectName: sourceLevel?.objectName,
|
|
752
|
+
features,
|
|
753
|
+
levelFeatures: shownFeatures,
|
|
754
|
+
getFeatureId: level?.getFeatureId ?? sourceLevel?.getFeatureId,
|
|
755
|
+
chain,
|
|
756
|
+
chainSig,
|
|
757
|
+
filterFeatures: active && hasShown ? keepFor(depth, shownPath) : void 0,
|
|
758
|
+
rootFeatures: hasShown ? shownFeatures[0] : void 0,
|
|
759
|
+
data: !active ? void 0 : shownIsTarget ? liveData : latest.data,
|
|
760
|
+
noteItem,
|
|
761
|
+
machine,
|
|
762
|
+
dispatch,
|
|
763
|
+
previous,
|
|
764
|
+
rememberFills,
|
|
765
|
+
chainLevelsFor,
|
|
766
|
+
target,
|
|
767
|
+
prefetch,
|
|
768
|
+
navigate
|
|
769
|
+
};
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/react/use-geography.ts
|
|
773
|
+
import { useEffect as useEffect5, useState as useState4 } from "react";
|
|
774
|
+
var promisesByKey = /* @__PURE__ */ new Map();
|
|
775
|
+
var promisesByIdentity = /* @__PURE__ */ new WeakMap();
|
|
776
|
+
var valuesByKey = /* @__PURE__ */ new Map();
|
|
777
|
+
var valuesByIdentity = /* @__PURE__ */ new WeakMap();
|
|
778
|
+
var isDeferred = (input) => typeof input === "function" || "then" in input;
|
|
779
|
+
var peek = (input, key) => {
|
|
780
|
+
if (!isDeferred(input)) return input;
|
|
781
|
+
return key === void 0 ? valuesByIdentity.get(input) : valuesByKey.get(key);
|
|
782
|
+
};
|
|
783
|
+
var load = (input, key) => {
|
|
784
|
+
const cached = key === void 0 ? promisesByIdentity.get(input) : promisesByKey.get(key);
|
|
785
|
+
if (cached !== void 0) return cached;
|
|
786
|
+
const promise = (typeof input === "function" ? input() : input).then((module) => {
|
|
787
|
+
const value = unwrapGeography(module);
|
|
788
|
+
if (key === void 0) valuesByIdentity.set(input, value);
|
|
789
|
+
else valuesByKey.set(key, value);
|
|
790
|
+
return value;
|
|
791
|
+
});
|
|
792
|
+
if (key === void 0) promisesByIdentity.set(input, promise);
|
|
793
|
+
else promisesByKey.set(key, promise);
|
|
794
|
+
promise.catch(() => {
|
|
795
|
+
if (key === void 0) promisesByIdentity.delete(input);
|
|
796
|
+
else promisesByKey.delete(key);
|
|
797
|
+
});
|
|
798
|
+
return promise;
|
|
799
|
+
};
|
|
800
|
+
var useGeography = (input, key, onError) => {
|
|
801
|
+
const ready = peek(input, key);
|
|
802
|
+
const [loaded, setLoaded] = useState4(void 0);
|
|
803
|
+
const [failed, setFailed] = useState4(false);
|
|
804
|
+
useEffect5(() => {
|
|
805
|
+
if (ready !== void 0 || !isDeferred(input)) return;
|
|
806
|
+
let cancelled = false;
|
|
807
|
+
load(input, key).then(
|
|
808
|
+
(value) => {
|
|
809
|
+
if (cancelled) return;
|
|
810
|
+
setFailed(false);
|
|
811
|
+
setLoaded(value);
|
|
812
|
+
},
|
|
813
|
+
(reason) => {
|
|
814
|
+
if (cancelled) return;
|
|
815
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
816
|
+
setFailed(true);
|
|
817
|
+
if (onError === void 0)
|
|
818
|
+
console.error(`[choropleth] Failed to load geography: ${error.message}`);
|
|
819
|
+
else onError(error);
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
return () => {
|
|
823
|
+
cancelled = true;
|
|
824
|
+
};
|
|
825
|
+
}, [input, key, ready]);
|
|
826
|
+
const geography = ready ?? loaded;
|
|
827
|
+
if (geography !== void 0) return { geography, status: "ready" };
|
|
828
|
+
return { geography: void 0, status: failed ? "error" : "loading" };
|
|
829
|
+
};
|
|
830
|
+
var clearGeographyCache = () => {
|
|
831
|
+
promisesByKey.clear();
|
|
832
|
+
valuesByKey.clear();
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/react/use-choropleth.ts
|
|
836
|
+
var FALLBACK_POINT_RADIUS = 4;
|
|
837
|
+
var useChoropleth = (rawOptions) => {
|
|
838
|
+
const [store] = useState5(createMapStore);
|
|
839
|
+
const engine = useDrillEngine({
|
|
840
|
+
drill: rawOptions.drill,
|
|
841
|
+
data: rawOptions.data,
|
|
842
|
+
filterFeatures: rawOptions.filterFeatures,
|
|
843
|
+
store,
|
|
844
|
+
onDrill: rawOptions.onDrill,
|
|
845
|
+
onDrillEnd: rawOptions.onDrillEnd,
|
|
846
|
+
onDrillError: rawOptions.onDrillError
|
|
847
|
+
});
|
|
848
|
+
const encoding = engine.level?.encoding;
|
|
849
|
+
const options = engine.active ? {
|
|
850
|
+
...rawOptions,
|
|
851
|
+
...encoding,
|
|
852
|
+
getId: encoding?.getId ?? rawOptions.getId,
|
|
853
|
+
data: engine.data,
|
|
854
|
+
getFeatureId: engine.getFeatureId ?? rawOptions.getFeatureId
|
|
855
|
+
} : { ...rawOptions, data: resolveDrillData(rawOptions.data, contextOf([])) };
|
|
856
|
+
const { getId, getFeatureId, projection, fallbackPoints } = options;
|
|
857
|
+
const isBubble = options.variant === "bubble";
|
|
858
|
+
const labels = resolveLabels(options.labels);
|
|
859
|
+
const single = useGeography(
|
|
860
|
+
engine.active ? NO_GEOGRAPHY : options.geography ?? NO_GEOGRAPHY,
|
|
861
|
+
options.geographyKey,
|
|
862
|
+
options.onGeographyError
|
|
863
|
+
);
|
|
864
|
+
if (!engine.active && options.geography === void 0) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
"[choropleth] Missing `geography`: pass `geography` (TopoJSON/GeoJSON) or `drill` (a list of levels)."
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const geography = engine.active ? engine.geography : single.geography;
|
|
870
|
+
const status = engine.active ? engine.status : single.status;
|
|
871
|
+
const objectName = engine.active ? engine.objectName : options.objectName;
|
|
872
|
+
const filterFeatures = engine.active ? engine.filterFeatures : options.filterFeatures;
|
|
873
|
+
const allFeatures = useMemo3(
|
|
874
|
+
() => engine.active ? engine.features ?? EMPTY_FEATURES : geography === void 0 ? EMPTY_FEATURES : toFeatureCollection({ geography, objectName }),
|
|
875
|
+
// `engine.features` already comes filtered and is stable (the engine's caches).
|
|
876
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
877
|
+
[engine.active, engine.features, geography, objectName]
|
|
878
|
+
);
|
|
879
|
+
const keptIndexes = filterFeatures === void 0 ? null : allFeatures.features.flatMap((f, index) => filterFeatures(f) ? [index] : []);
|
|
880
|
+
const keptKey = keptIndexes === null ? "" : keptIndexes.join(",");
|
|
881
|
+
const features = useMemo3(
|
|
882
|
+
() => keptIndexes === null ? allFeatures : {
|
|
883
|
+
...allFeatures,
|
|
884
|
+
features: keptIndexes.flatMap((index) => {
|
|
885
|
+
const feature = allFeatures.features[index];
|
|
886
|
+
return feature === void 0 ? [] : [feature];
|
|
887
|
+
})
|
|
888
|
+
},
|
|
889
|
+
// `keptIndexes` is summarized in `keptKey` (see above).
|
|
890
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
891
|
+
[allFeatures, keptKey]
|
|
892
|
+
);
|
|
893
|
+
const bordersGeometry = useMemo3(
|
|
894
|
+
() => geography === void 0 ? null : createBorders({ geography, objectName, filterFeatures }),
|
|
895
|
+
// `filterFeatures` is summarized in `keptKey`: its identity must not relaunch the computation.
|
|
896
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
897
|
+
[geography, objectName, keptKey]
|
|
898
|
+
);
|
|
899
|
+
const autoAspect = useMemo3(
|
|
900
|
+
() => options.aspectRatio === "auto" ? computeAspectRatio({
|
|
901
|
+
name: projection,
|
|
902
|
+
// With drill the proportion is the root's: it must not change when going down a level.
|
|
903
|
+
featureCollection: (engine.active ? engine.rootFeatures : void 0) ?? features
|
|
904
|
+
}) : void 0,
|
|
905
|
+
[options.aspectRatio, projection, features, engine.active, engine.rootFeatures]
|
|
906
|
+
);
|
|
907
|
+
const measured = useContainerSize({
|
|
908
|
+
aspectRatio: options.aspectRatio === "auto" ? autoAspect : options.aspectRatio
|
|
909
|
+
});
|
|
910
|
+
const width = options.width ?? measured.width;
|
|
911
|
+
const height = options.height ?? measured.height;
|
|
912
|
+
const rebasePadding = typeof options.zoom === "object" ? options.zoom.padding ?? 24 : 24;
|
|
913
|
+
const drillFocusBounds = engine.levels.reduce(
|
|
914
|
+
(all, level) => ({ ...all, ...level.focusBounds }),
|
|
915
|
+
{ ...DEFAULT_FOCUS_BOUNDS, ...options.focusBounds }
|
|
916
|
+
);
|
|
917
|
+
const focusKey = JSON.stringify(drillFocusBounds);
|
|
918
|
+
const chain = useMemo3(
|
|
919
|
+
() => engine.active && engine.chain.length > 0 && width > 0 && height > 0 ? buildProjectionChain({
|
|
920
|
+
levels: engine.chain,
|
|
921
|
+
path: engine.shownPath,
|
|
922
|
+
name: projection,
|
|
923
|
+
width,
|
|
924
|
+
height,
|
|
925
|
+
padding: rebasePadding,
|
|
926
|
+
focusBounds: drillFocusBounds
|
|
927
|
+
}) : null,
|
|
928
|
+
// `chainSig` summarizes the content of `engine.chain`; `focusKey`, that of `drillFocusBounds`.
|
|
929
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
930
|
+
[engine.active, engine.chainSig, projection, width, height, rebasePadding, focusKey]
|
|
931
|
+
);
|
|
932
|
+
const chainMissing = chain?.missing ?? null;
|
|
933
|
+
useEffect6(() => {
|
|
934
|
+
if (chainMissing !== null && !isProduction()) {
|
|
935
|
+
console.warn(
|
|
936
|
+
`[choropleth] drill: could not frame region "${chainMissing}" (it is not in its parent level, or it has no extent). The map keeps the parent's framing.`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}, [chainMissing]);
|
|
940
|
+
const chainFor = (levels, path) => buildProjectionChain({
|
|
941
|
+
levels,
|
|
942
|
+
path,
|
|
943
|
+
name: projection,
|
|
944
|
+
width,
|
|
945
|
+
height,
|
|
946
|
+
padding: rebasePadding,
|
|
947
|
+
focusBounds: drillFocusBounds
|
|
948
|
+
});
|
|
949
|
+
useDrillChoreography({ engine, store, shownChain: chain, chainFor });
|
|
950
|
+
const { machine, previous } = engine;
|
|
951
|
+
const leaving = machine.layers.some((layer) => layer.role === "exiting");
|
|
952
|
+
const previousChain = useMemo3(
|
|
953
|
+
() => engine.active && previous !== null && width > 0 && height > 0 && (leaving || machine.direction === "up") ? buildProjectionChain({
|
|
954
|
+
levels: [...previous.chain],
|
|
955
|
+
path: previous.path,
|
|
956
|
+
name: projection,
|
|
957
|
+
width,
|
|
958
|
+
height,
|
|
959
|
+
padding: rebasePadding,
|
|
960
|
+
focusBounds: drillFocusBounds
|
|
961
|
+
}) : null,
|
|
962
|
+
// `previous` changes only when the level changes; `focusKey` summarizes `drillFocusBounds`.
|
|
963
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
964
|
+
[
|
|
965
|
+
engine.active,
|
|
966
|
+
previous,
|
|
967
|
+
leaving,
|
|
968
|
+
machine.direction,
|
|
969
|
+
projection,
|
|
970
|
+
width,
|
|
971
|
+
height,
|
|
972
|
+
rebasePadding,
|
|
973
|
+
focusKey
|
|
974
|
+
]
|
|
975
|
+
);
|
|
976
|
+
const activeProjection = chain?.projections[chain.projections.length - 1];
|
|
977
|
+
const layersKey = machine.layers.map((layer) => `${layer.key}:${layer.role}`).join("|");
|
|
978
|
+
const contextMode = engine.level?.context ?? "siblings";
|
|
979
|
+
const drillLayers = useMemo3(() => {
|
|
980
|
+
if (!engine.active || chain === null || activeProjection === void 0) return [];
|
|
981
|
+
const views = [];
|
|
982
|
+
const activeSource = sourceDepth(engine.levels, engine.depth);
|
|
983
|
+
for (const layer of machine.layers) {
|
|
984
|
+
let features2;
|
|
985
|
+
let projectionOf;
|
|
986
|
+
let idOf2;
|
|
987
|
+
let focusId;
|
|
988
|
+
if (layer.role === "context") {
|
|
989
|
+
if (contextMode === "none" || layer.depth >= engine.depth) continue;
|
|
990
|
+
if (sourceDepth(engine.levels, layer.depth) === activeSource) continue;
|
|
991
|
+
features2 = engine.levelFeatures[layer.depth]?.features;
|
|
992
|
+
projectionOf = chain.projections[layer.depth];
|
|
993
|
+
idOf2 = engine.chain[layer.depth]?.idOf;
|
|
994
|
+
focusId = engine.shownPath[layer.depth];
|
|
995
|
+
} else if (layer.role === "exiting") {
|
|
996
|
+
features2 = previous?.chain[layer.depth]?.features;
|
|
997
|
+
projectionOf = previousChain?.projections[layer.depth];
|
|
998
|
+
idOf2 = previous?.chain[layer.depth]?.idOf;
|
|
999
|
+
} else {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (features2 === void 0 || projectionOf === void 0 || idOf2 === void 0) continue;
|
|
1003
|
+
const path = geoPath(projectionOf);
|
|
1004
|
+
const level = engine.levels[layer.depth];
|
|
1005
|
+
const paths2 = features2.flatMap((feature) => {
|
|
1006
|
+
const d = path(feature);
|
|
1007
|
+
if (d == null) return [];
|
|
1008
|
+
const id = idOf2(feature);
|
|
1009
|
+
const drillable = layer.role === "context" && id !== void 0 && isDrillable(engine.levels, layer.depth, {
|
|
1010
|
+
kind: "region",
|
|
1011
|
+
id,
|
|
1012
|
+
feature,
|
|
1013
|
+
datum: void 0,
|
|
1014
|
+
level: { id: level?.id ?? ROOT_LEVEL.id, depth: layer.depth },
|
|
1015
|
+
drillable: true
|
|
1016
|
+
});
|
|
1017
|
+
const fill = layer.role === "exiting" && id !== void 0 ? previous?.fills.get(id) : void 0;
|
|
1018
|
+
return [{ id, d, focus: id !== void 0 && id === focusId, drillable, fill }];
|
|
1019
|
+
});
|
|
1020
|
+
views.push({
|
|
1021
|
+
key: layer.key,
|
|
1022
|
+
role: layer.role,
|
|
1023
|
+
depth: layer.depth,
|
|
1024
|
+
transform: relativeTransform(projectionOf, activeProjection),
|
|
1025
|
+
paths: paths2
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return views;
|
|
1029
|
+
}, [engine.active, chain, previousChain, previous, layersKey, engine.chainSig, contextMode]);
|
|
1030
|
+
const controller = engine.controller;
|
|
1031
|
+
const drillNav = useMemo3(() => {
|
|
1032
|
+
if (!engine.active || controller === null) return null;
|
|
1033
|
+
const rootLabel = engine.levels[0]?.label ?? labels.breadcrumbRoot;
|
|
1034
|
+
const items = controller.breadcrumbs.map((crumb) => {
|
|
1035
|
+
let label = crumb.label;
|
|
1036
|
+
if (crumb.id === null) label = rootLabel;
|
|
1037
|
+
else if (crumb.label === crumb.id) {
|
|
1038
|
+
const parent = engine.chain[crumb.depth - 1];
|
|
1039
|
+
const feature = parent?.features.find((f) => parent.idOf(f) === crumb.id);
|
|
1040
|
+
label = (feature === void 0 ? void 0 : nameOf(feature)) ?? crumb.id;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
depth: crumb.depth,
|
|
1044
|
+
id: crumb.id,
|
|
1045
|
+
label,
|
|
1046
|
+
current: crumb.depth === controller.path.length
|
|
1047
|
+
};
|
|
1048
|
+
});
|
|
1049
|
+
return {
|
|
1050
|
+
items,
|
|
1051
|
+
canDrillUp: controller.canDrillUp,
|
|
1052
|
+
drillTo: (depth) => controller.drillTo(controller.path.slice(0, depth)),
|
|
1053
|
+
drillUp: (steps) => controller.drillUp(steps)
|
|
1054
|
+
};
|
|
1055
|
+
}, [
|
|
1056
|
+
engine.active,
|
|
1057
|
+
controller,
|
|
1058
|
+
controller?.path,
|
|
1059
|
+
engine.chainSig,
|
|
1060
|
+
engine.levels,
|
|
1061
|
+
labels.breadcrumbRoot
|
|
1062
|
+
]);
|
|
1063
|
+
const drillFocusId = engine.active && (machine.phase === "flying" || machine.phase === "loading") && machine.direction !== "up" && machine.direction !== null ? machine.path.at(-1) ?? null : null;
|
|
1064
|
+
const { paths, bordersPath, anchors, project, invert, toPath } = useMemo3(() => {
|
|
1065
|
+
const geoProjection = chain?.projections[chain.projections.length - 1] ?? createProjection({
|
|
1066
|
+
name: projection,
|
|
1067
|
+
width,
|
|
1068
|
+
height,
|
|
1069
|
+
featureCollection: features
|
|
1070
|
+
});
|
|
1071
|
+
const toPath2 = geoPath(geoProjection);
|
|
1072
|
+
const toPoint = (coordinates) => {
|
|
1073
|
+
const at = geoProjection([...coordinates]);
|
|
1074
|
+
return at !== null && Number.isFinite(at[0]) && Number.isFinite(at[1]) ? at : null;
|
|
1075
|
+
};
|
|
1076
|
+
return {
|
|
1077
|
+
paths: features.features.map((f) => toPath2(f)),
|
|
1078
|
+
bordersPath: bordersGeometry === null ? null : toPath2(bordersGeometry),
|
|
1079
|
+
// Anchors are only computed for `bubble` (the only thing that uses them). `geoPath`'s centroid counts
|
|
1080
|
+
// distant territories (France's bubble would land in the Atlantic): the anchor is the pole of
|
|
1081
|
+
// inaccessibility of the main body. The centroid remains as a fallback.
|
|
1082
|
+
anchors: isBubble ? features.features.map((f) => {
|
|
1083
|
+
const anchor = getAnchorPoint({
|
|
1084
|
+
geometry: getFocusGeometry({ feature: f, path: toPath2 }),
|
|
1085
|
+
project: toPoint
|
|
1086
|
+
});
|
|
1087
|
+
if (anchor !== null) return anchor;
|
|
1088
|
+
const [x, y] = toPath2.centroid(f);
|
|
1089
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
1090
|
+
}) : [],
|
|
1091
|
+
project: toPoint,
|
|
1092
|
+
toPath: toPath2,
|
|
1093
|
+
// The reverse: pixels → [lon, lat]. Used by `toViewState` for the public `ViewState`.
|
|
1094
|
+
invert: (point) => {
|
|
1095
|
+
const at = geoProjection.invert?.([point[0], point[1]]);
|
|
1096
|
+
return at != null && Number.isFinite(at[0]) && Number.isFinite(at[1]) ? [at[0], at[1]] : null;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}, [features, bordersGeometry, projection, width, height, isBubble, chain]);
|
|
1100
|
+
const lastData = useRef5(options.data);
|
|
1101
|
+
if (options.data !== void 0) lastData.current = options.data;
|
|
1102
|
+
const data = options.data ?? lastData.current ?? NO_DATA;
|
|
1103
|
+
const dataLoading = options.loading ?? options.data === void 0;
|
|
1104
|
+
const idOf = getFeatureId ?? defaultFeatureId2;
|
|
1105
|
+
const join = joinData({ features: features.features, data, getId, getFeatureId: idOf });
|
|
1106
|
+
const fallbacks = [];
|
|
1107
|
+
if (fallbackPoints !== void 0 && join.unmatchedData.length > 0) {
|
|
1108
|
+
const wanted = new Set(join.unmatchedData.filter((id) => fallbackPoints[id] !== void 0));
|
|
1109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1110
|
+
for (const datum of data) {
|
|
1111
|
+
const id = getId(datum);
|
|
1112
|
+
const coordinates = fallbackPoints[id];
|
|
1113
|
+
const at = coordinates === void 0 ? null : project(coordinates);
|
|
1114
|
+
if (wanted.has(id) && !seen.has(id) && coordinates !== void 0 && at !== null) {
|
|
1115
|
+
seen.add(id);
|
|
1116
|
+
fallbacks.push({ id, datum, coordinates, at });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const fallbackIds = new Set(fallbacks.map((f) => f.id));
|
|
1121
|
+
const hasOwnCoordinates = options.variant === "bubble" && options.getCoordinates !== void 0;
|
|
1122
|
+
const joinReport = {
|
|
1123
|
+
unmatchedData: hasOwnCoordinates ? [] : join.unmatchedData.filter((id) => !fallbackIds.has(id)),
|
|
1124
|
+
featuresWithoutData: hasOwnCoordinates ? [] : join.featuresWithoutData,
|
|
1125
|
+
duplicateDataIds: join.duplicateDataIds
|
|
1126
|
+
};
|
|
1127
|
+
const entries = features.features.map((feature) => {
|
|
1128
|
+
const id = idOf(feature);
|
|
1129
|
+
return { id, datum: id === void 0 ? void 0 : join.joined.get(id) };
|
|
1130
|
+
});
|
|
1131
|
+
const allEntries = [...entries, ...fallbacks.map(({ id, datum }) => ({ id, datum }))];
|
|
1132
|
+
const formatValue = options.formatValue ?? defaultFormatValue;
|
|
1133
|
+
const emptyFill = options.emptyFill ?? EMPTY_FILL_VAR;
|
|
1134
|
+
const variant = resolveVariant({
|
|
1135
|
+
config: options,
|
|
1136
|
+
entries: allEntries,
|
|
1137
|
+
data,
|
|
1138
|
+
getId,
|
|
1139
|
+
centroids: [...anchors, ...fallbacks.map((f) => f.at)],
|
|
1140
|
+
project,
|
|
1141
|
+
emptyFill,
|
|
1142
|
+
formatValue,
|
|
1143
|
+
emptyLabel: options.emptyLabel
|
|
1144
|
+
});
|
|
1145
|
+
const levelInfo = engine.active ? { id: engine.level?.id ?? ROOT_LEVEL.id, depth: engine.depth } : ROOT_LEVEL;
|
|
1146
|
+
const regions = [];
|
|
1147
|
+
const anonymousPaths = [];
|
|
1148
|
+
features.features.forEach((feature, index) => {
|
|
1149
|
+
const path = paths[index];
|
|
1150
|
+
const entry = entries[index];
|
|
1151
|
+
if (path == null) return;
|
|
1152
|
+
if (entry?.id === void 0) {
|
|
1153
|
+
anonymousPaths.push(path);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const datum = entry.datum;
|
|
1157
|
+
let wave;
|
|
1158
|
+
if (engine.active && machine.phase === "revealing") {
|
|
1159
|
+
const [cx2, cy] = toPath.centroid(feature);
|
|
1160
|
+
const reach = Math.hypot(width, height) / 2;
|
|
1161
|
+
wave = Number.isFinite(cx2) && Number.isFinite(cy) ? Math.min(1, Math.hypot(cx2 - width / 2, cy - height / 2) / reach) : 0;
|
|
1162
|
+
}
|
|
1163
|
+
const base = {
|
|
1164
|
+
id: entry.id,
|
|
1165
|
+
feature,
|
|
1166
|
+
datum,
|
|
1167
|
+
path,
|
|
1168
|
+
value: variant.values[index] ?? null,
|
|
1169
|
+
formattedValue: variant.labels[index] ?? "",
|
|
1170
|
+
fill: variant.fills[index] ?? "",
|
|
1171
|
+
wave,
|
|
1172
|
+
opacity: void 0,
|
|
1173
|
+
className: void 0,
|
|
1174
|
+
pattern: void 0,
|
|
1175
|
+
classIndex: variant.classes[index] ?? null,
|
|
1176
|
+
drillable: engine.active && isDrillable(engine.levels, engine.depth, {
|
|
1177
|
+
kind: "region",
|
|
1178
|
+
id: entry.id,
|
|
1179
|
+
feature,
|
|
1180
|
+
datum,
|
|
1181
|
+
level: levelInfo,
|
|
1182
|
+
drillable: true
|
|
1183
|
+
})
|
|
1184
|
+
};
|
|
1185
|
+
const custom = options.getRegionStyle?.(base);
|
|
1186
|
+
regions.push(
|
|
1187
|
+
custom === void 0 ? base : {
|
|
1188
|
+
...base,
|
|
1189
|
+
fill: custom.fill ?? base.fill,
|
|
1190
|
+
opacity: custom.opacity,
|
|
1191
|
+
className: custom.className,
|
|
1192
|
+
pattern: custom.pattern
|
|
1193
|
+
}
|
|
1194
|
+
);
|
|
1195
|
+
});
|
|
1196
|
+
const fallbackCoordinates = new Map(fallbacks.map((f) => [f.id, f.coordinates]));
|
|
1197
|
+
const bubbles = isBubble ? variant.bubbles.map((bubble) => {
|
|
1198
|
+
const coordinates = fallbackCoordinates.get(bubble.id);
|
|
1199
|
+
return bubble.coordinates === null && coordinates !== void 0 ? { ...bubble, coordinates } : bubble;
|
|
1200
|
+
}) : variant.bubbles;
|
|
1201
|
+
const fallbackMarks = isBubble ? [] : fallbacks.map((f, index) => {
|
|
1202
|
+
const at = features.features.length + index;
|
|
1203
|
+
return {
|
|
1204
|
+
id: f.id,
|
|
1205
|
+
x: f.at[0],
|
|
1206
|
+
y: f.at[1],
|
|
1207
|
+
r: FALLBACK_POINT_RADIUS,
|
|
1208
|
+
fill: variant.fills[at] ?? emptyFill,
|
|
1209
|
+
datum: f.datum,
|
|
1210
|
+
value: variant.values[at] ?? null,
|
|
1211
|
+
formattedValue: variant.labels[at] ?? "",
|
|
1212
|
+
coordinates: f.coordinates
|
|
1213
|
+
};
|
|
1214
|
+
});
|
|
1215
|
+
const reportKey = [
|
|
1216
|
+
data.length,
|
|
1217
|
+
joinReport.unmatchedData.join(""),
|
|
1218
|
+
joinReport.featuresWithoutData.join(""),
|
|
1219
|
+
joinReport.duplicateDataIds.join("")
|
|
1220
|
+
].join("\0");
|
|
1221
|
+
const { onJoinReport, diagnostics = true } = options;
|
|
1222
|
+
useEffect6(() => {
|
|
1223
|
+
if (status !== "ready") return;
|
|
1224
|
+
onJoinReport?.(joinReport);
|
|
1225
|
+
if (!diagnostics || isProduction()) return;
|
|
1226
|
+
const message = formatJoinDiagnostics({ ...joinReport, dataCount: data.length });
|
|
1227
|
+
const levelHint = formatWrongLevelDataHint({
|
|
1228
|
+
drillActive: engine.active,
|
|
1229
|
+
dataIsSelector: typeof rawOptions.data === "function",
|
|
1230
|
+
dataCount: data.length,
|
|
1231
|
+
unmatchedCount: joinReport.unmatchedData.length,
|
|
1232
|
+
levelId: engine.level?.id ?? null
|
|
1233
|
+
});
|
|
1234
|
+
if (message !== null) {
|
|
1235
|
+
console.warn(levelHint === null ? message : `${message}
|
|
1236
|
+
${levelHint}`);
|
|
1237
|
+
}
|
|
1238
|
+
}, [reportKey, status]);
|
|
1239
|
+
const noIdsMessage = formatNoIdsDiagnostic({
|
|
1240
|
+
featureCount: features.features.length,
|
|
1241
|
+
idCount: regions.length,
|
|
1242
|
+
levelId: engine.level?.id ?? null,
|
|
1243
|
+
customGetFeatureId: getFeatureId !== void 0
|
|
1244
|
+
});
|
|
1245
|
+
useEffect6(() => {
|
|
1246
|
+
if (status === "ready" && noIdsMessage !== null && diagnostics && !isProduction()) {
|
|
1247
|
+
console.warn(noIdsMessage);
|
|
1248
|
+
}
|
|
1249
|
+
}, [noIdsMessage, status, diagnostics]);
|
|
1250
|
+
const { onSelectedIdChange } = options;
|
|
1251
|
+
const controlled = options.selectedId !== void 0;
|
|
1252
|
+
const selectable = controlled || options.defaultSelectedId !== void 0 || onSelectedIdChange !== void 0;
|
|
1253
|
+
const [internalSelectedId, setInternalSelectedId] = useState5(
|
|
1254
|
+
options.defaultSelectedId ?? null
|
|
1255
|
+
);
|
|
1256
|
+
const selectedId = controlled ? options.selectedId ?? null : internalSelectedId;
|
|
1257
|
+
const [selectionLevel, setSelectionLevel] = useState5(engine.shownKey);
|
|
1258
|
+
if (selectionLevel !== engine.shownKey) {
|
|
1259
|
+
setSelectionLevel(engine.shownKey);
|
|
1260
|
+
if (!controlled && internalSelectedId !== null) setInternalSelectedId(null);
|
|
1261
|
+
}
|
|
1262
|
+
const setSelectedId = (id) => {
|
|
1263
|
+
if (!controlled) setInternalSelectedId(id);
|
|
1264
|
+
onSelectedIdChange?.(id);
|
|
1265
|
+
};
|
|
1266
|
+
const { onItemClick, onItemHover } = options;
|
|
1267
|
+
const { rememberFills } = engine;
|
|
1268
|
+
const drillActive = engine.active;
|
|
1269
|
+
useEffect6(() => {
|
|
1270
|
+
if (drillActive) rememberFills(new Map(regions.map((region) => [region.id, region.fill])));
|
|
1271
|
+
});
|
|
1272
|
+
const regionsById = new Map(regions.map((region) => [region.id, region]));
|
|
1273
|
+
const featuresById = /* @__PURE__ */ new Map();
|
|
1274
|
+
features.features.forEach((feature) => {
|
|
1275
|
+
const id = idOf(feature);
|
|
1276
|
+
if (id !== void 0) featuresById.set(id, feature);
|
|
1277
|
+
});
|
|
1278
|
+
const bubblesById = new Map(bubbles.filter((b) => b.coordinates !== null).map((b) => [b.id, b]));
|
|
1279
|
+
const fallbackMarksById = new Map(fallbackMarks.map((f) => [f.id, f]));
|
|
1280
|
+
const getItemContext = (ref) => {
|
|
1281
|
+
if (ref.kind === "region") {
|
|
1282
|
+
const region = regionsById.get(ref.id);
|
|
1283
|
+
if (region === void 0) return void 0;
|
|
1284
|
+
return {
|
|
1285
|
+
kind: "region",
|
|
1286
|
+
id: region.id,
|
|
1287
|
+
feature: region.feature,
|
|
1288
|
+
datum: region.datum,
|
|
1289
|
+
level: levelInfo,
|
|
1290
|
+
drillable: region.drillable,
|
|
1291
|
+
value: region.value,
|
|
1292
|
+
formattedValue: region.formattedValue,
|
|
1293
|
+
color: region.fill,
|
|
1294
|
+
label: nameOf(region.feature) ?? region.id
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const bubble = bubblesById.get(ref.id);
|
|
1298
|
+
const mark = bubble === void 0 ? fallbackMarksById.get(ref.id) : void 0;
|
|
1299
|
+
const coordinates = bubble?.coordinates ?? mark?.coordinates;
|
|
1300
|
+
const source = bubble ?? mark;
|
|
1301
|
+
if (source === void 0 || coordinates == null) return void 0;
|
|
1302
|
+
const feature = featuresById.get(source.id);
|
|
1303
|
+
const value = bubble === void 0 ? mark?.value ?? null : bubble.value;
|
|
1304
|
+
return {
|
|
1305
|
+
kind: "point",
|
|
1306
|
+
id: source.id,
|
|
1307
|
+
coordinates,
|
|
1308
|
+
feature,
|
|
1309
|
+
datum: source.datum,
|
|
1310
|
+
level: levelInfo,
|
|
1311
|
+
drillable: false,
|
|
1312
|
+
value,
|
|
1313
|
+
formattedValue: bubble === void 0 ? mark?.formattedValue ?? "" : formatValue(bubble.value),
|
|
1314
|
+
color: bubble === void 0 ? mark?.fill ?? emptyFill : bubble.fill,
|
|
1315
|
+
label: (feature === void 0 ? void 0 : nameOf(feature)) ?? source.id
|
|
1316
|
+
};
|
|
1317
|
+
};
|
|
1318
|
+
const previewed = useRef5(null);
|
|
1319
|
+
const hoverItem = (ref) => {
|
|
1320
|
+
if (isSameItem(store.getState().hovered, ref)) return;
|
|
1321
|
+
if (!isSameItem(previewed.current, ref)) previewed.current = null;
|
|
1322
|
+
store.setState({ hovered: ref });
|
|
1323
|
+
onItemHover?.(ref === null ? null : getItemContext(ref) ?? null);
|
|
1324
|
+
};
|
|
1325
|
+
const clickItem = (ref) => {
|
|
1326
|
+
const item = getItemContext(ref);
|
|
1327
|
+
if (item === void 0) return;
|
|
1328
|
+
if (ref.kind === "region" && item.drillable && engine.controller !== null) {
|
|
1329
|
+
if (store.getState().pointerType === "touch" && !isSameItem(previewed.current, ref)) {
|
|
1330
|
+
hoverItem(ref);
|
|
1331
|
+
previewed.current = ref;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
previewed.current = null;
|
|
1335
|
+
engine.noteItem(item);
|
|
1336
|
+
engine.controller.drillTo([...engine.shownPath, ref.id]);
|
|
1337
|
+
} else if (ref.kind === "region" && selectable) {
|
|
1338
|
+
setSelectedId(ref.id === selectedId ? null : ref.id);
|
|
1339
|
+
}
|
|
1340
|
+
onItemClick?.(item);
|
|
1341
|
+
};
|
|
1342
|
+
const { onDrillIntent } = options;
|
|
1343
|
+
const intentRef = useRef5({ regionsById, shownPath: engine.shownPath, idle: true, onDrillIntent });
|
|
1344
|
+
const { prefetch } = engine;
|
|
1345
|
+
useEffect6(() => {
|
|
1346
|
+
intentRef.current = {
|
|
1347
|
+
regionsById,
|
|
1348
|
+
shownPath: engine.shownPath,
|
|
1349
|
+
idle: engine.machine.phase === "idle",
|
|
1350
|
+
onDrillIntent
|
|
1351
|
+
};
|
|
1352
|
+
});
|
|
1353
|
+
useEffect6(() => {
|
|
1354
|
+
if (!engine.active) return;
|
|
1355
|
+
const timer = createIntentTimer({
|
|
1356
|
+
onIntent: (id) => {
|
|
1357
|
+
const current = intentRef.current;
|
|
1358
|
+
if (!current.idle) return;
|
|
1359
|
+
const path = [...current.shownPath, id];
|
|
1360
|
+
prefetch(path);
|
|
1361
|
+
current.onDrillIntent?.(contextOf(path));
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
const sync = () => {
|
|
1365
|
+
const hovered = store.getState().hovered;
|
|
1366
|
+
const drillable = hovered?.kind === "region" && intentRef.current.regionsById.get(hovered.id)?.drillable;
|
|
1367
|
+
timer.hover(hovered !== null && drillable === true ? hovered.id : null);
|
|
1368
|
+
};
|
|
1369
|
+
sync();
|
|
1370
|
+
const unsubscribe = store.subscribe(sync);
|
|
1371
|
+
return () => {
|
|
1372
|
+
unsubscribe();
|
|
1373
|
+
timer.dispose();
|
|
1374
|
+
};
|
|
1375
|
+
}, [engine.active, store, prefetch]);
|
|
1376
|
+
const findRequestProblem = (path) => {
|
|
1377
|
+
const shown = engine.shownPath;
|
|
1378
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
1379
|
+
if (depth > shown.length || depth > 0 && path[depth - 1] !== shown[depth - 1]) break;
|
|
1380
|
+
const id = path[depth];
|
|
1381
|
+
const level = engine.chain[depth];
|
|
1382
|
+
const knownIds = level === void 0 ? void 0 : new Set(level.features.flatMap((feature) => level.idOf(feature) ?? []));
|
|
1383
|
+
const problem = checkDrillStep({
|
|
1384
|
+
id,
|
|
1385
|
+
depth,
|
|
1386
|
+
levelId: engine.levels[depth]?.id ?? "?",
|
|
1387
|
+
knownIds,
|
|
1388
|
+
drillable: depth === shown.length && !dataLoading ? regionsById.get(id)?.drillable : void 0
|
|
1389
|
+
});
|
|
1390
|
+
if (problem !== null) return problem;
|
|
1391
|
+
}
|
|
1392
|
+
return null;
|
|
1393
|
+
};
|
|
1394
|
+
const latestDrill = useRef5({ findRequestProblem, engine });
|
|
1395
|
+
useEffect6(() => {
|
|
1396
|
+
latestDrill.current = { findRequestProblem, engine };
|
|
1397
|
+
});
|
|
1398
|
+
const drillApi = useMemo3(() => {
|
|
1399
|
+
const refuse = (message) => {
|
|
1400
|
+
if (!isProduction()) console.warn(message);
|
|
1401
|
+
return Promise.resolve({ completed: false });
|
|
1402
|
+
};
|
|
1403
|
+
const go = (path, source) => {
|
|
1404
|
+
const { findRequestProblem: find, engine: current } = latestDrill.current;
|
|
1405
|
+
if (clampPath(path, current.levels.length).clamped) {
|
|
1406
|
+
return refuse(formatTooDeepRequest(path, current.levels.length, source));
|
|
1407
|
+
}
|
|
1408
|
+
const problem = find(path);
|
|
1409
|
+
if (problem !== null) return refuse(formatDrillRequestProblem(problem, source));
|
|
1410
|
+
return current.navigate((controller2) => controller2.drillTo(path));
|
|
1411
|
+
};
|
|
1412
|
+
return {
|
|
1413
|
+
drillDown: (id) => go([...latestDrill.current.engine.target, id], "drillDown()"),
|
|
1414
|
+
drillUp: (steps) => latestDrill.current.engine.navigate((controller2) => controller2.drillUp(steps)),
|
|
1415
|
+
drillTo: (path) => go(path, "drillTo()")
|
|
1416
|
+
};
|
|
1417
|
+
}, []);
|
|
1418
|
+
useEffect6(() => {
|
|
1419
|
+
if (!engine.active) return;
|
|
1420
|
+
store.registerDrill(drillApi);
|
|
1421
|
+
return () => store.registerDrill(null);
|
|
1422
|
+
}, [engine.active, store, drillApi]);
|
|
1423
|
+
const targetKey = JSON.stringify(engine.target);
|
|
1424
|
+
useEffect6(() => {
|
|
1425
|
+
if (!engine.active || isProduction()) return;
|
|
1426
|
+
const problem = latestDrill.current.findRequestProblem(engine.target);
|
|
1427
|
+
if (problem !== null) console.warn(formatDrillRequestProblem(problem, "drill path"));
|
|
1428
|
+
}, [engine.active, targetKey]);
|
|
1429
|
+
const leaveHandler = (ref) => (event) => {
|
|
1430
|
+
if (event.pointerType !== "touch" && isSameItem(store.getState().hovered, ref)) {
|
|
1431
|
+
hoverItem(null);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
const getRegionProps = (region) => {
|
|
1435
|
+
const ref = { kind: "region", id: region.id };
|
|
1436
|
+
return {
|
|
1437
|
+
"data-region-id": region.id,
|
|
1438
|
+
"data-selected": region.id === selectedId ? true : void 0,
|
|
1439
|
+
onClick: () => clickItem(ref),
|
|
1440
|
+
onPointerEnter: () => hoverItem(ref),
|
|
1441
|
+
onPointerLeave: leaveHandler(ref)
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
1444
|
+
const getPointProps = (point) => {
|
|
1445
|
+
const ref = { kind: "point", id: point.id };
|
|
1446
|
+
return {
|
|
1447
|
+
"data-point-id": point.id,
|
|
1448
|
+
onClick: () => clickItem(ref),
|
|
1449
|
+
onPointerEnter: () => hoverItem(ref),
|
|
1450
|
+
onPointerLeave: leaveHandler(ref)
|
|
1451
|
+
};
|
|
1452
|
+
};
|
|
1453
|
+
const shownKey = engine.shownKey;
|
|
1454
|
+
const previousLevel = useRef5(shownKey);
|
|
1455
|
+
useIsoLayoutEffect(() => {
|
|
1456
|
+
if (!engine.active || previousLevel.current === shownKey) return;
|
|
1457
|
+
previousLevel.current = shownKey;
|
|
1458
|
+
const from = chain?.projections[chain.projections.length - 1];
|
|
1459
|
+
const to = previousChain?.projections[previousChain.projections.length - 1];
|
|
1460
|
+
const camera = machine.direction === "up" && from !== void 0 && to !== void 0 ? relativeTransform(from, to) : FIT_TRANSFORM;
|
|
1461
|
+
void store.getZoom()?.flyTo(camera, { duration: 0, silent: true });
|
|
1462
|
+
}, [engine.active, shownKey, store]);
|
|
1463
|
+
const zoomOptions = resolveZoomOptions(options.zoom, {
|
|
1464
|
+
// With `drill` the click already does something (go down a level): the double click must NOT also zoom
|
|
1465
|
+
// ×2, or the second click would interrupt the drill flight and end up as a plain zoom.
|
|
1466
|
+
clickIsBusy: onItemClick !== void 0 || selectable || engine.active
|
|
1467
|
+
});
|
|
1468
|
+
const focusBounds = { ...DEFAULT_FOCUS_BOUNDS, ...options.focusBounds };
|
|
1469
|
+
const getRegionExtent = (id) => {
|
|
1470
|
+
const feature = featuresById.get(id);
|
|
1471
|
+
return feature === void 0 ? null : getFocusExtent({ feature, path: toPath, project });
|
|
1472
|
+
};
|
|
1473
|
+
const resolveCamera = (target, cameraOptions) => {
|
|
1474
|
+
if (zoomOptions === null) return null;
|
|
1475
|
+
const size = { width, height };
|
|
1476
|
+
const transform = resolveZoomTarget({
|
|
1477
|
+
target,
|
|
1478
|
+
size,
|
|
1479
|
+
padding: cameraOptions?.padding ?? zoomOptions.padding,
|
|
1480
|
+
scaleExtent: [zoomOptions.min, zoomOptions.max],
|
|
1481
|
+
project,
|
|
1482
|
+
getRegionExtent,
|
|
1483
|
+
focusBounds
|
|
1484
|
+
});
|
|
1485
|
+
return transform !== null && zoomOptions.constrain === "contain" ? constrainTransform({
|
|
1486
|
+
transform,
|
|
1487
|
+
content: [
|
|
1488
|
+
[0, 0],
|
|
1489
|
+
[width, height]
|
|
1490
|
+
],
|
|
1491
|
+
view: [
|
|
1492
|
+
[0, 0],
|
|
1493
|
+
[width, height]
|
|
1494
|
+
]
|
|
1495
|
+
}) : transform;
|
|
1496
|
+
};
|
|
1497
|
+
return {
|
|
1498
|
+
containerRef: measured.ref,
|
|
1499
|
+
width,
|
|
1500
|
+
height,
|
|
1501
|
+
drillPhase: engine.active ? machine.phase : null,
|
|
1502
|
+
drillFocusId,
|
|
1503
|
+
drillLayers,
|
|
1504
|
+
drillNav,
|
|
1505
|
+
selectContextItem: (depth, id) => {
|
|
1506
|
+
engine.controller?.drillTo([...engine.shownPath.slice(0, depth), id]);
|
|
1507
|
+
},
|
|
1508
|
+
status,
|
|
1509
|
+
dataLoading,
|
|
1510
|
+
regions,
|
|
1511
|
+
anonymousPaths,
|
|
1512
|
+
emptyFill,
|
|
1513
|
+
fallbackPoints: fallbackMarks,
|
|
1514
|
+
labels,
|
|
1515
|
+
zoom: zoomOptions,
|
|
1516
|
+
onViewChange: typeof options.zoom === "object" ? options.zoom.onViewChange : void 0,
|
|
1517
|
+
invert,
|
|
1518
|
+
project,
|
|
1519
|
+
focusId: options.focusId,
|
|
1520
|
+
resolveCamera,
|
|
1521
|
+
borders: bordersPath,
|
|
1522
|
+
legend: variant.legend,
|
|
1523
|
+
sizeLegend: variant.sizeLegend,
|
|
1524
|
+
bubbles,
|
|
1525
|
+
scale: variant.scale,
|
|
1526
|
+
joinReport,
|
|
1527
|
+
formatValue,
|
|
1528
|
+
clickable: onItemClick !== void 0 || selectable,
|
|
1529
|
+
store,
|
|
1530
|
+
selectedId,
|
|
1531
|
+
setSelectedId,
|
|
1532
|
+
hoverItem,
|
|
1533
|
+
clickItem,
|
|
1534
|
+
getItemContext,
|
|
1535
|
+
getRegionProps,
|
|
1536
|
+
getPointProps
|
|
1537
|
+
};
|
|
1538
|
+
};
|
|
1539
|
+
var useIsoLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutEffect;
|
|
1540
|
+
var NO_GEOGRAPHY = { type: "FeatureCollection", features: [] };
|
|
1541
|
+
var EMPTY_FEATURES = { type: "FeatureCollection", features: [] };
|
|
1542
|
+
var NO_DATA = [];
|
|
1543
|
+
var EMPTY_FILL_VAR = `var(--cm-empty-fill, ${DEFAULT_EMPTY_FILL})`;
|
|
1544
|
+
var nameOf = (feature) => {
|
|
1545
|
+
const name = feature.properties?.name;
|
|
1546
|
+
return typeof name === "string" ? name : void 0;
|
|
1547
|
+
};
|
|
1548
|
+
var defaultFeatureId2 = (feature) => feature.id === void 0 ? void 0 : String(feature.id);
|
|
1549
|
+
|
|
1550
|
+
// src/react/choropleth-context.ts
|
|
1551
|
+
import { createContext, useContext } from "react";
|
|
1552
|
+
var ChoroplethContext = createContext(null);
|
|
1553
|
+
var useChoroplethContext = () => {
|
|
1554
|
+
const value = useContext(ChoroplethContext);
|
|
1555
|
+
if (value === null) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
"[choropleth] Choropleth components (Svg, Regions, Legend\u2026) must be rendered inside <Choropleth.Root>."
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return value;
|
|
1561
|
+
};
|
|
1562
|
+
var useChoroplethApi = () => useChoroplethContext().api;
|
|
1563
|
+
var useDefId = (name) => useChoroplethContext().defId(name);
|
|
1564
|
+
|
|
1565
|
+
// src/react/components/defs.tsx
|
|
1566
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1567
|
+
var Defs = ({ children }) => {
|
|
1568
|
+
const { defId } = useChoroplethContext();
|
|
1569
|
+
const color = {
|
|
1570
|
+
color: "var(--cm-pattern-color, color-mix(in srgb, var(--cm-label-color, #111111) 45%, transparent))"
|
|
1571
|
+
};
|
|
1572
|
+
return /* @__PURE__ */ jsxs("defs", { children: [
|
|
1573
|
+
/* @__PURE__ */ jsx(
|
|
1574
|
+
"pattern",
|
|
1575
|
+
{
|
|
1576
|
+
id: defId("hatch"),
|
|
1577
|
+
width: "6",
|
|
1578
|
+
height: "6",
|
|
1579
|
+
patternUnits: "userSpaceOnUse",
|
|
1580
|
+
patternTransform: "rotate(45)",
|
|
1581
|
+
style: color,
|
|
1582
|
+
children: /* @__PURE__ */ jsx("line", { x1: "0", y1: "0", x2: "0", y2: "6", stroke: "currentColor", strokeWidth: "2" })
|
|
1583
|
+
}
|
|
1584
|
+
),
|
|
1585
|
+
/* @__PURE__ */ jsx("pattern", { id: defId("dots"), width: "6", height: "6", patternUnits: "userSpaceOnUse", style: color, children: /* @__PURE__ */ jsx("circle", { cx: "3", cy: "3", r: "1.2", fill: "currentColor" }) }),
|
|
1586
|
+
children
|
|
1587
|
+
] });
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
// src/react/components/patterns.tsx
|
|
1591
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1592
|
+
var Patterns = () => {
|
|
1593
|
+
const { choropleth, defId, emptyPattern } = useChoroplethContext();
|
|
1594
|
+
const marks = choropleth.regions.flatMap((region) => {
|
|
1595
|
+
const name = region.pattern ?? (emptyPattern !== null && region.value === null ? emptyPattern : void 0);
|
|
1596
|
+
return name === void 0 ? [] : [{ id: region.id, d: region.path, name }];
|
|
1597
|
+
});
|
|
1598
|
+
if (marks.length === 0) return null;
|
|
1599
|
+
return /* @__PURE__ */ jsx2("g", { className: "cm-patterns", pointerEvents: "none", "aria-hidden": "true", children: marks.map(({ id, d, name }) => /* @__PURE__ */ jsx2(
|
|
1600
|
+
"path",
|
|
1601
|
+
{
|
|
1602
|
+
d,
|
|
1603
|
+
fill: `url(#${defId(name)})`
|
|
1604
|
+
},
|
|
1605
|
+
id
|
|
1606
|
+
)) });
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
// src/react/use-camera.ts
|
|
1610
|
+
import { useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
1611
|
+
var useCamera = (selector) => {
|
|
1612
|
+
const { store, api } = useChoroplethContext();
|
|
1613
|
+
const getSnapshot = useMemo4(
|
|
1614
|
+
() => createCameraSnapshot(store, () => api.getView(), selector),
|
|
1615
|
+
[store, api, selector]
|
|
1616
|
+
);
|
|
1617
|
+
return useSyncExternalStore3(store.subscribe, getSnapshot, getSnapshot);
|
|
1618
|
+
};
|
|
1619
|
+
var createCameraSnapshot = (store, getView, selector) => {
|
|
1620
|
+
let lastCamera;
|
|
1621
|
+
let last;
|
|
1622
|
+
return () => {
|
|
1623
|
+
const camera = store.getState().camera;
|
|
1624
|
+
if (last !== void 0 && Object.is(camera, lastCamera)) return last.value;
|
|
1625
|
+
const value = selector(getView());
|
|
1626
|
+
lastCamera = camera;
|
|
1627
|
+
if (last === void 0 || !shallowEqual(last.value, value)) last = { value };
|
|
1628
|
+
return last.value;
|
|
1629
|
+
};
|
|
1630
|
+
};
|
|
1631
|
+
var shallowEqual = (a, b) => {
|
|
1632
|
+
if (Object.is(a, b)) return true;
|
|
1633
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
1634
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1635
|
+
const keysA = Object.keys(a);
|
|
1636
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
1637
|
+
return keysA.every(
|
|
1638
|
+
(key) => Object.hasOwn(b, key) && Object.is(a[key], b[key])
|
|
1639
|
+
);
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
// src/react/use-zoom.ts
|
|
1643
|
+
import { useEffect as useEffect7, useRef as useRef6 } from "react";
|
|
1644
|
+
import { easeCubicInOut, easeCubicOut } from "d3-ease";
|
|
1645
|
+
import { select } from "d3-selection";
|
|
1646
|
+
import "d3-transition";
|
|
1647
|
+
import { zoom, zoomIdentity, zoomTransform } from "d3-zoom";
|
|
1648
|
+
|
|
1649
|
+
// src/react/fade.ts
|
|
1650
|
+
var FADE_OUT_MS = 100;
|
|
1651
|
+
var FADE_IN_MS = 150;
|
|
1652
|
+
var fadeThrough = (element, jump) => {
|
|
1653
|
+
if (typeof element.animate !== "function") {
|
|
1654
|
+
jump();
|
|
1655
|
+
return { promise: Promise.resolve({ completed: true }), cancel: () => {
|
|
1656
|
+
} };
|
|
1657
|
+
}
|
|
1658
|
+
const animations = [];
|
|
1659
|
+
const out = element.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
1660
|
+
duration: FADE_OUT_MS,
|
|
1661
|
+
fill: "forwards"
|
|
1662
|
+
});
|
|
1663
|
+
animations.push(out);
|
|
1664
|
+
const promise = out.finished.then(() => {
|
|
1665
|
+
jump();
|
|
1666
|
+
const back = element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: FADE_IN_MS });
|
|
1667
|
+
animations.push(back);
|
|
1668
|
+
out.cancel();
|
|
1669
|
+
return back.finished;
|
|
1670
|
+
}).then(
|
|
1671
|
+
() => ({ completed: true }),
|
|
1672
|
+
() => ({ completed: false })
|
|
1673
|
+
);
|
|
1674
|
+
return { promise, cancel: () => animations.forEach((animation) => animation.cancel()) };
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// src/react/use-zoom.ts
|
|
1678
|
+
var BUTTON_ZOOM_MS = 300;
|
|
1679
|
+
var isPressOnDrillTarget = (event, root) => {
|
|
1680
|
+
if (event.type !== "mousedown" && event.type !== "touchstart") return false;
|
|
1681
|
+
const phase = root.getAttribute("data-drill-phase");
|
|
1682
|
+
if (phase !== "flying" && phase !== "loading") return false;
|
|
1683
|
+
const target = event.target;
|
|
1684
|
+
return target instanceof Element && target.closest("[data-drill-target]") !== null;
|
|
1685
|
+
};
|
|
1686
|
+
var CLICK_DISTANCE_PX = 4;
|
|
1687
|
+
var ZOOMED_EPSILON = 1.001;
|
|
1688
|
+
var useZoom = ({
|
|
1689
|
+
svgRef,
|
|
1690
|
+
viewportRef,
|
|
1691
|
+
rootRef,
|
|
1692
|
+
store,
|
|
1693
|
+
width,
|
|
1694
|
+
height,
|
|
1695
|
+
options,
|
|
1696
|
+
invert,
|
|
1697
|
+
project,
|
|
1698
|
+
onViewChange,
|
|
1699
|
+
resolveCamera,
|
|
1700
|
+
focusId,
|
|
1701
|
+
ready
|
|
1702
|
+
}) => {
|
|
1703
|
+
const latest = useRef6({ invert, project, onViewChange, resolveCamera });
|
|
1704
|
+
useEffect7(() => {
|
|
1705
|
+
latest.current = { invert, project, onViewChange, resolveCamera };
|
|
1706
|
+
});
|
|
1707
|
+
const previous = useRef6(null);
|
|
1708
|
+
const enabled = options !== null;
|
|
1709
|
+
const {
|
|
1710
|
+
min,
|
|
1711
|
+
max,
|
|
1712
|
+
wheel,
|
|
1713
|
+
pinch,
|
|
1714
|
+
pan,
|
|
1715
|
+
doubleClick,
|
|
1716
|
+
constrain,
|
|
1717
|
+
initialView,
|
|
1718
|
+
bubbleScaling,
|
|
1719
|
+
inertia
|
|
1720
|
+
} = options ?? NO_ZOOM;
|
|
1721
|
+
useEffect7(() => {
|
|
1722
|
+
const svg = svgRef.current;
|
|
1723
|
+
const viewport = viewportRef.current;
|
|
1724
|
+
const root = rootRef.current;
|
|
1725
|
+
if (!enabled || !ready || svg === null || viewport === null || root === null) return;
|
|
1726
|
+
const size = { width, height };
|
|
1727
|
+
const content = [
|
|
1728
|
+
[0, 0],
|
|
1729
|
+
[width, height]
|
|
1730
|
+
];
|
|
1731
|
+
let restoring = false;
|
|
1732
|
+
let samples = [];
|
|
1733
|
+
let glide = null;
|
|
1734
|
+
let gesture = { k: 1, x: 0, y: 0, moving: false };
|
|
1735
|
+
const commit = (transform, notify) => {
|
|
1736
|
+
const camera = { k: transform.k, x: transform.x, y: transform.y };
|
|
1737
|
+
store.setState({ camera, interacting: false });
|
|
1738
|
+
if (!notify) return;
|
|
1739
|
+
latest.current.onViewChange?.(
|
|
1740
|
+
toViewState({
|
|
1741
|
+
transform: camera,
|
|
1742
|
+
size,
|
|
1743
|
+
invert: latest.current.invert,
|
|
1744
|
+
level: ROOT_LEVEL
|
|
1745
|
+
})
|
|
1746
|
+
);
|
|
1747
|
+
};
|
|
1748
|
+
const stopGlide = () => {
|
|
1749
|
+
if (glide === null) return;
|
|
1750
|
+
cancelAnimationFrame(glide.frame);
|
|
1751
|
+
glide = null;
|
|
1752
|
+
commit(zoomTransform(svg), true);
|
|
1753
|
+
};
|
|
1754
|
+
const baseFilter = createZoomFilter({
|
|
1755
|
+
wheel,
|
|
1756
|
+
pinch,
|
|
1757
|
+
pan,
|
|
1758
|
+
doubleClick,
|
|
1759
|
+
onGestureHint: (kind) => store.showGestureHint(kind)
|
|
1760
|
+
});
|
|
1761
|
+
const behavior = zoom().extent([
|
|
1762
|
+
[0, 0],
|
|
1763
|
+
[width, height]
|
|
1764
|
+
]).scaleExtent([min, max]).clickDistance(CLICK_DISTANCE_PX).filter((event) => {
|
|
1765
|
+
if (isPressOnDrillTarget(event, root)) return false;
|
|
1766
|
+
return baseFilter(event);
|
|
1767
|
+
}).on("start", (event) => {
|
|
1768
|
+
if (event.sourceEvent === null) return;
|
|
1769
|
+
stopGlide();
|
|
1770
|
+
samples = [];
|
|
1771
|
+
gesture = {
|
|
1772
|
+
k: event.transform.k,
|
|
1773
|
+
x: event.transform.x,
|
|
1774
|
+
y: event.transform.y,
|
|
1775
|
+
moving: false
|
|
1776
|
+
};
|
|
1777
|
+
}).on(
|
|
1778
|
+
"zoom",
|
|
1779
|
+
({ transform, sourceEvent }) => {
|
|
1780
|
+
viewport.setAttribute("transform", transform.toString());
|
|
1781
|
+
root.style.setProperty("--cm-k", String(transform.k));
|
|
1782
|
+
root.style.setProperty(
|
|
1783
|
+
"--cm-bubble-scale",
|
|
1784
|
+
String(bubbleScale(transform.k, bubbleScaling))
|
|
1785
|
+
);
|
|
1786
|
+
if (sourceEvent !== null && !gesture.moving) {
|
|
1787
|
+
const moved = sourceEvent.type === "wheel" || transform.k !== gesture.k || Math.hypot(transform.x - gesture.x, transform.y - gesture.y) > CLICK_DISTANCE_PX;
|
|
1788
|
+
if (moved) {
|
|
1789
|
+
gesture.moving = true;
|
|
1790
|
+
store.setState({ interacting: true });
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (sourceEvent !== null) {
|
|
1794
|
+
samples.push({
|
|
1795
|
+
time: performance.now(),
|
|
1796
|
+
k: transform.k,
|
|
1797
|
+
x: transform.x,
|
|
1798
|
+
y: transform.y
|
|
1799
|
+
});
|
|
1800
|
+
if (samples.length > MAX_SAMPLES) samples.shift();
|
|
1801
|
+
}
|
|
1802
|
+
root.toggleAttribute("data-zoomed", transform.k > ZOOMED_EPSILON);
|
|
1803
|
+
}
|
|
1804
|
+
).on(
|
|
1805
|
+
"end",
|
|
1806
|
+
({ transform, sourceEvent }) => {
|
|
1807
|
+
if (glide !== null) return;
|
|
1808
|
+
const velocity = inertia && sourceEvent !== null && !restoring && !prefersReducedMotion() ? releaseVelocity({ samples, now: performance.now() }) : null;
|
|
1809
|
+
samples = [];
|
|
1810
|
+
if (velocity !== null) {
|
|
1811
|
+
store.setState({ camera: { k: transform.k, x: transform.x, y: transform.y } });
|
|
1812
|
+
startGlide(transform, velocity);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
commit(transform, !restoring);
|
|
1816
|
+
}
|
|
1817
|
+
);
|
|
1818
|
+
if (constrain === "contain") {
|
|
1819
|
+
behavior.constrain((transform, extent) => {
|
|
1820
|
+
const c = constrainTransform({ transform, content, view: extent });
|
|
1821
|
+
return zoomIdentity.translate(c.x, c.y).scale(c.k);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
behavior.interpolate(createZoomInterpolator(DEFAULT_FLIGHT_CURVATURE));
|
|
1825
|
+
const selection = select(svg).call(behavior);
|
|
1826
|
+
if (wheel === "cooperative") svg.style.touchAction = "pan-x pan-y";
|
|
1827
|
+
let restored = store.getState().camera;
|
|
1828
|
+
const before = previous.current;
|
|
1829
|
+
if (before !== null && (before.size.width !== width || before.size.height !== height)) {
|
|
1830
|
+
const moved = preserveView({
|
|
1831
|
+
transform: before.camera,
|
|
1832
|
+
oldSize: before.size,
|
|
1833
|
+
newSize: size,
|
|
1834
|
+
invertOld: before.invert,
|
|
1835
|
+
projectNew: latest.current.project
|
|
1836
|
+
});
|
|
1837
|
+
restored = constrain === "contain" ? constrainTransform({ transform: moved, content, view: content }) : moved;
|
|
1838
|
+
}
|
|
1839
|
+
restoring = true;
|
|
1840
|
+
behavior.transform(selection, zoomIdentity.translate(restored.x, restored.y).scale(restored.k));
|
|
1841
|
+
restoring = false;
|
|
1842
|
+
previous.current = null;
|
|
1843
|
+
const startGlide = (origin, [vx, vy]) => {
|
|
1844
|
+
const start = performance.now();
|
|
1845
|
+
let last = origin;
|
|
1846
|
+
store.setState({ interacting: true });
|
|
1847
|
+
const step = () => {
|
|
1848
|
+
const elapsed = performance.now() - start;
|
|
1849
|
+
const wanted = {
|
|
1850
|
+
k: origin.k,
|
|
1851
|
+
x: origin.x + inertiaOffset({ velocity: vx, elapsed }),
|
|
1852
|
+
y: origin.y + inertiaOffset({ velocity: vy, elapsed })
|
|
1853
|
+
};
|
|
1854
|
+
const next = constrain === "contain" ? constrainTransform({ transform: wanted, content, view: content }) : wanted;
|
|
1855
|
+
behavior.transform(selection, zoomIdentity.translate(next.x, next.y).scale(next.k));
|
|
1856
|
+
const now = zoomTransform(svg);
|
|
1857
|
+
const moved = Math.hypot(now.x - last.x, now.y - last.y);
|
|
1858
|
+
last = now;
|
|
1859
|
+
const remaining = Math.hypot(vx, vy) * INERTIA_TAU_MS * Math.exp(-elapsed / INERTIA_TAU_MS);
|
|
1860
|
+
if (remaining < GLIDE_STOP_PX || elapsed > 0 && moved < 0.01) {
|
|
1861
|
+
glide = null;
|
|
1862
|
+
commit(now, true);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
glide = { frame: requestAnimationFrame(step) };
|
|
1866
|
+
};
|
|
1867
|
+
glide = { frame: requestAnimationFrame(step) };
|
|
1868
|
+
};
|
|
1869
|
+
const onPointerDown = () => stopGlide();
|
|
1870
|
+
svg.addEventListener("pointerdown", onPointerDown);
|
|
1871
|
+
let fade = null;
|
|
1872
|
+
const animate = (duration, apply, {
|
|
1873
|
+
ease = easeCubicInOut,
|
|
1874
|
+
silent = false
|
|
1875
|
+
} = {}) => {
|
|
1876
|
+
fade?.cancel();
|
|
1877
|
+
fade = null;
|
|
1878
|
+
stopGlide();
|
|
1879
|
+
if (duration <= 0) {
|
|
1880
|
+
restoring = silent;
|
|
1881
|
+
apply(selection);
|
|
1882
|
+
restoring = false;
|
|
1883
|
+
return Promise.resolve({ completed: true });
|
|
1884
|
+
}
|
|
1885
|
+
if (prefersReducedMotion()) {
|
|
1886
|
+
fade = fadeThrough(viewport, () => apply(selection));
|
|
1887
|
+
return fade.promise;
|
|
1888
|
+
}
|
|
1889
|
+
const transition = selection.transition().duration(duration).ease(ease);
|
|
1890
|
+
apply(transition);
|
|
1891
|
+
return transition.end().then(
|
|
1892
|
+
() => ({ completed: true }),
|
|
1893
|
+
() => ({ completed: false })
|
|
1894
|
+
);
|
|
1895
|
+
};
|
|
1896
|
+
const toZoomIdentity = ({ k, x, y }) => zoomIdentity.translate(x, y).scale(k);
|
|
1897
|
+
const current = () => {
|
|
1898
|
+
const { k, x, y } = zoomTransform(svg);
|
|
1899
|
+
return { k, x, y };
|
|
1900
|
+
};
|
|
1901
|
+
const api = {
|
|
1902
|
+
zoomTo: (target, { duration, padding, silent } = {}) => {
|
|
1903
|
+
const destination = latest.current.resolveCamera(target, { padding });
|
|
1904
|
+
if (destination === null) {
|
|
1905
|
+
if (!isProduction()) {
|
|
1906
|
+
console.warn(
|
|
1907
|
+
`[choropleth] zoomTo: could not resolve ${JSON.stringify(target)} (unknown region id, or a point outside the projection).`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return Promise.resolve({ completed: false });
|
|
1911
|
+
}
|
|
1912
|
+
const length = duration ?? flightDuration({ from: current(), to: destination, size });
|
|
1913
|
+
return animate(
|
|
1914
|
+
length,
|
|
1915
|
+
(t) => behavior.transform(t, toZoomIdentity(destination)),
|
|
1916
|
+
{
|
|
1917
|
+
silent
|
|
1918
|
+
}
|
|
1919
|
+
);
|
|
1920
|
+
},
|
|
1921
|
+
flyTo: (camera, { duration, silent } = {}) => {
|
|
1922
|
+
const length = duration ?? flightDuration({ from: current(), to: camera, size });
|
|
1923
|
+
return animate(
|
|
1924
|
+
length,
|
|
1925
|
+
(t) => behavior.transform(t, toZoomIdentity(camera)),
|
|
1926
|
+
{
|
|
1927
|
+
silent
|
|
1928
|
+
}
|
|
1929
|
+
);
|
|
1930
|
+
},
|
|
1931
|
+
zoomBy: (factor, { duration = BUTTON_ZOOM_MS } = {}) => animate(duration, (t) => behavior.scaleBy(t, factor), {
|
|
1932
|
+
ease: easeCubicOut
|
|
1933
|
+
}),
|
|
1934
|
+
resetView: ({ duration, silent } = {}) => {
|
|
1935
|
+
const length = duration ?? flightDuration({ from: current(), to: FIT_TRANSFORM, size });
|
|
1936
|
+
return animate(length, (t) => behavior.transform(t, zoomIdentity), {
|
|
1937
|
+
silent
|
|
1938
|
+
});
|
|
1939
|
+
},
|
|
1940
|
+
getView: () => toViewState({
|
|
1941
|
+
transform: current(),
|
|
1942
|
+
size,
|
|
1943
|
+
invert: latest.current.invert,
|
|
1944
|
+
level: ROOT_LEVEL
|
|
1945
|
+
})
|
|
1946
|
+
};
|
|
1947
|
+
store.registerZoom(api);
|
|
1948
|
+
return () => {
|
|
1949
|
+
const live = zoomTransform(svg);
|
|
1950
|
+
previous.current = {
|
|
1951
|
+
size,
|
|
1952
|
+
invert: latest.current.invert,
|
|
1953
|
+
camera: { k: live.k, x: live.x, y: live.y }
|
|
1954
|
+
};
|
|
1955
|
+
svg.removeEventListener("pointerdown", onPointerDown);
|
|
1956
|
+
if (glide !== null) cancelAnimationFrame(glide.frame);
|
|
1957
|
+
glide = null;
|
|
1958
|
+
fade?.cancel();
|
|
1959
|
+
store.registerZoom(null);
|
|
1960
|
+
selection.interrupt();
|
|
1961
|
+
selection.on(".zoom", null);
|
|
1962
|
+
svg.style.removeProperty("touch-action");
|
|
1963
|
+
root.toggleAttribute("data-zoomed", false);
|
|
1964
|
+
root.style.removeProperty("--cm-bubble-scale");
|
|
1965
|
+
store.setState({ interacting: false });
|
|
1966
|
+
};
|
|
1967
|
+
}, [
|
|
1968
|
+
svgRef,
|
|
1969
|
+
viewportRef,
|
|
1970
|
+
rootRef,
|
|
1971
|
+
store,
|
|
1972
|
+
enabled,
|
|
1973
|
+
ready,
|
|
1974
|
+
width,
|
|
1975
|
+
height,
|
|
1976
|
+
min,
|
|
1977
|
+
max,
|
|
1978
|
+
wheel,
|
|
1979
|
+
pinch,
|
|
1980
|
+
pan,
|
|
1981
|
+
doubleClick,
|
|
1982
|
+
constrain,
|
|
1983
|
+
bubbleScaling,
|
|
1984
|
+
inertia
|
|
1985
|
+
]);
|
|
1986
|
+
const applied = useRef6(UNSET);
|
|
1987
|
+
const focus = focusId ?? null;
|
|
1988
|
+
useEffect7(() => {
|
|
1989
|
+
if (!enabled || !ready) return;
|
|
1990
|
+
const api = store.getZoom();
|
|
1991
|
+
if (api === null) return;
|
|
1992
|
+
if (applied.current === UNSET) {
|
|
1993
|
+
applied.current = focus;
|
|
1994
|
+
const first = focus ?? initialView;
|
|
1995
|
+
if (first !== void 0 && first !== null)
|
|
1996
|
+
void api.zoomTo(first, { duration: 0, silent: true });
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (focus === applied.current) return;
|
|
2000
|
+
applied.current = focus;
|
|
2001
|
+
if (focus === null) void api.resetView();
|
|
2002
|
+
else void api.zoomTo(focus);
|
|
2003
|
+
}, [store, enabled, ready, focus]);
|
|
2004
|
+
useEffect7(() => () => store.dispose(), [store]);
|
|
2005
|
+
};
|
|
2006
|
+
var MAX_SAMPLES = 12;
|
|
2007
|
+
var GLIDE_STOP_PX = 0.5;
|
|
2008
|
+
var UNSET = /* @__PURE__ */ Symbol("unset");
|
|
2009
|
+
var NO_ZOOM = {
|
|
2010
|
+
min: 1,
|
|
2011
|
+
max: 1,
|
|
2012
|
+
wheel: "never",
|
|
2013
|
+
pinch: false,
|
|
2014
|
+
pan: false,
|
|
2015
|
+
doubleClick: false,
|
|
2016
|
+
constrain: "contain",
|
|
2017
|
+
bubbleScaling: "sqrt",
|
|
2018
|
+
inertia: false,
|
|
2019
|
+
keyboard: false,
|
|
2020
|
+
padding: 0,
|
|
2021
|
+
initialView: void 0
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
// src/react/class-names.ts
|
|
2025
|
+
var cx = (...parts) => parts.filter((part) => typeof part === "string" && part !== "").join(" ");
|
|
2026
|
+
|
|
2027
|
+
// src/react/components/borders.tsx
|
|
2028
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2029
|
+
var Borders = () => {
|
|
2030
|
+
const { choropleth, classNames } = useChoroplethContext();
|
|
2031
|
+
if (choropleth.borders === null) return null;
|
|
2032
|
+
return /* @__PURE__ */ jsx3(
|
|
2033
|
+
"path",
|
|
2034
|
+
{
|
|
2035
|
+
className: cx("cm-borders", classNames.borders),
|
|
2036
|
+
d: choropleth.borders,
|
|
2037
|
+
pointerEvents: "none"
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
// src/react/components/breadcrumb.tsx
|
|
2043
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2044
|
+
var CORNERS = {
|
|
2045
|
+
"top-left": { top: 12, left: 12 },
|
|
2046
|
+
"top-right": { top: 12, right: 12 },
|
|
2047
|
+
"bottom-left": { bottom: 12, left: 12 },
|
|
2048
|
+
"bottom-right": { bottom: 12, right: 12 }
|
|
2049
|
+
};
|
|
2050
|
+
var Breadcrumb = ({ position, className, children }) => {
|
|
2051
|
+
const { choropleth, classNames } = useChoroplethContext();
|
|
2052
|
+
const { drillNav, labels } = choropleth;
|
|
2053
|
+
if (drillNav === null) return null;
|
|
2054
|
+
if (children !== void 0) return /* @__PURE__ */ jsx4(Fragment, { children: children(drillNav) });
|
|
2055
|
+
const { items, canDrillUp, drillTo, drillUp } = drillNav;
|
|
2056
|
+
return /* @__PURE__ */ jsxs2(
|
|
2057
|
+
"nav",
|
|
2058
|
+
{
|
|
2059
|
+
className: cx("cm-breadcrumb", classNames.breadcrumb, className),
|
|
2060
|
+
"aria-label": labels.breadcrumb,
|
|
2061
|
+
style: position === void 0 ? void 0 : { position: "absolute", ...CORNERS[position] },
|
|
2062
|
+
children: [
|
|
2063
|
+
canDrillUp && /* @__PURE__ */ jsx4(
|
|
2064
|
+
"button",
|
|
2065
|
+
{
|
|
2066
|
+
type: "button",
|
|
2067
|
+
className: "cm-breadcrumb__back",
|
|
2068
|
+
"aria-label": labels.drillUp,
|
|
2069
|
+
onClick: () => drillUp(),
|
|
2070
|
+
children: "\u2190"
|
|
2071
|
+
}
|
|
2072
|
+
),
|
|
2073
|
+
/* @__PURE__ */ jsx4("ol", { className: "cm-breadcrumb__list", children: items.map((item) => /* @__PURE__ */ jsx4("li", { className: cx("cm-breadcrumb__item", classNames.breadcrumbItem), children: item.current ? /* @__PURE__ */ jsx4("span", { className: "cm-breadcrumb__current", "aria-current": "page", children: item.label }) : /* @__PURE__ */ jsx4(
|
|
2074
|
+
"button",
|
|
2075
|
+
{
|
|
2076
|
+
type: "button",
|
|
2077
|
+
className: "cm-breadcrumb__link",
|
|
2078
|
+
onClick: () => drillTo(item.depth),
|
|
2079
|
+
children: item.label
|
|
2080
|
+
}
|
|
2081
|
+
) }, item.depth)) })
|
|
2082
|
+
]
|
|
2083
|
+
}
|
|
2084
|
+
);
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// src/react/components/bubbles.tsx
|
|
2088
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2089
|
+
var Bubbles = () => {
|
|
2090
|
+
const { choropleth, classNames } = useChoroplethContext();
|
|
2091
|
+
const { bubbles, clickable, getPointProps } = choropleth;
|
|
2092
|
+
if (bubbles.length === 0) return null;
|
|
2093
|
+
return /* @__PURE__ */ jsx5("g", { className: cx("cm-bubbles", classNames.bubbles), pointerEvents: "none", children: bubbles.map((bubble) => {
|
|
2094
|
+
const isPoint = bubble.coordinates !== null;
|
|
2095
|
+
return /* @__PURE__ */ jsx5(
|
|
2096
|
+
"circle",
|
|
2097
|
+
{
|
|
2098
|
+
className: cx("cm-bubble", classNames.bubble),
|
|
2099
|
+
"data-bubble-id": bubble.id,
|
|
2100
|
+
cx: bubble.x,
|
|
2101
|
+
cy: bubble.y,
|
|
2102
|
+
r: bubble.r,
|
|
2103
|
+
style: { fill: bubble.fill, cursor: isPoint && clickable ? "pointer" : void 0 },
|
|
2104
|
+
pointerEvents: isPoint ? "auto" : void 0,
|
|
2105
|
+
...isPoint ? getPointProps(bubble) : void 0
|
|
2106
|
+
},
|
|
2107
|
+
bubble.id
|
|
2108
|
+
);
|
|
2109
|
+
}) });
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/react/components/drill-layers.tsx
|
|
2113
|
+
import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
|
|
2114
|
+
var DrillLayers = ({ role }) => {
|
|
2115
|
+
const { choropleth, classNames } = useChoroplethContext();
|
|
2116
|
+
const { drillLayers, selectContextItem } = choropleth;
|
|
2117
|
+
const layers = drillLayers.filter((layer) => layer.role === role);
|
|
2118
|
+
if (layers.length === 0) return null;
|
|
2119
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: layers.map((layer) => {
|
|
2120
|
+
const { k, x, y } = layer.transform;
|
|
2121
|
+
return /* @__PURE__ */ jsx6(
|
|
2122
|
+
"g",
|
|
2123
|
+
{
|
|
2124
|
+
className: cx("cm-layer", classNames.layer),
|
|
2125
|
+
"data-role": layer.role,
|
|
2126
|
+
"data-depth": layer.depth,
|
|
2127
|
+
transform: `translate(${x},${y}) scale(${k})`,
|
|
2128
|
+
pointerEvents: layer.role === "exiting" ? "none" : void 0,
|
|
2129
|
+
onClick: layer.role === "context" ? (event) => {
|
|
2130
|
+
const id = event.target.closest?.("[data-context-id]")?.getAttribute("data-context-id");
|
|
2131
|
+
const known = id == null ? void 0 : layer.paths.find((p) => p.id === id);
|
|
2132
|
+
if (known?.drillable === true && id != null) selectContextItem(layer.depth, id);
|
|
2133
|
+
} : void 0,
|
|
2134
|
+
children: layer.paths.map((path, index) => /* @__PURE__ */ jsx6(
|
|
2135
|
+
"path",
|
|
2136
|
+
{
|
|
2137
|
+
className: "cm-context-region",
|
|
2138
|
+
d: path.d,
|
|
2139
|
+
"data-context-id": path.id,
|
|
2140
|
+
"data-focus": path.focus ? "" : void 0,
|
|
2141
|
+
"data-drillable": path.drillable ? "" : void 0,
|
|
2142
|
+
style: path.fill === void 0 ? void 0 : { fill: path.fill }
|
|
2143
|
+
},
|
|
2144
|
+
path.id ?? `anonymous-${index}`
|
|
2145
|
+
))
|
|
2146
|
+
},
|
|
2147
|
+
layer.key
|
|
2148
|
+
);
|
|
2149
|
+
}) });
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// src/react/components/fallback-points.tsx
|
|
2153
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2154
|
+
var FallbackPoints = () => {
|
|
2155
|
+
const { choropleth } = useChoroplethContext();
|
|
2156
|
+
const { fallbackPoints, clickable, getPointProps } = choropleth;
|
|
2157
|
+
if (fallbackPoints.length === 0) return null;
|
|
2158
|
+
return /* @__PURE__ */ jsx7("g", { className: "cm-fallback-points", children: fallbackPoints.map((point) => /* @__PURE__ */ jsx7(
|
|
2159
|
+
"circle",
|
|
2160
|
+
{
|
|
2161
|
+
className: "cm-fallback-point",
|
|
2162
|
+
cx: point.x,
|
|
2163
|
+
cy: point.y,
|
|
2164
|
+
r: point.r,
|
|
2165
|
+
style: { fill: point.fill, cursor: clickable ? "pointer" : void 0 },
|
|
2166
|
+
...getPointProps(point)
|
|
2167
|
+
},
|
|
2168
|
+
point.id
|
|
2169
|
+
)) });
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2172
|
+
// src/react/components/gesture-hint.tsx
|
|
2173
|
+
import { useState as useState6 } from "react";
|
|
2174
|
+
|
|
2175
|
+
// src/react/platform.ts
|
|
2176
|
+
import { useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
2177
|
+
var isApplePlatform = () => {
|
|
2178
|
+
if (typeof navigator === "undefined") return false;
|
|
2179
|
+
const platform = navigator.userAgentData?.platform ?? navigator.platform ?? "";
|
|
2180
|
+
return /Mac|iPhone|iPad|iPod/i.test(platform);
|
|
2181
|
+
};
|
|
2182
|
+
var subscribeNothing = () => () => {
|
|
2183
|
+
};
|
|
2184
|
+
var useIsApplePlatform = () => useSyncExternalStore4(subscribeNothing, isApplePlatform, () => false);
|
|
2185
|
+
|
|
2186
|
+
// src/react/components/gesture-hint.tsx
|
|
2187
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
2188
|
+
var GestureHint = ({ className }) => {
|
|
2189
|
+
const { choropleth, store, classNames } = useChoroplethContext();
|
|
2190
|
+
const hint = useMapStore(store, (s) => s.gestureHint);
|
|
2191
|
+
const apple = useIsApplePlatform();
|
|
2192
|
+
const [last, setLast] = useState6("wheel");
|
|
2193
|
+
if (hint !== null && hint !== last) setLast(hint);
|
|
2194
|
+
const { labels } = choropleth;
|
|
2195
|
+
const text = last === "touch" ? labels.gestureHintTouch : apple ? labels.gestureHintWheelMac : labels.gestureHintWheel;
|
|
2196
|
+
return /* @__PURE__ */ jsx8(
|
|
2197
|
+
"div",
|
|
2198
|
+
{
|
|
2199
|
+
className: cx("cm-gesture-hint", classNames.gestureHint, className),
|
|
2200
|
+
"aria-hidden": "true",
|
|
2201
|
+
"data-visible": hint === null ? void 0 : "",
|
|
2202
|
+
children: text
|
|
2203
|
+
}
|
|
2204
|
+
);
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
// src/react/components/legend.tsx
|
|
2208
|
+
import { useEffect as useEffect8 } from "react";
|
|
2209
|
+
import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2210
|
+
var GRADIENT_STOPS = 5;
|
|
2211
|
+
var Legend = ({
|
|
2212
|
+
title,
|
|
2213
|
+
showCount = false,
|
|
2214
|
+
className,
|
|
2215
|
+
variant = "swatches",
|
|
2216
|
+
interactive = true,
|
|
2217
|
+
children
|
|
2218
|
+
}) => {
|
|
2219
|
+
const { choropleth, classNames, emptyPattern } = useChoroplethContext();
|
|
2220
|
+
const { legend: items, sizeLegend: sizeItems, scale, formatValue, containerRef } = choropleth;
|
|
2221
|
+
const canHighlight = interactive && choropleth.regions.some((r) => r.classIndex !== null);
|
|
2222
|
+
const highlight = (key) => {
|
|
2223
|
+
const root = containerRef.current;
|
|
2224
|
+
if (root === null) return;
|
|
2225
|
+
if (key === null) root.removeAttribute("data-highlight-class");
|
|
2226
|
+
else root.setAttribute("data-highlight-class", key);
|
|
2227
|
+
};
|
|
2228
|
+
useEffect8(
|
|
2229
|
+
() => () => containerRef.current?.removeAttribute("data-highlight-class"),
|
|
2230
|
+
[containerRef]
|
|
2231
|
+
);
|
|
2232
|
+
const classKey = (item, index) => item.empty === true ? "empty" : String(index);
|
|
2233
|
+
const hoverProps = (item, index) => canHighlight ? {
|
|
2234
|
+
"data-class": classKey(item, index),
|
|
2235
|
+
onPointerEnter: () => highlight(classKey(item, index)),
|
|
2236
|
+
onPointerLeave: () => highlight(null)
|
|
2237
|
+
} : {};
|
|
2238
|
+
const swatchProps = (item) => ({
|
|
2239
|
+
style: { backgroundColor: item.color },
|
|
2240
|
+
"data-pattern": item.empty === true && emptyPattern !== null ? emptyPattern : void 0
|
|
2241
|
+
});
|
|
2242
|
+
if (children !== void 0) return /* @__PURE__ */ jsx9(Fragment3, { children: children({ items, sizeItems, scale, formatValue }) });
|
|
2243
|
+
const rootClass = cx("cm-legend", classNames.legend, className);
|
|
2244
|
+
const indexed = items.map((item, index) => ({ item, index }));
|
|
2245
|
+
const classRows = indexed.filter(({ item }) => item.empty !== true);
|
|
2246
|
+
const emptyItem = indexed.find(({ item }) => item.empty === true) ?? null;
|
|
2247
|
+
const barItems = variant === "bar" && scale?.kind !== "continuous" && classRows.length > 0 && classRows.every(({ item }) => item.from !== void 0 && item.to !== void 0) ? classRows : null;
|
|
2248
|
+
const barTicks = barItems === null ? [] : [...barItems.map(({ item }) => item.from), barItems.at(-1)?.item.to];
|
|
2249
|
+
const [min, max] = scale?.domain ?? [0, 1];
|
|
2250
|
+
return /* @__PURE__ */ jsxs3("div", { className: rootClass, children: [
|
|
2251
|
+
title !== void 0 && /* @__PURE__ */ jsx9("div", { className: "cm-legend__title", children: title }),
|
|
2252
|
+
scale !== null && scale.kind === "continuous" ? /* @__PURE__ */ jsxs3("div", { children: [
|
|
2253
|
+
/* @__PURE__ */ jsx9(
|
|
2254
|
+
"div",
|
|
2255
|
+
{
|
|
2256
|
+
className: "cm-legend__gradient",
|
|
2257
|
+
"data-legend-gradient": true,
|
|
2258
|
+
style: {
|
|
2259
|
+
background: `linear-gradient(to right, ${Array.from(
|
|
2260
|
+
{ length: GRADIENT_STOPS },
|
|
2261
|
+
(_, i) => scale.getColor(min + (max - min) * i / (GRADIENT_STOPS - 1))
|
|
2262
|
+
).join(", ")})`
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
),
|
|
2266
|
+
/* @__PURE__ */ jsxs3("div", { className: "cm-legend__range", children: [
|
|
2267
|
+
/* @__PURE__ */ jsx9("span", { children: formatValue(min) }),
|
|
2268
|
+
/* @__PURE__ */ jsx9("span", { children: formatValue(max) })
|
|
2269
|
+
] })
|
|
2270
|
+
] }) : barItems !== null ? /* @__PURE__ */ jsxs3("div", { className: "cm-legend__bar-wrap", children: [
|
|
2271
|
+
/* @__PURE__ */ jsx9("div", { className: "cm-legend__bar", role: "list", children: barItems.map(({ item, index }) => /* @__PURE__ */ jsx9(
|
|
2272
|
+
"span",
|
|
2273
|
+
{
|
|
2274
|
+
role: "listitem",
|
|
2275
|
+
className: cx("cm-legend__segment", classNames.legendItem),
|
|
2276
|
+
style: { backgroundColor: item.color },
|
|
2277
|
+
title: `${item.label} (${item.count})`,
|
|
2278
|
+
...hoverProps(item, index)
|
|
2279
|
+
},
|
|
2280
|
+
item.label
|
|
2281
|
+
)) }),
|
|
2282
|
+
/* @__PURE__ */ jsx9("div", { className: "cm-legend__ticks", "aria-hidden": "true", children: barTicks.map((value, i) => /* @__PURE__ */ jsx9(
|
|
2283
|
+
"span",
|
|
2284
|
+
{
|
|
2285
|
+
className: "cm-legend__tick",
|
|
2286
|
+
style: { left: `${i / (barTicks.length - 1) * 100}%` },
|
|
2287
|
+
children: formatValue(value)
|
|
2288
|
+
},
|
|
2289
|
+
i
|
|
2290
|
+
)) }),
|
|
2291
|
+
emptyItem !== null && /* @__PURE__ */ jsxs3(
|
|
2292
|
+
"div",
|
|
2293
|
+
{
|
|
2294
|
+
className: cx("cm-legend__item", classNames.legendItem),
|
|
2295
|
+
...hoverProps(emptyItem.item, emptyItem.index),
|
|
2296
|
+
children: [
|
|
2297
|
+
/* @__PURE__ */ jsx9("span", { "aria-hidden": true, className: "cm-legend__swatch", ...swatchProps(emptyItem.item) }),
|
|
2298
|
+
/* @__PURE__ */ jsx9("span", { children: emptyItem.item.label }),
|
|
2299
|
+
showCount && /* @__PURE__ */ jsxs3("span", { className: "cm-legend__count", children: [
|
|
2300
|
+
"(",
|
|
2301
|
+
emptyItem.item.count,
|
|
2302
|
+
")"
|
|
2303
|
+
] })
|
|
2304
|
+
]
|
|
2305
|
+
}
|
|
2306
|
+
)
|
|
2307
|
+
] }) : items.length > 0 && /* @__PURE__ */ jsx9("ul", { className: "cm-legend__items", children: items.map((item, index) => /* @__PURE__ */ jsxs3(
|
|
2308
|
+
"li",
|
|
2309
|
+
{
|
|
2310
|
+
className: cx("cm-legend__item", classNames.legendItem),
|
|
2311
|
+
...hoverProps(item, index),
|
|
2312
|
+
children: [
|
|
2313
|
+
/* @__PURE__ */ jsx9("span", { "aria-hidden": true, className: "cm-legend__swatch", ...swatchProps(item) }),
|
|
2314
|
+
/* @__PURE__ */ jsx9("span", { children: item.label }),
|
|
2315
|
+
showCount && /* @__PURE__ */ jsxs3("span", { className: "cm-legend__count", children: [
|
|
2316
|
+
"(",
|
|
2317
|
+
item.count,
|
|
2318
|
+
")"
|
|
2319
|
+
] })
|
|
2320
|
+
]
|
|
2321
|
+
},
|
|
2322
|
+
item.label
|
|
2323
|
+
)) }),
|
|
2324
|
+
sizeItems.length > 0 && /* @__PURE__ */ jsx9("ul", { className: "cm-legend__sizes", children: sizeItems.map((item) => /* @__PURE__ */ jsxs3("li", { className: "cm-legend__size", children: [
|
|
2325
|
+
/* @__PURE__ */ jsx9(
|
|
2326
|
+
"svg",
|
|
2327
|
+
{
|
|
2328
|
+
"aria-hidden": true,
|
|
2329
|
+
width: item.radius * 2 + 2,
|
|
2330
|
+
height: item.radius * 2 + 2,
|
|
2331
|
+
className: "cm-legend__circle",
|
|
2332
|
+
children: /* @__PURE__ */ jsx9("circle", { cx: item.radius + 1, cy: item.radius + 1, r: item.radius })
|
|
2333
|
+
}
|
|
2334
|
+
),
|
|
2335
|
+
/* @__PURE__ */ jsx9("span", { children: item.label })
|
|
2336
|
+
] }, item.label)) })
|
|
2337
|
+
] });
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
// src/react/components/regions.tsx
|
|
2341
|
+
import { memo } from "react";
|
|
2342
|
+
import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2343
|
+
var RegionPath = memo(
|
|
2344
|
+
({
|
|
2345
|
+
id,
|
|
2346
|
+
d,
|
|
2347
|
+
fill,
|
|
2348
|
+
drillable,
|
|
2349
|
+
empty,
|
|
2350
|
+
dataClass,
|
|
2351
|
+
className,
|
|
2352
|
+
opacity,
|
|
2353
|
+
target,
|
|
2354
|
+
wave
|
|
2355
|
+
}) => /* @__PURE__ */ jsx10(
|
|
2356
|
+
"path",
|
|
2357
|
+
{
|
|
2358
|
+
className: cx("cm-region", className),
|
|
2359
|
+
"data-region-id": id,
|
|
2360
|
+
"data-empty": empty ? "" : void 0,
|
|
2361
|
+
"data-class": dataClass,
|
|
2362
|
+
"data-drillable": drillable ? "" : void 0,
|
|
2363
|
+
"data-drill-target": target ? "" : void 0,
|
|
2364
|
+
d,
|
|
2365
|
+
style: {
|
|
2366
|
+
fill,
|
|
2367
|
+
...opacity === void 0 ? void 0 : { opacity },
|
|
2368
|
+
...wave === void 0 ? void 0 : { "--cm-wave": wave }
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
)
|
|
2372
|
+
);
|
|
2373
|
+
var AnonymousPath = memo(({ d, fill }) => /* @__PURE__ */ jsx10("path", { className: "cm-region cm-region--anonymous", d, style: { fill, cursor: "default" } }));
|
|
2374
|
+
var regionIdFrom = (event) => event.target.closest?.("[data-region-id]")?.getAttribute("data-region-id") ?? null;
|
|
2375
|
+
var Regions = () => {
|
|
2376
|
+
const { choropleth, store, classNames } = useChoroplethContext();
|
|
2377
|
+
const {
|
|
2378
|
+
regions,
|
|
2379
|
+
anonymousPaths,
|
|
2380
|
+
emptyFill,
|
|
2381
|
+
borders,
|
|
2382
|
+
clickable,
|
|
2383
|
+
hoverItem,
|
|
2384
|
+
clickItem,
|
|
2385
|
+
drillFocusId
|
|
2386
|
+
} = choropleth;
|
|
2387
|
+
const known = new Set(regions.map((region) => region.id));
|
|
2388
|
+
const regionRef = (event) => {
|
|
2389
|
+
const id = regionIdFrom(event);
|
|
2390
|
+
return id !== null && known.has(id) ? { kind: "region", id } : null;
|
|
2391
|
+
};
|
|
2392
|
+
const outlined = borders === null;
|
|
2393
|
+
return /* @__PURE__ */ jsxs4(
|
|
2394
|
+
"g",
|
|
2395
|
+
{
|
|
2396
|
+
className: cx("cm-regions", classNames.regions),
|
|
2397
|
+
"data-outlined": outlined ? "" : void 0,
|
|
2398
|
+
style: clickable ? { cursor: "pointer" } : void 0,
|
|
2399
|
+
onClick: (event) => {
|
|
2400
|
+
const ref = regionRef(event);
|
|
2401
|
+
if (ref !== null) clickItem(ref);
|
|
2402
|
+
},
|
|
2403
|
+
onPointerOver: (event) => {
|
|
2404
|
+
const ref = regionRef(event);
|
|
2405
|
+
if (ref !== null) {
|
|
2406
|
+
hoverItem(ref);
|
|
2407
|
+
} else if (store.getState().hovered?.kind === "region") {
|
|
2408
|
+
hoverItem(null);
|
|
2409
|
+
}
|
|
2410
|
+
},
|
|
2411
|
+
onPointerLeave: (event) => {
|
|
2412
|
+
if (event.pointerType === "touch") return;
|
|
2413
|
+
if (store.getState().hovered?.kind === "region") hoverItem(null);
|
|
2414
|
+
},
|
|
2415
|
+
children: [
|
|
2416
|
+
anonymousPaths.map((d, index) => /* @__PURE__ */ jsx10(AnonymousPath, { d, fill: emptyFill }, `anonymous-${index}`)),
|
|
2417
|
+
regions.map((region) => /* @__PURE__ */ jsx10(
|
|
2418
|
+
RegionPath,
|
|
2419
|
+
{
|
|
2420
|
+
id: region.id,
|
|
2421
|
+
d: region.path,
|
|
2422
|
+
fill: region.fill,
|
|
2423
|
+
drillable: region.drillable,
|
|
2424
|
+
empty: region.value === null,
|
|
2425
|
+
dataClass: region.classIndex === null ? "empty" : String(region.classIndex),
|
|
2426
|
+
className: cx(classNames.region, region.className),
|
|
2427
|
+
opacity: region.opacity,
|
|
2428
|
+
target: region.id === drillFocusId,
|
|
2429
|
+
wave: region.wave
|
|
2430
|
+
},
|
|
2431
|
+
region.id
|
|
2432
|
+
))
|
|
2433
|
+
]
|
|
2434
|
+
}
|
|
2435
|
+
);
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
// src/react/components/root.tsx
|
|
2439
|
+
import {
|
|
2440
|
+
forwardRef,
|
|
2441
|
+
useCallback as useCallback3,
|
|
2442
|
+
useEffect as useEffect9,
|
|
2443
|
+
useId,
|
|
2444
|
+
useImperativeHandle,
|
|
2445
|
+
useMemo as useMemo5,
|
|
2446
|
+
useRef as useRef7
|
|
2447
|
+
} from "react";
|
|
2448
|
+
|
|
2449
|
+
// src/react/handle.ts
|
|
2450
|
+
var EMPTY_VIEW = {
|
|
2451
|
+
k: 1,
|
|
2452
|
+
x: 0,
|
|
2453
|
+
y: 0,
|
|
2454
|
+
zoom: 1,
|
|
2455
|
+
center: [0, 0],
|
|
2456
|
+
bounds: [
|
|
2457
|
+
[-180, -90],
|
|
2458
|
+
[180, 90]
|
|
2459
|
+
],
|
|
2460
|
+
level: { id: "root", depth: 0 }
|
|
2461
|
+
};
|
|
2462
|
+
var NOT_AVAILABLE = Promise.resolve({ completed: false });
|
|
2463
|
+
var noDrill = (method) => {
|
|
2464
|
+
if (!isProduction()) {
|
|
2465
|
+
console.warn(`[choropleth] ${method}() needs the \`drill\` prop; this map has none.`);
|
|
2466
|
+
}
|
|
2467
|
+
return NOT_AVAILABLE;
|
|
2468
|
+
};
|
|
2469
|
+
var createHandle = (store) => ({
|
|
2470
|
+
zoomTo: (target, options) => store.getZoom()?.zoomTo(target, options) ?? NOT_AVAILABLE,
|
|
2471
|
+
zoomBy: (factor, options) => store.getZoom()?.zoomBy(factor, options) ?? NOT_AVAILABLE,
|
|
2472
|
+
resetView: (options) => store.getZoom()?.resetView(options) ?? NOT_AVAILABLE,
|
|
2473
|
+
getView: () => store.getZoom()?.getView() ?? store.getFallbackView() ?? EMPTY_VIEW,
|
|
2474
|
+
drillDown: (id) => store.getDrill()?.drillDown(id) ?? noDrill("drillDown"),
|
|
2475
|
+
drillUp: (steps) => store.getDrill()?.drillUp(steps) ?? noDrill("drillUp"),
|
|
2476
|
+
drillTo: (path) => store.getDrill()?.drillTo(path) ?? noDrill("drillTo")
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
// src/react/components/root.tsx
|
|
2480
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
2481
|
+
var RootInner = ({
|
|
2482
|
+
children,
|
|
2483
|
+
className,
|
|
2484
|
+
style,
|
|
2485
|
+
unstyled,
|
|
2486
|
+
theme,
|
|
2487
|
+
emptyPattern,
|
|
2488
|
+
classNames,
|
|
2489
|
+
dimOthers = "selection",
|
|
2490
|
+
...options
|
|
2491
|
+
}, ref) => {
|
|
2492
|
+
const choropleth = useChoropleth({
|
|
2493
|
+
...options,
|
|
2494
|
+
dimOthers
|
|
2495
|
+
});
|
|
2496
|
+
const { containerRef, store, status, dataLoading, selectedId, drillPhase } = choropleth;
|
|
2497
|
+
const viewportRef = useRef7(null);
|
|
2498
|
+
useEffect9(() => {
|
|
2499
|
+
const sync = () => {
|
|
2500
|
+
containerRef.current?.toggleAttribute("data-interacting", store.getState().interacting);
|
|
2501
|
+
};
|
|
2502
|
+
sync();
|
|
2503
|
+
return store.subscribe(sync);
|
|
2504
|
+
}, [store, containerRef]);
|
|
2505
|
+
const { width, height, invert } = choropleth;
|
|
2506
|
+
useEffect9(() => {
|
|
2507
|
+
store.registerFallbackView(
|
|
2508
|
+
() => toViewState({ transform: FIT_TRANSFORM, size: { width, height }, invert, level: ROOT_LEVEL })
|
|
2509
|
+
);
|
|
2510
|
+
return () => store.registerFallbackView(null);
|
|
2511
|
+
}, [store, width, height, invert]);
|
|
2512
|
+
const api = useMemo5(() => createHandle(store), [store]);
|
|
2513
|
+
useImperativeHandle(ref, () => api, [api]);
|
|
2514
|
+
const themeVars = useMemo5(() => themeToCssVars(resolveTheme(theme)), [theme]);
|
|
2515
|
+
const slots = useMemo5(() => classNames ?? {}, [classNames]);
|
|
2516
|
+
const instance = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
2517
|
+
const defId = useCallback3((name) => `cm${instance}-${name}`, [instance]);
|
|
2518
|
+
const pattern = emptyPattern ?? resolveTheme(theme)?.empty?.pattern;
|
|
2519
|
+
const resolvedPattern = pattern === "hatch" || pattern === "dots" ? pattern : null;
|
|
2520
|
+
const handlePointer = (event) => {
|
|
2521
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2522
|
+
store.setState({
|
|
2523
|
+
pointer: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
|
2524
|
+
// The drill asks for one or two taps depending on what is touching (a pen's wheel counts as a mouse).
|
|
2525
|
+
pointerType: event.pointerType === "touch" ? "touch" : event.pointerType === "pen" ? "pen" : "mouse"
|
|
2526
|
+
});
|
|
2527
|
+
};
|
|
2528
|
+
return /* @__PURE__ */ jsx11(
|
|
2529
|
+
ChoroplethContext.Provider,
|
|
2530
|
+
{
|
|
2531
|
+
value: {
|
|
2532
|
+
choropleth,
|
|
2533
|
+
store,
|
|
2534
|
+
viewportRef,
|
|
2535
|
+
api,
|
|
2536
|
+
classNames: slots,
|
|
2537
|
+
defId,
|
|
2538
|
+
emptyPattern: resolvedPattern
|
|
2539
|
+
},
|
|
2540
|
+
children: /* @__PURE__ */ jsx11(
|
|
2541
|
+
"div",
|
|
2542
|
+
{
|
|
2543
|
+
ref: containerRef,
|
|
2544
|
+
className: cx("cm-root", slots.root, className),
|
|
2545
|
+
"data-unstyled": unstyled ? "" : void 0,
|
|
2546
|
+
"data-theme": theme === void 0 ? void 0 : isThemeName(theme) ? theme : "custom",
|
|
2547
|
+
"data-status": status,
|
|
2548
|
+
"data-loading": dataLoading ? "" : void 0,
|
|
2549
|
+
"data-dim": dimOthers,
|
|
2550
|
+
"data-drill-phase": drillPhase === null || drillPhase === "idle" ? void 0 : drillPhase,
|
|
2551
|
+
"data-has-selection": selectedId === null ? void 0 : "",
|
|
2552
|
+
style: { position: "relative", ...themeVars, ...style },
|
|
2553
|
+
onPointerMove: handlePointer,
|
|
2554
|
+
onPointerDown: handlePointer,
|
|
2555
|
+
onPointerLeave: (event) => {
|
|
2556
|
+
if (event.pointerType !== "touch") store.setState({ pointer: null });
|
|
2557
|
+
},
|
|
2558
|
+
children
|
|
2559
|
+
}
|
|
2560
|
+
)
|
|
2561
|
+
}
|
|
2562
|
+
);
|
|
2563
|
+
};
|
|
2564
|
+
var Root = forwardRef(RootInner);
|
|
2565
|
+
|
|
2566
|
+
// src/react/components/selection.tsx
|
|
2567
|
+
import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2568
|
+
var Selection = () => {
|
|
2569
|
+
const { choropleth, store, classNames } = useChoroplethContext();
|
|
2570
|
+
const { regions, bubbles, fallbackPoints, selectedId } = choropleth;
|
|
2571
|
+
const hovered = useMapStore(store, (s) => s.hovered);
|
|
2572
|
+
const hoveredRegion = hovered?.kind === "region" ? regions.find((r) => r.id === hovered.id) : void 0;
|
|
2573
|
+
const hoveredPoint = hovered?.kind === "point" ? bubbles.find((b) => b.coordinates !== null && b.id === hovered.id) ?? fallbackPoints.find((p) => p.id === hovered.id) : void 0;
|
|
2574
|
+
const selectedRegion = selectedId === null ? void 0 : regions.find((r) => r.id === selectedId);
|
|
2575
|
+
return /* @__PURE__ */ jsxs5("g", { className: cx("cm-selection", classNames.selection), pointerEvents: "none", children: [
|
|
2576
|
+
hoveredRegion !== void 0 && /* @__PURE__ */ jsx12("path", { "data-selection": "hover", d: hoveredRegion.path, style: { fill: hoveredRegion.fill } }),
|
|
2577
|
+
hoveredPoint !== void 0 && /* @__PURE__ */ jsx12("circle", { "data-selection": "hover", cx: hoveredPoint.x, cy: hoveredPoint.y, r: hoveredPoint.r }),
|
|
2578
|
+
selectedRegion !== void 0 && /* @__PURE__ */ jsx12(
|
|
2579
|
+
"path",
|
|
2580
|
+
{
|
|
2581
|
+
"data-selection": "selected",
|
|
2582
|
+
d: selectedRegion.path,
|
|
2583
|
+
style: { fill: selectedRegion.fill }
|
|
2584
|
+
}
|
|
2585
|
+
)
|
|
2586
|
+
] });
|
|
2587
|
+
};
|
|
2588
|
+
|
|
2589
|
+
// src/react/components/svg.tsx
|
|
2590
|
+
import { useRef as useRef8 } from "react";
|
|
2591
|
+
|
|
2592
|
+
// src/react/components/viewport.tsx
|
|
2593
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2594
|
+
var Viewport = ({ children }) => {
|
|
2595
|
+
const { viewportRef } = useChoroplethContext();
|
|
2596
|
+
return /* @__PURE__ */ jsx13("g", { ref: viewportRef, className: "cm-viewport", children });
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// src/react/components/svg.tsx
|
|
2600
|
+
import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2601
|
+
var Svg = ({ children, className, ariaLabel }) => {
|
|
2602
|
+
const { choropleth, store, viewportRef, classNames } = useChoroplethContext();
|
|
2603
|
+
const {
|
|
2604
|
+
width,
|
|
2605
|
+
height,
|
|
2606
|
+
status,
|
|
2607
|
+
labels,
|
|
2608
|
+
zoom: zoom2,
|
|
2609
|
+
invert,
|
|
2610
|
+
project,
|
|
2611
|
+
onViewChange,
|
|
2612
|
+
resolveCamera,
|
|
2613
|
+
focusId,
|
|
2614
|
+
containerRef
|
|
2615
|
+
} = choropleth;
|
|
2616
|
+
const svgRef = useRef8(null);
|
|
2617
|
+
useZoom({
|
|
2618
|
+
svgRef,
|
|
2619
|
+
viewportRef,
|
|
2620
|
+
rootRef: containerRef,
|
|
2621
|
+
store,
|
|
2622
|
+
width,
|
|
2623
|
+
height,
|
|
2624
|
+
options: zoom2,
|
|
2625
|
+
invert,
|
|
2626
|
+
project,
|
|
2627
|
+
onViewChange,
|
|
2628
|
+
resolveCamera,
|
|
2629
|
+
focusId,
|
|
2630
|
+
ready: status === "ready"
|
|
2631
|
+
});
|
|
2632
|
+
if (status === "loading") {
|
|
2633
|
+
return /* @__PURE__ */ jsx14("div", { className: "cm-skeleton", style: { aspectRatio: `${width} / ${height}` } });
|
|
2634
|
+
}
|
|
2635
|
+
if (status === "error") {
|
|
2636
|
+
return /* @__PURE__ */ jsx14("div", { className: "cm-error", role: "alert", style: { aspectRatio: `${width} / ${height}` }, children: labels.geographyError });
|
|
2637
|
+
}
|
|
2638
|
+
const { drillNav } = choropleth;
|
|
2639
|
+
const keyboard = zoom2 !== null && zoom2.keyboard || drillNav !== null;
|
|
2640
|
+
const handleKeyDown = (event) => {
|
|
2641
|
+
if (drillNav?.canDrillUp === true && (event.key === "Escape" || event.key === "Backspace") && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
2642
|
+
event.preventDefault();
|
|
2643
|
+
drillNav.drillUp();
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (zoom2 === null || !zoom2.keyboard) return;
|
|
2647
|
+
const action = zoomKeyAction(event);
|
|
2648
|
+
if (action === null) return;
|
|
2649
|
+
event.preventDefault();
|
|
2650
|
+
const api = store.getZoom();
|
|
2651
|
+
if (action === "reset") void api?.resetView();
|
|
2652
|
+
else void api?.zoomBy(action === "in" ? 2 : 0.5);
|
|
2653
|
+
};
|
|
2654
|
+
return /* @__PURE__ */ jsx14(
|
|
2655
|
+
"svg",
|
|
2656
|
+
{
|
|
2657
|
+
ref: svgRef,
|
|
2658
|
+
tabIndex: keyboard ? 0 : void 0,
|
|
2659
|
+
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
2660
|
+
className: cx("cm-svg", classNames.svg, className),
|
|
2661
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
2662
|
+
role: "img",
|
|
2663
|
+
"aria-label": ariaLabel,
|
|
2664
|
+
style: { display: "block", width: "100%", height: "auto" },
|
|
2665
|
+
children: children ?? /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
2666
|
+
/* @__PURE__ */ jsx14(Defs, {}),
|
|
2667
|
+
/* @__PURE__ */ jsxs6(Viewport, { children: [
|
|
2668
|
+
/* @__PURE__ */ jsx14(DrillLayers, { role: "context" }),
|
|
2669
|
+
/* @__PURE__ */ jsx14(Regions, {}),
|
|
2670
|
+
/* @__PURE__ */ jsx14(Patterns, {}),
|
|
2671
|
+
/* @__PURE__ */ jsx14(Borders, {}),
|
|
2672
|
+
/* @__PURE__ */ jsx14(Bubbles, {}),
|
|
2673
|
+
/* @__PURE__ */ jsx14(FallbackPoints, {}),
|
|
2674
|
+
/* @__PURE__ */ jsx14(Selection, {}),
|
|
2675
|
+
/* @__PURE__ */ jsx14(DrillLayers, { role: "exiting" })
|
|
2676
|
+
] })
|
|
2677
|
+
] })
|
|
2678
|
+
}
|
|
2679
|
+
);
|
|
2680
|
+
};
|
|
2681
|
+
|
|
2682
|
+
// src/react/components/tooltip.tsx
|
|
2683
|
+
import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2684
|
+
var OFFSET = 12;
|
|
2685
|
+
var DefaultContent = ({
|
|
2686
|
+
label,
|
|
2687
|
+
formattedValue,
|
|
2688
|
+
drillable,
|
|
2689
|
+
noData,
|
|
2690
|
+
drillHint
|
|
2691
|
+
}) => {
|
|
2692
|
+
return /* @__PURE__ */ jsxs7(Fragment5, { children: [
|
|
2693
|
+
/* @__PURE__ */ jsx15("strong", { children: label }),
|
|
2694
|
+
/* @__PURE__ */ jsx15("div", { children: formattedValue === "" ? noData : formattedValue }),
|
|
2695
|
+
drillable && /* @__PURE__ */ jsx15("div", { className: "cm-tooltip__hint", children: drillHint })
|
|
2696
|
+
] });
|
|
2697
|
+
};
|
|
2698
|
+
var Tooltip = ({ className, children }) => {
|
|
2699
|
+
const { choropleth, store, classNames } = useChoroplethContext();
|
|
2700
|
+
const point = useMapStore(store, (s) => s.pointer);
|
|
2701
|
+
const hovered = useMapStore(store, (s) => s.hovered);
|
|
2702
|
+
const touch = useMapStore(store, (s) => s.pointerType === "touch");
|
|
2703
|
+
const context = hovered === null ? void 0 : choropleth.getItemContext(hovered);
|
|
2704
|
+
if (context === void 0 || point === null) return null;
|
|
2705
|
+
const flipX = point.x > choropleth.width / 2;
|
|
2706
|
+
const flipY = point.y > choropleth.height / 2;
|
|
2707
|
+
return /* @__PURE__ */ jsx15(
|
|
2708
|
+
"div",
|
|
2709
|
+
{
|
|
2710
|
+
role: "tooltip",
|
|
2711
|
+
className: cx(
|
|
2712
|
+
"cm-tooltip",
|
|
2713
|
+
// Only the default content has a box; if you pass `children`, the look is yours.
|
|
2714
|
+
children === void 0 && "cm-tooltip--default",
|
|
2715
|
+
classNames.tooltip,
|
|
2716
|
+
className
|
|
2717
|
+
),
|
|
2718
|
+
style: {
|
|
2719
|
+
position: "absolute",
|
|
2720
|
+
left: point.x + (flipX ? -OFFSET : OFFSET),
|
|
2721
|
+
top: point.y + (flipY ? -OFFSET : OFFSET),
|
|
2722
|
+
transform: `translate(${flipX ? "-100%" : "0"}, ${flipY ? "-100%" : "0"})`,
|
|
2723
|
+
pointerEvents: "none"
|
|
2724
|
+
},
|
|
2725
|
+
children: children === void 0 ? /* @__PURE__ */ jsx15(
|
|
2726
|
+
DefaultContent,
|
|
2727
|
+
{
|
|
2728
|
+
...context,
|
|
2729
|
+
noData: choropleth.labels.noData,
|
|
2730
|
+
drillHint: touch ? choropleth.labels.tapAgainHint : choropleth.labels.drillHint
|
|
2731
|
+
}
|
|
2732
|
+
) : children(context)
|
|
2733
|
+
}
|
|
2734
|
+
);
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
// src/react/components/zoom-controls.tsx
|
|
2738
|
+
import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2739
|
+
var CORNERS2 = {
|
|
2740
|
+
"top-left": { top: 12, left: 12 },
|
|
2741
|
+
"top-right": { top: 12, right: 12 },
|
|
2742
|
+
"bottom-left": { bottom: 12, left: 12 },
|
|
2743
|
+
"bottom-right": { bottom: 12, right: 12 }
|
|
2744
|
+
};
|
|
2745
|
+
var STEP = 2;
|
|
2746
|
+
var ZoomControls = ({
|
|
2747
|
+
position = "bottom-right",
|
|
2748
|
+
reset = true,
|
|
2749
|
+
className
|
|
2750
|
+
}) => {
|
|
2751
|
+
const { choropleth, store, classNames } = useChoroplethContext();
|
|
2752
|
+
const { zoom: zoom2, labels } = choropleth;
|
|
2753
|
+
const camera = useMapStore(store, (s) => s.camera);
|
|
2754
|
+
if (zoom2 === null) return null;
|
|
2755
|
+
const atMax = camera.k >= zoom2.max - 1e-6;
|
|
2756
|
+
const atMin = camera.k <= zoom2.min + 1e-6;
|
|
2757
|
+
return /* @__PURE__ */ jsxs8(
|
|
2758
|
+
"div",
|
|
2759
|
+
{
|
|
2760
|
+
className: cx("cm-zoom-controls", classNames.controls, className),
|
|
2761
|
+
style: { position: "absolute", ...CORNERS2[position] },
|
|
2762
|
+
children: [
|
|
2763
|
+
/* @__PURE__ */ jsx16(
|
|
2764
|
+
"button",
|
|
2765
|
+
{
|
|
2766
|
+
type: "button",
|
|
2767
|
+
className: cx("cm-zoom-button", classNames.controlButton),
|
|
2768
|
+
"aria-label": labels.zoomIn,
|
|
2769
|
+
disabled: atMax,
|
|
2770
|
+
onClick: () => void store.getZoom()?.zoomBy(STEP),
|
|
2771
|
+
children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "+" })
|
|
2772
|
+
}
|
|
2773
|
+
),
|
|
2774
|
+
/* @__PURE__ */ jsx16(
|
|
2775
|
+
"button",
|
|
2776
|
+
{
|
|
2777
|
+
type: "button",
|
|
2778
|
+
className: cx("cm-zoom-button", classNames.controlButton),
|
|
2779
|
+
"aria-label": labels.zoomOut,
|
|
2780
|
+
disabled: atMin,
|
|
2781
|
+
onClick: () => void store.getZoom()?.zoomBy(1 / STEP),
|
|
2782
|
+
children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u2212" })
|
|
2783
|
+
}
|
|
2784
|
+
),
|
|
2785
|
+
reset && /* @__PURE__ */ jsx16(
|
|
2786
|
+
"button",
|
|
2787
|
+
{
|
|
2788
|
+
type: "button",
|
|
2789
|
+
className: cx("cm-zoom-button", classNames.controlButton),
|
|
2790
|
+
"aria-label": labels.resetView,
|
|
2791
|
+
disabled: isFitTransform(camera),
|
|
2792
|
+
onClick: () => void store.getZoom()?.resetView(),
|
|
2793
|
+
children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u27F2" })
|
|
2794
|
+
}
|
|
2795
|
+
)
|
|
2796
|
+
]
|
|
2797
|
+
}
|
|
2798
|
+
);
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
// src/react/choropleth.ts
|
|
2802
|
+
var Choropleth = {
|
|
2803
|
+
Root,
|
|
2804
|
+
Svg,
|
|
2805
|
+
Regions,
|
|
2806
|
+
Borders,
|
|
2807
|
+
Defs,
|
|
2808
|
+
Patterns,
|
|
2809
|
+
Breadcrumb,
|
|
2810
|
+
Bubbles,
|
|
2811
|
+
DrillLayers,
|
|
2812
|
+
FallbackPoints,
|
|
2813
|
+
Selection,
|
|
2814
|
+
Viewport,
|
|
2815
|
+
ZoomControls,
|
|
2816
|
+
GestureHint,
|
|
2817
|
+
Legend,
|
|
2818
|
+
Tooltip
|
|
2819
|
+
};
|
|
2820
|
+
|
|
2821
|
+
// src/react/choropleth-map.tsx
|
|
2822
|
+
import {
|
|
2823
|
+
forwardRef as forwardRef2
|
|
2824
|
+
} from "react";
|
|
2825
|
+
import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2826
|
+
var CORNERS3 = {
|
|
2827
|
+
"top-left": { top: 12, left: 12 },
|
|
2828
|
+
"top-right": { top: 12, right: 12 },
|
|
2829
|
+
"bottom-left": { bottom: 12, left: 12 },
|
|
2830
|
+
"bottom-right": { bottom: 12, right: 12 }
|
|
2831
|
+
};
|
|
2832
|
+
var ChoroplethMapInner = ({
|
|
2833
|
+
legend,
|
|
2834
|
+
breadcrumb,
|
|
2835
|
+
renderTooltip,
|
|
2836
|
+
className,
|
|
2837
|
+
style,
|
|
2838
|
+
unstyled,
|
|
2839
|
+
theme,
|
|
2840
|
+
emptyPattern,
|
|
2841
|
+
classNames,
|
|
2842
|
+
ariaLabel,
|
|
2843
|
+
...options
|
|
2844
|
+
}, ref) => {
|
|
2845
|
+
const legendOptions = legend === false ? null : legend ?? {};
|
|
2846
|
+
const controls = resolveControls(options.zoom);
|
|
2847
|
+
return /* @__PURE__ */ jsxs9(
|
|
2848
|
+
Root,
|
|
2849
|
+
{
|
|
2850
|
+
ref,
|
|
2851
|
+
...options,
|
|
2852
|
+
emptyLabel: options.emptyLabel ?? resolveLabels(options.labels).noData,
|
|
2853
|
+
className,
|
|
2854
|
+
style,
|
|
2855
|
+
unstyled,
|
|
2856
|
+
theme,
|
|
2857
|
+
emptyPattern,
|
|
2858
|
+
classNames,
|
|
2859
|
+
children: [
|
|
2860
|
+
/* @__PURE__ */ jsx17(Svg, { ariaLabel }),
|
|
2861
|
+
legendOptions !== null && /* @__PURE__ */ jsx17("div", { style: { position: "absolute", ...CORNERS3[legendOptions.position ?? "bottom-left"] }, children: /* @__PURE__ */ jsx17(
|
|
2862
|
+
Legend,
|
|
2863
|
+
{
|
|
2864
|
+
title: legendOptions.title,
|
|
2865
|
+
showCount: legendOptions.showCount,
|
|
2866
|
+
variant: legendOptions.variant,
|
|
2867
|
+
interactive: legendOptions.interactive
|
|
2868
|
+
}
|
|
2869
|
+
) }),
|
|
2870
|
+
options.drill !== void 0 && breadcrumb !== false && /* @__PURE__ */ jsx17(Breadcrumb, { position: breadcrumb?.position ?? "top-left" }),
|
|
2871
|
+
controls !== null && /* @__PURE__ */ jsx17(ZoomControls, { position: controls.position, reset: controls.reset }),
|
|
2872
|
+
/* @__PURE__ */ jsx17(GestureHint, {}),
|
|
2873
|
+
/* @__PURE__ */ jsx17(Tooltip, { children: renderTooltip })
|
|
2874
|
+
]
|
|
2875
|
+
}
|
|
2876
|
+
);
|
|
2877
|
+
};
|
|
2878
|
+
var ChoroplethMap = forwardRef2(ChoroplethMapInner);
|
|
2879
|
+
export {
|
|
2880
|
+
Borders,
|
|
2881
|
+
Breadcrumb,
|
|
2882
|
+
Bubbles,
|
|
2883
|
+
Choropleth,
|
|
2884
|
+
ChoroplethMap,
|
|
2885
|
+
DRILL_BRIDGE,
|
|
2886
|
+
Defs,
|
|
2887
|
+
DrillLayers,
|
|
2888
|
+
FALLBACK_POINT_RADIUS,
|
|
2889
|
+
FallbackPoints,
|
|
2890
|
+
GestureHint,
|
|
2891
|
+
Legend,
|
|
2892
|
+
Patterns,
|
|
2893
|
+
REVEAL_MS,
|
|
2894
|
+
ROOT_LEVEL,
|
|
2895
|
+
Regions,
|
|
2896
|
+
Root,
|
|
2897
|
+
Selection,
|
|
2898
|
+
Svg,
|
|
2899
|
+
THEMES,
|
|
2900
|
+
Tooltip,
|
|
2901
|
+
Viewport,
|
|
2902
|
+
ZoomControls,
|
|
2903
|
+
buildDrillEvent,
|
|
2904
|
+
clearGeographyCache,
|
|
2905
|
+
createDrillBridge,
|
|
2906
|
+
createMapStore,
|
|
2907
|
+
defineDrillLevels,
|
|
2908
|
+
defineTheme,
|
|
2909
|
+
extendTheme,
|
|
2910
|
+
isDrillController,
|
|
2911
|
+
isDrillable,
|
|
2912
|
+
sourceDepth,
|
|
2913
|
+
themeToCssVars,
|
|
2914
|
+
useCamera,
|
|
2915
|
+
useChoropleth,
|
|
2916
|
+
useChoroplethApi,
|
|
2917
|
+
useContainerSize,
|
|
2918
|
+
useDefId,
|
|
2919
|
+
useDrillChoreography,
|
|
2920
|
+
useDrillDown,
|
|
2921
|
+
useDrillEngine,
|
|
2922
|
+
useGeography,
|
|
2923
|
+
useMapStore,
|
|
2924
|
+
useZoom
|
|
2925
|
+
};
|
|
2926
|
+
//# sourceMappingURL=index.js.map
|