TanStack
Catalog

Snapped axis pointer with grouped tooltip

interaction

233 lines · 4 files · 6.3 kB

cases/80-echarts-axis-pointer/colors.ts8 lines · dependency
cases/80-echarts-axis-pointer/colors.ts
import type { AxisPointerIndustry } from './selection'

export const axisPointerColors: Readonly<Record<AxisPointerIndustry, string>> =
  {
    Manufacturing: '#2563eb',
    Construction: '#f97316',
    Finance: '#10b981',
  }
cases/80-echarts-axis-pointer/example.tsx154 lines · entry
cases/80-echarts-axis-pointer/example.tsx
import { Chart } from '@tanstack/charts/react/tooltip'
import { industries } from '@tanstack/charts-data/industries'
import { colorLegend, defineChart, dot, lineY } from '@tanstack/charts'
import { focusGuideX } from '@tanstack/charts/focus/guide'
import { tooltip } from '@tanstack/charts/tooltip'
import { scaleLinear, scaleUtc } from 'd3-scale'
import { axisPointerColors } from './colors'
import { axisPointerDateKey } from './model'
import { axisPointerData, axisPointerIndustries } from './selection'
import type { ChartTooltipOptions } from '@tanstack/charts'
import type { AxisPointerDatum } from './selection'

export const exampleAriaLabel = 'Snapped axis pointer with grouped tooltip'

const month = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  timeZone: 'UTC',
})

const monthYear = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  year: 'numeric',
  timeZone: 'UTC',
})

const axisPointerTooltip: ChartTooltipOptions<AxisPointerDatum> = {
  className: 'conformance-tooltip-grouped',
  sticky: false,
  anchor: { x: 'value', y: 'plot-top' },
  placement: ['bottom-right', 'bottom-left', 'right', 'left'],
  offset: 10,
  sort: 'color-domain',
  items: [
    {
      channel: 'x',
      label: '',
      text: (point) => monthYear.format(point.datum.date),
    },
    {
      channel: 'y',
      text: (point) => point.datum.unemployed.toLocaleString('en-US'),
    },
    { channel: 'group', label: 'Industry' },
  ],
}

export function createExampleChart(input: ExampleChartInput) {
  const rows = axisPointerData(industries, input.revision)

  return defineChart(
    {
      marks: [
        lineY(rows, {
          id: 'industry-lines',
          x: 'date',
          y: 'unemployed',
          z: 'industry',
          color: 'industry',
          key: axisPointerKey,
          strokeWidth: 2,
        }),
        dot(rows, {
          id: 'industry-points',
          x: 'date',
          y: 'unemployed',
          z: 'industry',
          color: 'industry',
          key: axisPointerKey,
          r: 3,
          stroke: '#ffffff',
          strokeWidth: 1,
        }),
        focusGuideX(rows, {
          id: 'axis-pointer-guide',
          x: 'date',
          y: 'unemployed',
          z: 'industry',
          key: axisPointerKey,
          xRule: {
            stroke: '#64748b',
            strokeWidth: 1,
            strokeDasharray: '4 4',
          },
        }),
      ],
      x: {
        scale: scaleUtc,
        axis: { ticks: { format: (value) => month.format(value) } },
      },
      y: {
        scale: scaleLinear,
        grid: input.preview !== true,
        axis: {
          ticks: { count: 5 },
          ...(input.preview === true
            ? {}
            : { label: 'Unemployed (thousands)' }),
        },
      },
      color: {
        domain: axisPointerIndustries,
        range: axisPointerIndustries.map(
          (industry) => axisPointerColors[industry],
        ),
        legend: colorLegend({ itemWidth: 100 }),
      },
      focus: 'group-x',
      focusRing: false,
      maxFocusDistance: Number.POSITIVE_INFINITY,
      svgAnimation: false,
      margin:
        input.preview === true
          ? { top: 4, right: 4, bottom: 22, left: 38 }
          : { top: 38, right: 24, bottom: 45, left: 60 },
    },
    {
      keyboard: true,
      tooltip: {
        use: tooltip,
        ...axisPointerTooltip,
      },
    },
  )
}

function axisPointerKey(row: AxisPointerDatum) {
  return `${row.industry}:${axisPointerDateKey(row.date)}`
}

export interface ExampleChartInput {
  width: number
  height: number
  revision: number
  preview?: boolean
  interactive?: boolean
}

export const chart = createExampleChart({
  width: 640,
  height: 480,
  revision: 0,
  preview: false,
})

export default function Example() {
  return (
    <Chart
      definition={chart}
      height={480}
      ariaLabel={exampleAriaLabel}
      ariaDescription="Move across the chart or use the arrow keys to compare all three industries at the nearest month."
    />
  )
}
cases/80-echarts-axis-pointer/model.ts37 lines · dependency
cases/80-echarts-axis-pointer/model.ts
import { axisPointerDates, axisPointerIndustries } from './selection'
import type { AxisPointerDatum } from './selection'

export function axisPointerDateKey(date: Date) {
  return date.toISOString().slice(0, 10)
}

export function axisPointerRowsAtDate(
  rows: readonly AxisPointerDatum[],
  date: Date,
) {
  const timestamp = date.getTime()
  return axisPointerIndustries.flatMap((industry) => {
    const row = rows.find(
      (candidate) =>
        candidate.industry === industry &&
        candidate.date.getTime() === timestamp,
    )
    return row ? [row] : []
  })
}

export function axisPointerTargetValue(rows: readonly AxisPointerDatum[]) {
  if (!rows.length) return null
  return rows.reduce((sum, row) => sum + row.unemployed, 0) / rows.length
}

export function axisPointerAnchorDate(
  anchor: string,
  rows: readonly AxisPointerDatum[],
) {
  const key = anchor.startsWith('date:') ? anchor.slice(5) : ''
  return (
    axisPointerDates(rows).find((date) => axisPointerDateKey(date) === key) ??
    null
  )
}
cases/80-echarts-axis-pointer/selection.ts34 lines · dependency
cases/80-echarts-axis-pointer/selection.ts
import type { IndustriesRow } from '@tanstack/charts-data/industries'

export const axisPointerIndustries = [
  'Manufacturing',
  'Construction',
  'Finance',
]
export type AxisPointerIndustry = (typeof axisPointerIndustries)[number]
export type AxisPointerDatum = IndustriesRow & {
  readonly industry: AxisPointerIndustry
}

export function axisPointerData(
  rows: readonly IndustriesRow[],
  revision = 0,
): readonly AxisPointerDatum[] {
  const offset = Math.abs(revision) % 2
  const start = Date.UTC(2005, offset)
  const end = Date.UTC(2005, offset + 8)

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

export function axisPointerDates(rows: readonly AxisPointerDatum[]) {
  return Array.from(
    new Set(rows.map((row) => row.date.getTime())),
    (timestamp) => new Date(timestamp),
  )
}