chartcn

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.

PropTypeDefaultDescription
heightnumber400Chart height in px
loadingbooleanShow loading spinner (true) or auto-detect (undefined)
classNamestringCSS class for the outer container
dataT[]Tabular data (omit for geo charts)
xDataKeystringX-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.

PropTypeDefaultDescription
geoDataFeatureCollection | nullBase-level GeoJSON
data{ id, name, value }[]Flat data for the base level
featureIdKeystringProperty key for feature IDs (falls back to feature.id)
featureNameKeystringProperty key for feature display names
colorPalettestring[]YlGnBu 7Hex stops for quantile scale
projection"mercator" | "albersUsa""mercator"Geo projection
formatValue(v: number) => stringtoLocaleStringValue 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.

PropTypeDefaultDescription
geoDataFeatureCollection | nullGeoJSON boundaries
fillstring"#e5e7eb"Fill colour
strokestringcurrentColor 15%Stroke colour
strokeWidthnumber0.5Stroke width
projection"mercator" | "albersUsa""mercator"Geo projection

<DrillDown>

Manages drill state, zoom animation, and stacked feature layers. Renders inside <Choropleth>.

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

PropTypeDescription
featureIdKeystringProperty key for child feature IDs
featureNameKeystringProperty 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.

PropTypeDescription
formatValue(v: number) => stringFormat 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.

PropTypeDefaultDescription
children({ datum, index }) => ReactNodeRender prop (cartesian mode)
valueLabelstringLabel shown next to the value (geo mode)
formatValue(v: number) => stringValue formatter (geo mode)
stiffnessnumber0.18Spring 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.

PropTypeDefaultDescription
pointsT[]Array of point data objects
latKeystringKey for latitude
lngKeystringKey for longitude
colorstring"#dc2626"Dot fill colour
radiusnumber4Dot radius (px)
activeRadiusnumber7Hovered dot radius (px)
opacitynumber0.85Dot opacity
strokestring"white"Dot stroke colour
strokeWidthnumber1.5Dot stroke width
snapRadiusnumberInfinityMax snap distance (px)
stiffnessnumber0.3Spring animation stiffness
tooltip({ point }) => ReactNodeCustom 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

On this page