Choropleth Map
ProValue-per-region map. Equal-area projection by default, quantile / equal-interval / threshold class breaks, an explicit no-data fill and legend entry, a hatch for figures the host flags as estimates, and one tab stop with arrow-key navigation. Bring your own GeoJSON or TopoJSON — adapters and a simplifier are included. Pure SVG.
import { SvChoroplethMapComponent } from '@styloviz-pro/charts';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Equal-interval breaks against a fixed scale
ProRegulatory thresholds
ProPre-projected geometry (Albers USA and friends)
ProRegion interactions
ProTopoJSON in, simplified for a dashboard-sized map
ProGeoJSON with non-standard property keys
ProA figure that is an estimate
ProWhen to use
- A single number per administrative region, where WHERE is the question.
- Rates, densities and shares — anything already normalised by population or area.
- For raw counts, prefer a bar chart or proportional symbols: a count map mostly reproduces the population map.
- For a handful of regions, a ranked bar chart is read faster and more precisely.
Async states
- loading
- empty
- error
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the geometry or the values are being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. |
| emptyMessage | string | 'No regions to map' | Copy for the no-data state. |
| filtered | boolean | false | True when the emptiness is caused by a filter rather than an empty set. |
| features | readonly SvMapFeature[] | [] | The shapes. Bring your own — see the class comment on why the library does not ship a world. |
| values | readonly SvChoroplethDatum[] | [] | One row per region with a figure. Regions with no row are drawn as no-data. |
| projection | SvMapProjection | 'equal-earth' | How lon/lat becomes a plane. |
| scale | SvChoroplethScale | 'quantile' | How values are sorted into colours. |
| classes | number | 5 | Number of colour bands, clamped to `2…9`. Ignored when `scale` is `continuous`. |
| thresholds | readonly number[] | [] | Explicit cut points, used only when `scale` is `threshold`. |
| ramp | SvChoroplethRamp | 'blue' | Sequential colour ramp. |
| width | number | 640 | Chart width in pixels. |
| height | number | 360 | Chart height in pixels. |
| noDataFill | string | 'var(--color-gray-200)' | Fill for regions with no figure. |
| noDataLabel | string | 'No data' | Legend wording for regions with no figure. |
| uncertainLabel | string | 'Estimate' | Legend wording, and the accessible suffix, for a figure the host flagged as uncertain. It is a word rather than only a hatch because a hatch is not a label. A reader who cannot see the texture — low contrast, a small map, a printed page — otherwise reads an estimate as a measurement, which is the whole thing the flag exists to prevent. |
| showLegend | boolean | true | Draw the legend. |
| legendTitle | string | '' | What the colours mean — the units, ideally. Worth filling in every time. "Cases" and "cases per 100 000" produce very different maps and the reader cannot tell them apart from the picture. |
| strokeColor | string | '#ffffff' | Border between regions. |
| title | string | '' | Chart title, rendered above the map. |
| ariaLabel | string | '' | Accessible name override. Falls back to a generated summary. |
| customClass | string | '' | Extra CSS classes applied to the root wrapper. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| regionHover | SvChoroplethRegionEvent | Emitted when a region is hovered or keyboard-focused. |
| regionSelect | SvChoroplethRegionEvent | Emitted when a region is clicked or activated with Enter/Space. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
ProBound to the names this component actually exposes.
Host class
ProDrive the three async inputs from your own fetch.
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `features`, `loading` and `error`.
- Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Four projections, including identity for pre-projected composites such as Albers USA.
- Quantile, equal-interval, explicit-threshold and continuous scales.
- Seven sequential ramps, plus a configurable no-data fill and label.
- A hatch overlay for figures flagged `uncertain`, with its own legend entry and `uncertainLabel` wording.
- `svGeoJsonFeatures`, `svTopoJsonFeatures` and `svSimplifyFeatures` convert real boundary files into the shape the component takes. None of them fetches anything — the host loads its own file and hands over the parsed object.
Best practices
- Do: Map a rate, not a count, and say which in legendTitle.
- Do: Leave the projection equal-area unless you must match a tile layer — Mercator inflates high latitudes by 1/cos²(latitude).
- Do: Let regions with no figure stay in the no-data fill; never substitute zero.
- Do: Flag a modelled, suppressed or wide-interval figure with `uncertain: true`. The region keeps its band colour and gains a hatch, so the number is still readable and is no longer presented with the same confidence as a counted one. It is a third state: "measured", "measured loosely", "not measured".
- Do: Load boundaries with svTopoJsonFeatures or svGeoJsonFeatures rather than writing the conversion by hand. TopoJSON stores a shared border once and delta-encodes its coordinates, so a hand-rolled reader that misses the negative arc index or the delta pass still renders — as a bow tie, or as a spray of triangles.
- Do: Run svSimplifyFeatures over anything detailed. Natural Earth at 1:10m is megabytes and a dashboard map is a few hundred pixels wide, so most of those vertices are smaller than a pixel. Start at a tolerance of 0.05 degrees and halve it until the outline stops changing at the size you render.
- Don't: Do not use equal-interval breaks on skewed data — nearly every region lands in the palest band.
- Tip: Split rings at ±180° before passing them in (RFC 7946 §3.1.9); the projection is point-wise and will not cut geometry for you.
Accessibility
- The SVG is a `role="group"` with a generated summary naming the region count, the value range and the arrow-key model (override via `ariaLabel`).
- Colour is the value here, and a sequential ramp is the least precise encoding in the kit — two adjacent bands are one shade apart. `sv-data-summary-table` publishes one row per region with its figure (the column takes its name from `legendTitle`) and its band where the scale is classed, and writes "No data" in words rather than leaving a blank cell that reads as zero.
- One roving tab stop for the whole map rather than one per region: Tab enters and leaves, arrow keys and Home/End move between regions in reading order, and Enter/Space emits `regionSelect`. Every region carries an `aria-label` with its name and value, and says "No data" in words where there is none.
- An estimate says so in words, not only in the hatch. A texture is invisible to a screen reader and easy to miss on a small or printed map, so `uncertainLabel` travels with the figure into the region's accessible name, the tooltip and the table row.
Feedback on Choropleth Map
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.