choropleth-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manuel Martín
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,216 @@
1
- # Temporary Holding Version
1
+ # choropleth-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![The same map with the four built-in themes: light, dark, minimal and contrast](https://raw.githubusercontent.com/Manoplo/choropleth-kit/master/docs/assets/themes.png)
4
+
5
+ Choropleth (thematic) maps for React: SVG on top of `d3-geo`, with zoom, drill-down between levels,
6
+ legends, tooltips and theming. The common case is **ten lines**, and when you need more you can drop
7
+ down a level without rewriting anything.
8
+
9
+ - **Three API levels, one implementation:** `<ChoroplethMap>` (preset) → `<Choropleth.Root>` and its pieces (compound) → `useChoropleth` (headless).
10
+ - **Four variants:** `sequential`, `diverging`, `categorical` and `bubble` (size + color).
11
+ - **Camera:** zoom and pan with a cooperative wheel, pinch, inertia, controls and programmatic flights (`await map.zoomTo("ES")`).
12
+ - **Drill-down:** world → country → region → stores, declaring the levels once.
13
+ - **Performance:** hover, zoom and pan do not re-render the map (interaction state lives outside React).
14
+ - **Theming:** `--cm-*` variables, four themes, `classNames` (Tailwind) and a `data-*` contract.
15
+ - **Two maps on the same page never share ids** (patterns, filters).
16
+
17
+ > Status: **MVP (v0.1)**. The API may change before 1.0. What is left out of the MVP and the known
18
+ > issues live in [`evolution-and-issues/`](./evolution-and-issues/README.md).
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add choropleth-kit # or: npm i choropleth-kit / yarn add choropleth-kit
24
+ ```
25
+
26
+ Requires `react` and `react-dom` ≥ 18 (peer dependencies). The package is **ESM only** and ships its own types.
27
+
28
+ ## A map in 10 lines
29
+
30
+ ```tsx
31
+ import { ChoroplethMap } from "choropleth-kit";
32
+ import { world110m } from "choropleth-kit/geo"; // world at 1:110m with ISO alpha-2 ids ("ES")
33
+ import "choropleth-kit/styles.css";
34
+
35
+ const data = [
36
+ { country: "ES", sales: 120 },
37
+ { country: "FR", sales: 95 },
38
+ { country: "DE", sales: 150 },
39
+ ];
40
+
41
+ export const SalesMap = () => (
42
+ <ChoroplethMap
43
+ geography={world110m}
44
+ objectName="countries"
45
+ data={data}
46
+ getId={(row) => row.country}
47
+ getValue={(row) => row.sales}
48
+ variant="sequential"
49
+ legend={{ title: "Sales" }}
50
+ />
51
+ );
52
+ ```
53
+
54
+ The map takes the width of its container. Regions without data are painted as "no data" (gray, or
55
+ hatched with `emptyPattern`). If an id in your data does not match any region, in development the
56
+ console shows a warning that explains it (and `onJoinReport` gives you the full report).
57
+ In development the console also reports how many regions have no data (with a 3-country sample you
58
+ will see `172 region(s) have no data`); `diagnostics={false}` turns it off.
59
+
60
+ ## What to import from where
61
+
62
+ | Import | Contents |
63
+ | --------------------------- | -------------------------------------------------------------------------------------------- |
64
+ | `choropleth-kit` | React components and hooks (`ChoroplethMap`, `Choropleth.*`, `useChoropleth`, `useCamera`…). |
65
+ | `choropleth-kit/styles.css` | Base styles (everything uses `:where()` and `--cm-*` variables, easy to override). |
66
+ | `choropleth-kit/core` | Pure functions with no React or DOM (scales, classification, camera, drill machine, `es`…). |
67
+ | `choropleth-kit/geo` | Geodata: `world110m`, `isoNumericToAlpha2`, `worldFallbackPoints` (microstates as points). |
68
+
69
+ `geo` weighs ≈ 40 KB gzip (it is data): if you bring your own geography (TopoJSON or GeoJSON, also
70
+ async with `() => import("./map.json")`), do not import it and it stays out of your bundle.
71
+
72
+ ## Variants
73
+
74
+ ```tsx
75
+ // Categories with fixed colors
76
+ <ChoroplethMap {...common} variant="categorical" getCategory={(r) => r.zone} />
77
+
78
+ // Diverging (with a midpoint)
79
+ <ChoroplethMap {...common} variant="diverging" getValue={(r) => r.growth} midpoint={0} />
80
+
81
+ // Bubbles with their own coordinates (stores): size = sales, color = change
82
+ <ChoroplethMap
83
+ {...common}
84
+ variant="bubble"
85
+ getValue={(s) => s.sales}
86
+ getColorValue={(s) => s.growth}
87
+ getCoordinates={(s) => s.at} // [lon, lat]
88
+ />
89
+ ```
90
+
91
+ ## Interaction
92
+
93
+ ```tsx
94
+ const [selectedId, setSelectedId] = useState<string | null>(null);
95
+
96
+ <ChoroplethMap
97
+ {...common}
98
+ selectedId={selectedId}
99
+ onItemClick={({ id }) => setSelectedId(id)}
100
+ onItemHover={(item) => console.log(item?.id)}
101
+ zoom={{ wheel: "cooperative", controls: true }} // the wheel only zooms with Ctrl/⌘
102
+ renderTooltip={({ label, formattedValue }) => (
103
+ <b>
104
+ {label}: {formattedValue}
105
+ </b>
106
+ )}
107
+ />;
108
+ ```
109
+
110
+ - `zoom={false}` turns the camera off. By default the wheel zooms with the mouse over the map
111
+ (`wheel: "always"`); if the map lives in a scrolling page, use `"cooperative"`.
112
+ - The **imperative handle** (`ref`) returns _promises_, so flights can be chained without timers:
113
+
114
+ ```tsx
115
+ const ref = useRef<ChoroplethMapHandle>(null);
116
+ await ref.current?.zoomTo("ES");
117
+ await ref.current?.zoomTo("FR");
118
+ ref.current?.getView(); // { zoom, center, bounds, level… }
119
+ ```
120
+
121
+ - `onViewChange` fires **at the end** of each gesture or flight, never per frame. Inside the compound,
122
+ `useCamera((view) => view.zoom > 3)` subscribes to that confirmed camera with a selector.
123
+
124
+ ## Drill-down
125
+
126
+ ```tsx
127
+ import { ChoroplethMap, defineDrillLevels } from "choropleth-kit";
128
+
129
+ const levels = defineDrillLevels<Row>([
130
+ { id: "country", label: "Countries", geography: world110m, objectName: "countries" },
131
+ {
132
+ id: "region",
133
+ geography: ({ parentId }) => import(`./geo/admin1/${parentId}.json`), // loaded on demand
134
+ },
135
+ ]);
136
+
137
+ <ChoroplethMap
138
+ drill={levels}
139
+ data={({ depth, parentId }) => rows.filter((r) => r.depth === depth && r.parentId === parentId)}
140
+ getId={(r) => r.id}
141
+ variant="sequential"
142
+ getValue={(r) => r.value}
143
+ />;
144
+ ```
145
+
146
+ The library loads **geometry**, never business data: you pass the `data` of the current level. There
147
+ are breadcrumbs (`Breadcrumb`), Escape/Backspace to go up, `onDrillIntent` (for prefetching), and it can
148
+ be driven from the URL with `useDrillDown({ path, onPathChange })`. On touch, the first tap on a
149
+ drillable region previews it and the second one drills down.
150
+
151
+ ## Theming
152
+
153
+ ```tsx
154
+ <ChoroplethMap {...common} theme="dark" emptyPattern="hatch" />
155
+ ```
156
+
157
+ - Themes: `"light"`, `"dark"`, `"minimal"`, `"contrast"`; or an object built with `defineTheme` / `extendTheme`.
158
+ - Any `--cm-*` variable can be overridden on an ancestor (`--cm-border-color`, `--cm-hover-stroke`…).
159
+ - `classNames={{ root: "…", tooltip: "…" }}` (also `region`, `legend`, `controls`, `breadcrumb`…) adds your classes
160
+ (Tailwind, CSS Modules) to the `cm-*` ones; states are exposed as `data-*` attributes on the regions
161
+ (`data-class`, `data-empty`, `data-drillable`…) so you can style them with selectors.
162
+ - `unstyled` removes all the base CSS.
163
+ - UI texts are in English and can be replaced with `labels` (a Spanish pack ships with the library: `import { es } from "choropleth-kit/core"`).
164
+
165
+ ## Geodata and borders
166
+
167
+ `world110m` is Natural Earth 1:110m (public domain) with ISO alpha-2 ids, Kosovo (`XK`) included.
168
+ Northern Cyprus and Somaliland have no id and are painted as a non-interactive layer.
169
+
170
+ > ⚠️ **Disputed borders** (Western Sahara, Crimea, Kashmir…) follow Natural Earth's _de facto_ boundaries.
171
+ > If you publish the map in a product, check that they are what you want to show. You can always pass your
172
+ > own geography through the `geography` prop.
173
+
174
+ ## Size
175
+
176
+ Measured with [`size-limit`](https://github.com/ai/size-limit) (gzip, minified; `pnpm size`):
177
+
178
+ | What | Gzip |
179
+ | --------------------------------------------------------- | ------- |
180
+ | Own code (`core` + `react`, dependencies external) | ≈ 29 KB |
181
+ | `ChoroplethMap` with all of `d3` included (without React) | ≈ 72 KB |
182
+ | `styles.css` | ≈ 2 KB |
183
+ | `choropleth-kit/geo` (data) | ≈ 40 KB |
184
+
185
+ ## Compatibility and known limits
186
+
187
+ - ESM only. In CommonJS: `await import("choropleth-kit")`.
188
+ - **Full accessibility** (arrow-key navigation between regions, alternative table, `aria-live`
189
+ announcements) is **not in the MVP**. Today there is semantic structure in the breadcrumb, `Escape`
190
+ and keyboard zoom.
191
+ - Out of the MVP: label/marker layers, ⭐ animations, SVG/PNG export, globe, clustering. List and
192
+ priorities in [`evolution-and-issues/`](./evolution-and-issues/README.md).
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ pnpm install # pnpm only
198
+ pnpm storybook # living documentation: one story per case
199
+ pnpm verify # typecheck + lint + format + tests + build + size + publint/attw
200
+ ```
201
+
202
+ Planned features and known issues: [`evolution-and-issues/`](./evolution-and-issues/README.md).
203
+ Rules for coding agents: [`AGENTS.md`](./AGENTS.md). Publishing steps: [`PUBLISHING.md`](./PUBLISHING.md).
204
+
205
+ ## Data and third-party notices
206
+
207
+ - **Geometry** in `choropleth-kit/geo` is derived from [`world-atlas`](https://github.com/topojson/world-atlas)
208
+ (ISC License, Copyright (c) Mike Bostock), which is built from [Natural Earth](https://www.naturalearthdata.com/)
209
+ (public domain).
210
+ - The ISO 3166 numeric → alpha-2 table was generated at build time with
211
+ [`i18n-iso-countries`](https://github.com/michaelwittig/node-i18n-iso-countries) (MIT).
212
+ - Runtime dependencies (`d3-*`, `topojson-client`, `polylabel`) are ISC-licensed and installed separately.
213
+
214
+ ## License
215
+
216
+ [MIT](./LICENSE) © Manuel Martín