Source

111 chart lines · 1 file · 3.0 kBBenchmark harness excluded · shared/mount.ts

Michelson speed-of-light experiments100 records · CSV · 874 B
@charts-poc/demo-data/morley

Selection: Complete published snapshot

Expt
number
Run
number
Speed
number

R datasets / Michelson–Morley experimentobservablehq/plot@356f579b1d94 · revision 356f579b1d94 · test/data/morley.csv · Observable Plot repository MIT; upstream source credited · SHA-256 a9c06023c0f2Pinned snapshot

cases/15-boxplot/tanstack.ts111 lines · entry
cases/15-boxplot/tanstack.ts
import { barY, defineChart, dot, link, tickY } from '@tanstack/charts'
import { group, max, min, quantileSorted } from 'd3-array'
import { scaleBand, scaleLinear } from 'd3-scale'
import { morley } from '@charts-poc/demo-data/morley'
import type { MorleyRow } from '@charts-poc/demo-data/morley'
import { tanstackMount } from '../../shared/mount'

interface BoxSummary {
  Expt: number
  q1: number
  median: number
  q3: number
  low: number
  high: number
}

const { summaries, outliers } = summarizeBoxplots(morley)

const definition = () =>
  defineChart({
    marks: [
      link(summaries, {
        x1: 'Expt',
        y1: 'low',
        x2: 'Expt',
        y2: 'high',
        stroke: '#2563eb',
      }),
      barY(summaries, {
        x: 'Expt',
        y1: 'q1',
        y2: 'q3',
        fill: '#bfdbfe',
        inset: 18,
      }),
      tickY(summaries, {
        x: 'Expt',
        y: 'median',
        stroke: '#2563eb',
        strokeWidth: 2,
        inset: 18,
      }),
      dot(outliers, {
        x: 'Expt',
        y: 'Speed',
        fill: '#ffffff',
        stroke: '#2563eb',
        r: 2.5,
      }),
    ],
    x: {
      scale: () => scaleBand<number>().padding(0.22),
    },
    y: {
      scale: scaleLinear,
      grid: true,
      axis: { label: 'Speed of light (km/s minus 299,000)' },
    },
  })

function summarizeBoxplots(rows: readonly MorleyRow[]) {
  const summaries: BoxSummary[] = []
  const outliers: MorleyRow[] = []

  for (const [experiment, observations] of group(rows, (row) => row.Expt)) {
    const values = observations.map((row) => row.Speed).sort((a, b) => a - b)
    const q1 = quantileSorted(values, 0.25) ?? 0
    const median = quantileSorted(values, 0.5) ?? 0
    const q3 = quantileSorted(values, 0.75) ?? 0
    const lowerFence = q1 - (q3 - q1) * 1.5
    const upperFence = q3 + (q3 - q1) * 1.5
    const inside = observations.filter(
      (row) => row.Speed >= lowerFence && row.Speed <= upperFence,
    )
    summaries.push({
      Expt: experiment,
      q1,
      median,
      q3,
      low: min(inside, (row) => row.Speed) ?? q1,
      high: max(inside, (row) => row.Speed) ?? q3,
    })
    outliers.push(
      ...observations.filter(
        (row) => row.Speed < lowerFence || row.Speed > upperFence,
      ),
    )
  }
  return { summaries, outliers }
}

export const mount = tanstackMount(definition, 'Grouped boxplots', {
  format: ({ datum }) =>
    'median' in datum
      ? `Experiment ${datum.Expt} · median ${datum.median.toLocaleString(
          'en-US',
          {
            maximumFractionDigits: 1,
          },
        )} · IQR ${datum.q1.toLocaleString('en-US', {
          maximumFractionDigits: 1,
        })}${datum.q3.toLocaleString('en-US', {
          maximumFractionDigits: 1,
        })}`
      : `Experiment ${datum.Expt} outlier · ${datum.Speed.toLocaleString(
          'en-US',
          {
            maximumFractionDigits: 1,
          },
        )}`,
})