Source

182 chart lines · 2 files · 5.0 kB

U.S. industry unemployment1,708 records · CSV · 53.9 kB
@charts-poc/demo-data/industries

Selection: Complete published snapshot

date
Date
industry
string
unemployed
number

U.S. Bureau of Labor Statistics@observablehq/sample-datasets@1.0.1 · revision 732c0148de74 · industries.csv · ISC distribution; upstream source credited · SHA-256 4cfc13b6ebe0Pinned snapshot

cases/35-grouped-tooltip/tanstack.ts159 lines · entry
cases/35-grouped-tooltip/tanstack.ts
import {
  bandX,
  defineChart,
  dot,
  lineY,
  mountChart,
  whenFocused,
} from '@tanstack/charts'
import type { ChartHostOptions } from '@tanstack/charts'
import { tooltip } from '@tanstack/charts/tooltip'
import { portal } from '@tanstack/charts/tooltip/portal'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { industries } from '@charts-poc/demo-data/industries'
import type {
  ConformanceHandle,
  ConformanceInput,
  ConformanceTestDriver,
} from '../../types'
import { industryNames, selectGroupedTooltipData } from './selection'
import type { GroupedTooltipDatum } from './selection'

const colors = ['#2563eb', '#f97316', '#10b981']

const definition = (input: ConformanceInput) => {
  const rows = selectGroupedTooltipData(industries, input.revision)
  const dates = rows.filter((row) => row.industry === industryNames[0])
  return defineChart({
    marks: [
      whenFocused(
        bandX(dates, {
          x: 'date',
          fill: '#64748b',
          fillOpacity: 0.14,
          inset: 3,
          radius: 4,
        }),
        { match: 'x' },
      ),
      lineY(rows, {
        x: 'date',
        y: 'unemployed',
        color: 'industry',
      }),
      dot(rows, {
        x: 'date',
        y: 'unemployed',
        color: 'industry',
        r: 2.5,
        states: [
          {
            when: { focus: 'group' },
            style: { r: 5, stroke: 'Canvas', strokeWidth: 1.5 },
            transition: { duration: 140, easing: 'ease-out' },
          },
          {
            when: { focus: 'unmatched' },
            style: { opacity: 0.3 },
          },
        ],
      }),
    ],
    x: { scale: scaleUtc },
    y: {
      scale: scaleLinear,
      grid: true,
      axis: { label: 'Unemployed (thousands)' },
    },
    color: {
      domain: industryNames,
      range: colors,
    },
  })
}

const configuredDefinition = (input: ConformanceInput) =>
  defineChart(definition(input), {
    animate: false,
    keyboard: true,
    focus: 'group-x',
    tooltip: {
      use: tooltip,
      portal,
      anchor: 'group-center',
      placement: ['top', 'right', 'left', 'bottom'],
      sort: 'color-domain',
    },
  })

export function mount(
  container: HTMLElement,
  input: ConformanceInput,
): ConformanceHandle {
  let focusedRows: string[] = []
  const options: ChartHostOptions<GroupedTooltipDatum> = {
    definition: configuredDefinition(input),
    width: input.width,
    height: input.height,
    ariaLabel: 'Grouped industry unemployment tooltip',
    onFocusGroupChange(points) {
      focusedRows = points.map(
        (point) => `${dateKey(point.datum.date)}:${point.datum.industry}`,
      )
    },
  }
  const host = mountChart(container, options)
  const driver: ConformanceTestDriver = {
    resolveTarget(target) {
      if (target.view && target.view !== 'main') return null
      const date = target.anchor.startsWith('date:')
        ? target.anchor.slice('date:'.length)
        : null
      const point = host
        .getScene()
        .points.find(
          (candidate) =>
            dateKey(candidate.datum.date) === date &&
            candidate.datum.industry === industryNames[0],
        )
      const svg = container.querySelector('svg')
      if (!point || !svg) return null
      const scene = host.getScene()
      const bounds = svg.getBoundingClientRect()
      return {
        x: bounds.left + (point.x / scene.width) * bounds.width,
        y: bounds.top + (point.y / scene.height) * bounds.height,
        focusElement: svg,
      }
    },
    readState() {
      const tooltip = container.querySelector<HTMLElement>('.ts-chart-tooltip')
      return {
        focus: { rows: [...focusedRows] },
        tooltip: {
          visible: Boolean(tooltip && !tooltip.hidden),
          text: tooltip?.textContent?.trim() ?? '',
        },
      }
    },
  }

  return {
    driver,
    update(nextInput) {
      host.update({
        ...options,
        definition: configuredDefinition(nextInput),
        width: nextInput.width,
        height: nextInput.height,
      })
    },
    destroy() {
      host.destroy()
    },
  }
}

function dateKey(date: Date) {
  return date.toISOString().slice(0, 10)
}
cases/35-grouped-tooltip/selection.ts23 lines · support
cases/35-grouped-tooltip/selection.ts
import type { IndustriesRow } from '@charts-poc/demo-data/industries'

export const industryNames = ['Manufacturing', 'Construction', 'Finance']
export type IndustryName = (typeof industryNames)[number]
export type GroupedTooltipDatum = IndustriesRow & {
  readonly industry: IndustryName
}

export function selectGroupedTooltipData(
  rows: readonly IndustriesRow[],
  revision = 0,
): readonly GroupedTooltipDatum[] {
  const offset = Math.abs(revision) % 2
  const start = Date.UTC(2000, offset)
  const end = Date.UTC(2003, offset)

  return rows.filter(
    (row): row is GroupedTooltipDatum =>
      row.date.getTime() >= start &&
      row.date.getTime() < end &&
      industryNames.some((industry) => industry === row.industry),
  )
}