Choropleth Explorer
betaA world map of one measure. Choose a country to see its states, provinces, or regions, with a breadcrumb back, a country search, a ranked list, and a tab per measure.
Built from Breadcrumb, Breakdown List, Card, Chart, Choropleth Map, Combobox, and Tabs.
Preview
Members
Loading
a country
Loading
loading
Loading
map missing
Loading
empty
The map fills in once members sign up.
narrow container
Loading
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the block. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/choropleth-explorer
Use it for
- A measure by place, where people start with the world and look closer at one country, like members, sales, or sign-ups.
- Pass metrics, each with values for countries and their regions together, like { USA: 340, 'US-TX': 31 }. Countries use ISO 3166-1 alpha-3 codes, and regions ISO 3166-2 codes, where the place has one.
- The maps load from src, '/maps' by default: world.json, then admin1/<ID>.json for the country chosen. This site serves them. In your app, copy the files into public/maps, or pass loadShape.
- The line under the map says how many regions there are, how many have data, and that states or provinces are as deep as the maps go.
- Pass country and onCountryChange to keep the chosen country in the URL.
Avoid it for
- Cities, counties, or streets. These maps stop at states and provinces.
- One country only. Use ChoroplethMap with that country's shape.
- Exact comparisons across many places. Use a table, sorted by the measure.
Props
- metrics
One tab per metric. Values hold countries and their regions together, by id.
{ id: string; label: string; values: Record<string, number | null>; valueFormat?: Intl.NumberFormatOptions }[]
- title
The block's heading. It also names the section.
ReactNode
- src
Where world.json and admin1/<ID>.json are.
string · default "/maps"
- loadShape
Loads a map yourself, by 'world' or a country id, in place of src. Define it outside the component, so each map loads once.
(id: string) => Promise<ExplorerShape>
- country
The country shown, by id. null shows the world. Use with onCountryChange.
string | null
- defaultCountry
The country shown first, when you do not control it.
string | null · default null
- onCountryChange
Called when someone chooses a country or goes back to the world.
(country: string | null) => void
- loading
Shows placeholders while the metrics load.
boolean · default false
- emptyText
Shown when there are no metrics.
ReactNode · default "No data to map yet."
- top
How many places the ranked list shows.
number · default 8
- className
Extra classes on the section, which holds the container query.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| metrics | { id: string; label: string; values: Record<string, number | null>; valueFormat?: Intl.NumberFormatOptions }[] | - | One tab per metric. Values hold countries and their regions together, by id. |
| title | ReactNode | - | The block's heading. It also names the section. |
| src | string | "/maps" | Where world.json and admin1/<ID>.json are. |
| loadShape | (id: string) => Promise<ExplorerShape> | - | Loads a map yourself, by 'world' or a country id, in place of src. Define it outside the component, so each map loads once. |
| country | string | null | - | The country shown, by id. null shows the world. Use with onCountryChange. |
| defaultCountry | string | null | null | The country shown first, when you do not control it. |
| onCountryChange | (country: string | null) => void | - | Called when someone chooses a country or goes back to the world. |
| loading | boolean | false | Shows placeholders while the metrics load. |
| emptyText | ReactNode | "No data to map yet." | Shown when there are no metrics. |
| top | number | 8 | How many places the ranked list shows. |
| className | string | - | Extra classes on the section, which holds the container query. |
Accessibility
- The section is named by its title. The breadcrumb is a nav named 'Map level', and World is a button while a country shows.
- Every country has its own option in the search, so a country too small to point at is still one choice away.
- The map is one Tab stop, and arrow keys move between places. On the world map, Enter opens the country. In a country, Enter marks a region.
- A status line says each new map as it loads, like 'Members, Japan: 47 prefectures'.
- The ranked list repeats the top values as text, so nobody has to read them from shades.
Examples
Members by country and region
<ChoroplethExplorer
title="Members"
metrics={[
{ id: "members", label: "Members", values: { USA: 340, "US-TX": 31, JPN: 120, "JP-13": 40 } },
{ id: "growth", label: "Growth", values: growth, valueFormat: { style: "percent" } },
]}
/>Keep the country in the URL
<ChoroplethExplorer metrics={metrics} country={params.country ?? null} onCountryChange={setCountry} />