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 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 renders a supplied state layer, supports keyboard and pointer inspection,
5
- and can lazy-load district layers when a user selects a state, then sub-district
6
- layers when a user selects a district.
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
- ## 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.
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 its related layer/context types are exported for
102
- 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)
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 { Feature, Geometry, GeoJsonProperties, FeatureCollection } from 'geojson';
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
- * A data-agnostic, accessible SVG India map renderer. Import `@india-choropleth/react/style.css`
196
- * once in the host app; data and boundaries intentionally remain separate.
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 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;
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 };