Skip to content
txbrendan/ui

Choropleth Explorer

beta

A 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

Top countries

Loading

a country

Top regions

Loading

loading

Top countries

Loading

map missing

Top countries

Loading

empty

The map fills in once members sign up.

narrow container

Top regions

Loading

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the block. It also adds every component it uses. Its own files go in your components folder, 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

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