Choropleth Chart
Regional data on a map — each region filled by its value's bin on a sequential ramp, with a stepped legend, a keyboard walk over named regions, and optional zoom, graticule and textures.
pnpm add @tessinaui/uiWhen to use
- One measure across regions, where location matters. Users by country, revenue by state, turnout by district: the map shows where the high and low regions are, and whether they cluster.
- To spot a regional pattern at a glance. A north–south split, a coast against an interior, a single outlier region.
- Beside the numbers. Pair the map with a ranked table (Accessible name and data table); the map shows the pattern and the table gives the values.
When not to use
- To compare exact values between regions. A tint is a bin, not a number. Use a sorted BarChart.
- When the regions' sizes would mislead. A large, sparsely populated region draws the eye whatever its value. Show a rate (per person, per store) rather than a total, or use a bar chart.
- For change over time. Use LineChart; one map per period is hard to compare.
- For places that are points. Stores, offices and events are locations, not areas. This chart fills regions only.
- For a handful of regions. With five regions, a list or a bar chart reads faster than a map.
Installation
pnpm add @tessinaui/ui d3-geoThe package ships no map data. Load GeoJSON for your regions; to convert a
TopoJSON atlas, add topojson-client:
pnpm add topojson-clientUsage
import * as React from "react";
import { feature } from "topojson-client";
import { ChoroplethChart } from "@tessinaui/ui/choropleth-chart";
import type { ChoroplethFeatureCollection } from "@tessinaui/ui/choropleth-chart";
const users = { France: 9100, Germany: 12800, Spain: 4300 };
export function UsersByCountry() {
const [countries, setCountries] = React.useState<ChoroplethFeatureCollection>();
React.useEffect(() => {
// Any TopoJSON or GeoJSON of your regions. Convert TopoJSON once.
fetch("/countries-110m.json")
.then((res) => res.json())
.then((topology) =>
setCountries(
feature(topology, topology.objects.countries) as ChoroplethFeatureCollection,
),
);
}, []);
return (
<ChoroplethChart
data={countries}
loading={!countries}
featureId="name"
values={users}
label="Most users are in Germany and France"
/>
);
}values is keyed by the id featureId reads: the feature's own id by
default, or a properties key such as "name". A function of the feature works
too.
Examples
Default
Countries take one of five bins by quantile: each bin holds about the same number of countries, which suits long-tailed data. Countries with no value are hatched, never drawn as zero.
US states
projection="albersUsa" draws the 50 states and DC with Alaska and Hawaii moved
beside the rest. Use it with US data only: regions it cannot draw are left out of
the map and of the list.
Thresholds
scale={{ thresholds }} sets the cuts yourself, in the units the reader thinks
in. formatValue spells the values in the legend, the names and the tooltip.
Zoom
zoomEnabled adds zoom in, zoom out and reset buttons under the map. Once zoomed
in, drag to move around. A region reached with the keyboard is brought into view.
Textures and graticule
patterns gives each bin a texture beside its tint (solid, dots, two hatches,
crosshatch), so the map still reads in greyscale and in print; the legend
repeats them. graticule draws lines of latitude and longitude and the globe's
outline.
Accessible name and data table
On a coloured plate
Inside a coloured Surface the bins keep their tints; the seams, the no-data hatch, the legend text, the zoom buttons and the focus ring follow the plate.
States
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
data | ChoroplethFeatureCollection | — | The regions, as GeoJSON. Unset or empty shows emptyState. |
values | Record<string, number> | (feature) => number | null | undefined | — | A value per region. A region with no finite value is "no data". |
featureId | string | (feature) => string | number | null | undefined | the feature's id | How a region's id is read: a properties key, or a function. The id keys values. |
featureName | string | (feature) => string | properties.name | How a region's name is read. |
getFeatureLabel | (feature, value: number | null) => string | — | A region's full accessible name. Default: "France, 9,100". |
formatValue | (value: number) => string | Intl.NumberFormat(locale) | Spells values in names, the tooltip and the legend. |
scale | "quantile" | "quantize" | { thresholds: number[] } | (value) => number | "quantile" | How values become bins. A function returns the bin, 0 being the lightest. |
steps | number | 5 | Number of bins, 2–5. Ignored by { thresholds }. |
projection | "equalEarth" | "naturalEarth1" | "mercator" | "albersUsa" | projection | "equalEarth" | A preset, or any projection with fitWidth and fitExtent. The chart fits it to the data. |
tooltip | boolean | true | Region name and value on hover and keyboard focus; a tap holds it open on touch. |
legend | boolean | true | The bins and their ranges. Hidden, it stays for assistive tech. |
zoomEnabled | boolean | false | Zoom buttons and drag-to-pan while zoomed in. |
graticule | boolean | false | Lines of latitude and longitude, and the globe's outline. |
patterns | boolean | false | A texture per bin beside its tint. |
locale | string | "en-US" | Number format and name order. |
labels | Partial<ChoroplethChartLabels> | English | Every string the chart writes: the list name, "no data", the zoom buttons and announcement. |
label | string | — | Accessible name; renders the chart as a role="figure". |
description | string | — | Visually hidden summary for assistive tech. |
loading | boolean | false | Render the skeleton. |
error | ReactNode | — | Render an error message instead of the map. |
emptyState | ReactNode | "No data to display" | Shown when there is no region to draw. |
dir | "ltr" | "rtl" | inherited | Reading direction. The map itself does not mirror. |
Bins and colour
- Quantile (the default) puts about the same number of regions in each bin. It keeps a few very large values from pushing every other region into the lightest bin.
- Quantize splits the range between the smallest and largest value into equal widths. Use it when the values are spread evenly.
- Thresholds are cuts you choose. A region is in the bin of the last cut it reaches.
- The ramp runs from light to dark in five steps of
--chart-1, so it follows the theme. Each step is opaque, so a bin reads the same on any ground. - Regions without a value take the page's muted fill and a sparse hatch, so they differ from the lightest bin without colour.
Accessibility
- Give every map a
label. State the finding ("Most users are in the United States, India and Brazil"), not "Map of users". Put the numbers indescription. - Offer the values as a table. A tint is a range, not a number (Accessible name and data table).
- Keyboard. The regions are one list in alphabetical order, with one Tab stop. The arrow keys step through it, Home and End jump to the first and last region, Page Up and Page Down move ten, and typing a letter jumps to the next region starting with it. The focused region is outlined over its neighbours and its tooltip opens.
- Names. Each region is named with its value ("France, 9,100", or "Greenland, no data") and described by its legend bin ("4,300–9,100").
- Zoom. The zoom buttons are the way in for touch and keyboard; dragging is never the only way. Each zoom step is announced, and a button at its limit stays focusable.
- Colour. Turn on
patternswhere the map must read without colour. - Touch. Small regions are smaller than a fingertip. A tap focuses the region and holds its tooltip open; zoom in to reach small regions, or use the table.
- Screen readers. Not yet verified with VoiceOver, NVDA or JAWS, which is why the component is in beta.