Skip to content

ChoroplethMap

A US choropleth map using D3's Albers USA projection, which repositions Alaska and Hawaii to the bottom left. Supports state-level, county-level, and HSA-level (Health Service Areas) rendering via the geoType prop.

You must provide your own TopoJSON topology data via the topology prop. We recommend the us-atlas package:

sh
npm install us-atlas
  • State-level maps: use us-atlas/states-10m.json
  • County or HSA maps: use us-atlas/counties-10m.json (includes both county and state boundaries)
vue
<script setup>
import { ChoroplethMap } from "@cfasim-ui/charts";
import statesTopo from "us-atlas/states-10m.json";
import countiesTopo from "us-atlas/counties-10m.json";
</script>

<!-- State map -->
<ChoroplethMap :topology="statesTopo" :data="stateData" />

<!-- County map -->
<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  :data="countyData"
/>

Examples

Basic with state data

Cases by State
Cases
51.367.583.8
vue
<script setup>
import statesTopo from "us-atlas/states-10m.json";
</script>

<ChoroplethMap
  :topology="statesTopo"
  :data="[
    { id: '06', value: 100 },
    { id: '36', value: 80 },
    { id: '48', value: 90 },
    { id: '12', value: 70 },
    { id: '17', value: 60 },
  ]"
  title="Cases by State"
  :legend-title="'Cases'"
  :height="400"
/>

Custom color scale

Severity
51.367.583.8
vue
<ChoroplethMap
  :topology="statesTopo"
  :data="[
    { id: 'California', value: 100 },
    { id: 'Texas', value: 85 },
    { id: 'Florida', value: 70 },
    { id: 'New York', value: 90 },
  ]"
  :color-scale="{ min: '#fff5f0', max: '#a50f15' }"
  :legend-title="'Severity'"
  :height="400"
/>

Threshold color scale

Use an array of ThresholdStop objects instead of a linear scale. Each stop defines a min threshold — values at or above that threshold get the stop's color. The highest matching stop wins.

Risk Level
Risk Low Some Moderate High
vue
<ChoroplethMap
  :topology="statesTopo"
  :data="stateData"
  :color-scale="[
    { min: 0, color: '#fee5d9', label: 'Low' },
    { min: 10, color: '#fcae91', label: 'Some' },
    { min: 30, color: '#fb6a4a', label: 'Moderate' },
    { min: 60, color: '#cb181d', label: 'High' },
  ]"
  title="Risk Level"
  :legend-title="'Risk'"
  :height="400"
/>

Categorical color scale

Use an array of CategoricalStop objects to map string values to colors. Each stop defines a value to match and a color to apply.

Risk Category
Risk low medium high
vue
<ChoroplethMap
  :topology="statesTopo"
  :data="stateData"
  :color-scale="[
    { value: 'low', color: '#fee5d9' },
    { value: 'medium', color: '#fb6a4a' },
    { value: 'high', color: '#cb181d' },
  ]"
  title="Risk Category"
  :legend-title="'Risk'"
  :height="400"
/>

County-level map with pan and zoom

Set geoType="counties" to render county-level data using 5-digit FIPS codes. State borders are drawn on top for context. Use pan and zoom props to enable interactive navigation — useful for dense county data.

Cases by County
Cases
62.57587.5
vue
<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  pan
  zoom
  :data="[
    { id: '06037', value: 100 },
    { id: '36061', value: 90 },
    { id: '17031', value: 85 },
    { id: '48201', value: 65 },
    { id: '04013', value: 60 },
  ]"
  title="Cases by County"
  :legend-title="'Cases'"
  :height="400"
/>

HSA-level map

Set geoType="hsas" to render Health Service Area boundaries. HSAs are dissolved from county boundaries using a built-in FIPS-to-HSA mapping. Use 6-digit HSA codes as IDs. State borders are overlaid for context.

