bharat-choropleth 0.1.0 → 0.3.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/CHANGELOG.md +121 -0
- package/README.md +239 -5
- package/dist/index.d.ts +228 -10
- package/dist/index.js +575 -23
- package/dist/index.js.map +1 -1
- package/dist/style.css +4 -0
- package/package.json +2 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Values keyed by a feature id the state registry does not know now match. A
|
|
8
|
+
feature was keyed by its id only if the registry recognised it, and otherwise
|
|
9
|
+
fell back to its *label* — so a dataset keyed by id against geometry outside
|
|
10
|
+
the registry, such as this repo's own historical Census bundle with its
|
|
11
|
+
`in-hs-*` ids, silently rendered as "No data" on every region while looking
|
|
12
|
+
perfectly healthy. Each key a caller writes is now recorded against the
|
|
13
|
+
canonical key it addresses, and a feature is matched by exact id, exact label,
|
|
14
|
+
then either resolved through the registry. Found by building a dashboard on
|
|
15
|
+
the historical bundle.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `IndiaChoropleth` warns when `loadDistricts` or `loadSubDistricts` has been a
|
|
20
|
+
different function on three renders running while the level it loads has not
|
|
21
|
+
moved. That is the signature of an inline arrow: the loaders are compared by
|
|
22
|
+
identity because a genuinely different loader must refetch, so an unstable one
|
|
23
|
+
silently refetches the level over the network on every unrelated re-render
|
|
24
|
+
while still rendering correctly. One deliberate swap does not warn.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Documented `renderInsights` as a pure render slot and pointed at `onInsight`
|
|
29
|
+
for reacting to the inspected region. Calling `setState` from `renderInsights`
|
|
30
|
+
is a state update during render; the two props carry the same payload.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The default data source now points at the `v0.3.0` boundary bundle. A release
|
|
35
|
+
pins the geometry a consumer receives, not only the renderer that draws it.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Changing values no longer reloads the level below. Repainting means handing
|
|
40
|
+
the renderer a new `MapLayer`, and the district and sub-district loaders keyed
|
|
41
|
+
their effects on the prepared regions — which carry values — so every number
|
|
42
|
+
that moved called the loader again and blanked the level while the promise was
|
|
43
|
+
in flight. A map drilled into a state, driven by a timer or a slider, blinked
|
|
44
|
+
its districts away on every tick. The loaders now key on the geometry and id
|
|
45
|
+
accessor, the things that decide which regions exist. A real geometry change
|
|
46
|
+
still reloads.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Repainting no longer re-unpacks the topology or refits the projection. Both
|
|
51
|
+
are keyed on `states.geometry`, and the decoded features are passed into layer
|
|
52
|
+
preparation instead of being unpacked a second time. Values live on the layer,
|
|
53
|
+
not the geometry, so neither step could have changed its answer.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `districtValues`, district numbers nested under the state they belong to:
|
|
58
|
+
`districtValues={{ Telangana: { Hyderabad: 90 } }}`. The nesting is what makes
|
|
59
|
+
it safe — district names repeat across states (Aurangabad, Bilaspur and
|
|
60
|
+
Hamirpur each name two) and there is no district registry to resolve a bare
|
|
61
|
+
name against. Outer keys resolve through the state registry and are checked at
|
|
62
|
+
once; inner keys match a district's name, slug or id and are checked when that
|
|
63
|
+
state's districts arrive. It overlays whichever district layer is in use,
|
|
64
|
+
including one from a caller's own `loadDistricts`, leaving districts it does
|
|
65
|
+
not name at whatever that layer returned. There is deliberately no
|
|
66
|
+
`subDistrictValues`.
|
|
67
|
+
- `loadDistrictTopology` and `loadSubDistrictTopology` are exported, so fetching
|
|
68
|
+
the prepared bundles from a custom loader does not mean re-deriving their URL
|
|
69
|
+
scheme by hand. The framework-free package exports them too now.
|
|
70
|
+
- `BharatChoropleth`, a zero-config component over the existing renderer:
|
|
71
|
+
`<BharatChoropleth values={{ Telangana: 82 }} />` is a working map. Keys
|
|
72
|
+
resolve through the state registry the framework-free package already used —
|
|
73
|
+
display names, slugs, LGD ids, case-insensitive and separator-free forms, `&`
|
|
74
|
+
normalized to `and`, and former names such as `Orissa` — so it accepts the
|
|
75
|
+
same spellings as `bharat-choropleth-js`. Row-shaped input is read through
|
|
76
|
+
`data` + `regionKey` + `valueKey`; `values` wins when both are given.
|
|
77
|
+
- Boundary data is fetched from `dataBaseUrl` when no `geometry` is supplied, and
|
|
78
|
+
district / sub-district drill-down defaults on in that case, matching the
|
|
79
|
+
framework-free facade. Drill-down geometry is fetched once per id and reused,
|
|
80
|
+
so changing `values` repaints without refetching it.
|
|
81
|
+
- Exported `BharatChoroplethProps`, the state registry (`STATES`, `resolveState`,
|
|
82
|
+
`normalizeStateKey`), `ATTRIBUTION`, `DEFAULT_DATA_BASE_URL` and `GeometryInput`,
|
|
83
|
+
matching what `bharat-choropleth-js` exports.
|
|
84
|
+
- The stylesheet gained the `.bharat-choropleth__status` placeholder rules the
|
|
85
|
+
framework-free package already carried, used while boundary data loads and for
|
|
86
|
+
the message if it fails.
|
|
87
|
+
|
|
88
|
+
`IndiaChoropleth` is unchanged and remains the full API — `BharatChoropleth` is
|
|
89
|
+
sugar over it, and passes every one of its props except `states` straight through.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- The default data source now points at the `v0.2.0` boundary bundle. It was
|
|
94
|
+
still pinned to `v0.1.0`, which predates
|
|
95
|
+
`data/generated/current-2019-subdistricts/`, so every sub-district request
|
|
96
|
+
404'd — and because a 404 means "this district has no sub-district level", the
|
|
97
|
+
level 0.2.0 added was silently unreachable for anyone on the default
|
|
98
|
+
`dataBaseUrl`. Self-hosted deployments were unaffected.
|
|
99
|
+
|
|
100
|
+
## 0.2.0
|
|
101
|
+
|
|
102
|
+
- Added an optional sub-district level below districts — tehsils, taluks,
|
|
103
|
+
mandals and blocks. `loadSubDistricts` is called only after a district is
|
|
104
|
+
activated, so that geometry stays code-split the way districts already are,
|
|
105
|
+
and `subDistrictDrillDownId` / `defaultSubDistrictDrillDownId` /
|
|
106
|
+
`onSubDistrictDrillDownChange` drive it controlled or uncontrolled.
|
|
107
|
+
- A `loadSubDistricts` that resolves `null` marks that district a leaf: the map
|
|
108
|
+
stays on the district view and selects it rather than opening an empty level,
|
|
109
|
+
and the district is not asked again. Omit the prop entirely and every district
|
|
110
|
+
stays a leaf, exactly as before.
|
|
111
|
+
- Exported `MapLevel` and `SubDistrictLoader`.
|
|
112
|
+
- **Possibly breaking for TypeScript callers:** `level` on `TooltipContext` and
|
|
113
|
+
`InsightContext` widened from `"state" | "district"` to `MapLevel`, which adds
|
|
114
|
+
`"subdistrict"`. A `switch` over it that the compiler checks for
|
|
115
|
+
exhaustiveness now needs the third case. Nothing changes at runtime for a
|
|
116
|
+
two-level map.
|
|
117
|
+
|
|
118
|
+
## 0.1.0
|
|
119
|
+
|
|
120
|
+
- Initial public release of the React India state and district choropleth
|
|
121
|
+
component.
|
package/README.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# `bharat-choropleth`
|
|
2
2
|
|
|
3
3
|
An accessible React SVG choropleth for India state and district dashboards.
|
|
4
|
-
It
|
|
5
|
-
|
|
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.
|
|
6
13
|
|
|
7
14
|
## Install
|
|
8
15
|
|
|
@@ -16,7 +23,153 @@ Import the stylesheet once in the application that mounts the map:
|
|
|
16
23
|
import "bharat-choropleth/style.css";
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
##
|
|
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.
|
|
20
173
|
|
|
21
174
|
The package intentionally contains no geographic boundary data. Provide a
|
|
22
175
|
GeoJSON feature collection or TopoJSON object plus stable IDs, labels, and
|
|
@@ -56,6 +209,83 @@ selected stable state ID and returns another `MapLayer`:
|
|
|
56
209
|
/>
|
|
57
210
|
```
|
|
58
211
|
|
|
212
|
+
Add `loadSubDistricts` for a third level below districts. It receives the district
|
|
213
|
+
ID, the district region, and the state ID it sits in:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<IndiaChoropleth
|
|
217
|
+
states={states}
|
|
218
|
+
loadDistricts={loadDistricts}
|
|
219
|
+
loadSubDistricts={async (districtId) => {
|
|
220
|
+
const topology = await import(`./subdistricts/${districtId}.topo.json`);
|
|
221
|
+
return {
|
|
222
|
+
geometry: { topology: topology.default, object: "subdistricts" },
|
|
223
|
+
getId: (feature) => String(feature.properties?.id),
|
|
224
|
+
getLabel: (feature) => String(feature.properties?.name),
|
|
225
|
+
getValue: (feature) => subDistrictValues[String(feature.properties?.id)] ?? null,
|
|
226
|
+
};
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Return `null` for a district that has no sub-district level. Not every district has
|
|
232
|
+
one, and a district that returns `null` is left as a leaf — the map stays on the
|
|
233
|
+
district view and selects it, rather than opening a level with nothing in it. Once a
|
|
234
|
+
district has answered `null` it stops offering the level and is not asked again.
|
|
235
|
+
|
|
236
|
+
Without `loadSubDistricts`, a district is a leaf and activation only selects it,
|
|
237
|
+
exactly as before.
|
|
238
|
+
|
|
239
|
+
The breadcrumb gains a third segment. Its back step goes up exactly one level;
|
|
240
|
+
"All states" is the one-step return to the national map. Controlled usage adds
|
|
241
|
+
`subDistrictDrillDownId` / `onSubDistrictDrillDownChange`, with
|
|
242
|
+
`defaultSubDistrictDrillDownId` as the uncontrolled path — and because a district ID
|
|
243
|
+
means nothing outside the state it came from, changing `drillDownId` clears it.
|
|
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
|
+
|
|
59
289
|
## Features
|
|
60
290
|
|
|
61
291
|
- Keyboard-accessible regions with Enter/Space activation and focus inspection.
|
|
@@ -63,9 +293,13 @@ selected stable state ID and returns another `MapLayer`:
|
|
|
63
293
|
- Controlled or uncontrolled selection and drill-down state.
|
|
64
294
|
- Optional neutral reference overlays kept outside statistical values.
|
|
65
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.
|
|
66
298
|
|
|
67
|
-
`IndiaChoroplethProps` and
|
|
68
|
-
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)
|
|
69
303
|
for the full API, examples, boundary-data attribution, and the framework-free
|
|
70
304
|
[`bharat-choropleth-js`](https://www.npmjs.com/package/bharat-choropleth-js)
|
|
71
305
|
package.
|