Source

200 chart lines · 2 files · 5.2 kBBenchmark harness excluded · shared/mount.ts

U.S. county unemployment3,219 records · CSV · 109.8 kB
@charts-poc/demo-data/us-county-unemployment

Selection: Complete published snapshot

id
number
state
string
county
string
rate
number

U.S. Bureau of Labor Statisticsobservablehq/plot@356f579b1d94 · revision 356f579b1d94 · test/data/us-county-unemployment.csv · Observable Plot repository MIT; upstream source credited · SHA-256 dd3cc76e5e21Pinned snapshot

cases/109-us-state-choropleth/tanstack.ts60 lines · entry
cases/109-us-state-choropleth/tanstack.ts
import { defineChart } from '@tanstack/charts'
import { geoShape } from '@tanstack/charts/geo'
import { scaleQuantile } from 'd3-scale'
import {
  fitUnemploymentProjection,
  projectedUnemploymentCounties,
} from './transform'
import { tanstackMount } from '../../shared/mount'
import type { ConformanceInput } from '../../types'

const colorRanges = [
  [
    '#f7fbff',
    '#deebf7',
    '#c6dbef',
    '#9ecae1',
    '#6baed6',
    '#4292c6',
    '#2171b5',
    '#08519c',
    '#08306b',
  ],
  [
    '#f7fcf5',
    '#e5f5e0',
    '#c7e9c0',
    '#a1d99b',
    '#74c476',
    '#41ab5d',
    '#238b45',
    '#006d2c',
    '#00441b',
  ],
]

const definition = (input: ConformanceInput) =>
  defineChart({
    marks: [
      geoShape(projectedUnemploymentCounties, {
        projection: ({ chart }) => fitUnemploymentProjection(chart),
        color: (county) => county.properties.rate,
        stroke: '#f8fafc',
        strokeWidth: 0.35,
      }),
    ],
    color: {
      scale: scaleQuantile<number, string>,
      range: colorRanges[input.revision % 2] ?? colorRanges[0],
    },
    margin: 10,
  })

export const mount = tanstackMount(
  definition,
  'United States county unemployment choropleth',
  {
    format: ({ datum }) =>
      `${datum.properties.county}, ${datum.properties.state} · ${datum.properties.rate}% unemployment`,
  },
)
cases/109-us-state-choropleth/transform.ts140 lines · support
cases/109-us-state-choropleth/transform.ts
import { usCountyUnemployment } from '@charts-poc/demo-data/us-county-unemployment'
import countiesAtlasJson from 'us-atlas/counties-10m.json'
import { geoAlbersUsa, geoPath } from 'd3-geo'
import { feature } from 'topojson-client'
import type { UsCountyUnemploymentRow } from '@charts-poc/demo-data/us-county-unemployment'
import type {
  ExtendedFeature,
  ExtendedFeatureCollection,
  GeoGeometryObjects,
} from 'd3-geo'

type AtlasTopology = Parameters<typeof feature>[0]
type CountyGeometry = Extract<
  GeoGeometryObjects,
  { type: 'Polygon' | 'MultiPolygon' }
>

export interface UnemploymentCountyProperties extends UsCountyUnemploymentRow {
  name: string
}

export type UnemploymentCounty = ExtendedFeature<
  CountyGeometry,
  UnemploymentCountyProperties
>

export interface ProjectionBounds {
  x: number
  y: number
  width: number
  height: number
}

const countyAtlasSource: unknown = countiesAtlasJson
if (!isAtlasTopology(countyAtlasSource)) {
  throw new TypeError('us-atlas counties-10m is not valid TopoJSON')
}

const countiesObject = countyAtlasSource.objects.counties
if (!countiesObject) {
  throw new TypeError('us-atlas counties-10m is missing counties')
}

const convertedCounties = feature(countyAtlasSource, countiesObject)
if (convertedCounties.type !== 'FeatureCollection') {
  throw new TypeError('us-atlas counties did not produce a collection')
}

const unemploymentByFips = new Map(
  usCountyUnemployment.map((row) => [String(row.id).padStart(5, '0'), row]),
)

export const unemploymentCounties: readonly UnemploymentCounty[] =
  convertedCounties.features.flatMap<UnemploymentCounty>((county) => {
    const fips =
      county.id === undefined ? null : String(county.id).padStart(5, '0')
    const row = fips === null ? undefined : unemploymentByFips.get(fips)
    if (
      fips === null ||
      row === undefined ||
      !isCountyGeometry(county.geometry) ||
      !isRecord(county.properties) ||
      typeof county.properties.name !== 'string'
    ) {
      return []
    }

    return [
      {
        type: 'Feature',
        id: fips,
        geometry: county.geometry,
        properties: {
          name: county.properties.name,
          ...row,
        },
      },
    ]
  })

if (unemploymentCounties.length !== 3219) {
  throw new TypeError(
    `Expected 3219 counties with unemployment data, got ${unemploymentCounties.length}`,
  )
}

const albersUsaPath = geoPath(geoAlbersUsa())

// Albers USA intentionally excludes territories outside the composite
// projection. Filter them before rendering so both libraries receive only
// geometries that can produce a path.
export const projectedUnemploymentCounties = unemploymentCounties.filter(
  (county) => albersUsaPath(county) !== null,
)

if (projectedUnemploymentCounties.length !== 3141) {
  throw new TypeError(
    `Expected 3141 Albers-USA counties, got ${projectedUnemploymentCounties.length}`,
  )
}

export const unemploymentCountyCollection: ExtendedFeatureCollection<UnemploymentCounty> =
  {
    type: 'FeatureCollection',
    features: [...projectedUnemploymentCounties],
  }

export function fitUnemploymentProjection({
  x,
  y,
  width,
  height,
}: ProjectionBounds) {
  return geoAlbersUsa().fitExtent(
    [
      [x, y],
      [x + width, y + height],
    ],
    unemploymentCountyCollection,
  )
}

function isCountyGeometry(
  geometry: GeoGeometryObjects,
): geometry is CountyGeometry {
  return geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'
}

function isAtlasTopology(value: unknown): value is AtlasTopology {
  return (
    isRecord(value) &&
    value.type === 'Topology' &&
    Array.isArray(value.arcs) &&
    isRecord(value.objects)
  )
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}