Source
115 chart lines · 1 file · 3.3 kBBenchmark harness excluded · shared/mount.ts
Palmer penguins344 records · CSV · 13.5 kB
@charts-poc/demo-data/penguinsSelection: Complete published snapshot
- species
- string
- island
- string
- culmen_length_mm
- number | null
- culmen_depth_mm
- number | null
- flipper_length_mm
- number | null
- body_mass_g
- number | null
- sex
- string | null
Dr. Kristen Gorman / Palmer Station LTER@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · penguins.csv · ISC distribution; upstream source credited · SHA-256 dfee817d1c14Pinned snapshot
cases/51-faceted-distributions/tanstack.ts115 lines · entry
cases/51-faceted-distributions/tanstack.ts
import { penguins } from '@charts-poc/demo-data/penguins'
import { defineChart, facet, rect } from '@tanstack/charts'
import { bin } from 'd3-array'
import { scaleLinear } from 'd3-scale'
import { tanstackMount } from '../../shared/mount'
import type { PenguinsRow } from '@charts-poc/demo-data/penguins'
import type { ConformanceInput } from '../../types'
export const species = ['Adelie', 'Chinstrap', 'Gentoo'] as const
export type PenguinSpecies = (typeof species)[number]
export type PenguinMass = PenguinsRow & {
readonly species: PenguinSpecies
readonly body_mass_g: number
}
export interface DistributionBin {
id: string
species: PenguinSpecies
x1: number
x2: number
count: number
proportion: number
}
const boundaries = [2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500]
const createBins = bin<PenguinMass, number>()
.value((row) => row.body_mass_g)
.domain([2500, 6500])
.thresholds(boundaries.slice(1, -1))
const percent = new Intl.NumberFormat('en-US', {
style: 'percent',
maximumFractionDigits: 0,
})
export function prepareFacetedDistributionBins(
rows: readonly PenguinMass[],
): readonly DistributionBin[] {
return species.flatMap((speciesName) => {
const groupRows = rows.filter((row) => row.species === speciesName)
if (groupRows.length === 0) return []
return createBins(groupRows).flatMap((bucket, index) =>
bucket.x0 === undefined || bucket.x1 === undefined || bucket.length === 0
? []
: [
{
id: `${speciesName}:${index}`,
species: speciesName,
x1: bucket.x0,
x2: bucket.x1,
count: bucket.length,
proportion: bucket.length / groupRows.length,
},
],
)
})
}
const definition = (input: ConformanceInput) => {
const rows = penguins
.filter((row): row is PenguinMass => {
return (
row.body_mass_g !== null &&
species.includes(row.species as PenguinSpecies)
)
})
.slice(input.revision * 8, input.revision * 8 + 320)
const bins = prepareFacetedDistributionBins(rows)
return defineChart({
marks: [
facet(bins, {
by: 'species',
columns: 1,
gap: 8,
label: (group) => String(group),
chart: (facetBins) => ({
marks: [
rect(facetBins, {
x1: 'x1',
x2: 'x2',
y1: () => 0,
y2: 'proportion',
fill: '#8b5cf6',
inset: 0.75,
}),
],
x: {
scale: scaleLinear().domain([2500, 6500]),
grid: true,
axis: { label: 'Body mass (g)' },
},
y: {
scale: scaleLinear().domain([0, 0.4]),
grid: true,
axis: {
ticks: { count: 3, format: (value) => percent.format(value) },
label: 'Proportion',
},
},
}),
}),
],
margin: 0,
})
}
export const mount = tanstackMount(
definition,
'Faceted distribution comparison',
{
format: (point) =>
`${point.datum.species} · Body mass: ${point.datum.x1}–${point.datum.x2} g · Proportion: ${percent.format(point.datum.proportion)}`,
},
)