Source
91 chart lines · 1 file · 2.6 kBBenchmark harness excluded · shared/mount.ts
1983 ASA automobile data406 records · CSV · 17.4 kB
@charts-poc/demo-data/carsSelection: Complete published snapshot
- name
- string
- economy (mpg)
- number | null
- cylinders
- number
- displacement (cc)
- number
- power (hp)
- number | null
- weight (lb)
- number
- 0-60 mph (s)
- number
- year
- number
1983 ASA Data Exposition@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · cars.csv · ISC distribution; upstream source credited · SHA-256 eaad8395d37ePinned snapshot
cases/52-beeswarm-dodge/tanstack.ts91 lines · entry
cases/52-beeswarm-dodge/tanstack.ts
import { cars } from '@charts-poc/demo-data/cars'
import { defineChart, dot } from '@tanstack/charts'
import { forceCollide, forceSimulation, forceY } from 'd3-force'
import { scaleLinear } from 'd3-scale'
import { tanstackMount } from '../../shared/mount'
import type { CarsRow } from '@charts-poc/demo-data/cars'
import type { ConformanceInput } from '../../types'
import type { SimulationNodeDatum } from 'd3-force'
interface BeeswarmNode extends SimulationNodeDatum {
name: string
'economy (mpg)': number
targetX: number
}
interface PositionedBeeswarmPoint {
name: string
'economy (mpg)': number
offset: number
}
type CarWithEconomy = CarsRow & { 'economy (mpg)': number }
const completeCars = cars.filter(
(row): row is CarWithEconomy => row['economy (mpg)'] !== null,
)
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 20,
}
const layoutTicks = 60
const definition = (input: ConformanceInput) =>
defineChart(({ width, height }) => {
const innerWidth = Math.max(1, width - margin.left - margin.right)
const innerHeight = Math.max(1, height - margin.top - margin.bottom)
const centerY = innerHeight / 2
const valuePosition = scaleLinear().domain([5, 50]).range([0, innerWidth])
const nodes: BeeswarmNode[] = completeCars
.slice(input.revision * 8, input.revision * 8 + 72)
.map((row) => {
const targetX = valuePosition(row['economy (mpg)'])
return {
name: row.name,
'economy (mpg)': row['economy (mpg)'],
targetX,
x: targetX,
fx: targetX,
y: centerY,
}
})
const simulation = forceSimulation(nodes)
.alphaDecay(1 - Math.pow(0.001, 1 / layoutTicks))
.force('y', forceY<BeeswarmNode>(centerY).strength(0.06))
.force('collide', forceCollide<BeeswarmNode>(4.5).iterations(2))
.stop()
simulation.tick(layoutTicks)
const rows: readonly PositionedBeeswarmPoint[] = nodes.map((node) => ({
name: node.name,
'economy (mpg)': valuePosition.invert(node.x ?? node.targetX),
offset: node.y ?? centerY,
}))
return {
marks: [
dot(rows, {
x: 'economy (mpg)',
y: 'offset',
r: 4,
fill: '#0d9488',
stroke: '#ffffff',
strokeWidth: 1,
}),
],
guides: false,
margin,
x: {
scale: scaleLinear().domain([5, 50]),
},
y: {
scale: scaleLinear().domain([0, innerHeight]),
},
}
})
export const mount = tanstackMount(definition, 'Beeswarm distribution')