Choropleth Map
Geographic heatmap with quantile colour scale, drill-down zoom, and composable controls.
A geographic heatmap built from d3-geo with the composable <Choropleth> primitive. All geo charts use <Chart> as the outer wrapper — the same container used by line and bar charts — with geo-specific children providing projection, colour scale, and drill-down behaviour.
UK NUTS Regions
Three levels of UK geography (NUTS 1 → 2 → 3) with time-scrubbing across 36 months of synthetic data.
Affluence index
Affluence is measured on a scale from 0–1,000 points, with higher values indicating greater economic prosperity.
Usage
import {
Chart,
Choropleth,
ChoroplethBreadcrumb,
DrillDown,
Geometry,
Legend,
Tooltip,
} from "@exhibit/charts";
<Chart height={620} loading={loading}>
<ChoroplethBreadcrumb />
<Choropleth
geoData={baseGeo}
data={flatData}
featureIdKey="nuts118cd"
featureNameKey="nuts118nm"
colorPalette={["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#225ea8", "#081d58"]}
>
<DrillDown onDrill={async (regionId) => {
return { geo: childGeo, data: childData };
}}>
<Geometry featureIdKey="nuts218cd" featureNameKey="nuts218nm" />
<Geometry featureIdKey="nuts318cd" featureNameKey="nuts318nm" />
</DrillDown>
</Choropleth>
<Legend formatValue={(v) => `${Math.round(v)} pts`} />
<Tooltip valueLabel="Affluence" formatValue={(v) => `${Math.round(v)} pts`} />
</Chart>US States → Counties
US states with drill-down into counties. Uses the albersUsa projection and us-atlas TopoJSON data. Click a state to zoom into its counties.
Usage
import {
Chart,
Choropleth,
ChoroplethBreadcrumb,
DrillDown,
Geometry,
Legend,
Tooltip,
} from "@exhibit/charts";
import { feature } from "topojson-client";
const states = feature(topology, topology.objects.states);
const counties = feature(topology, topology.objects.counties);
<Chart height={520} loading={loading}>
<ChoroplethBreadcrumb />
<Choropleth
geoData={states}
data={statesData}
featureIdKey="id"
featureNameKey="name"
projection="albersUsa"
colorPalette={["#f7fcf5", "#d5efcf", "#9ed898", "#5bb561", "#238b45", "#005a32", "#00391f"]}
>
<DrillDown onDrill={async (stateId) => ({
geo: filterCountiesByState(counties, stateId),
data: countyData.filter(d => d.id.startsWith(stateId)),
})}>
<Geometry featureIdKey="id" featureNameKey="name" />
</DrillDown>
</Choropleth>
<Legend formatValue={(v) => v.toLocaleString()} />
<Tooltip valueLabel="Prosperity" formatValue={(v) => v.toLocaleString()} />
</Chart>UK LAD Boundaries + Costa Locations
Local Authority District boundaries with 50 Costa store locations plotted as coordinate points. The <PointLayer> uses Voronoi-based cursor snapping — hover anywhere on the map and the nearest store highlights with a spring-animated dot.
Usage
import {
Boundary,
Chart,
PointLayer,
TooltipHeader,
TooltipRow,
TooltipShell,
} from "@exhibit/charts";
<Chart height={700} loading={loading}>
<Boundary geoData={ladBoundaries} fill="#f0f0f0" stroke="#ccc" />
<PointLayer
points={costaLocations}
latKey="latitude"
lngKey="longitude"
color="#3182bd"
radius={3.5}
tooltip={({ point }) => (
<TooltipShell>
<TooltipHeader>{point.name}</TooltipHeader>
<TooltipRow label="City" value={point.city} />
<TooltipRow label="Turnover" value={formatGBP(point.annual_turnover)} />
</TooltipShell>
)}
/>
</Chart>API
<Chart> (wrapper)
The universal container for all chart types. For geo charts, omit data and xDataKey to enter shell mode — just sizing, loading, and child categorisation.
| Prop | Type | Default | Description |
|---|---|---|---|
height | number | 400 | Chart height in px |
loading | boolean | — | Show loading spinner (true) or auto-detect (undefined) |
className | string | — | CSS class for the outer container |
data | T[] | — | Tabular data (omit for geo charts) |
xDataKey | string | — | X-axis key (omit for geo charts) |
<Choropleth>
Renders inside <Chart>. Computes a geo projection and quantile colour scale, then registers the geo context for sibling components (<Legend>, <Tooltip>, <PointLayer>) to read.
| Prop | Type | Default | Description |
|---|---|---|---|
geoData | FeatureCollection | null | — | Base-level GeoJSON |
data | { id, name, value }[] | — | Flat data for the base level |
featureIdKey | string | — | Property key for feature IDs (falls back to feature.id) |
featureNameKey | string | — | Property key for feature display names |
colorPalette | string[] | YlGnBu 7 | Hex stops for quantile scale |
projection | "mercator" | "albersUsa" | "mercator" | Geo projection |
formatValue | (v: number) => string | toLocaleString | Value formatter for tooltips |
<Boundary>
Renders plain geo features with fill and stroke — no data colouring. Registers a minimal geo context so <PointLayer> can project coordinates.
| Prop | Type | Default | Description |
|---|---|---|---|
geoData | FeatureCollection | null | — | GeoJSON boundaries |
fill | string | "#e5e7eb" | Fill colour |
stroke | string | currentColor 15% | Stroke colour |
strokeWidth | number | 0.5 | Stroke width |
projection | "mercator" | "albersUsa" | "mercator" | Geo projection |
<DrillDown>
Manages drill state, zoom animation, and stacked feature layers. Renders inside <Choropleth>.
| Prop | Type | Description |
|---|---|---|
onDrill | (regionId, regionName, depth) => Promise<{ geo, data } | null> | Fetch child data on click. depth is 0-indexed. |
<Geometry>
Declarative drill-level config. Each <Geometry> child of <DrillDown> declares one drill level in order.
| Prop | Type | Description |
|---|---|---|
featureIdKey | string | Property key for child feature IDs |
featureNameKey | string | Property key for child feature names |
<ChoroplethBreadcrumb>
Renders drill-down breadcrumbs above the map. Auto-discovers drill state from context. Place as a direct child of <Chart>.
<Legend>
Polymorphic. In cartesian mode, shows series toggles from mark registration. In geo mode, auto-discovers the quantile scale from context and shows a colour bar with min/max labels and a spring-animated marker tracking the hovered value.
| Prop | Type | Description |
|---|---|---|
formatValue | (v: number) => string | Format breakpoint labels |
<Tooltip>
Polymorphic — auto-detects cartesian vs geo context. In cartesian mode, uses spring-positioned render props. In geo mode, uses the same spring physics reading from the geo mouse position ref.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ({ datum, index }) => ReactNode | — | Render prop (cartesian mode) |
valueLabel | string | — | Label shown next to the value (geo mode) |
formatValue | (v: number) => string | — | Value formatter (geo mode) |
stiffness | number | 0.18 | Spring stiffness for positioning |
offset | { x?: number; y?: number } | { x: 16, y: -8 } | Tooltip offset from cursor |
<PointLayer>
Renders lat/lng coordinate data as dots on the map with Voronoi-based cursor snapping. The active dot animates with spring physics. Tooltip content is provided via a render prop.
| Prop | Type | Default | Description |
|---|---|---|---|
points | T[] | — | Array of point data objects |
latKey | string | — | Key for latitude |
lngKey | string | — | Key for longitude |
color | string | "#dc2626" | Dot fill colour |
radius | number | 4 | Dot radius (px) |
activeRadius | number | 7 | Hovered dot radius (px) |
opacity | number | 0.85 | Dot opacity |
stroke | string | "white" | Dot stroke colour |
strokeWidth | number | 1.5 | Dot stroke width |
snapRadius | number | Infinity | Max snap distance (px) |
stiffness | number | 0.3 | Spring animation stiffness |
tooltip | ({ point }) => ReactNode | — | Custom tooltip render function |
Features
- Universal
<Chart>wrapper — geo charts use the same container as cartesian charts - Composable API —
<Choropleth>,<Boundary>,<DrillDown>,<PointLayer>compose declaratively - Polymorphic components —
<Tooltip>and<Legend>auto-detect cartesian vs geo context - Quantile colour scale — distributes colours evenly across the data distribution
- Animated drill-down — rAF-driven cubicOut tween with stacked cross-fade layers
- Focus/dim — surrounding regions desaturate + fade when drilled
- Drill outlines — subtle boundary stroke around the drilled region
- Breadcrumb navigation — styled breadcrumb bar for navigating back through levels
vectorEffect="non-scaling-stroke"— strokes stay crisp at any zoom level- Multiple projections — Mercator (UK/world) and Albers USA
- Point overlay —
<PointLayer>renders coordinate data as dots with Voronoi cursor snapping - Responsive — fills container width, legend stays max-w-lg