Source
169 chart lines · 2 files · 4.4 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/63-violin-distributions/tanstack.ts79 lines · entry
cases/63-violin-distributions/tanstack.ts
import { penguins } from '@charts-poc/demo-data/penguins'
import { areaX, d3AreaXCurve, defineChart, dot, link } from '@tanstack/charts'
import { scaleLinear } from 'd3-scale'
import { curveBasis } from 'd3-shape'
import {
isPenguinMass,
violinDensity,
violinMedians,
violinSpecies,
} from './transform'
import { tanstackMount } from '../../shared/mount'
import type { ConformanceInput } from '../../types'
const colors = ['#64748b', '#0d9488', '#7c3aed']
const definition = (input: ConformanceInput) => {
const observations = penguins
.filter(isPenguinMass)
.slice(input.revision * 8, input.revision * 8 + 320)
const rows = violinDensity(observations)
const summaries = violinMedians(observations)
return defineChart({
marks: [
areaX(rows, {
x1: 'x1',
x2: 'x2',
y: 'body_mass_g',
color: 'species',
fillOpacity: 0.58,
curve: d3AreaXCurve(curveBasis),
}),
link(summaries, {
x1: 'x1',
x2: 'x2',
y1: 'body_mass_g',
y2: 'body_mass_g',
stroke: '#0f172a',
strokeWidth: 2,
}),
dot(summaries, {
x: 'center',
y: 'body_mass_g',
color: 'species',
stroke: '#ffffff',
strokeWidth: 1,
r: 3.5,
}),
],
x: {
scale: scaleLinear().domain([0.5, 3.5]),
axis: {
ticks: {
count: violinSpecies.length,
format: (value) => violinSpecies[Math.round(value) - 1] ?? '',
},
},
},
y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } },
color: {
range: colors,
},
})
}
export const mount = tanstackMount(
definition,
'Violin distribution comparison',
{
format: ({ datum }) =>
'center' in datum
? `${datum.species} · median body mass ${datum.body_mass_g.toLocaleString(
'en-US',
)} g`
: `${datum.species} · distribution at ${datum.body_mass_g.toLocaleString(
'en-US',
)} g`,
},
)cases/63-violin-distributions/transform.ts90 lines · support
cases/63-violin-distributions/transform.ts
import { bin, median } from 'd3-array'
import type { PenguinsRow } from '@charts-poc/demo-data/penguins'
export const violinSpecies = ['Adelie', 'Chinstrap', 'Gentoo'] as const
export type ViolinSpecies = (typeof violinSpecies)[number]
export type PenguinMass = PenguinsRow & {
readonly species: ViolinSpecies
readonly body_mass_g: number
}
export interface ViolinPoint {
id: string
species: ViolinSpecies
body_mass_g: number
x1: number
x2: number
}
export interface ViolinMedian {
id: string
species: ViolinSpecies
x1: number
x2: number
body_mass_g: number
center: number
}
const boundaries = [
2500, 2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500,
5750, 6000, 6250, 6500,
]
const createBins = bin<PenguinMass, number>()
.value((row) => row.body_mass_g)
.domain([2500, 6500])
.thresholds(boundaries.slice(1, -1))
export function isPenguinMass(row: PenguinsRow): row is PenguinMass {
return (
row.body_mass_g !== null &&
violinSpecies.includes(row.species as ViolinSpecies)
)
}
export function violinDensity(
rows: readonly PenguinMass[],
): readonly ViolinPoint[] {
return violinSpecies.flatMap((species, speciesIndex) => {
const buckets = createBins(rows.filter((row) => row.species === species))
const maximum = Math.max(...buckets.map((bucket) => bucket.length), 1)
const center = speciesIndex + 1
return buckets.flatMap((bucket, index) => {
if (bucket.x0 === undefined || bucket.x1 === undefined) return []
const halfWidth = (bucket.length / maximum) * 0.38
return [
{
id: `${species}:${index}`,
species,
body_mass_g: (bucket.x0 + bucket.x1) / 2,
x1: center - halfWidth,
x2: center + halfWidth,
},
]
})
})
}
export function violinMedians(
rows: readonly PenguinMass[],
): readonly ViolinMedian[] {
return violinSpecies.flatMap((species, index) => {
const bodyMass = median(
rows.filter((row) => row.species === species),
(row) => row.body_mass_g,
)
if (bodyMass === undefined) return []
return [
{
id: species,
species,
x1: index + 0.82,
x2: index + 1.18,
body_mass_g: bodyMass,
center: index + 1,
},
]
})
}