Source
108 chart lines · 1 file · 2.9 kBBenchmark harness excluded · shared/mount.ts
U.S. industry unemployment1,708 records · CSV · 53.9 kB
@charts-poc/demo-data/industriesSelection: Complete published snapshot
- date
- Date
- industry
- string
- unemployed
- number
U.S. Bureau of Labor Statistics@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · industries.csv · ISC distribution; upstream source credited · SHA-256 4cfc13b6ebe0Pinned snapshot
cases/21-streamgraph/tanstack.ts108 lines · entry
cases/21-streamgraph/tanstack.ts
import { areaY, colorLegend, defineChart } from '@tanstack/charts'
import { group, min } from 'd3-array'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { stack, stackOffsetWiggle, stackOrderInsideOut } from 'd3-shape'
import { industries } from '@charts-poc/demo-data/industries'
import type { IndustriesRow } from '@charts-poc/demo-data/industries'
import { tanstackMount } from '../../shared/mount'
import type { ConformanceInput, ConformanceMount } from '../../types'
interface WideTimePoint {
date: Date
byIndustry: ReadonlyMap<string, IndustriesRow>
}
interface StreamIndustryPoint extends IndustriesRow {
y1: number
y2: number
}
const colors = [
'#4e79a7',
'#f28e2c',
'#e15759',
'#76b7b2',
'#59a14f',
'#edc949',
'#af7aa1',
'#ff9da7',
'#9c755f',
'#bab0ab',
]
const definition = (_input: ConformanceInput) => {
const rows = streamIntervals(industries)
return defineChart({
marks: [
areaY(rows, {
x: 'date',
y1: 'y1',
y2: 'y2',
color: 'industry',
fillOpacity: 0.85,
}),
],
x: { scale: scaleUtc, axis: { label: 'Month' } },
y: {
scale: scaleLinear,
grid: true,
axis: { label: 'Unemployed (thousands)' },
},
color: {
range: colors,
legend: colorLegend({ label: 'Industry' }),
},
})
}
export const mount: ConformanceMount = tanstackMount(
definition,
'Unemployment by industry as a streamgraph',
{
format: ({ datum }) =>
`${datum.industry} · ${datum.date.toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
timeZone: 'UTC',
})} · ${datum.unemployed.toLocaleString('en-US')} thousand unemployed`,
},
)
function streamIntervals(
rows: readonly IndustriesRow[],
): readonly StreamIndustryPoint[] {
const industryNames = Array.from(new Set(rows.map((row) => row.industry)))
const wideRows = Array.from(
group(rows, (row) => row.date.getTime()).values(),
toWideRow,
)
const layers = stack<WideTimePoint, string>()
.keys(industryNames)
.value((row, industry) => row.byIndustry.get(industry)?.unemployed ?? 0)
.order(stackOrderInsideOut)
.offset(stackOffsetWiggle)(wideRows)
const baseline = min(layers, (layer) => min(layer, (point) => point[0])) ?? 0
return layers.flatMap((series) =>
series.flatMap((point): readonly StreamIndustryPoint[] => {
const source = point.data.byIndustry.get(series.key)
return source
? [
{
...source,
y1: point[0] - baseline,
y2: point[1] - baseline,
},
]
: []
}),
)
}
function toWideRow(rows: IndustriesRow[]): WideTimePoint {
return {
date: rows[0]?.date ?? new Date(0),
byIndustry: new Map(rows.map((row) => [row.industry, row] as const)),
}
}