Source

160 chart lines · 1 file · 4.8 kBBenchmark harness excluded · shared/mount.ts

Palmer penguins344 records · CSV · 13.5 kB
@charts-poc/demo-data/penguins

Selection: 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/39-density-contours/tanstack.ts160 lines · entry
cases/39-density-contours/tanstack.ts
import { penguins } from '@charts-poc/demo-data/penguins'
import { createMark, defineChart } from '@tanstack/charts'
import { contourDensity } from 'd3-contour'
import { geoPath } from 'd3-geo'
import { scaleLinear } from 'd3-scale'
import { tanstackMount } from '../../shared/mount'
import type { PenguinsRow } from '@charts-poc/demo-data/penguins'
import type { ContourMultiPolygon } from 'd3-contour'
import type { ConformanceInput } from '../../types'
import type { ChartPoint, SceneNode } from '@tanstack/charts'

type PenguinBill = PenguinsRow & {
  readonly culmen_length_mm: number
  readonly culmen_depth_mm: number
}

const densityBandwidth = 18
export const densityThresholds = [0.04, 0.08, 0.12, 0.16, 0.2, 0.24]
export const densityXDomain: [number, number] = [30, 62]
export const densityYDomain: [number, number] = [12, 23]

export interface DensityContourDatum {
  id: string
  centroidX: number
  centroidY: number
  density: number
}

const densityPercent = new Intl.NumberFormat('en-US', {
  style: 'percent',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
})
const densityCoordinate = new Intl.NumberFormat('en-US', {
  maximumFractionDigits: 1,
})

export const densityDefinition = (input: ConformanceInput) => {
  const points = penguins
    .filter((row): row is PenguinBill => {
      return row.culmen_length_mm !== null && row.culmen_depth_mm !== null
    })
    .slice(input.revision * 8, input.revision * 8 + 320)

  return defineChart({
    marks: [densityMark(points)],
    x: {
      scale: scaleLinear().domain(densityXDomain),
    },
    y: {
      scale: scaleLinear().domain(densityYDomain),
    },
    guides: false,
    margin: 0,
  })
}

function densityMark(data: PenguinBill[]) {
  return createMark<DensityContourDatum, number, number>(({ markIndex }) => {
    const id = `density-${markIndex}`

    return {
      id,
      channels: {
        x: {
          scale: 'x',
          values: data.map((point) => point.culmen_length_mm),
        },
        y: {
          scale: 'y',
          values: data.map((point) => point.culmen_depth_mm),
        },
      },
      render: ({ chart, scales }) => {
        const estimator = contourDensity<PenguinBill>()
          .x((point) => scales.x.map(point.culmen_length_mm))
          .y((point) => scales.y.map(point.culmen_depth_mm))
          .size([chart.width, chart.height])
          .bandwidth(densityBandwidth)
          .thresholds(densityThresholds.map((threshold) => threshold / 100))
        const geometry: ContourMultiPolygon[] = estimator(data)

        const path = geoPath()
        const children: SceneNode[] = []
        const points: ChartPoint<DensityContourDatum, number, number>[] = []
        const centroidXScale = scaleLinear()
          .domain(densityXDomain)
          .range([0, chart.width])
        const centroidYScale = scaleLinear()
          .domain(densityYDomain)
          .range([chart.height, 0])

        for (let index = 0; index < geometry.length; index++) {
          const contour = geometry[index]
          if (contour === undefined) continue
          const pathData = path(contour)
          if (pathData === null) continue
          const key = `${id}:${index}`
          children.push({
            kind: 'area',
            key,
            points: [],
            path: pathData,
            style: {
              fill: '#2563eb',
              fillOpacity: 0.16,
              stroke: '#1e3a8a',
              strokeWidth: 1,
            },
          })

          const [x, y] = path.centroid(contour)
          if (!Number.isFinite(x) || !Number.isFinite(y)) continue
          const centroidX = centroidXScale.invert(x)
          const centroidY = centroidYScale.invert(y)
          points.push({
            key,
            markId: id,
            group: contour.value,
            groupLabel: 'Density contour',
            datum: {
              id: key,
              centroidX,
              centroidY,
              density: contour.value,
            },
            datumIndex: index,
            xValue: centroidX,
            yValue: centroidY,
            x,
            y,
            color: '#2563eb',
          })
        }

        return {
          nodes: [
            {
              kind: 'group',
              key: id,
              className: 'ts-chart__area',
              ariaHidden: true,
              children,
            },
          ],
          points,
        }
      },
    }
  })
}

export const mount = tanstackMount(
  densityDefinition,
  'Point density contours',
  {
    format: (point) =>
      `Density: ${densityPercent.format(point.datum.density)} · Bill centroid: (${densityCoordinate.format(point.datum.centroidX)} mm, ${densityCoordinate.format(point.datum.centroidY)} mm)`,
  },
)