bharat-choropleth 0.2.0 → 0.3.1
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/CHANGELOG.md +126 -0
- package/README.md +206 -6
- package/dist/index.d.ts +193 -5
- package/dist/index.js +515 -32
- package/dist/index.js.map +1 -1
- package/dist/style.css +12 -2
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 - 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Drilling in no longer loses keyboard focus. Stepping back out had an obvious
|
|
8
|
+
target — the region just left — and going in had none, so focus fell to
|
|
9
|
+
`<body>` on every drill-down, dropping a keyboard user at the top of the
|
|
10
|
+
document and telling a screen reader nothing about where they now were. Focus
|
|
11
|
+
now lands on the first region of the level entered; on the message when that
|
|
12
|
+
level holds nothing; and back on the district itself when an optimistic drill
|
|
13
|
+
turns out to be a leaf, which had unmounted the region under the cursor.
|
|
14
|
+
- Changing the state now tells the host that the sub-district level was dropped.
|
|
15
|
+
The component clears that level itself and fired no
|
|
16
|
+
`onSubDistrictDrillDownChange`, so anything mirroring the level kept pointing
|
|
17
|
+
at a district of the state just left — the wrong level, reported against the
|
|
18
|
+
wrong map. There is no prior district to hand back, because it belonged to the
|
|
19
|
+
state that is gone, so the callback receives `(null, undefined)`.
|
|
20
|
+
- A host that stops controlling a prop now keeps what is on screen. The
|
|
21
|
+
uncontrolled slot held whatever it contained before control began, often many
|
|
22
|
+
interactions stale, and that was what came back.
|
|
23
|
+
- Two rows naming the same region warn instead of resolving in silence. That
|
|
24
|
+
silence is how a mis-shaped query becomes a believed wrong number: a state
|
|
25
|
+
showing one of its districts' totals looks exactly like a state showing its
|
|
26
|
+
own. The last value still wins — changing that would move numbers under
|
|
27
|
+
existing callers — and the warning quotes the spelling the caller wrote.
|
|
28
|
+
- Legend swatches size from the legend row rather than the viewport. `vw`
|
|
29
|
+
measures the browser window, so in any embed narrower than the page the
|
|
30
|
+
swatches pinned to their maximum and overflowed the map they belong to.
|
|
31
|
+
|
|
32
|
+
## 0.3.0 - 2026-09-05
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Values keyed by a feature id the state registry does not know now match. A
|
|
37
|
+
feature was keyed by its id only if the registry recognised it, and otherwise
|
|
38
|
+
fell back to its *label* — so a dataset keyed by id against geometry outside
|
|
39
|
+
the registry, such as this repo's own historical Census bundle with its
|
|
40
|
+
`in-hs-*` ids, silently rendered as "No data" on every region while looking
|
|
41
|
+
perfectly healthy. Each key a caller writes is now recorded against the
|
|
42
|
+
canonical key it addresses, and a feature is matched by exact id, exact label,
|
|
43
|
+
then either resolved through the registry. Found by building a dashboard on
|
|
44
|
+
the historical bundle.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `IndiaChoropleth` warns when `loadDistricts` or `loadSubDistricts` has been a
|
|
49
|
+
different function on three renders running while the level it loads has not
|
|
50
|
+
moved. That is the signature of an inline arrow: the loaders are compared by
|
|
51
|
+
identity because a genuinely different loader must refetch, so an unstable one
|
|
52
|
+
silently refetches the level over the network on every unrelated re-render
|
|
53
|
+
while still rendering correctly. One deliberate swap does not warn.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Documented `renderInsights` as a pure render slot and pointed at `onInsight`
|
|
58
|
+
for reacting to the inspected region. Calling `setState` from `renderInsights`
|
|
59
|
+
is a state update during render; the two props carry the same payload.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- The default data source now points at the `v0.3.0` boundary bundle. A release
|
|
64
|
+
pins the geometry a consumer receives, not only the renderer that draws it.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Changing values no longer reloads the level below. Repainting means handing
|
|
69
|
+
the renderer a new `MapLayer`, and the district and sub-district loaders keyed
|
|
70
|
+
their effects on the prepared regions — which carry values — so every number
|
|
71
|
+
that moved called the loader again and blanked the level while the promise was
|
|
72
|
+
in flight. A map drilled into a state, driven by a timer or a slider, blinked
|
|
73
|
+
its districts away on every tick. The loaders now key on the geometry and id
|
|
74
|
+
accessor, the things that decide which regions exist. A real geometry change
|
|
75
|
+
still reloads.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- Repainting no longer re-unpacks the topology or refits the projection. Both
|
|
80
|
+
are keyed on `states.geometry`, and the decoded features are passed into layer
|
|
81
|
+
preparation instead of being unpacked a second time. Values live on the layer,
|
|
82
|
+
not the geometry, so neither step could have changed its answer.
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- `districtValues`, district numbers nested under the state they belong to:
|
|
87
|
+
`districtValues={{ Telangana: { Hyderabad: 90 } }}`. The nesting is what makes
|
|
88
|
+
it safe — district names repeat across states (Aurangabad, Bilaspur and
|
|
89
|
+
Hamirpur each name two) and there is no district registry to resolve a bare
|
|
90
|
+
name against. Outer keys resolve through the state registry and are checked at
|
|
91
|
+
once; inner keys match a district's name, slug or id and are checked when that
|
|
92
|
+
state's districts arrive. It overlays whichever district layer is in use,
|
|
93
|
+
including one from a caller's own `loadDistricts`, leaving districts it does
|
|
94
|
+
not name at whatever that layer returned. There is deliberately no
|
|
95
|
+
`subDistrictValues`.
|
|
96
|
+
- `loadDistrictTopology` and `loadSubDistrictTopology` are exported, so fetching
|
|
97
|
+
the prepared bundles from a custom loader does not mean re-deriving their URL
|
|
98
|
+
scheme by hand. The framework-free package exports them too now.
|
|
99
|
+
- `BharatChoropleth`, a zero-config component over the existing renderer:
|
|
100
|
+
`<BharatChoropleth values={{ Telangana: 82 }} />` is a working map. Keys
|
|
101
|
+
resolve through the state registry the framework-free package already used —
|
|
102
|
+
display names, slugs, LGD ids, case-insensitive and separator-free forms, `&`
|
|
103
|
+
normalized to `and`, and former names such as `Orissa` — so it accepts the
|
|
104
|
+
same spellings as `bharat-choropleth-js`. Row-shaped input is read through
|
|
105
|
+
`data` + `regionKey` + `valueKey`; `values` wins when both are given.
|
|
106
|
+
- Boundary data is fetched from `dataBaseUrl` when no `geometry` is supplied, and
|
|
107
|
+
district / sub-district drill-down defaults on in that case, matching the
|
|
108
|
+
framework-free facade. Drill-down geometry is fetched once per id and reused,
|
|
109
|
+
so changing `values` repaints without refetching it.
|
|
110
|
+
- Exported `BharatChoroplethProps`, the state registry (`STATES`, `resolveState`,
|
|
111
|
+
`normalizeStateKey`), `ATTRIBUTION`, `DEFAULT_DATA_BASE_URL` and `GeometryInput`,
|
|
112
|
+
matching what `bharat-choropleth-js` exports.
|
|
113
|
+
- The stylesheet gained the `.bharat-choropleth__status` placeholder rules the
|
|
114
|
+
framework-free package already carried, used while boundary data loads and for
|
|
115
|
+
the message if it fails.
|
|
116
|
+
|
|
117
|
+
`IndiaChoropleth` is unchanged and remains the full API — `BharatChoropleth` is
|
|
118
|
+
sugar over it, and passes every one of its props except `states` straight through.
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- The default data source now points at the `v0.2.0` boundary bundle. It was
|
|
123
|
+
still pinned to `v0.1.0`, which predates
|
|
124
|
+
`data/generated/current-2019-subdistricts/`, so every sub-district request
|
|
125
|
+
404'd — and because a 404 means "this district has no sub-district level", the
|
|
126
|
+
level 0.2.0 added was silently unreachable for anyone on the default
|
|
127
|
+
`dataBaseUrl`. Self-hosted deployments were unaffected.
|
|
128
|
+
|
|
3
129
|
## 0.2.0
|
|
4
130
|
|
|
5
131
|
- Added an optional sub-district level below districts — tehsils, taluks,
|
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# `bharat-choropleth`
|
|
2
2
|
|
|
3
3
|
An accessible React SVG choropleth for India state and district dashboards.
|
|
4
|
-
It
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
It supports keyboard and pointer inspection, and lazy-loads district layers when
|
|
5
|
+
a user selects a state, then sub-district layers when a user selects a district.
|
|
6
|
+
|
|
7
|
+
Two entry points, same renderer:
|
|
8
|
+
|
|
9
|
+
- **`BharatChoropleth`** — map state names to numbers and you have a map. Fetches
|
|
10
|
+
boundary data for you. Start here.
|
|
11
|
+
- **`IndiaChoropleth`** — the core renderer. You supply every layer, accessor and
|
|
12
|
+
piece of navigation state. Everything below the quick start uses it.
|
|
7
13
|
|
|
8
14
|
## Install
|
|
9
15
|
|
|
@@ -17,7 +23,153 @@ Import the stylesheet once in the application that mounts the map:
|
|
|
17
23
|
import "bharat-choropleth/style.css";
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
##
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { BharatChoropleth } from "bharat-choropleth";
|
|
30
|
+
import "bharat-choropleth/style.css";
|
|
31
|
+
|
|
32
|
+
export function Map() {
|
|
33
|
+
return (
|
|
34
|
+
<BharatChoropleth
|
|
35
|
+
values={{
|
|
36
|
+
Telangana: 82,
|
|
37
|
+
Karnataka: 74,
|
|
38
|
+
Maharashtra: 91,
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That is the whole setup. Boundary data is fetched from the prepared bundle (this
|
|
46
|
+
package still ships none), states you omit render as "no data", and clicking a
|
|
47
|
+
state drills into its districts, then into that district's sub-districts.
|
|
48
|
+
|
|
49
|
+
Keys are resolved through the state registry, so every spelling of a state that
|
|
50
|
+
a reader might reasonably type lands on the same region — display name, slug,
|
|
51
|
+
LGD id, case-insensitive, separator-free, and former names:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<BharatChoropleth
|
|
55
|
+
values={{
|
|
56
|
+
Goa: 6,
|
|
57
|
+
"Tamil Nadu": 18,
|
|
58
|
+
tamilnadu: 18, // separator-free
|
|
59
|
+
"Jammu & Kashmir": 2, // & normalizes to "and"
|
|
60
|
+
Orissa: 8, // former name → Odisha
|
|
61
|
+
"in-cs-30-goa": 6, // LGD id
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A name it does not recognize is ignored with one console warning naming what you
|
|
67
|
+
typed; it never throws, and the rest of the map still renders. `0` is a value,
|
|
68
|
+
not missing data — only `null` or an omitted state reads as "no data".
|
|
69
|
+
|
|
70
|
+
Row-shaped data works without reshaping it first:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<BharatChoropleth
|
|
74
|
+
data={[
|
|
75
|
+
{ state: "Telangana", value: 82 },
|
|
76
|
+
{ state: "Karnataka", value: 74 },
|
|
77
|
+
]}
|
|
78
|
+
regionKey="state"
|
|
79
|
+
valueKey="value"
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`regionKey` and `valueKey` default to `"region"` and `"value"`. `values` is the
|
|
84
|
+
primary API: if you pass both, `values` wins and `data` is ignored rather than
|
|
85
|
+
merged.
|
|
86
|
+
|
|
87
|
+
Every `IndiaChoropleth` prop except `states` passes straight through, so reaching
|
|
88
|
+
for the full renderer's behaviour is a prop rather than a rewrite:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<BharatChoropleth
|
|
92
|
+
values={values}
|
|
93
|
+
colorScale={["#eef7f5", "#075b55"]}
|
|
94
|
+
formatValue={(value) => `${value}%`}
|
|
95
|
+
renderTooltip={(context) => <MyTooltip {...context} />}
|
|
96
|
+
onSelectedChange={(region, level) => track(region, level)}
|
|
97
|
+
showLegend={false}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Point `dataBaseUrl` at your own copy of `data/generated` to self-host the
|
|
102
|
+
boundary bundles, or pass `geometry` to supply the state layer yourself — which
|
|
103
|
+
also turns drill-down off, since the district files are no longer known to sit
|
|
104
|
+
beside it. Turn it back on explicitly with `districts` / `subDistricts` plus your
|
|
105
|
+
own `loadDistricts` / `loadSubDistricts`.
|
|
106
|
+
|
|
107
|
+
### District values
|
|
108
|
+
|
|
109
|
+
`values` is state-level. District numbers nest under the state they belong to:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<BharatChoropleth
|
|
113
|
+
values={{ Telangana: 82, Maharashtra: 91 }}
|
|
114
|
+
districtValues={{
|
|
115
|
+
Telangana: { Hyderabad: 90, "Ranga Reddy": 76 },
|
|
116
|
+
Maharashtra: { Aurangabad: 44 },
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The nesting is load-bearing, not decoration. District names repeat across states —
|
|
122
|
+
Aurangabad, Bilaspur and Hamirpur each name a district in two — and unlike states
|
|
123
|
+
there is no district registry to resolve a bare name against, so a flat map could
|
|
124
|
+
not say which one you meant.
|
|
125
|
+
|
|
126
|
+
Outer keys go through the state registry, so every spelling `values` accepts works
|
|
127
|
+
here too, and they are checked immediately. Inner keys match a district's name,
|
|
128
|
+
slug or id, case-insensitively — but they can only be checked once that state's
|
|
129
|
+
districts have been fetched, so a typo there is warned about when you first drill
|
|
130
|
+
into that state, not at first render.
|
|
131
|
+
|
|
132
|
+
It applies to whichever district layer is in use, including one from your own
|
|
133
|
+
`loadDistricts`. A district named here takes this value; one that is not keeps
|
|
134
|
+
whatever the layer returned, so you can override a few and leave the rest:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<BharatChoropleth
|
|
138
|
+
loadDistricts={myLoader} // supplies most districts
|
|
139
|
+
districtValues={{ Telangana: { Hyderabad: 90 } }} // overrides one
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
There is no `subDistrictValues`. Three levels of nesting stops reading clearly,
|
|
144
|
+
and sub-district naming is much less settled than district naming. Set those
|
|
145
|
+
through `loadSubDistricts`, which is also how you would supply district values
|
|
146
|
+
from ids rather than names:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { loadSubDistrictTopology, DEFAULT_DATA_BASE_URL } from "bharat-choropleth";
|
|
150
|
+
|
|
151
|
+
<BharatChoropleth
|
|
152
|
+
values={values}
|
|
153
|
+
loadSubDistricts={async (districtId) => {
|
|
154
|
+
const geometry = await loadSubDistrictTopology(DEFAULT_DATA_BASE_URL, districtId);
|
|
155
|
+
if (!geometry) return null; // this district has no sub-district level
|
|
156
|
+
return {
|
|
157
|
+
geometry,
|
|
158
|
+
getId: (feature) => String(feature.properties?.id),
|
|
159
|
+
getLabel: (feature) => String(feature.properties?.name),
|
|
160
|
+
getValue: (feature) => subDistrictValues[String(feature.properties?.id)] ?? null,
|
|
161
|
+
};
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`loadDistrictTopology` and `loadSubDistrictTopology` are exported so fetching the
|
|
167
|
+
prepared bundles does not mean re-deriving their URL scheme by hand.
|
|
168
|
+
|
|
169
|
+
## Advanced usage
|
|
170
|
+
|
|
171
|
+
`IndiaChoropleth` is the core renderer and is not going anywhere — the component
|
|
172
|
+
above is sugar over it. Use it directly when you own the geometry.
|
|
21
173
|
|
|
22
174
|
The package intentionally contains no geographic boundary data. Provide a
|
|
23
175
|
GeoJSON feature collection or TopoJSON object plus stable IDs, labels, and
|
|
@@ -90,6 +242,50 @@ The breadcrumb gains a third segment. Its back step goes up exactly one level;
|
|
|
90
242
|
`defaultSubDistrictDrillDownId` as the uncontrolled path — and because a district ID
|
|
91
243
|
means nothing outside the state it came from, changing `drillDownId` clears it.
|
|
92
244
|
|
|
245
|
+
## Reacting to what the reader is looking at
|
|
246
|
+
|
|
247
|
+
Two props carry the same payload, and picking the wrong one is the most common
|
|
248
|
+
mistake with this component:
|
|
249
|
+
|
|
250
|
+
| | |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `renderInsights` | A **render slot**. Called during render, must be pure, returns nodes. |
|
|
253
|
+
| `onInsight` | A **callback**. Called from an effect. Set state, fire analytics, drive another panel from here. |
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// Wrong — this is a state update during render.
|
|
257
|
+
<BharatChoropleth renderInsights={(context) => { setInspected(context); return null; }} />
|
|
258
|
+
|
|
259
|
+
// Right.
|
|
260
|
+
<BharatChoropleth onInsight={(context) => setInspected(context)} />
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`onInspect` is the narrower version, firing for hover and keyboard focus with the
|
|
264
|
+
region alone; `onInsight` adds the scope total, share and rank.
|
|
265
|
+
|
|
266
|
+
## Lazy loaders must keep a stable identity
|
|
267
|
+
|
|
268
|
+
`loadDistricts` and `loadSubDistricts` are compared by identity, because a
|
|
269
|
+
genuinely different loader — a different boundary edition or reporting year —
|
|
270
|
+
has to refetch. An inline arrow is a new function on every render, and the
|
|
271
|
+
renderer cannot tell the two apart, so the level is refetched over the network
|
|
272
|
+
every time anything in the parent re-renders:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
// Refetches the district topology on every render.
|
|
276
|
+
<BharatChoropleth values={values} loadDistricts={async (id) => fetchDistricts(id, metric)} />
|
|
277
|
+
|
|
278
|
+
// Fetches once per state, and again only when `metric` actually changes.
|
|
279
|
+
const loadDistricts = useCallback(async (id) => fetchDistricts(id, metric), [metric]);
|
|
280
|
+
<BharatChoropleth values={values} loadDistricts={loadDistricts} />
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The component warns on the console when it sees a loader change three times
|
|
284
|
+
running without the level below it moving, which is the signature of the mistake.
|
|
285
|
+
|
|
286
|
+
If all you need is district *numbers* rather than different geometry, prefer
|
|
287
|
+
[`districtValues`](#district-values) — it repaints without refetching anything.
|
|
288
|
+
|
|
93
289
|
## Features
|
|
94
290
|
|
|
95
291
|
- Keyboard-accessible regions with Enter/Space activation and focus inspection.
|
|
@@ -97,9 +293,13 @@ means nothing outside the state it came from, changing `drillDownId` clears it.
|
|
|
97
293
|
- Controlled or uncontrolled selection and drill-down state.
|
|
98
294
|
- Optional neutral reference overlays kept outside statistical values.
|
|
99
295
|
- GeoJSON and TopoJSON inputs, with `d3-geo` projection sized to the SVG.
|
|
296
|
+
- `BharatChoropleth` for name-keyed values, fetched boundary data, and drill-down
|
|
297
|
+
configured for you.
|
|
100
298
|
|
|
101
|
-
`IndiaChoroplethProps` and
|
|
102
|
-
TypeScript consumers
|
|
299
|
+
`BharatChoroplethProps`, `IndiaChoroplethProps` and the related layer/context
|
|
300
|
+
types are exported for TypeScript consumers, along with the state registry
|
|
301
|
+
(`STATES`, `resolveState`, `normalizeStateKey`) if you want to resolve names
|
|
302
|
+
yourself. See the [repository](https://github.com/shashankbudem/bharat-choropleth)
|
|
103
303
|
for the full API, examples, boundary-data attribution, and the framework-free
|
|
104
304
|
[`bharat-choropleth-js`](https://www.npmjs.com/package/bharat-choropleth-js)
|
|
105
305
|
package.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { FeatureCollection, Geometry, GeoJsonProperties, Feature } from 'geojson';
|
|
4
4
|
import { Topology, GeometryObject } from 'topojson-specification';
|
|
5
5
|
|
|
6
6
|
/** A GeoJSON feature as consumed by the renderer. Keep properties data-provider defined. */
|
|
@@ -141,6 +141,15 @@ interface IndiaChoroplethProps {
|
|
|
141
141
|
/** Format both tooltip and legend values. */
|
|
142
142
|
formatValue?: (value: number) => string;
|
|
143
143
|
renderTooltip?: (context: TooltipContext) => ReactNode;
|
|
144
|
+
/**
|
|
145
|
+
* Renders host-owned content for the inspected region. A render slot: it is
|
|
146
|
+
* called during render and must be pure.
|
|
147
|
+
*
|
|
148
|
+
* To *react* to the inspected region — set state, fire analytics, sync another
|
|
149
|
+
* panel — use {@link IndiaChoroplethProps.onInsight}, which is the same payload
|
|
150
|
+
* delivered from an effect. Calling `setState` from here is a state update
|
|
151
|
+
* during render, and React will say so.
|
|
152
|
+
*/
|
|
144
153
|
renderInsights?: (context: InsightContext | null) => ReactNode;
|
|
145
154
|
/**
|
|
146
155
|
* Map-level visual chrome can be independently disabled/composed.
|
|
@@ -191,10 +200,189 @@ interface IndiaChoroplethProps {
|
|
|
191
200
|
interactive?: boolean;
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
declare function IndiaChoropleth({ states, referenceOverlay, loadDistricts, loadSubDistricts, loadDistrictReferenceOverlay, drillDownId, defaultDrillDownId, onDrillDownChange, subDistrictDrillDownId, defaultSubDistrictDrillDownId, onSubDistrictDrillDownChange, selectedId, defaultSelectedId, onSelectedChange, onInspect, onInsight, onRegionClick, onBackgroundClick, colorScale, formatValue, renderTooltip, renderInsights, showLegend, showBreadcrumb, legendLabels, referenceOverlayLegendLabel, referenceOverlayMergeIds, referenceOverlayFill, showRegionValues, minPartExtent, minDistrictPartExtent, className, ariaLabel, interactive, }: IndiaChoroplethProps): react.JSX.Element;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Where the optional prepared boundary bundles are fetched from when the caller
|
|
207
|
+
* doesn't supply their own `geometry`.
|
|
208
|
+
*
|
|
209
|
+
* The files are *fetched*, never bundled — the package still ships no boundary
|
|
210
|
+
* geometry, and each asset keeps its own source's licence and attribution
|
|
211
|
+
* alongside it (see `data/ATTRIBUTION.md`). The default bundle is
|
|
212
|
+
* `datta07/INDIAN-SHAPEFILES` (MIT):
|
|
213
|
+
*
|
|
214
|
+
* > State/UT boundaries derived from datta07/INDIAN-SHAPEFILES (MIT).
|
|
215
|
+
*
|
|
216
|
+
* Self-host by copying `data/generated/` next to your app and passing
|
|
217
|
+
* `dataBaseUrl: "/maps"`, which is the right call for offline or air-gapped
|
|
218
|
+
* deployments and avoids a third-party CDN request at runtime.
|
|
219
|
+
*
|
|
220
|
+
* The ref is pinned to an immutable tag on purpose. A branch ref (`@main`) is
|
|
221
|
+
* mutable and cached by jsDelivr for hours, so a data change would silently
|
|
222
|
+
* alter — or break — every consumer's map at a time nobody chose. Bump this
|
|
223
|
+
* deliberately, alongside a release.
|
|
224
|
+
*
|
|
225
|
+
* Bump it *in* the release, and check one asset of every level actually resolves
|
|
226
|
+
* at the new tag. Leaving it behind does not fail loudly: a level whose files
|
|
227
|
+
* the pinned tag predates 404s, and `loadSubDistrictTopology` reads a 404 as
|
|
228
|
+
* "this district has no sub-districts", so the whole level just quietly goes
|
|
229
|
+
* missing. That is exactly what 0.2.0 shipped with.
|
|
230
|
+
*/
|
|
231
|
+
declare const DEFAULT_DATA_BASE_URL = "https://cdn.jsdelivr.net/gh/shashankbudem/bharat-choropleth@v0.3.0/data/generated";
|
|
232
|
+
declare const ATTRIBUTION = "State/UT, district and sub-district boundaries derived from datta07/INDIAN-SHAPEFILES (MIT).";
|
|
233
|
+
/** Anything `geometry` accepts: inline data, a URL to fetch, or a promise of either. */
|
|
234
|
+
type GeometryInput = GeometrySource | string | Promise<GeometrySource | Topology | MapFeatureCollection>;
|
|
235
|
+
declare function loadDistrictTopology(baseUrl: string, stateId: string, signal?: AbortSignal): Promise<GeometrySource>;
|
|
194
236
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
237
|
+
* Sub-districts for one district, or `null` where the bundle has no file for it.
|
|
238
|
+
*
|
|
239
|
+
* A missing file is the bundle's way of saying a district has no sub-district
|
|
240
|
+
* level — three of the 788 current districts are in that position, and the
|
|
241
|
+
* prepared bundle deliberately ships no asset for them. So a 404 resolves to
|
|
242
|
+
* `null` (the district is a leaf) rather than raising, while any other failure
|
|
243
|
+
* still surfaces as an error the map can report.
|
|
197
244
|
*/
|
|
198
|
-
declare function
|
|
245
|
+
declare function loadSubDistrictTopology(baseUrl: string, districtId: string, signal?: AbortSignal): Promise<GeometrySource | null>;
|
|
246
|
+
|
|
247
|
+
interface BharatChoroplethProps extends Omit<IndiaChoroplethProps, "states"> {
|
|
248
|
+
/**
|
|
249
|
+
* Per-state values, keyed by any spelling the state registry accepts: display
|
|
250
|
+
* name, slug, LGD id, former name, or a separator-free form. `Goa`, `goa`,
|
|
251
|
+
* `Tamil Nadu`, `tamilnadu`, `Jammu & Kashmir`, `Orissa` and
|
|
252
|
+
* `in-cs-30-goa` all resolve. Names it does not recognize are ignored with a
|
|
253
|
+
* console warning rather than throwing.
|
|
254
|
+
*
|
|
255
|
+
* States you omit render as "no data", exactly as an explicit `null` does.
|
|
256
|
+
*
|
|
257
|
+
* This is the primary API. When both `values` and `data` are given, `values`
|
|
258
|
+
* wins and `data` is ignored — they are not merged.
|
|
259
|
+
*/
|
|
260
|
+
values?: Readonly<Record<string, number | null>>;
|
|
261
|
+
/**
|
|
262
|
+
* The same values as a row array, for data that already arrives that way.
|
|
263
|
+
* Read through `regionKey` and `valueKey`. Ignored when `values` is given.
|
|
264
|
+
*
|
|
265
|
+
* ```tsx
|
|
266
|
+
* <BharatChoropleth
|
|
267
|
+
* data={[{ state: "Telangana", value: 82 }]}
|
|
268
|
+
* regionKey="state"
|
|
269
|
+
* valueKey="value"
|
|
270
|
+
* />
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
data?: readonly Readonly<Record<string, unknown>>[];
|
|
274
|
+
/** Field on a `data` row holding the state name. Defaults to `"region"`. */
|
|
275
|
+
regionKey?: string;
|
|
276
|
+
/** Field on a `data` row holding the number. Defaults to `"value"`. Non-finite values read as "no data". */
|
|
277
|
+
valueKey?: string;
|
|
278
|
+
/**
|
|
279
|
+
* Boundary data for the state/UT layer: inline GeoJSON/TopoJSON, a URL string
|
|
280
|
+
* to fetch, or a promise of either. Omit to fetch the prepared current-vintage
|
|
281
|
+
* state bundle from `dataBaseUrl`. The package itself bundles no geometry.
|
|
282
|
+
*/
|
|
283
|
+
geometry?: GeometryInput;
|
|
284
|
+
/**
|
|
285
|
+
* District values, nested under the state each district belongs to.
|
|
286
|
+
*
|
|
287
|
+
* ```tsx
|
|
288
|
+
* districtValues={{
|
|
289
|
+
* Telangana: { Hyderabad: 90, "Ranga Reddy": 76 },
|
|
290
|
+
* Maharashtra: { Aurangabad: 44 },
|
|
291
|
+
* }}
|
|
292
|
+
* ```
|
|
293
|
+
*
|
|
294
|
+
* The nesting is not decoration. District names repeat across states —
|
|
295
|
+
* Aurangabad, Bilaspur and Hamirpur each name a district in two — and unlike
|
|
296
|
+
* states there is no district registry to resolve a bare name against, so a
|
|
297
|
+
* flat map could not say which one you meant. Under a state it is unambiguous.
|
|
298
|
+
*
|
|
299
|
+
* Outer keys resolve through the state registry, exactly like `values`, and are
|
|
300
|
+
* checked immediately. Inner keys match a district's name, slug or id,
|
|
301
|
+
* case-insensitively; they can only be checked once that state's districts have
|
|
302
|
+
* been fetched, so a typo there is warned about when you first drill into it.
|
|
303
|
+
*
|
|
304
|
+
* Applies to whichever district layer is in use, including one from your own
|
|
305
|
+
* `loadDistricts`: a district listed here takes this value, and any district not
|
|
306
|
+
* listed keeps whatever the layer itself returned.
|
|
307
|
+
*
|
|
308
|
+
* There is no `subDistrictValues`. Three levels of nesting stops reading
|
|
309
|
+
* clearly, and sub-district naming is far less settled than district naming —
|
|
310
|
+
* set those through a custom `loadSubDistricts` instead.
|
|
311
|
+
*/
|
|
312
|
+
districtValues?: Readonly<Record<string, Readonly<Record<string, number | null>>>>;
|
|
313
|
+
/** Base URL for the prepared boundary bundles. Point it at your own copy of `data/generated` to self-host. */
|
|
314
|
+
dataBaseUrl?: string;
|
|
315
|
+
/**
|
|
316
|
+
* Click-to-drill-down into districts. Defaults to `true` when the state layer
|
|
317
|
+
* came from `dataBaseUrl` (district files live beside it), `false` when you
|
|
318
|
+
* supplied your own `geometry` — pass `loadDistricts` yourself in that case.
|
|
319
|
+
*/
|
|
320
|
+
districts?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Click-to-drill-down from a district into its sub-districts (tehsils / taluks /
|
|
323
|
+
* mandals / blocks). Defaults the same way `districts` does. Districts the
|
|
324
|
+
* bundle has no sub-districts for stay leaves rather than erroring.
|
|
325
|
+
*/
|
|
326
|
+
subDistricts?: boolean;
|
|
327
|
+
/** Reads a feature's stable id. Defaults to `feature.properties.id`. Memoize a custom one — a new identity re-projects the map. */
|
|
328
|
+
getId?: (feature: MapFeature) => string;
|
|
329
|
+
/** Reads a feature's display name. Defaults to `feature.properties.name`. Memoize a custom one — a new identity re-projects the map. */
|
|
330
|
+
getLabel?: (feature: MapFeature) => string;
|
|
331
|
+
/** Called if boundary data fails to load. Without it the error is logged; either way the message is rendered in place of the map. */
|
|
332
|
+
onError?: (error: Error) => void;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The zero-config map: give it numbers keyed by state name, get a choropleth.
|
|
336
|
+
*
|
|
337
|
+
* ```tsx
|
|
338
|
+
* import { BharatChoropleth } from "bharat-choropleth";
|
|
339
|
+
* import "bharat-choropleth/style.css";
|
|
340
|
+
*
|
|
341
|
+
* <BharatChoropleth values={{ Telangana: 82, Karnataka: 74, Maharashtra: 91 }} />
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* Boundary data is fetched (never bundled), so the map shows a placeholder until
|
|
345
|
+
* it lands. Drill-down into districts and sub-districts is on by default when
|
|
346
|
+
* that default data source is in use.
|
|
347
|
+
*
|
|
348
|
+
* This is sugar over {@link IndiaChoropleth}, which remains the full renderer —
|
|
349
|
+
* custom layers, controlled selection and drill-down, reference overlays, custom
|
|
350
|
+
* tooltips. Every one of its props except `states` passes straight through, so
|
|
351
|
+
* reaching for one is a prop, not a rewrite.
|
|
352
|
+
*/
|
|
353
|
+
declare function BharatChoropleth({ values, data, districtValues, regionKey, valueKey, geometry, dataBaseUrl, districts, subDistricts, getId, getLabel, onError, ...rest }: BharatChoroplethProps): react.JSX.Element;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Static registry of the 36 current state/UT identities: id, display name, slug.
|
|
357
|
+
*
|
|
358
|
+
* This is *metadata only* — no coordinates, no boundary geometry. It exists so
|
|
359
|
+
* `map.goa = 6` can be recognized, validated and warned about the instant the
|
|
360
|
+
* script runs, before the (asynchronously fetched) boundary file has landed.
|
|
361
|
+
* Boundary geometry itself still never ships inside this package.
|
|
362
|
+
*
|
|
363
|
+
* Kept in sync with `data/generated/current-2019-states/manifest.json`; ids are
|
|
364
|
+
* LGD-derived and match the `districts/{stateId}.topo.json` filenames.
|
|
365
|
+
*/
|
|
366
|
+
interface StateIdentity {
|
|
367
|
+
/** LGD-derived stable id, e.g. `in-cs-30-goa`. */
|
|
368
|
+
id: string;
|
|
369
|
+
/** Display name as it appears in the boundary data, e.g. `Jammu & Kashmir`. */
|
|
370
|
+
name: string;
|
|
371
|
+
/** Hyphenated slug as it appears in the boundary data, e.g. `jammu-and-kashmir`. */
|
|
372
|
+
slug: string;
|
|
373
|
+
}
|
|
374
|
+
declare const STATES: readonly StateIdentity[];
|
|
375
|
+
/**
|
|
376
|
+
* Canonical key for any user-supplied spelling: lowercase, `&` → `and`, every
|
|
377
|
+
* run of non-alphanumerics → a single `-`. So `Tamil Nadu`, `tamil_nadu`,
|
|
378
|
+
* `TAMIL-NADU` and `tamil nadu` all collapse to `tamil-nadu`.
|
|
379
|
+
*/
|
|
380
|
+
declare function normalizeStateKey(input: string): string;
|
|
381
|
+
/**
|
|
382
|
+
* Resolve any spelling of a state/UT — display name, slug, LGD id, underscore
|
|
383
|
+
* form, alias, or separator-free form — to its canonical identity.
|
|
384
|
+
* Returns `undefined` for anything unrecognized, which callers treat as a typo.
|
|
385
|
+
*/
|
|
386
|
+
declare function resolveState(input: string): StateIdentity | undefined;
|
|
199
387
|
|
|
200
|
-
export { type ColorContext, type ColorScale, type DistrictLoader, type DistrictReferenceOverlayLoader, type GeometrySource, IndiaChoropleth, type IndiaChoroplethProps, type InsightContext, type MapFeature, type MapFeatureCollection, type MapLayer, type MapLevel, type MapRegion, type ReferenceOverlay, type SubDistrictLoader, type TooltipContext };
|
|
388
|
+
export { ATTRIBUTION, BharatChoropleth, type BharatChoroplethProps, type ColorContext, type ColorScale, DEFAULT_DATA_BASE_URL, type DistrictLoader, type DistrictReferenceOverlayLoader, type GeometryInput, type GeometrySource, IndiaChoropleth, type IndiaChoroplethProps, type InsightContext, type MapFeature, type MapFeatureCollection, type MapLayer, type MapLevel, type MapRegion, type ReferenceOverlay, STATES, type StateIdentity, type SubDistrictLoader, type TooltipContext, loadDistrictTopology, loadSubDistrictTopology, normalizeStateKey, resolveState };
|