bharat-choropleth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +833 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +120 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
// src/IndiaChoropleth.tsx
|
|
2
|
+
import { geoMercator, geoPath } from "d3-geo";
|
|
3
|
+
import { useCallback as useCallback2, useEffect, useId, useLayoutEffect, useMemo, useRef, useState as useState2 } from "react";
|
|
4
|
+
|
|
5
|
+
// src/geometry.ts
|
|
6
|
+
import { feature as topoFeature } from "topojson-client";
|
|
7
|
+
function asFeatureCollection(source) {
|
|
8
|
+
if ("type" in source && source.type === "FeatureCollection") {
|
|
9
|
+
return source;
|
|
10
|
+
}
|
|
11
|
+
const topoSource = source;
|
|
12
|
+
const object = typeof topoSource.object === "string" ? topoSource.topology.objects[topoSource.object] : topoSource.object;
|
|
13
|
+
if (!object) {
|
|
14
|
+
throw new Error("The named TopoJSON object does not exist in this topology.");
|
|
15
|
+
}
|
|
16
|
+
const unpacked = topoFeature(topoSource.topology, object);
|
|
17
|
+
return unpacked.type === "FeatureCollection" ? unpacked : { type: "FeatureCollection", features: [unpacked] };
|
|
18
|
+
}
|
|
19
|
+
function totalOf(values) {
|
|
20
|
+
return values.reduce((total, value) => total + (value ?? 0), 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/legend.ts
|
|
24
|
+
function swatchIndexOf(value, min, max, count) {
|
|
25
|
+
if (value === null || count <= 0) return null;
|
|
26
|
+
if (count === 1 || max === min) return count - 1;
|
|
27
|
+
const index = Math.round((value - min) / (max - min) * (count - 1));
|
|
28
|
+
return Math.min(Math.max(index, 0), count - 1);
|
|
29
|
+
}
|
|
30
|
+
function legendBuckets(colors, values, min, max) {
|
|
31
|
+
const count = colors.length;
|
|
32
|
+
return colors.map((color, index) => {
|
|
33
|
+
const members = values.filter((value) => swatchIndexOf(value, min, max, count) === index);
|
|
34
|
+
return {
|
|
35
|
+
index,
|
|
36
|
+
color,
|
|
37
|
+
from: members.length ? Math.min(...members) : null,
|
|
38
|
+
to: members.length ? Math.max(...members) : null,
|
|
39
|
+
matches: members.length
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function legendBucketLabel(bucket, formatValue) {
|
|
44
|
+
if (bucket.matches === 0 || bucket.from === null || bucket.to === null) return "No regions in this band";
|
|
45
|
+
const range = bucket.from === bucket.to ? formatValue(bucket.from) : `${formatValue(bucket.from)} to ${formatValue(bucket.to)}`;
|
|
46
|
+
return `Highlight ${bucket.matches} ${bucket.matches === 1 ? "region" : "regions"}, ${range}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/tooltip-position.ts
|
|
50
|
+
function placeTooltip(tooltip, bounds, gap, padding = 4) {
|
|
51
|
+
let dx = 0;
|
|
52
|
+
if (tooltip.right > bounds.right - padding) dx = bounds.right - padding - tooltip.right;
|
|
53
|
+
if (tooltip.left + dx < bounds.left + padding) dx = bounds.left + padding - tooltip.left;
|
|
54
|
+
const side = tooltip.top < bounds.top + padding ? "below" : "above";
|
|
55
|
+
return { dx, side };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/small-regions.ts
|
|
59
|
+
var TRUE_GEOMETRY_REGIONS = ["puducherry"];
|
|
60
|
+
function keepsTrueGeometry(id) {
|
|
61
|
+
const normalized = id.toLowerCase();
|
|
62
|
+
return TRUE_GEOMETRY_REGIONS.some((name) => normalized.includes(name));
|
|
63
|
+
}
|
|
64
|
+
function boundsOfRing(ring) {
|
|
65
|
+
let minX = Infinity;
|
|
66
|
+
let minY = Infinity;
|
|
67
|
+
let maxX = -Infinity;
|
|
68
|
+
let maxY = -Infinity;
|
|
69
|
+
for (const [x, y] of ring) {
|
|
70
|
+
if (x < minX) minX = x;
|
|
71
|
+
if (x > maxX) maxX = x;
|
|
72
|
+
if (y < minY) minY = y;
|
|
73
|
+
if (y > maxY) maxY = y;
|
|
74
|
+
}
|
|
75
|
+
return [minX, minY, maxX, maxY];
|
|
76
|
+
}
|
|
77
|
+
function largestRingExtent(rings) {
|
|
78
|
+
let largest = 0;
|
|
79
|
+
for (const ring of rings) {
|
|
80
|
+
if (ring.length === 0) continue;
|
|
81
|
+
const [minX, minY, maxX, maxY] = boundsOfRing(ring);
|
|
82
|
+
const extent = Math.max(maxX - minX, maxY - minY);
|
|
83
|
+
if (extent > largest) largest = extent;
|
|
84
|
+
}
|
|
85
|
+
return largest;
|
|
86
|
+
}
|
|
87
|
+
function signedArea(ring) {
|
|
88
|
+
let total = 0;
|
|
89
|
+
for (let i = 0; i < ring.length; i++) {
|
|
90
|
+
const [ax, ay] = ring[i];
|
|
91
|
+
const [bx, by] = ring[(i + 1) % ring.length];
|
|
92
|
+
total += ax * by - bx * ay;
|
|
93
|
+
}
|
|
94
|
+
return total / 2;
|
|
95
|
+
}
|
|
96
|
+
function labelPointFor(rings, fallback) {
|
|
97
|
+
let largest = null;
|
|
98
|
+
let largestArea = 0;
|
|
99
|
+
for (const ring of rings) {
|
|
100
|
+
if (ring.length < 3) continue;
|
|
101
|
+
const area2 = Math.abs(signedArea(ring));
|
|
102
|
+
if (area2 > largestArea) {
|
|
103
|
+
largestArea = area2;
|
|
104
|
+
largest = ring;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!largest || largestArea === 0) return fallback;
|
|
108
|
+
const area = signedArea(largest);
|
|
109
|
+
let cx = 0;
|
|
110
|
+
let cy = 0;
|
|
111
|
+
for (let i = 0; i < largest.length; i++) {
|
|
112
|
+
const [ax, ay] = largest[i];
|
|
113
|
+
const [bx, by] = largest[(i + 1) % largest.length];
|
|
114
|
+
const cross = ax * by - bx * ay;
|
|
115
|
+
cx += (ax + bx) * cross;
|
|
116
|
+
cy += (ay + by) * cross;
|
|
117
|
+
}
|
|
118
|
+
const centroid = [cx / (6 * area), cy / (6 * area)];
|
|
119
|
+
return Number.isFinite(centroid[0]) && Number.isFinite(centroid[1]) ? centroid : fallback;
|
|
120
|
+
}
|
|
121
|
+
function distanceToBox(point, box) {
|
|
122
|
+
const [x, y] = point;
|
|
123
|
+
const [minX, minY, maxX, maxY] = box;
|
|
124
|
+
const dx = Math.max(minX - x, 0) + Math.max(x - maxX, 0);
|
|
125
|
+
const dy = Math.max(minY - y, 0) + Math.max(y - maxY, 0);
|
|
126
|
+
return Math.hypot(dx, dy);
|
|
127
|
+
}
|
|
128
|
+
function distanceToParts(point, parts) {
|
|
129
|
+
let nearest = Infinity;
|
|
130
|
+
for (const part of parts) {
|
|
131
|
+
const distance = distanceToBox(point, part);
|
|
132
|
+
if (distance < nearest) nearest = distance;
|
|
133
|
+
}
|
|
134
|
+
return nearest;
|
|
135
|
+
}
|
|
136
|
+
var LABEL_SEARCH_TURNS = [
|
|
137
|
+
0,
|
|
138
|
+
Math.PI / 6,
|
|
139
|
+
-Math.PI / 6,
|
|
140
|
+
Math.PI / 3,
|
|
141
|
+
-Math.PI / 3,
|
|
142
|
+
Math.PI / 2,
|
|
143
|
+
-Math.PI / 2,
|
|
144
|
+
2 * Math.PI / 3,
|
|
145
|
+
-(2 * Math.PI) / 3,
|
|
146
|
+
5 * Math.PI / 6,
|
|
147
|
+
-(5 * Math.PI) / 6,
|
|
148
|
+
Math.PI
|
|
149
|
+
];
|
|
150
|
+
function placeOutsideLabel(options) {
|
|
151
|
+
const { anchor, clearance, halfSize, viewBox, centre, isBlocked } = options;
|
|
152
|
+
const dx = anchor[0] - centre[0];
|
|
153
|
+
const dy = anchor[1] - centre[1];
|
|
154
|
+
const base = dx === 0 && dy === 0 ? 0 : Math.atan2(dy, dx);
|
|
155
|
+
for (const turn of LABEL_SEARCH_TURNS) {
|
|
156
|
+
const angle = base + turn;
|
|
157
|
+
const candidate = [
|
|
158
|
+
clamp(anchor[0] + Math.cos(angle) * clearance, halfSize[0], viewBox[0] - halfSize[0]),
|
|
159
|
+
clamp(anchor[1] + Math.sin(angle) * clearance, halfSize[1], viewBox[1] - halfSize[1])
|
|
160
|
+
];
|
|
161
|
+
if (!isBlocked(candidate)) return candidate;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function clamp(value, lower, upper) {
|
|
166
|
+
return Math.min(Math.max(value, lower), upper);
|
|
167
|
+
}
|
|
168
|
+
function enlargeSmallParts(rings, minExtent, maxScale = 8) {
|
|
169
|
+
const unchanged = () => rings.map((ring) => [...ring]);
|
|
170
|
+
if (minExtent <= 0) return unchanged();
|
|
171
|
+
const largest = largestRingExtent(rings);
|
|
172
|
+
if (largest <= 0 || largest >= minExtent) return unchanged();
|
|
173
|
+
return rings.map((ring) => {
|
|
174
|
+
const [minX, minY, maxX, maxY] = boundsOfRing(ring);
|
|
175
|
+
const extent = Math.max(maxX - minX, maxY - minY);
|
|
176
|
+
const scale = extent <= 0 ? 1 : Math.min(minExtent / extent, maxScale);
|
|
177
|
+
const cx = (minX + maxX) / 2;
|
|
178
|
+
const cy = (minY + maxY) / 2;
|
|
179
|
+
return ring.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale]);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function convexHull(points) {
|
|
183
|
+
const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
184
|
+
const unique = [];
|
|
185
|
+
for (const point of sorted) {
|
|
186
|
+
const prior = unique.at(-1);
|
|
187
|
+
if (!prior || prior[0] !== point[0] || prior[1] !== point[1]) unique.push(point);
|
|
188
|
+
}
|
|
189
|
+
if (unique.length < 3) return unique.map((point) => [...point]);
|
|
190
|
+
const turn = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
191
|
+
const half = (ordered) => {
|
|
192
|
+
const chain = [];
|
|
193
|
+
for (const point of ordered) {
|
|
194
|
+
while (chain.length >= 2 && turn(chain[chain.length - 2], chain[chain.length - 1], point) <= 0) chain.pop();
|
|
195
|
+
chain.push(point);
|
|
196
|
+
}
|
|
197
|
+
return chain;
|
|
198
|
+
};
|
|
199
|
+
const lower = half(unique);
|
|
200
|
+
const upper = half([...unique].reverse());
|
|
201
|
+
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
|
202
|
+
return hull.length >= 3 ? hull : unique.map((point) => [...point]);
|
|
203
|
+
}
|
|
204
|
+
function scatteredHitArea(rings, maxExtent) {
|
|
205
|
+
if (rings.length < 2 || maxExtent <= 0) return null;
|
|
206
|
+
const extent = largestRingExtent(rings);
|
|
207
|
+
if (extent <= 0 || extent >= maxExtent) return null;
|
|
208
|
+
const hull = convexHull(rings.flat());
|
|
209
|
+
return hull.length >= 3 ? hull : null;
|
|
210
|
+
}
|
|
211
|
+
function ringsToPath(rings) {
|
|
212
|
+
let d = "";
|
|
213
|
+
for (const ring of rings) {
|
|
214
|
+
if (ring.length === 0) continue;
|
|
215
|
+
d += `M${ring[0][0]},${ring[0][1]}`;
|
|
216
|
+
for (let i = 1; i < ring.length; i++) d += `L${ring[i][0]},${ring[i][1]}`;
|
|
217
|
+
d += "Z";
|
|
218
|
+
}
|
|
219
|
+
return d;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/useControllableState.ts
|
|
223
|
+
import { useCallback, useState } from "react";
|
|
224
|
+
function useControllableState(controlled, initial) {
|
|
225
|
+
const [uncontrolled, setUncontrolled] = useState(initial);
|
|
226
|
+
const value = controlled === void 0 ? uncontrolled : controlled;
|
|
227
|
+
const setValue = useCallback((next) => {
|
|
228
|
+
if (controlled === void 0) setUncontrolled(next);
|
|
229
|
+
}, [controlled]);
|
|
230
|
+
return [value, setValue];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/IndiaChoropleth.tsx
|
|
234
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
235
|
+
var VIEWBOX = { width: 960, height: 640, padding: 28 };
|
|
236
|
+
var DEFAULT_COLORS = ["#d9f1ed", "#b9e3dd", "#8fd1c8", "#5bb9ae", "#2f9c90", "#147b71", "#075b55"];
|
|
237
|
+
var DEFAULT_FORMAT = new Intl.NumberFormat("en-IN").format;
|
|
238
|
+
var TOOLTIP_GAP_PX = 12;
|
|
239
|
+
var SMALL_REGION_EXTENT = 22;
|
|
240
|
+
var SMALL_REGION_CLICK_RADIUS = 14;
|
|
241
|
+
var MIN_REGION_MARKER_SIZE = 7;
|
|
242
|
+
function projectedRings(feature, projection) {
|
|
243
|
+
const geometry = feature.geometry;
|
|
244
|
+
if (!geometry) return [];
|
|
245
|
+
const polygons = geometry.type === "Polygon" ? [geometry.coordinates] : geometry.type === "MultiPolygon" ? geometry.coordinates : [];
|
|
246
|
+
const rings = [];
|
|
247
|
+
for (const polygon of polygons) {
|
|
248
|
+
for (const ring of polygon) {
|
|
249
|
+
const projected = [];
|
|
250
|
+
for (const position of ring) {
|
|
251
|
+
const point = projection(position);
|
|
252
|
+
if (point && Number.isFinite(point[0]) && Number.isFinite(point[1])) projected.push([point[0], point[1]]);
|
|
253
|
+
}
|
|
254
|
+
if (projected.length > 0) rings.push(projected);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return rings;
|
|
258
|
+
}
|
|
259
|
+
function colorFor(value, region, min, max, scale) {
|
|
260
|
+
if (typeof scale === "function") {
|
|
261
|
+
const context = { min, max, feature: region.feature, id: region.id };
|
|
262
|
+
return scale(value, context);
|
|
263
|
+
}
|
|
264
|
+
const index = swatchIndexOf(value, min, max, scale.length);
|
|
265
|
+
return index === null ? "var(--india-map-empty)" : scale[index] ?? "var(--india-map-empty)";
|
|
266
|
+
}
|
|
267
|
+
function makeProjection(collection) {
|
|
268
|
+
return geoMercator().fitExtent(
|
|
269
|
+
[[VIEWBOX.padding, VIEWBOX.padding], [VIEWBOX.width - VIEWBOX.padding, VIEWBOX.height - VIEWBOX.padding]],
|
|
270
|
+
collection
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
function prepareLayer(layer, projection = makeProjection(asFeatureCollection(layer.geometry)), minPartExtent = 0) {
|
|
274
|
+
const collection = asFeatureCollection(layer.geometry);
|
|
275
|
+
const path = geoPath(projection);
|
|
276
|
+
return collection.features.map((feature) => {
|
|
277
|
+
const centroid = path.centroid(feature);
|
|
278
|
+
const bounds = path.bounds(feature);
|
|
279
|
+
const fallbackCentroid = [
|
|
280
|
+
(bounds[0][0] + bounds[1][0]) / 2,
|
|
281
|
+
(bounds[0][1] + bounds[1][1]) / 2
|
|
282
|
+
];
|
|
283
|
+
const region = {
|
|
284
|
+
id: layer.getId(feature),
|
|
285
|
+
label: layer.getLabel(feature),
|
|
286
|
+
value: layer.getValue(feature),
|
|
287
|
+
meta: layer.getMeta?.(feature),
|
|
288
|
+
feature
|
|
289
|
+
};
|
|
290
|
+
const exaggerate = minPartExtent > 0 && !keepsTrueGeometry(region.id);
|
|
291
|
+
const rings = exaggerate ? enlargeSmallParts(projectedRings(feature, projection), minPartExtent) : projectedRings(feature, projection);
|
|
292
|
+
const fallback = centroid.every(Number.isFinite) ? centroid : fallbackCentroid;
|
|
293
|
+
const hull = scatteredHitArea(rings, SMALL_REGION_EXTENT);
|
|
294
|
+
return {
|
|
295
|
+
...region,
|
|
296
|
+
path: exaggerate ? ringsToPath(rings) : path(feature) ?? "",
|
|
297
|
+
hitPath: hull ? ringsToPath([hull]) : null,
|
|
298
|
+
// The largest part's centroid, not the whole feature's: averaging across
|
|
299
|
+
// parts puts an island group's label out at sea between its islands.
|
|
300
|
+
centroid: rings.length > 0 ? labelPointFor(rings, fallback) : fallback,
|
|
301
|
+
partBounds: rings.map(boundsOfRing),
|
|
302
|
+
extent: largestRingExtent(rings)
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function prepareReferenceOverlay(overlay, projection) {
|
|
307
|
+
const path = geoPath(projection);
|
|
308
|
+
return asFeatureCollection(overlay.geometry).features.map((feature) => ({
|
|
309
|
+
id: overlay.getId(feature),
|
|
310
|
+
label: overlay.getLabel(feature),
|
|
311
|
+
description: overlay.getDescription(feature),
|
|
312
|
+
path: path(feature) ?? ""
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
function ordinal(n) {
|
|
316
|
+
const lastTwo = n % 100;
|
|
317
|
+
if (lastTwo >= 11 && lastTwo <= 13) return `${n}th`;
|
|
318
|
+
return `${n}${["th", "st", "nd", "rd"][n % 10] ?? "th"}`;
|
|
319
|
+
}
|
|
320
|
+
function defaultTooltip(context, formatValue) {
|
|
321
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
322
|
+
/* @__PURE__ */ jsx("strong", { children: context.label }),
|
|
323
|
+
/* @__PURE__ */ jsx("b", { children: context.value === null ? "No data" : formatValue(context.value) }),
|
|
324
|
+
context.share !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
325
|
+
/* @__PURE__ */ jsx("span", { className: "india-choropleth__tooltip-bar", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { style: { width: `${Math.max(context.share, 1.5)}%` } }) }),
|
|
326
|
+
/* @__PURE__ */ jsx("small", { children: [
|
|
327
|
+
`${context.share.toFixed(1)}% of total`,
|
|
328
|
+
...context.rank !== null ? [`${ordinal(context.rank)} of ${context.rankedCount}`] : []
|
|
329
|
+
].join(" \xB7 ") })
|
|
330
|
+
] }) : null
|
|
331
|
+
] });
|
|
332
|
+
}
|
|
333
|
+
function IndiaChoropleth({
|
|
334
|
+
states,
|
|
335
|
+
referenceOverlay,
|
|
336
|
+
loadDistricts,
|
|
337
|
+
loadDistrictReferenceOverlay,
|
|
338
|
+
drillDownId,
|
|
339
|
+
defaultDrillDownId = null,
|
|
340
|
+
onDrillDownChange,
|
|
341
|
+
selectedId,
|
|
342
|
+
defaultSelectedId = null,
|
|
343
|
+
onSelectedChange,
|
|
344
|
+
onInspect,
|
|
345
|
+
onInsight,
|
|
346
|
+
onRegionClick,
|
|
347
|
+
onBackgroundClick,
|
|
348
|
+
colorScale = DEFAULT_COLORS,
|
|
349
|
+
formatValue = DEFAULT_FORMAT,
|
|
350
|
+
renderTooltip,
|
|
351
|
+
renderInsights,
|
|
352
|
+
showLegend = true,
|
|
353
|
+
showBreadcrumb = true,
|
|
354
|
+
legendLabels = ["Lower", "Higher"],
|
|
355
|
+
referenceOverlayLegendLabel = "Reference context \xB7 data unavailable",
|
|
356
|
+
referenceOverlayMergeIds = [],
|
|
357
|
+
referenceOverlayFill = "hatch",
|
|
358
|
+
showRegionValues = false,
|
|
359
|
+
minPartExtent = 0,
|
|
360
|
+
minDistrictPartExtent,
|
|
361
|
+
className,
|
|
362
|
+
ariaLabel = "Interactive choropleth map",
|
|
363
|
+
interactive = true
|
|
364
|
+
}) {
|
|
365
|
+
const tooltipId = useId();
|
|
366
|
+
const canvasRef = useRef(null);
|
|
367
|
+
const tooltipAnchorRef = useRef(null);
|
|
368
|
+
const hatchId = `${useId()}-reference-hatch`;
|
|
369
|
+
const [activeDrillDownId, setActiveDrillDownId] = useControllableState(drillDownId, defaultDrillDownId);
|
|
370
|
+
const [activeSelectedId, setActiveSelectedId] = useControllableState(selectedId, defaultSelectedId);
|
|
371
|
+
const [loadedDistricts, setLoadedDistricts] = useState2(null);
|
|
372
|
+
const [loadedDistrictReferenceOverlay, setLoadedDistrictReferenceOverlay] = useState2(null);
|
|
373
|
+
const [loadingState, setLoadingState] = useState2(null);
|
|
374
|
+
const [loadError, setLoadError] = useState2(null);
|
|
375
|
+
const pathRefs = useRef({});
|
|
376
|
+
const restoreFocusId = useRef(null);
|
|
377
|
+
const stateCollection = useMemo(() => asFeatureCollection(states.geometry), [states]);
|
|
378
|
+
const referenceCollection = useMemo(
|
|
379
|
+
() => referenceOverlay ? asFeatureCollection(referenceOverlay.geometry) : null,
|
|
380
|
+
[referenceOverlay]
|
|
381
|
+
);
|
|
382
|
+
const nationalProjection = useMemo(
|
|
383
|
+
() => makeProjection({ type: "FeatureCollection", features: [...stateCollection.features, ...referenceCollection?.features ?? []] }),
|
|
384
|
+
[referenceCollection, stateCollection]
|
|
385
|
+
);
|
|
386
|
+
const stateRegions = useMemo(() => prepareLayer(states, nationalProjection, minPartExtent), [minPartExtent, nationalProjection, states]);
|
|
387
|
+
const referenceRegions = useMemo(
|
|
388
|
+
() => referenceOverlay ? prepareReferenceOverlay(referenceOverlay, nationalProjection) : [],
|
|
389
|
+
[nationalProjection, referenceOverlay]
|
|
390
|
+
);
|
|
391
|
+
const drilledState = useMemo(
|
|
392
|
+
() => stateRegions.find((region) => region.id === activeDrillDownId) ?? null,
|
|
393
|
+
[activeDrillDownId, stateRegions]
|
|
394
|
+
);
|
|
395
|
+
const isDrillRequested = Boolean(drilledState && activeDrillDownId);
|
|
396
|
+
const level = isDrillRequested ? "district" : "state";
|
|
397
|
+
const districtLayer = loadedDistricts?.stateId === activeDrillDownId ? loadedDistricts.layer : null;
|
|
398
|
+
const districtReferenceOverlay = loadedDistrictReferenceOverlay?.stateId === activeDrillDownId ? loadedDistrictReferenceOverlay.overlay : null;
|
|
399
|
+
const districtCollection = useMemo(
|
|
400
|
+
() => districtLayer ? asFeatureCollection(districtLayer.geometry) : null,
|
|
401
|
+
[districtLayer]
|
|
402
|
+
);
|
|
403
|
+
const districtReferenceCollection = useMemo(
|
|
404
|
+
() => districtReferenceOverlay ? asFeatureCollection(districtReferenceOverlay.geometry) : null,
|
|
405
|
+
[districtReferenceOverlay]
|
|
406
|
+
);
|
|
407
|
+
const districtProjection = useMemo(
|
|
408
|
+
() => districtCollection ? makeProjection({ type: "FeatureCollection", features: [...districtCollection.features, ...districtReferenceCollection?.features ?? []] }) : null,
|
|
409
|
+
[districtCollection, districtReferenceCollection]
|
|
410
|
+
);
|
|
411
|
+
const regions = useMemo(
|
|
412
|
+
() => level === "district" && districtLayer && districtProjection ? prepareLayer(districtLayer, districtProjection, minDistrictPartExtent ?? minPartExtent) : level === "state" ? stateRegions : [],
|
|
413
|
+
[districtLayer, districtProjection, level, minDistrictPartExtent, minPartExtent, stateRegions]
|
|
414
|
+
);
|
|
415
|
+
const districtReferenceRegions = useMemo(
|
|
416
|
+
() => level === "district" && districtReferenceOverlay && districtProjection ? prepareReferenceOverlay(districtReferenceOverlay, districtProjection) : [],
|
|
417
|
+
[districtProjection, districtReferenceOverlay, level]
|
|
418
|
+
);
|
|
419
|
+
const selected = regions.find((region) => region.id === activeSelectedId) ?? null;
|
|
420
|
+
const [inspectedId, setInspectedId] = useState2(null);
|
|
421
|
+
const inspectedIdRef = useRef(null);
|
|
422
|
+
const inspected = regions.find((region) => region.id === inspectedId) ?? null;
|
|
423
|
+
const values = useMemo(() => regions.map((region) => region.value).filter((value) => value !== null), [regions]);
|
|
424
|
+
const total = useMemo(() => totalOf(values), [values]);
|
|
425
|
+
const min = values.length ? Math.min(...values) : 0;
|
|
426
|
+
const max = values.length ? Math.max(...values) : 0;
|
|
427
|
+
const legendColors = typeof colorScale === "function" ? DEFAULT_COLORS : colorScale;
|
|
428
|
+
const buckets = useMemo(
|
|
429
|
+
() => legendBuckets(legendColors, regions.map((region) => region.value), min, max),
|
|
430
|
+
[legendColors, max, min, regions]
|
|
431
|
+
);
|
|
432
|
+
const [activeBucket, setActiveBucket] = useState2(null);
|
|
433
|
+
const filterBucket = activeBucket !== null && activeBucket < buckets.length ? activeBucket : null;
|
|
434
|
+
const colorScaleKey = typeof colorScale === "function" ? "function" : colorScale.join(",");
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
setActiveBucket(null);
|
|
437
|
+
}, [activeDrillDownId, colorScaleKey, level]);
|
|
438
|
+
const highlighted = useMemo(
|
|
439
|
+
() => filterBucket === null ? null : new Set(regions.filter((region) => swatchIndexOf(region.value, min, max, legendColors.length) === filterBucket).map((region) => region.id)),
|
|
440
|
+
[filterBucket, legendColors.length, max, min, regions]
|
|
441
|
+
);
|
|
442
|
+
const isDimmed = (id) => highlighted !== null && !highlighted.has(id);
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
let cancelled = false;
|
|
445
|
+
if (!activeDrillDownId || !loadDistricts) {
|
|
446
|
+
setLoadedDistricts(null);
|
|
447
|
+
setLoadingState(null);
|
|
448
|
+
setLoadError(null);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const sourceState = stateRegions.find((region) => region.id === activeDrillDownId);
|
|
452
|
+
if (!sourceState) return;
|
|
453
|
+
setLoadingState(activeDrillDownId);
|
|
454
|
+
setLoadError(null);
|
|
455
|
+
setLoadedDistricts(null);
|
|
456
|
+
loadDistricts(activeDrillDownId, sourceState).then((loaded) => {
|
|
457
|
+
if (!cancelled) setLoadedDistricts({ stateId: activeDrillDownId, layer: loaded });
|
|
458
|
+
}).catch((error) => {
|
|
459
|
+
if (!cancelled) setLoadError(error instanceof Error ? error : new Error("Unable to load districts."));
|
|
460
|
+
}).finally(() => {
|
|
461
|
+
if (!cancelled) setLoadingState(null);
|
|
462
|
+
});
|
|
463
|
+
return () => {
|
|
464
|
+
cancelled = true;
|
|
465
|
+
};
|
|
466
|
+
}, [activeDrillDownId, loadDistricts, stateRegions]);
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
let cancelled = false;
|
|
469
|
+
if (!activeDrillDownId || !loadDistrictReferenceOverlay) {
|
|
470
|
+
setLoadedDistrictReferenceOverlay(null);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const sourceState = stateRegions.find((region) => region.id === activeDrillDownId);
|
|
474
|
+
if (!sourceState) return;
|
|
475
|
+
setLoadedDistrictReferenceOverlay(null);
|
|
476
|
+
loadDistrictReferenceOverlay(activeDrillDownId, sourceState).then((overlay) => {
|
|
477
|
+
if (!cancelled) setLoadedDistrictReferenceOverlay({ stateId: activeDrillDownId, overlay });
|
|
478
|
+
}).catch(() => {
|
|
479
|
+
if (!cancelled) setLoadedDistrictReferenceOverlay({ stateId: activeDrillDownId, overlay: null });
|
|
480
|
+
});
|
|
481
|
+
return () => {
|
|
482
|
+
cancelled = true;
|
|
483
|
+
};
|
|
484
|
+
}, [activeDrillDownId, loadDistrictReferenceOverlay, stateRegions]);
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
const regionId = restoreFocusId.current;
|
|
487
|
+
if (!regionId || level !== "state") return;
|
|
488
|
+
restoreFocusId.current = null;
|
|
489
|
+
pathRefs.current[regionId]?.focus();
|
|
490
|
+
}, [level, regions]);
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
if (!inspectedId) return;
|
|
493
|
+
if (!regions.some((region) => region.id === inspectedId)) {
|
|
494
|
+
inspectedIdRef.current = null;
|
|
495
|
+
setInspectedId(null);
|
|
496
|
+
}
|
|
497
|
+
}, [inspectedId, regions]);
|
|
498
|
+
const inspect = (region) => {
|
|
499
|
+
const nextId = region?.id ?? null;
|
|
500
|
+
if (nextId === null && inspectedIdRef.current === null) return;
|
|
501
|
+
inspectedIdRef.current = nextId;
|
|
502
|
+
setInspectedId(nextId);
|
|
503
|
+
onInspect?.(region ?? null, level);
|
|
504
|
+
};
|
|
505
|
+
const activate = (region) => {
|
|
506
|
+
onRegionClick?.(region, level);
|
|
507
|
+
setActiveSelectedId(region.id);
|
|
508
|
+
onSelectedChange?.(region, level);
|
|
509
|
+
if (level === "state" && loadDistricts) {
|
|
510
|
+
setActiveSelectedId(null);
|
|
511
|
+
setActiveDrillDownId(region.id);
|
|
512
|
+
onDrillDownChange?.(region.id, region);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const goBack = () => {
|
|
516
|
+
const priorState = drilledState ?? void 0;
|
|
517
|
+
setActiveDrillDownId(null);
|
|
518
|
+
setActiveSelectedId(priorState?.id ?? null);
|
|
519
|
+
inspectedIdRef.current = priorState?.id ?? null;
|
|
520
|
+
setInspectedId(priorState?.id ?? null);
|
|
521
|
+
restoreFocusId.current = priorState?.id ?? null;
|
|
522
|
+
onDrillDownChange?.(null, priorState);
|
|
523
|
+
};
|
|
524
|
+
const toTooltipContext = useCallback2((region) => {
|
|
525
|
+
const valued = regions.filter((candidate) => candidate.value !== null);
|
|
526
|
+
const rank = region.value === null ? null : valued.filter((candidate) => (candidate.value ?? 0) > (region.value ?? 0)).length + 1;
|
|
527
|
+
return {
|
|
528
|
+
...region,
|
|
529
|
+
level,
|
|
530
|
+
total,
|
|
531
|
+
share: region.value === null || total === 0 ? null : region.value / total * 100,
|
|
532
|
+
rank,
|
|
533
|
+
rankedCount: valued.length
|
|
534
|
+
};
|
|
535
|
+
}, [level, regions, total]);
|
|
536
|
+
const tooltipContext = useMemo(
|
|
537
|
+
() => inspected ? toTooltipContext(inspected) : null,
|
|
538
|
+
[inspected, toTooltipContext]
|
|
539
|
+
);
|
|
540
|
+
const insightSource = inspected ?? selected;
|
|
541
|
+
const insightContext = useMemo(() => insightSource ? { ...toTooltipContext(insightSource), selected: insightSource.id === selected?.id } : null, [insightSource, selected?.id, toTooltipContext]);
|
|
542
|
+
const valuePlacements = useMemo(() => {
|
|
543
|
+
const coversAnother = (region, at, halfWidth, halfHeight) => {
|
|
544
|
+
const probes = [
|
|
545
|
+
at,
|
|
546
|
+
[at[0] - halfWidth, at[1] - halfHeight],
|
|
547
|
+
[at[0] + halfWidth, at[1] + halfHeight],
|
|
548
|
+
[at[0] + halfWidth, at[1] - halfHeight],
|
|
549
|
+
[at[0] - halfWidth, at[1] + halfHeight]
|
|
550
|
+
];
|
|
551
|
+
return regions.some((other) => other.id !== region.id && other.partBounds.some(([minX, minY, maxX, maxY]) => probes.some((probe) => probe[0] >= minX && probe[0] <= maxX && probe[1] >= minY && probe[1] <= maxY)));
|
|
552
|
+
};
|
|
553
|
+
return regions.map((region) => {
|
|
554
|
+
const text = region.value === null ? "\u2014" : formatValue(region.value);
|
|
555
|
+
const halfWidth = Math.max(text.length * 3.1, 3);
|
|
556
|
+
const halfHeight = 5.5;
|
|
557
|
+
const isSmall = region.extent > 0 && region.extent < SMALL_REGION_EXTENT;
|
|
558
|
+
const fitsInside = region.partBounds.some(([minX, minY, maxX, maxY]) => maxX - minX >= halfWidth * 2 && maxY - minY >= halfHeight * 2);
|
|
559
|
+
if (!isSmall && fitsInside) return { region, text, at: region.centroid, leader: null };
|
|
560
|
+
const outside = isSmall ? placeOutsideLabel({
|
|
561
|
+
anchor: region.centroid,
|
|
562
|
+
clearance: Math.max(region.extent, MIN_REGION_MARKER_SIZE) / 2 + 4 + halfWidth,
|
|
563
|
+
halfSize: [halfWidth, halfHeight],
|
|
564
|
+
viewBox: [VIEWBOX.width, VIEWBOX.height],
|
|
565
|
+
centre: [VIEWBOX.width / 2, VIEWBOX.height / 2],
|
|
566
|
+
// Moving a label out only helps if there is open space to move it into.
|
|
567
|
+
// Goa and Puducherry have sea beside them; Delhi is ringed by other
|
|
568
|
+
// states, so its number stays put rather than landing on a neighbour.
|
|
569
|
+
isBlocked: (candidate) => coversAnother(region, candidate, halfWidth, halfHeight)
|
|
570
|
+
}) : null;
|
|
571
|
+
if (!outside) {
|
|
572
|
+
return fitsInside ? { region, text, at: region.centroid, leader: null } : null;
|
|
573
|
+
}
|
|
574
|
+
const gap = Math.max(region.extent, MIN_REGION_MARKER_SIZE) / 2 + 1;
|
|
575
|
+
const dx = outside[0] - region.centroid[0];
|
|
576
|
+
const dy = outside[1] - region.centroid[1];
|
|
577
|
+
const length = Math.hypot(dx, dy);
|
|
578
|
+
if (length <= gap + halfWidth) return { region, text, at: outside, leader: null };
|
|
579
|
+
return {
|
|
580
|
+
region,
|
|
581
|
+
text,
|
|
582
|
+
at: outside,
|
|
583
|
+
leader: [
|
|
584
|
+
[region.centroid[0] + dx / length * gap, region.centroid[1] + dy / length * gap],
|
|
585
|
+
[outside[0] - dx / length * (halfWidth + 1.5), outside[1] - dy / length * (halfWidth + 1.5)]
|
|
586
|
+
]
|
|
587
|
+
};
|
|
588
|
+
}).filter((placement) => placement !== null);
|
|
589
|
+
}, [formatValue, regions]);
|
|
590
|
+
const smallMarkers = useMemo(
|
|
591
|
+
() => regions.filter((region) => region.extent > 0 && region.extent < MIN_REGION_MARKER_SIZE),
|
|
592
|
+
[regions]
|
|
593
|
+
);
|
|
594
|
+
const handleBackgroundClick = (event) => {
|
|
595
|
+
if (event.target !== event.currentTarget) return;
|
|
596
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
597
|
+
const point = rect.width > 0 && rect.height > 0 ? (() => {
|
|
598
|
+
const scale = Math.min(rect.width / VIEWBOX.width, rect.height / VIEWBOX.height);
|
|
599
|
+
return [
|
|
600
|
+
(event.clientX - rect.left - (rect.width - VIEWBOX.width * scale) / 2) / scale,
|
|
601
|
+
(event.clientY - rect.top - (rect.height - VIEWBOX.height * scale) / 2) / scale
|
|
602
|
+
];
|
|
603
|
+
})() : null;
|
|
604
|
+
let nearest = null;
|
|
605
|
+
if (point) {
|
|
606
|
+
let nearestDistance = Infinity;
|
|
607
|
+
for (const region of regions) {
|
|
608
|
+
if (region.extent <= 0 || region.extent >= SMALL_REGION_EXTENT) continue;
|
|
609
|
+
const distance = distanceToParts(point, region.partBounds);
|
|
610
|
+
if (distance <= SMALL_REGION_CLICK_RADIUS && distance < nearestDistance) {
|
|
611
|
+
nearestDistance = distance;
|
|
612
|
+
nearest = region;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (nearest) {
|
|
617
|
+
activate(nearest);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
onBackgroundClick?.();
|
|
621
|
+
if (activeSelectedId !== null) {
|
|
622
|
+
setActiveSelectedId(null);
|
|
623
|
+
onSelectedChange?.(null, level);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const canDrill = Boolean(loadDistricts && level === "state");
|
|
627
|
+
const visibleReferenceRegions = level === "state" ? referenceRegions : districtReferenceRegions;
|
|
628
|
+
const mergedReferenceIds = useMemo(() => new Set(referenceOverlayMergeIds), [referenceOverlayMergeIds]);
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
onInsight?.(insightContext);
|
|
631
|
+
}, [insightContext, onInsight]);
|
|
632
|
+
useLayoutEffect(() => {
|
|
633
|
+
const anchor = tooltipAnchorRef.current;
|
|
634
|
+
const canvas = canvasRef.current;
|
|
635
|
+
if (!anchor || !canvas) return;
|
|
636
|
+
anchor.style.removeProperty("--india-map-tooltip-dx");
|
|
637
|
+
anchor.style.removeProperty("--india-map-tooltip-dy");
|
|
638
|
+
const tooltipRect = anchor.getBoundingClientRect();
|
|
639
|
+
const boundsRect = canvas.getBoundingClientRect();
|
|
640
|
+
if (tooltipRect.width === 0 || boundsRect.width === 0) return;
|
|
641
|
+
const { dx, side } = placeTooltip(tooltipRect, boundsRect, TOOLTIP_GAP_PX);
|
|
642
|
+
if (dx !== 0) anchor.style.setProperty("--india-map-tooltip-dx", `${Math.round(dx)}px`);
|
|
643
|
+
if (side === "below") anchor.style.setProperty("--india-map-tooltip-dy", `${TOOLTIP_GAP_PX}px`);
|
|
644
|
+
}, [tooltipContext?.id, tooltipContext?.value, tooltipContext?.label, renderTooltip]);
|
|
645
|
+
return /* @__PURE__ */ jsxs("section", { className: ["india-choropleth", !interactive && "india-choropleth--static", className].filter(Boolean).join(" "), "aria-busy": loadingState ? "true" : void 0, children: [
|
|
646
|
+
showBreadcrumb ? /* @__PURE__ */ jsx("div", { className: "india-choropleth__toolbar", children: /* @__PURE__ */ jsxs("nav", { className: "india-choropleth__breadcrumb", "aria-label": "Map hierarchy", children: [
|
|
647
|
+
drilledState ? /* @__PURE__ */ jsx("button", { className: "india-choropleth__back", type: "button", onClick: goBack, children: "All states" }) : /* @__PURE__ */ jsx("span", { children: "All states" }),
|
|
648
|
+
drilledState ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
649
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" }),
|
|
650
|
+
/* @__PURE__ */ jsx("span", { "aria-current": "page", children: drilledState.label })
|
|
651
|
+
] }) : null
|
|
652
|
+
] }) }) : null,
|
|
653
|
+
/* @__PURE__ */ jsxs("div", { ref: canvasRef, className: "india-choropleth__canvas", onMouseLeave: interactive ? () => inspect(null) : void 0, children: [
|
|
654
|
+
isDrillRequested && (!districtLayer || loadError) ? /* @__PURE__ */ jsx("div", { className: "india-choropleth__status", role: loadError ? "alert" : "status", children: loadError ? loadError.message : loadingState ? "Loading districts\u2026" : "District data is unavailable for this state." }) : regions.length === 0 ? /* @__PURE__ */ jsx("div", { className: "india-choropleth__status", role: "status", children: "No district data is available for this state." }) : /* @__PURE__ */ jsxs(
|
|
655
|
+
"svg",
|
|
656
|
+
{
|
|
657
|
+
className: `india-choropleth__svg${filterBucket !== null ? " india-choropleth__svg--filtered" : ""}`,
|
|
658
|
+
viewBox: `0 0 ${VIEWBOX.width} ${VIEWBOX.height}`,
|
|
659
|
+
role: "group",
|
|
660
|
+
"aria-label": ariaLabel,
|
|
661
|
+
onKeyDown: interactive ? (event) => {
|
|
662
|
+
if (event.key === "Escape") {
|
|
663
|
+
event.preventDefault();
|
|
664
|
+
inspect(null);
|
|
665
|
+
}
|
|
666
|
+
} : void 0,
|
|
667
|
+
onClick: interactive ? handleBackgroundClick : void 0,
|
|
668
|
+
children: [
|
|
669
|
+
visibleReferenceRegions.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
670
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("pattern", { id: hatchId, width: "8", height: "8", patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)", children: [
|
|
671
|
+
/* @__PURE__ */ jsx("rect", { width: "8", height: "8", fill: "var(--india-map-reference-bg)" }),
|
|
672
|
+
/* @__PURE__ */ jsx("line", { x1: "0", y1: "0", x2: "0", y2: "8", stroke: "var(--india-map-reference-hatch)", strokeWidth: "2" })
|
|
673
|
+
] }) }),
|
|
674
|
+
/* @__PURE__ */ jsx("g", { className: "india-choropleth__reference-fill", role: "group", "aria-label": "Non-statistical reference context.", children: visibleReferenceRegions.map((region) => /* @__PURE__ */ jsx(
|
|
675
|
+
"path",
|
|
676
|
+
{
|
|
677
|
+
d: region.path,
|
|
678
|
+
fill: referenceOverlayFill === "solid" ? "var(--india-map-reference-bg)" : `url(#${hatchId})`,
|
|
679
|
+
"aria-label": `${region.label}.${region.description ? ` ${region.description}` : ""}`,
|
|
680
|
+
role: "img"
|
|
681
|
+
},
|
|
682
|
+
region.id
|
|
683
|
+
)) })
|
|
684
|
+
] }) : null,
|
|
685
|
+
interactive ? /* @__PURE__ */ jsx("g", { className: "india-choropleth__hit-areas", "aria-hidden": "true", children: regions.filter((region) => region.hitPath).map((region) => /* @__PURE__ */ jsx(
|
|
686
|
+
"path",
|
|
687
|
+
{
|
|
688
|
+
d: region.hitPath,
|
|
689
|
+
fill: "none",
|
|
690
|
+
pointerEvents: "all",
|
|
691
|
+
tabIndex: -1,
|
|
692
|
+
onMouseEnter: () => inspect(region),
|
|
693
|
+
onMouseLeave: () => inspect(null),
|
|
694
|
+
onClick: () => activate(region)
|
|
695
|
+
},
|
|
696
|
+
region.id
|
|
697
|
+
)) }) : null,
|
|
698
|
+
regions.map((region) => {
|
|
699
|
+
const isInspected = region.id === inspected?.id;
|
|
700
|
+
const isSelected = region.id === selected?.id;
|
|
701
|
+
const action = canDrill ? "Activate to view districts." : "Activate to select.";
|
|
702
|
+
const textValue = region.value === null ? "No data" : formatValue(region.value);
|
|
703
|
+
return /* @__PURE__ */ jsx(
|
|
704
|
+
"path",
|
|
705
|
+
{
|
|
706
|
+
className: `india-choropleth__region${isInspected ? " india-choropleth__region--inspected" : ""}${isSelected ? " india-choropleth__region--selected" : ""}${mergedReferenceIds.has(region.id) ? " india-choropleth__region--reference-merged" : ""}${isDimmed(region.id) ? " india-choropleth__dimmed" : ""}`,
|
|
707
|
+
d: region.path,
|
|
708
|
+
fill: colorFor(region.value, region, min, max, colorScale),
|
|
709
|
+
tabIndex: interactive ? 0 : -1,
|
|
710
|
+
role: interactive ? "button" : void 0,
|
|
711
|
+
"aria-label": `${region.label}, ${textValue}. ${action}`,
|
|
712
|
+
"aria-pressed": interactive ? isSelected : void 0,
|
|
713
|
+
"aria-describedby": isInspected ? tooltipId : void 0,
|
|
714
|
+
ref: (element) => {
|
|
715
|
+
pathRefs.current[region.id] = element;
|
|
716
|
+
},
|
|
717
|
+
onMouseEnter: interactive ? () => inspect(region) : void 0,
|
|
718
|
+
onMouseLeave: interactive ? () => inspect(null) : void 0,
|
|
719
|
+
onFocus: interactive ? () => inspect(region) : void 0,
|
|
720
|
+
onBlur: interactive ? () => inspect(null) : void 0,
|
|
721
|
+
onClick: interactive ? () => activate(region) : void 0,
|
|
722
|
+
onKeyDown: interactive ? (event) => {
|
|
723
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
724
|
+
event.preventDefault();
|
|
725
|
+
activate(region);
|
|
726
|
+
}
|
|
727
|
+
} : void 0
|
|
728
|
+
},
|
|
729
|
+
region.id
|
|
730
|
+
);
|
|
731
|
+
}),
|
|
732
|
+
smallMarkers.length > 0 ? /* @__PURE__ */ jsx("g", { className: "india-choropleth__small-markers", "aria-hidden": "true", children: smallMarkers.map((region) => /* @__PURE__ */ jsx(
|
|
733
|
+
"circle",
|
|
734
|
+
{
|
|
735
|
+
className: isDimmed(region.id) ? "india-choropleth__dimmed" : void 0,
|
|
736
|
+
cx: region.centroid[0],
|
|
737
|
+
cy: region.centroid[1],
|
|
738
|
+
r: MIN_REGION_MARKER_SIZE / 2,
|
|
739
|
+
fill: colorFor(region.value, region, min, max, colorScale),
|
|
740
|
+
onMouseEnter: interactive ? () => inspect(region) : void 0,
|
|
741
|
+
onMouseLeave: interactive ? () => inspect(null) : void 0,
|
|
742
|
+
onClick: interactive ? () => activate(region) : void 0
|
|
743
|
+
},
|
|
744
|
+
region.id
|
|
745
|
+
)) }) : null,
|
|
746
|
+
showRegionValues ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
747
|
+
/* @__PURE__ */ jsx("g", { className: "india-choropleth__value-leaders", "aria-hidden": "true", children: valuePlacements.filter((p) => p.leader).map((p) => /* @__PURE__ */ jsx(
|
|
748
|
+
"line",
|
|
749
|
+
{
|
|
750
|
+
className: isDimmed(p.region.id) ? "india-choropleth__dimmed" : void 0,
|
|
751
|
+
x1: p.leader[0][0],
|
|
752
|
+
y1: p.leader[0][1],
|
|
753
|
+
x2: p.leader[1][0],
|
|
754
|
+
y2: p.leader[1][1]
|
|
755
|
+
},
|
|
756
|
+
p.region.id
|
|
757
|
+
)) }),
|
|
758
|
+
/* @__PURE__ */ jsx("g", { className: `india-choropleth__region-values${level === "district" ? " india-choropleth__region-values--district" : ""}`, "aria-hidden": "true", children: valuePlacements.map((p) => /* @__PURE__ */ jsx(
|
|
759
|
+
"text",
|
|
760
|
+
{
|
|
761
|
+
className: isDimmed(p.region.id) ? "india-choropleth__dimmed" : void 0,
|
|
762
|
+
x: p.at[0],
|
|
763
|
+
y: p.at[1],
|
|
764
|
+
textAnchor: "middle",
|
|
765
|
+
dominantBaseline: "central",
|
|
766
|
+
children: p.text
|
|
767
|
+
},
|
|
768
|
+
p.region.id
|
|
769
|
+
)) })
|
|
770
|
+
] }) : null,
|
|
771
|
+
visibleReferenceRegions.length > 0 ? /* @__PURE__ */ jsx("g", { className: "india-choropleth__reference-outline", "aria-hidden": "true", children: visibleReferenceRegions.map((region) => /* @__PURE__ */ jsx("path", { d: region.path, fill: "none" }, region.id)) }) : null,
|
|
772
|
+
selected ? /* @__PURE__ */ jsxs(
|
|
773
|
+
"g",
|
|
774
|
+
{
|
|
775
|
+
className: `india-choropleth__selection${selected.id === inspected?.id ? " india-choropleth__selection--lifted" : ""}`,
|
|
776
|
+
"aria-hidden": "true",
|
|
777
|
+
children: [
|
|
778
|
+
/* @__PURE__ */ jsx("path", { className: "india-choropleth__selection-halo", d: selected.path, fill: "none" }),
|
|
779
|
+
/* @__PURE__ */ jsx("path", { className: "india-choropleth__selection-ring", d: selected.path, fill: "none" })
|
|
780
|
+
]
|
|
781
|
+
}
|
|
782
|
+
) : null
|
|
783
|
+
]
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
tooltipContext && regions.length > 0 ? /* @__PURE__ */ jsx("div", { ref: tooltipAnchorRef, className: "india-choropleth__tooltip-anchor", style: { left: `${inspected.centroid[0] / VIEWBOX.width * 100}%`, top: `${inspected.centroid[1] / VIEWBOX.height * 100}%` }, children: /* @__PURE__ */ jsx("div", { id: tooltipId, className: "india-choropleth__tooltip", children: renderTooltip ? renderTooltip(tooltipContext) : defaultTooltip(tooltipContext, formatValue) }) }) : null
|
|
787
|
+
] }),
|
|
788
|
+
showLegend ? /* @__PURE__ */ jsxs(
|
|
789
|
+
"div",
|
|
790
|
+
{
|
|
791
|
+
className: "india-choropleth__legend",
|
|
792
|
+
role: "group",
|
|
793
|
+
"aria-label": `Color scale: ${legendLabels[0]} to ${legendLabels[1]} values`,
|
|
794
|
+
onKeyDown: interactive ? (event) => {
|
|
795
|
+
if (event.key === "Escape") {
|
|
796
|
+
event.preventDefault();
|
|
797
|
+
setActiveBucket(null);
|
|
798
|
+
}
|
|
799
|
+
} : void 0,
|
|
800
|
+
children: [
|
|
801
|
+
/* @__PURE__ */ jsx("span", { children: legendLabels[0] }),
|
|
802
|
+
/* @__PURE__ */ jsx("div", { className: "india-choropleth__swatches", "aria-hidden": interactive ? void 0 : true, children: buckets.map((bucket) => interactive ? /* @__PURE__ */ jsx(
|
|
803
|
+
"button",
|
|
804
|
+
{
|
|
805
|
+
type: "button",
|
|
806
|
+
className: `india-choropleth__swatch${filterBucket === bucket.index ? " india-choropleth__swatch--active" : ""}${filterBucket !== null && filterBucket !== bucket.index ? " india-choropleth__swatch--muted" : ""}`,
|
|
807
|
+
style: { backgroundColor: bucket.color },
|
|
808
|
+
"aria-pressed": filterBucket === bucket.index,
|
|
809
|
+
"aria-disabled": bucket.matches === 0 ? true : void 0,
|
|
810
|
+
"aria-label": legendBucketLabel(bucket, formatValue),
|
|
811
|
+
title: legendBucketLabel(bucket, formatValue),
|
|
812
|
+
onClick: () => {
|
|
813
|
+
if (bucket.matches === 0) return;
|
|
814
|
+
setActiveBucket(filterBucket === bucket.index ? null : bucket.index);
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
bucket.index
|
|
818
|
+
) : /* @__PURE__ */ jsx("i", { className: "india-choropleth__swatch", style: { backgroundColor: bucket.color } }, bucket.index)) }),
|
|
819
|
+
/* @__PURE__ */ jsx("span", { children: legendLabels[1] }),
|
|
820
|
+
visibleReferenceRegions.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
821
|
+
/* @__PURE__ */ jsx("i", { className: `india-choropleth__reference-key${referenceOverlayFill === "solid" ? " india-choropleth__reference-key--solid" : ""}`, "aria-hidden": "true" }),
|
|
822
|
+
/* @__PURE__ */ jsx("span", { children: referenceOverlayLegendLabel })
|
|
823
|
+
] }) : null
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
) : null,
|
|
827
|
+
renderInsights ? /* @__PURE__ */ jsx("aside", { className: "india-choropleth__insights", "aria-live": "polite", children: renderInsights(insightContext) }) : null
|
|
828
|
+
] });
|
|
829
|
+
}
|
|
830
|
+
export {
|
|
831
|
+
IndiaChoropleth
|
|
832
|
+
};
|
|
833
|
+
//# sourceMappingURL=index.js.map
|