283 lines · 3 files · 9.2 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,
}))packages/charts-demo-data/src/crimean-war.js28 lines · dependency
packages/charts-demo-data/src/crimean-war.js
// Generated by scripts/sync-demo-data.mjs from crimean-war.csv.
// Do not edit this file directly.
export const crimeanWar = [
{date: new Date("1854-04-01T00:00:00.000Z"), army: 8571, disease: 1, wounds: 0, other: 5},
{date: new Date("1854-05-01T00:00:00.000Z"), army: 23333, disease: 12, wounds: 0, other: 9},
{date: new Date("1854-06-01T00:00:00.000Z"), army: 28333, disease: 11, wounds: 0, other: 6},
{date: new Date("1854-07-01T00:00:00.000Z"), army: 28772, disease: 359, wounds: 0, other: 23},
{date: new Date("1854-08-01T00:00:00.000Z"), army: 30246, disease: 828, wounds: 1, other: 30},
{date: new Date("1854-09-01T00:00:00.000Z"), army: 30290, disease: 788, wounds: 81, other: 70},
{date: new Date("1854-10-01T00:00:00.000Z"), army: 30643, disease: 503, wounds: 132, other: 128},
{date: new Date("1854-11-01T00:00:00.000Z"), army: 29736, disease: 844, wounds: 287, other: 106},
{date: new Date("1854-12-01T00:00:00.000Z"), army: 32779, disease: 1725, wounds: 114, other: 131},
{date: new Date("1855-01-01T00:00:00.000Z"), army: 32393, disease: 2761, wounds: 83, other: 324},
{date: new Date("1855-02-01T00:00:00.000Z"), army: 30919, disease: 2120, wounds: 42, other: 361},
{date: new Date("1855-03-01T00:00:00.000Z"), army: 30107, disease: 1205, wounds: 32, other: 172},
{date: new Date("1855-04-01T00:00:00.000Z"), army: 32252, disease: 477, wounds: 48, other: 57},
{date: new Date("1855-05-01T00:00:00.000Z"), army: 35473, disease: 508, wounds: 49, other: 37},
{date: new Date("1855-06-01T00:00:00.000Z"), army: 38863, disease: 802, wounds: 209, other: 31},
{date: new Date("1855-07-01T00:00:00.000Z"), army: 42647, disease: 382, wounds: 134, other: 33},
{date: new Date("1855-08-01T00:00:00.000Z"), army: 44614, disease: 483, wounds: 164, other: 25},
{date: new Date("1855-09-01T00:00:00.000Z"), army: 47751, disease: 189, wounds: 276, other: 20},
{date: new Date("1855-10-01T00:00:00.000Z"), army: 46852, disease: 128, wounds: 53, other: 18},
{date: new Date("1855-11-01T00:00:00.000Z"), army: 37853, disease: 178, wounds: 33, other: 32},
{date: new Date("1855-12-01T00:00:00.000Z"), army: 43217, disease: 91, wounds: 18, other: 28},
{date: new Date("1856-01-01T00:00:00.000Z"), army: 44212, disease: 42, wounds: 2, other: 48},
{date: new Date("1856-02-01T00:00:00.000Z"), army: 43485, disease: 24, wounds: 0, other: 19},
{date: new Date("1856-03-01T00:00:00.000Z"), army: 46140, disease: 15, wounds: 0, other: 35},
]