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 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 renders a supplied state layer, supports keyboard and pointer inspection,
5
- and can lazy-load district layers when a user selects a state.
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
- ## Use
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 its related layer/context types are exported for
68
- TypeScript consumers. See the [repository](https://github.com/shashankbudem/bharat-choropleth)
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.