Cases by HSA
Cases
51.367.583.8
vue
<ChoroplethMap
  :topology="countiesTopo"
  geo-type="hsas"
  pan
  zoom
  :data="[
    { id: '010259', value: 100 },
    { id: '060766', value: 90 },
    { id: '120159', value: 85 },
    { id: '090121', value: 70 },
    { id: '110061', value: 60 },
  ]"
  title="Cases by HSA"
  :legend-title="'Cases'"
  :height="400"
/>

Click to focus (v-model:focus)

Bind the focus prop to pan and zoom to a specific feature. Pass a feature id (FIPS code, HSA code, or name) — or an array of ids to focus on a region. With v-model:focus, clicking an unfocused feature focuses it and clicking the focused feature toggles back off. If a tooltip is configured, focusing shows that feature's tooltip. Users can pan/zoom freely around the focused area; the built-in Reset button clears focus and snaps back.

Counties are tiny without a zoom — focus is a natural fit for drill-down.

Click a county to focus
Cases
62.57587.5
vue
<script setup>
import { ref } from "vue";
const focused = ref(null);
</script>

<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  v-model:focus="focused"
  :focus-zoom-level="8"
  :data="data"
  title="Click a county to focus"
>
  <template #tooltip="{ name, value }">
    <div style="font-weight: 600">{{ name }}</div>
    <div v-if="value != null">Cases: {{ value }}</div>
    <div v-else style="opacity: 0.6">No data</div>
  </template>
</ChoroplethMap>

Color by HSA, interact by county (dataGeoType)

Set dataGeoType when your data is keyed by a coarser geography than the one you want to render. Each county fills with its parent HSA's value (via the built-in FIPS → HSA mapping); hover, click, and focus still operate on the county geometry, and you can layer an HSA outline on top with a FocusItem.

HSA-keyed data on a county map
Cases
38.852.566.3
vue
<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  data-geo-type="hsas"
  :data="hsaData"
  :focus="[{ id: '06043' }, { id: '060737', geoType: 'hsas', style: 'dashed' }]"
  title="HSA-keyed data on a county map"
/>

Outline a focused feature's parent

Use v-model:focus together with a computed that derives a parent feature (e.g. an HSA from a county via fipsToHsa). Pass both as a FocusItem array — the focused county lights up as usual and the parent HSA renders on top as a dashed overlay (stroke: "#666" here — default is white).

fipsToHsa and hsaNames ship from the @cfasim-ui/charts/hsa-mapping subpath so consumers that don't need HSA lookups don't pay for the ~25 KB of mapping data.

Click a county to outline its HSA
Cases
38.852.566.3
vue
<script setup>
import { ref, computed } from "vue";
import { fipsToHsa } from "@cfasim-ui/charts/hsa-mapping";

const focusedCounty = ref(null);
const focus = computed(() => {
  const fips = focusedCounty.value;
  if (!fips) return null;
  const hsa = fipsToHsa[fips];
  return hsa
    ? [fips, { id: hsa, geoType: "hsas", style: "dashed", stroke: "#666" }]
    : fips;
});
</script>

<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  data-geo-type="hsas"
  :data="hsaData"
  :focus="focus"
  @update:focus="focusedCounty = typeof $event === 'string' ? $event : null"
  title="Click a county to outline its HSA"
/>

Custom tooltip number format

Pass tooltip-value-format to format numeric values shown in the tooltip (both the native SVG <title> and the interactive HTML tooltip). Use the #tooltip slot if you want full control over the tooltip's content.

US population (2020)
25M29.9M34.7M
vue
<ChoroplethMap
  :topology="statesTopo"
  :data="[
    { id: '06', value: 39538223 },
    { id: '48', value: 29145505 },
    { id: '12', value: 21538187 },
    { id: '36', value: 20201249 },
  ]"
  :tooltip-value-format="(v) => v.toLocaleString('en-US')"
  title="US population (2020)"
  :height="300"
/>

Dense county map

Renders every US county with a value and a custom tooltip slot.

All US counties — tooltip perf demo
24.849.574.3
vue
<script setup>
import countiesTopo from "us-atlas/counties-10m.json";

// One row per county
const data = countiesTopo.objects.counties.geometries.map((g, i) => ({
  id: String(g.id).padStart(5, "0"),
  value: (i * 37) % 100,
}));
</script>

