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 +21 -0
- package/README.md +215 -2
- package/dist/chunk-R3XS6XPP.js +1816 -0
- package/dist/core.d.ts +123 -0
- package/dist/core.js +197 -0
- package/dist/drill-chain-DCiuiZKK.d.ts +1247 -0
- package/dist/geo.d.ts +35 -0
- package/dist/geo.js +361 -0
- package/dist/index.d.ts +1186 -0
- package/dist/index.js +2926 -0
- package/dist/styles.css +417 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +138 -3
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
|
-
#
|
|
1
|
+
# choropleth-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
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
|