150 lines · 2 files · 4.3 kB
cases/75-radar/example.tsx129 lines · entry
cases/75-radar/example.tsx
import { Chart } from '@tanstack/charts/react/tooltip'
import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip'
import { defineChart, normalize, select } from '@tanstack/charts'
import { fold } from '@tanstack/charts/transform/fold'
import {
angleGrid,
polar,
radialArea,
radialGrid,
} from '@tanstack/charts/polar'
import { decathlon } from '@tanstack/charts-data/decathlon'
import { scaleLinear, scalePoint } from 'd3-scale'
import { curveLinearClosed } from 'd3-shape'
import { radarEvents, timedEvents } from './selection'
import type { PolarGuideLabelContext } from '@tanstack/charts/polar'
const ringValues = [0.2, 0.4, 0.6, 0.8, 1] as const
const angleScale = scalePoint<string>().domain(radarEvents)
const radiusScale = scaleLinear().domain([0, 1])
export const foldedDecathlon = fold(decathlon, {
fields: radarEvents,
as: { key: 'event', value: 'result' },
})
export const normalizedDecathlon = normalize(foldedDecathlon, {
by: 'event',
value: (datum) =>
timedEvents.has(datum.event) ? -datum.result : datum.result,
basis: 'extent',
as: 'relativePerformance',
})
export const radarProfile = select(normalizedDecathlon, {
by: 'event',
select: 'first',
})
function angleLabelIsTopOrBottom(angle: number): boolean {
return Math.abs(Math.sin(angle)) <= Math.SQRT1_2
}
function angleLabelDy({ angle, y }: PolarGuideLabelContext): number {
if (!angleLabelIsTopOrBottom(angle)) return 1.1
return y > 0 ? -1.1 : 0
}
export const createExampleChart = (input: ChartOptions) => {
const margin =
input.width < 480
? { top: 20, right: 55, bottom: 20, left: 105 }
: { top: 20, right: 20, bottom: 20, left: 20 }
return defineChart(
{
marks: [
polar({
angle: { scale: angleScale, wrap: true },
radius: { scale: radiusScale },
inset: 0,
radiusRatio: input.preview === true ? 0.94 : 0.8,
guides: [
radialGrid({
values: ringValues,
shape: 'polygon',
labels: input.preview !== true,
labelAngle: Math.PI / 3,
labelRotate: 60,
labelBaseline: 'auto',
labelFontSize: 12,
format: (value) => String(Number(value) * 100),
labelFill: '#cccccc',
stroke: '#cbd5e1',
}),
angleGrid({
values: radarEvents,
labels: input.preview !== true,
labelOffset: 8,
labelDy: angleLabelDy,
labelFill: '#808080',
labelFontSize: 12,
format: String,
stroke: '#cbd5e1',
}),
],
marks: [
radialArea(radarProfile, {
id: 'athlete-profile',
angle: 'event',
radius: 'relativePerformance',
key: 'event',
className: 'ts-chart__radar',
curve: curveLinearClosed,
fill: '#8884d8',
fillOpacity: 0.6,
stroke: '#8884d8',
strokeWidth: 2,
}),
],
}),
],
margin: input.preview === true ? 0 : margin,
},
{
keyboard: true,
tooltip: {
use: exampleTooltip,
...{
format: ({ datum }) =>
`${datum.Country} · ${datum.event} · ${(datum.relativePerformance * 100).toFixed(1)}%`,
},
},
},
)
}
export interface ChartOptions {
width: number
preview?: boolean
}
export const exampleAriaLabel = 'Decathlon radar chart'
export const chart = createExampleChart({
width: 640,
preview: false,
})
export default function Example() {
return <Chart ariaLabel={exampleAriaLabel} definition={chart} height={480} />
}cases/75-radar/selection.ts21 lines · dependency
cases/75-radar/selection.ts
import type { DecathlonRow } from '@tanstack/charts-data/decathlon'
export const radarEvents = [
'100 Meters',
'Long Jump',
'High Jump',
'100 Meter Hurdles',
] as const
export type RadarEvent = (typeof radarEvents)[number]
export const timedEvents: ReadonlySet<RadarEvent> = new Set([
'100 Meters',
'100 Meter Hurdles',
])
export function selectRadarAthlete(rows: readonly DecathlonRow[]) {
const firstAthlete = rows[0]
if (!firstAthlete) throw new Error('The decathlon snapshot is empty')
return firstAthlete
}