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/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