Source
142 chart lines · 1 file · 3.5 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/57-scatter-marginal-histograms/tanstack.ts142 lines · entry
cases/57-scatter-marginal-histograms/tanstack.ts
import { penguins } from '@charts-poc/demo-data/penguins'
import {
colorLegend,
defineChart,
dot,
rect,
ruleX,
ruleY,
} from '@tanstack/charts'
import { bin, max } 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'
type CompletePenguin = PenguinsRow & {
readonly flipper_length_mm: number
readonly body_mass_g: number
}
interface MarginalRect {
id: string
x1: number
x2: number
y1: number
y2: number
}
const colors = ['#2563eb', '#ea580c', '#059669']
const definition = (input: ConformanceInput) => {
const rows = penguins
.filter((row): row is CompletePenguin => {
return row.flipper_length_mm !== null && row.body_mass_g !== null
})
.slice(input.revision * 8, input.revision * 8 + 320)
const { xRects, yRects } = marginalRects(rows)
return defineChart({
marks: [
dot(rows, {
x: 'flipper_length_mm',
y: 'body_mass_g',
color: 'species',
r: 3,
fillOpacity: 0.78,
}),
rect(xRects, {
x1: 'x1',
x2: 'x2',
y1: 'y1',
y2: 'y2',
fill: '#0ea5e9',
fillOpacity: 0.78,
inset: 1,
}),
rect(yRects, {
x1: 'x1',
x2: 'x2',
y1: 'y1',
y2: 'y2',
fill: '#f97316',
fillOpacity: 0.78,
inset: 1,
}),
ruleX([236], { strokeOpacity: 0.5 }),
ruleY([6550], { strokeOpacity: 0.5 }),
],
x: {
scale: scaleLinear().domain([170, 245]),
grid: true,
axis: {
ticks: { format: visibleFlipperTick },
label: 'Flipper length (mm)',
},
},
y: {
scale: scaleLinear().domain([2500, 7000]),
grid: true,
axis: { ticks: { format: visibleMassTick }, label: 'Body mass (g)' },
},
color: {
range: colors,
legend: colorLegend({ label: 'Species' }),
},
})
}
export const mount = tanstackMount(
definition,
'Scatterplot with marginal histograms',
)
function marginalRects(rows: readonly CompletePenguin[]): {
xRects: readonly MarginalRect[]
yRects: readonly MarginalRect[]
} {
const xBins = bin<CompletePenguin, number>()
.domain([170, 240])
.thresholds([180, 190, 200, 210, 220, 230])
.value((row) => row.flipper_length_mm)(rows)
const yBins = bin<CompletePenguin, number>()
.domain([2500, 6500])
.thresholds([3000, 3500, 4000, 4500, 5000, 5500, 6000])
.value((row) => row.body_mass_g)(rows)
const maxX = max(xBins, (bucket) => bucket.length) ?? 1
const maxY = max(yBins, (bucket) => bucket.length) ?? 1
const xRects: MarginalRect[] = []
const yRects: MarginalRect[] = []
xBins.forEach((bucket, index) => {
if (bucket.x0 === undefined || bucket.x1 === undefined) return
xRects.push({
id: `x:${index}`,
x1: bucket.x0,
x2: bucket.x1,
y1: 6600,
y2: 6600 + (bucket.length / maxX) * 350,
})
})
yBins.forEach((bucket, index) => {
if (bucket.x0 === undefined || bucket.x1 === undefined) return
yRects.push({
id: `y:${index}`,
x1: 237,
x2: 237 + (bucket.length / maxY) * 7,
y1: bucket.x0,
y2: bucket.x1,
})
})
return { xRects, yRects }
}
function visibleFlipperTick(value: number): string {
return value <= 235 ? `${value}` : ''
}
function visibleMassTick(value: number): string {
return value <= 6500 ? `${value}` : ''
}