255 lines · 2 files · 6.7 kB
cases/119-stacked-bar-band-cursor/example.tsx155 lines · entry
cases/119-stacked-bar-band-cursor/example.tsx
import { useMemo } from 'react'
import { defineChart } from '@tanstack/charts'
import { Chart } from '@tanstack/charts/react/core'
import { stackedCursorRowsForRevision } from './model'
import { barY, colorLegend, crosshair, stack } from '@tanstack/charts'
import { motion } from '@tanstack/charts/motion'
import { scaleBand, scaleLinear } from 'd3-scale'
import {
stackedCursorBandInset,
stackedCursorBarInset,
stackedCursorCauses,
stackedCursorColors,
formatStackedCursorEndpoint,
stackedCursorMaximum,
stackedCursorPeriods,
} from './model'
import type { StackedCursorRow } from './model'
export interface ExampleProps {
width?: number
height?: number
revision?: number
}
const cursorTransition = {
type: 'spring' as const,
stiffness: 260,
damping: 26,
mass: 0.72,
restDelta: 0.02,
restSpeed: 0.02,
}
export const createStackedCursorRenderer = () =>
motion({
initial: false,
transition: {
type: 'spring',
stiffness: 190,
damping: 22,
mass: 0.85,
},
})
export const createExampleChart = (rows: readonly StackedCursorRow[]) =>
defineChart(
{
marks: [
crosshair<string, number>({
id: 'stacked-cursor-band',
x: {
band: {
fill: '#64748b',
fillOpacity: 0.26,
inset: stackedCursorBandInset,
radius: 3,
},
label: {
format: String,
fill: 'CanvasText',
stroke: 'Canvas',
strokeWidth: 5,
fontSize: 11,
fontWeight: 700,
},
},
y: false,
motion: { transition: cursorTransition },
}),
barY(rows, {
id: 'stacked-cursor-bars',
x: 'period',
y: 'deaths',
z: 'cause',
color: 'cause',
key: 'id',
layout: stack({ order: stackedCursorCauses }),
inset: stackedCursorBarInset,
radius: 2,
}),
crosshair<string, number>({
id: 'stacked-cursor-rule',
x: false,
y: {
stroke: '#475569',
strokeOpacity: 0.82,
strokeWidth: 1,
strokeDasharray: '4 4',
label: {
format: formatStackedCursorEndpoint,
fill: 'CanvasText',
stroke: 'Canvas',
strokeWidth: 16,
fontSize: 11,
fontWeight: 700,
},
},
motion: { transition: cursorTransition },
}),
],
scales: {
x: {
scale: scaleBand<string>().domain(stackedCursorPeriods).padding(0.18),
},
y: {
scale: scaleLinear().domain([0, stackedCursorMaximum]),
grid: true,
axis: { ticks: { count: 5 }, label: 'Deaths' },
},
},
color: {
domain: stackedCursorCauses,
range: stackedCursorColors,
legend: colorLegend({ label: 'Cause' }),
},
focus: 'group-x',
focusRing: false,
maxFocusDistance: Number.POSITIVE_INFINITY,
tooltip: false,
keyboard: true,
},
{ svgAnimation: false },
)
export default function StackedCursorCatalogView({
width = 640,
height = 480,
revision = 0,
}: ExampleProps = {}) {
const renderer = useMemo(createStackedCursorRenderer, [])
const definition = useMemo(
() => createExampleChart(stackedCursorRowsForRevision(revision)),
[revision],
)
const ariaLabel = 'Crimean War deaths with x band and y rule cursors'
const ariaDescription =
'Move over a stacked bar. The x cursor highlights the full stack and the dotted y cursor marks the focused segment endpoint.'
return (
<div data-conformance-view="main" style={{ width, height }}>
<Chart
idPrefix="119-stacked-bar-band-cursor"
definition={definition}
renderer={renderer}
width={width}
height={height}
ariaLabel={ariaLabel}
ariaDescription={ariaDescription}
/>
</div>
)
}cases/119-stacked-bar-band-cursor/model.ts100 lines · dependency
cases/119-stacked-bar-band-cursor/model.ts
import { crimeanWar } from '@tanstack/charts-data/crimean-war'
export const stackedCursorCauses = ['disease', 'wounds', 'other'] as const
export const stackedCursorColors = ['#4269d0', '#ff725c', '#efb118']
export const stackedCursorBarInset = 4
export const stackedCursorOutset = 4
export const stackedCursorBandInset =
stackedCursorBarInset - stackedCursorOutset
export type StackedCursorCause = (typeof stackedCursorCauses)[number]
export interface StackedCursorRow {
id: string
period: string
cause: StackedCursorCause
deaths: number
start: number
end: number
}
export interface StackedCursorBandRow {
period: string
total: number
}
export type StackedCursorDatum = StackedCursorRow | StackedCursorBandRow
const month = new Intl.DateTimeFormat('en-US', {
month: 'short',
timeZone: 'UTC',
})
const integer = new Intl.NumberFormat('en-US')
export function formatStackedCursorEndpoint(value: number) {
return integer.format(value)
}
const sourceRows = crimeanWar.slice(3, 11)
const revisedMultipliers = [
[0.72, 1.5, 1.22],
[1.2, 0.65, 0.82],
[0.78, 1.3, 1.18],
[1.18, 0.72, 0.86],
[0.74, 1.28, 1.22],
[1.15, 0.7, 0.86],
[0.72, 1.35, 1.15],
[1.18, 0.68, 0.84],
] as const
export const stackedCursorPeriods = sourceRows.map((row) =>
month.format(row.date),
)
const stackedCursorRevisions = [false, true].map((revised) =>
sourceRows.flatMap((row, periodIndex) => {
const period = month.format(row.date)
let start = 0
return stackedCursorCauses.map((cause, causeIndex) => {
const multiplier = revised
? (revisedMultipliers[periodIndex]?.[causeIndex] ?? 1)
: 1
const deaths = Math.round(row[cause] * multiplier)
const result = {
id: `${period}:${cause}`,
period,
cause,
deaths,
start,
end: start + deaths,
}
start = result.end
return result
})
}),
)
export function stackedCursorRowsForRevision(
revision: number,
): readonly StackedCursorRow[] {
const index = Number.isFinite(revision)
? Math.abs(Math.trunc(revision)) % stackedCursorRevisions.length
: 0
return stackedCursorRevisions[index] ?? stackedCursorRevisions[0]!
}
export const stackedCursorRows = stackedCursorRowsForRevision(0)
export const stackedCursorMaximum =
Math.ceil(
Math.max(
...stackedCursorRevisions.flatMap((rows) => rows.map((row) => row.end)),
) / 500,
) * 500
export const stackedCursorBands: readonly StackedCursorBandRow[] =
stackedCursorPeriods.map((period) => ({
period,
total: stackedCursorMaximum,
}))