351 lines · 2 files · 10.9 kB
cases/121-active-bar-dashboard/example.tsx300 lines · entry
cases/121-active-bar-dashboard/example.tsx
import { useMemo, useRef, useState } from 'react'
import { motion } from '@tanstack/charts/motion'
import { Chart } from '@tanstack/charts/react/core'
import { dashboardRows, metricTotal } from './model'
import type { CSSProperties } from 'react'
import type { DashboardMetric, DashboardRow } from './model'
import { barY, crosshair, defineChart } from '@tanstack/charts'
import { scaleBand } from '@tanstack/charts/scales/band'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { tooltip } from '@tanstack/charts/tooltip'
import type { ChartTooltipOptions } from '@tanstack/charts'
const metricLabels: Record<DashboardMetric, string> = {
desktop: 'Desktop',
mobile: 'Mobile',
}
const cardStyle: CSSProperties & Record<`--${string}`, string> = {
'--ts-chart-1': '#7c3aed',
'--ts-chart-tooltip-background':
'color-mix(in srgb, Canvas 94%, transparent)',
'--ts-chart-tooltip-color': 'CanvasText',
'--ts-chart-tooltip-border':
'1px solid color-mix(in srgb, CanvasText 12%, transparent)',
'--ts-chart-tooltip-border-radius': '10px',
'--ts-chart-tooltip-shadow':
'0 12px 34px color-mix(in srgb, CanvasText 14%, transparent)',
'--ts-chart-tooltip-font':
'600 12px/1.35 Inter, ui-sans-serif, system-ui, sans-serif',
boxSizing: 'border-box',
width: '100%',
height: '100%',
overflow: 'hidden',
border: '1px solid color-mix(in srgb, currentColor 12%, transparent)',
borderRadius: 18,
color: 'CanvasText',
background: 'Canvas',
fontFamily:
"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
boxShadow: '0 18px 55px color-mix(in srgb, CanvasText 7%, transparent)',
}
export interface ExampleProps {
width?: number
height?: number
revision?: number
}
export const dashboardSpring = {
type: 'spring' as const,
stiffness: 210,
damping: 24,
mass: 0.78,
}
const tickIds = ['01', '06', '12', '18', '24']
const tooltipOptions: ChartTooltipOptions<DashboardRow> = {
anchor: 'point',
placement: ['top', 'right', 'left'],
className: 'active-bar-tooltip',
}
export function activeBarDashboardDefinition(
input: ExampleChartInput,
metric: DashboardMetric,
) {
const rows = dashboardRows(input.revision)
const maximum = Math.max(...rows.map((row) => row[metric]))
return defineChart(
{
marks: [
barY(rows, {
id: 'daily-visitors',
x: 'id',
y: metric,
key: 'id',
fill: 'url(#visitor-bars)',
radius: { end: 4 },
inset: input.preview ? 1.5 : 2.5,
states: [
{
when: { focus: 'unmatched' },
style: { opacity: 0.26 },
transition: dashboardSpring,
},
{
when: { focus: 'primary' },
style: { opacity: 1, inset: input.preview ? 0.5 : 1.5 },
transition: dashboardSpring,
},
],
}),
crosshair<string, number>({
id: 'active-bar-ring',
x: {
band: {
inset: -2,
radius: 6,
fill: 'transparent',
stroke: 'var(--ts-chart-1)',
strokeOpacity: 0.92,
strokeWidth: 1.5,
},
},
y: false,
motion: { transition: dashboardSpring },
}),
],
scales: {
x: {
scale: scaleBand<string>()
.domain(rows.map((row) => row.id))
.paddingInner(0.18)
.paddingOuter(0.08),
axis: {
line: false,
ticks: {
values: tickIds,
size: 0,
padding: input.preview ? 4 : 8,
format: (value) =>
rows.find((row) => row.id === value)?.day ?? '',
},
tickLabels: {
fontSize: input.preview ? 8 : 10,
opacity: 0.52,
},
},
},
y: {
scale: scaleLinear().domain([0, Math.ceil(maximum * 1.12)]),
grid: true,
axis: false,
},
},
gradients: [
{
id: 'visitor-bars',
x1: 0,
y1: 1,
x2: 0,
y2: 0,
stops: [
{ offset: 0, color: 'var(--ts-chart-1)', opacity: 0.42 },
{ offset: 1, color: 'var(--ts-chart-1)', opacity: 0.96 },
],
},
],
motion: { transition: dashboardSpring },
margin: input.preview
? { top: 8, right: 18, bottom: 22, left: 18 }
: { top: 12, right: 8, bottom: 34, left: 8 },
},
{
focus: 'nearest',
keyboard: input.interactive,
tooltip: input.interactive
? {
use: tooltip,
...tooltipOptions,
format: ({ datum }) =>
`${datum.day} · ${datum[metric].toLocaleString('en-US')} ${metric} visitors`,
}
: false,
},
)
}
export interface ExampleChartInput {
width: number
height: number
revision: number
preview?: boolean
interactive?: boolean
}
export default function ActiveBarDashboard({
width = 640,
height = 480,
revision = 0,
}: ExampleProps = {}) {
const input = { width, height, revision, preview: false, interactive: true }
const idPrefix = '121-active-bar-dashboard'
const [metric, setMetric] = useState<DashboardMetric>('desktop')
const root = useRef<HTMLElement>(null)
const chartHost = useRef<HTMLElement | null>(null)
const desktopButton = useRef<HTMLButtonElement>(null)
const mobileButton = useRef<HTMLButtonElement>(null)
const focusedId = useRef<string | null>(null)
const renderer = useMemo(() => motion({ initial: !false }), [false])
const rows = dashboardRows(input.revision)
const definition = useMemo(
() => activeBarDashboardDefinition(input, metric),
[input, metric],
)
const chartWidth = Math.max(180, input.width - 32)
const chartHeight = Math.max(150, input.height - 132)
return (
<section
ref={root}
data-conformance-view="main"
aria-label="Visitor analytics"
style={{ ...cardStyle, width: input.width, height: input.height }}
>
<header
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) auto',
alignItems: 'stretch',
borderBottom:
'1px solid color-mix(in srgb, currentColor 10%, transparent)',
}}
>
<div style={{ padding: '20px 22px' }}>
<div style={{ fontSize: 13, fontWeight: 650, opacity: 0.64 }}>
Total visitors
</div>
<div
style={{
marginTop: 3,
fontSize: 24,
fontWeight: 720,
letterSpacing: '-0.04em',
}}
>
{metricTotal(rows, metric).toLocaleString('en-US')}
</div>
</div>
<div style={{ display: 'flex' }}>
{(['desktop', 'mobile'] as const).map((id) => (
<button
key={id}
ref={id === 'desktop' ? desktopButton : mobileButton}
type="button"
aria-pressed={metric === id}
onClick={() => setMetric(id)}
style={{
minWidth: 104,
padding: '14px 18px',
border: 0,
borderLeft:
'1px solid color-mix(in srgb, currentColor 10%, transparent)',
color: 'inherit',
background:
metric === id
? 'color-mix(in srgb, currentColor 7%, Canvas)'
: 'transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<span style={{ display: 'block', fontSize: 11, opacity: 0.58 }}>
{metricLabels[id]}
</span>
<span
style={{
display: 'block',
marginTop: 3,
fontSize: 16,
fontWeight: 680,
}}
>
{metricTotal(rows, id).toLocaleString('en-US')}
</span>
</button>
))}
</div>
</header>
<div style={{ padding: '14px 16px 16px' }}>
<Chart<DashboardRow, string, number>
idPrefix={idPrefix}
definition={definition}
renderer={renderer}
width={chartWidth}
height={chartHeight}
ariaLabel={`Daily ${metricLabels[metric].toLowerCase()} visitors`}
onRender={({ container }) => {
chartHost.current = container
}}
onFocusChange={(point) => {
focusedId.current = point?.datum.id ?? null
}}
/>
</div>
</section>
)
}cases/121-active-bar-dashboard/model.ts51 lines · dependency
cases/121-active-bar-dashboard/model.ts
export type DashboardMetric = 'desktop' | 'mobile'
export interface DashboardRow {
id: string
day: string
desktop: number
mobile: number
}
const baseRows: readonly DashboardRow[] = [
{ id: '01', day: 'May 1', desktop: 186, mobile: 80 },
{ id: '02', day: 'May 2', desktop: 305, mobile: 200 },
{ id: '03', day: 'May 3', desktop: 237, mobile: 120 },
{ id: '04', day: 'May 4', desktop: 73, mobile: 190 },
{ id: '05', day: 'May 5', desktop: 209, mobile: 130 },
{ id: '06', day: 'May 6', desktop: 214, mobile: 140 },
{ id: '07', day: 'May 7', desktop: 318, mobile: 240 },
{ id: '08', day: 'May 8', desktop: 138, mobile: 92 },
{ id: '09', day: 'May 9', desktop: 281, mobile: 176 },
{ id: '10', day: 'May 10', desktop: 172, mobile: 126 },
{ id: '11', day: 'May 11', desktop: 352, mobile: 258 },
{ id: '12', day: 'May 12', desktop: 197, mobile: 148 },
{ id: '13', day: 'May 13', desktop: 264, mobile: 189 },
{ id: '14', day: 'May 14', desktop: 154, mobile: 118 },
{ id: '15', day: 'May 15', desktop: 329, mobile: 231 },
{ id: '16', day: 'May 16', desktop: 218, mobile: 155 },
{ id: '17', day: 'May 17', desktop: 371, mobile: 274 },
{ id: '18', day: 'May 18', desktop: 247, mobile: 181 },
{ id: '19', day: 'May 19', desktop: 291, mobile: 205 },
{ id: '20', day: 'May 20', desktop: 168, mobile: 122 },
{ id: '21', day: 'May 21', desktop: 336, mobile: 246 },
{ id: '22', day: 'May 22', desktop: 228, mobile: 164 },
{ id: '23', day: 'May 23', desktop: 389, mobile: 287 },
{ id: '24', day: 'May 24', desktop: 259, mobile: 194 },
]
export function dashboardRows(revision: number): readonly DashboardRow[] {
if (revision % 2 === 0) return baseRows
return baseRows.map((row, index) => ({
...row,
desktop: Math.round(row.desktop * (0.9 + ((index * 7) % 9) / 50)),
mobile: Math.round(row.mobile * (0.92 + ((index * 5) % 7) / 45)),
}))
}
export function metricTotal(
rows: readonly DashboardRow[],
metric: DashboardMetric,
) {
return rows.reduce((total, row) => total + row[metric], 0)
}