<ChoroplethMap
  :topology="countiesTopo"
  geo-type="counties"
  :data="data"
  pan
  zoom
>
  <template #tooltip="{ id, name, value }">
    <div style="font-weight: 600">{{ name }}</div>
    <div style="opacity: 0.7; font-size: 0.85em">FIPS {{ id }}</div>
    <div>Value: {{ value }}</div>
  </template>
</ChoroplethMap>

Custom tooltip content (#tooltip slot)

Use the #tooltip slot to render any Vue template — components, scoped styles, multi-line layouts — instead of the default name: value. The slot receives { id, name, value, feature } for the hovered region. Providing the slot automatically enables interactive (HTML) tooltips, so you don't need to set tooltip-trigger.

US population (2020)
19.5M26.2M32.9M
vue
<ChoroplethMap :topology="statesTopo" :data="data" title="US population (2020)">
  <template #tooltip="{ name, value }">
    <div style="font-weight: 600">{{ name }}</div>
    <div v-if="typeof value === 'number'">
      Pop: {{ value.toLocaleString("en-US") }}
    </div>
    <div v-else style="opacity: 0.6">No data</div>
  </template>
</ChoroplethMap>

Props

PropTypeRequiredDefault
topologyTopologyYes
dataStateData[]No
geoTypeGeoTypeNo"states"
dataGeoTypeGeoTypeNo
widthnumberNo
heightnumberNo
colorScaleChoroplethColorScale | ThresholdStop[] | CategoricalStop[]No
titlestringNo
titleStyleTitleStyleNo
legendStyleLabelStyleNo
noDataColorstringNo"#ddd"
strokeColorstringNo"#fff"
strokeWidthnumberNo0.5
menuboolean | stringNotrue
legendbooleanNotrue
legendTitlestringNo
zoombooleanNofalse
panbooleanNofalse
tooltipTrigger"hover" | "click"No
tooltipFormat`(data: {
  id: string` | No | — |

| name | string | Yes | — | | value | number \| string | No | — | | tooltipValueFormat | NumberFormat | No | — | | tooltipClamp | "none" \| "chart" \| "window" | No | "chart" | | focus | FocusValue | No | — | | focusZoomLevel | number | No | 4 | | fullscreenTarget | string \| HTMLElement | No | — |

StateData

ts
interface StateData {
  /** FIPS code (e.g. "06" for California, "06037" for LA County) or name */
  id: string;
  value: number | string;
}

ChoroplethColorScale

ts
interface ChoroplethColorScale {
  /** Minimum color (CSS color string). Default: "#e5f0fa" */
  min?: string;
  /** Maximum color (CSS color string). Default: "#08519c" */
  max?: string;
}

ThresholdStop

Pass an array of ThresholdStop as colorScale for discrete color buckets instead of a linear gradient. The highest matching min wins.

ts
interface ThresholdStop {
  /** Lower bound (inclusive). Values at or above this get this color. */
  min: number;
  color: string;
  /** Optional label for the legend (defaults to the min value) */
  label?: string;
}

CategoricalStop

Pass an array of CategoricalStop as colorScale to map string values to colors. States whose value matches a stop's value get that color; unmatched values get noDataColor.

ts
interface CategoricalStop {
  /** The categorical value to match */
  value: string;
  /** CSS color string */
  color: string;
}

FocusItem

The focus prop accepts a bare id, a FocusItem, or an array of either. Use objects when you want to pin features from a different geoType than the base map, or pick a non-default outline style.

ts
interface FocusItem {
  /** Feature id (FIPS code, HSA code) or name. */
  id: string;
  /** Defaults to the map's geoType. Cross-geoType items render as
   * non-interactive outlines on top of the base map. */
  geoType?: "states" | "counties" | "hsas";
  /** Outline style. "solid" (default) matches the hover highlight;
   * "dashed" uses long dashes; "dotted" uses small round dots. */
  style?: "solid" | "dashed" | "dotted";
  /** Stroke color for cross-geoType overlay paths. Default: "#fff". */
  stroke?: string;
}