TanStack
Catalog

Violin distribution comparison

distribution

142 lines · 2 files · 3.6 kB

cases/63-violin-distributions/example.tsx121 lines · entry
cases/63-violin-distributions/example.tsx
import { Chart } from '@tanstack/charts/react/tooltip'
import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip'

import { penguins } from '@tanstack/charts-data/penguins'
import {
  binY,
  d3AreaXCurve,
  defineChart,
  dot,
  groupBy,
  median,
  normalize,
  tickY,
  violinY,
} from '@tanstack/charts'
import { scaleLinear, scalePoint } from 'd3-scale'
import { curveBasis } from 'd3-shape'
import { isPenguinMass, massBoundaries, violinSpecies } from './selection'

const colors = ['#64748b', '#0d9488', '#7c3aed']

export const createExampleChart = (input: ChartOptions) => {
  const observations = penguins
    .filter(isPenguinMass)
    .slice(input.revision * 8, input.revision * 8 + 320)
  const bins = binY(observations, {
    value: 'body_mass_g',
    by: 'species',
    thresholds: massBoundaries,
    outputs: { count: { reduce: 'count' } },
  })
  const profiles = normalize(bins, {
    value: 'count',
    by: 'species',
    basis: 'max',
    as: 'width',
  })
  const summaries = groupBy(observations, {
    by: 'species',
    outputs: {
      median: { value: 'body_mass_g', reduce: median },
    },
  })

  return defineChart(
    {
      marks: [
        violinY(profiles, {
          id: 'mass-violins',
          x: 'species',
          y: 'y',
          width: 'width',
          key: (row) => `${row.species}:${row.y}`,
          span: 0.76,
          color: 'species',
          fillOpacity: 0.58,
          curve: d3AreaXCurve(curveBasis),
        }),
        tickY(summaries, {
          id: 'median-ticks',
          x: 'species',
          y: 'median',
          key: 'species',
          span: 0.36,
          stroke: '#0f172a',
          strokeWidth: 2,
        }),
        dot(summaries, {
          id: 'median-dots',
          x: 'species',
          y: 'median',
          key: 'species',
          color: 'species',
          stroke: '#ffffff',
          strokeWidth: 1,
          r: 3.5,
        }),
      ],
      scales: {
        x: {
          scale: scalePoint<string>().domain(violinSpecies).padding(0.5),
        },
        y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } },
      },

      color: {
        domain: violinSpecies,
        range: colors,
      },
    },
    {
      keyboard: true,
      tooltip: {
        use: exampleTooltip,
        ...{
          format: ({ datum }) =>
            'median' in datum
              ? `${datum.species} · median body mass ${datum.median.toLocaleString(
                  'en-US',
                )} g`
              : `${datum.species} · distribution at ${datum.y.toLocaleString(
                  'en-US',
                )} g`,
        },
      },
    },
  )
}
export interface ChartOptions {
  revision: number
}

export const exampleAriaLabel = 'Violin distribution comparison'

export const chart = createExampleChart({
  revision: 0,
})

export default function Example() {
  return <Chart ariaLabel={exampleAriaLabel} definition={chart} height={480} />
}
cases/63-violin-distributions/selection.ts21 lines · dependency
cases/63-violin-distributions/selection.ts
import type { PenguinsRow } from '@tanstack/charts-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 const massBoundaries = [
  2500, 2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500,
  5750, 6000, 6250, 6500,
] as const

export function isPenguinMass(row: PenguinsRow): row is PenguinMass {
  return (
    row.body_mass_g !== null &&
    violinSpecies.includes(row.species as ViolinSpecies)
  )
}