3,924 lines · 10 files · 110.1 kB
cases/192-shadcn-tooltip-icons/tanstack.ts7 lines · entry
cases/192-shadcn-tooltip-icons/tanstack.ts
import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack'
const example = createShadcnTanStackExample('chart-tooltip-icons')
export const shadcnDefinition = example.definition
export const catalogCase = example.catalogCase
export const mount = example.mountshadcn/area-interactive-data.json457 lines · dependency
shadcn/area-interactive-data.json
[
{
"date": "2024-04-01",
"desktop": 222,
"mobile": 150
},
{
"date": "2024-04-02",
"desktop": 97,
"mobile": 180
},
{
"date": "2024-04-03",
"desktop": 167,
"mobile": 120
},
{
"date": "2024-04-04",
"desktop": 242,
"mobile": 260
},
{
"date": "2024-04-05",
"desktop": 373,
"mobile": 290
},
{
"date": "2024-04-06",
"desktop": 301,
"mobile": 340
},
{
"date": "2024-04-07",
"desktop": 245,
"mobile": 180
},
{
"date": "2024-04-08",
"desktop": 409,
"mobile": 320
},
{
"date": "2024-04-09",
"desktop": 59,
"mobile": 110
},
{
"date": "2024-04-10",
"desktop": 261,
"mobile": 190
},
{
"date": "2024-04-11",
"desktop": 327,
"mobile": 350
},
{
"date": "2024-04-12",
"desktop": 292,
"mobile": 210
},
{
"date": "2024-04-13",
"desktop": 342,
"mobile": 380
},
{
"date": "2024-04-14",
"desktop": 137,
"mobile": 220
},
{
"date": "2024-04-15",
"desktop": 120,
"mobile": 170
},
{
"date": "2024-04-16",
"desktop": 138,
"mobile": 190
},
{
"date": "2024-04-17",
"desktop": 446,
"mobile": 360
},
{
"date": "2024-04-18",
"desktop": 364,
"mobile": 410
},
{
"date": "2024-04-19",
"desktop": 243,
"mobile": 180
},
{
"date": "2024-04-20",
"desktop": 89,
"mobile": 150
},
{
"date": "2024-04-21",
"desktop": 137,
"mobile": 200
},
{
"date": "2024-04-22",
"desktop": 224,
"mobile": 170
},
{
"date": "2024-04-23",
"desktop": 138,
"mobile": 230
},
{
"date": "2024-04-24",
"desktop": 387,
"mobile": 290
},
{
"date": "2024-04-25",
"desktop": 215,
"mobile": 250
},
{
"date": "2024-04-26",
"desktop": 75,
"mobile": 130
},
{
"date": "2024-04-27",
"desktop": 383,
"mobile": 420
},
{
"date": "2024-04-28",
"desktop": 122,
"mobile": 180
},
{
"date": "2024-04-29",
"desktop": 315,
"mobile": 240
},
{
"date": "2024-04-30",
"desktop": 454,
"mobile": 380
},
{
"date": "2024-05-01",
"desktop": 165,
"mobile": 220
},
{
"date": "2024-05-02",
"desktop": 293,
"mobile": 310
},
{
"date": "2024-05-03",
"desktop": 247,
"mobile": 190
},
{
"date": "2024-05-04",
"desktop": 385,
"mobile": 420
},
{
"date": "2024-05-05",
"desktop": 481,
"mobile": 390
},
{
"date": "2024-05-06",
"desktop": 498,
"mobile": 520
},
{
"date": "2024-05-07",
"desktop": 388,
"mobile": 300
},
{
"date": "2024-05-08",
"desktop": 149,
"mobile": 210
},
{
"date": "2024-05-09",
"desktop": 227,
"mobile": 180
},
{
"date": "2024-05-10",
"desktop": 293,
"mobile": 330
},
{
"date": "2024-05-11",
"desktop": 335,
"mobile": 270
},
{
"date": "2024-05-12",
"desktop": 197,
"mobile": 240
},
{
"date": "2024-05-13",
"desktop": 197,
"mobile": 160
},
{
"date": "2024-05-14",
"desktop": 448,
"mobile": 490
},
{
"date": "2024-05-15",
"desktop": 473,
"mobile": 380
},
{
"date": "2024-05-16",
"desktop": 338,
"mobile": 400
},
{
"date": "2024-05-17",
"desktop": 499,
"mobile": 420
},
{
"date": "2024-05-18",
"desktop": 315,
"mobile": 350
},
{
"date": "2024-05-19",
"desktop": 235,
"mobile": 180
},
{
"date": "2024-05-20",
"desktop": 177,
"mobile": 230
},
{
"date": "2024-05-21",
"desktop": 82,
"mobile": 140
},
{
"date": "2024-05-22",
"desktop": 81,
"mobile": 120
},
{
"date": "2024-05-23",
"desktop": 252,
"mobile": 290
},
{
"date": "2024-05-24",
"desktop": 294,
"mobile": 220
},
{
"date": "2024-05-25",
"desktop": 201,
"mobile": 250
},
{
"date": "2024-05-26",
"desktop": 213,
"mobile": 170
},
{
"date": "2024-05-27",
"desktop": 420,
"mobile": 460
},
{
"date": "2024-05-28",
"desktop": 233,
"mobile": 190
},
{
"date": "2024-05-29",
"desktop": 78,
"mobile": 130
},
{
"date": "2024-05-30",
"desktop": 340,
"mobile": 280
},
{
"date": "2024-05-31",
"desktop": 178,
"mobile": 230
},
{
"date": "2024-06-01",
"desktop": 178,
"mobile": 200
},
{
"date": "2024-06-02",
"desktop": 470,
"mobile": 410
},
{
"date": "2024-06-03",
"desktop": 103,
"mobile": 160
},
{
"date": "2024-06-04",
"desktop": 439,
"mobile": 380
},
{
"date": "2024-06-05",
"desktop": 88,
"mobile": 140
},
{
"date": "2024-06-06",
"desktop": 294,
"mobile": 250
},
{
"date": "2024-06-07",
"desktop": 323,
"mobile": 370
},
{
"date": "2024-06-08",
"desktop": 385,
"mobile": 320
},
{
"date": "2024-06-09",
"desktop": 438,
"mobile": 480
},
{
"date": "2024-06-10",
"desktop": 155,
"mobile": 200
},
{
"date": "2024-06-11",
"desktop": 92,
"mobile": 150
},
{
"date": "2024-06-12",
"desktop": 492,
"mobile": 420
},
{
"date": "2024-06-13",
"desktop": 81,
"mobile": 130
},
{
"date": "2024-06-14",
"desktop": 426,
"mobile": 380
},
{
"date": "2024-06-15",
"desktop": 307,
"mobile": 350
},
{
"date": "2024-06-16",
"desktop": 371,
"mobile": 310
},
{
"date": "2024-06-17",
"desktop": 475,
"mobile": 520
},
{
"date": "2024-06-18",
"desktop": 107,
"mobile": 170
},
{
"date": "2024-06-19",
"desktop": 341,
"mobile": 290
},
{
"date": "2024-06-20",
"desktop": 408,
"mobile": 450
},
{
"date": "2024-06-21",
"desktop": 169,
"mobile": 210
},
{
"date": "2024-06-22",
"desktop": 317,
"mobile": 270
},
{
"date": "2024-06-23",
"desktop": 480,
"mobile": 530
},
{
"date": "2024-06-24",
"desktop": 132,
"mobile": 180
},
{
"date": "2024-06-25",
"desktop": 141,
"mobile": 190
},
{
"date": "2024-06-26",
"desktop": 434,
"mobile": 380
},
{
"date": "2024-06-27",
"desktop": 448,
"mobile": 490
},
{
"date": "2024-06-28",
"desktop": 149,
"mobile": 200
},
{
"date": "2024-06-29",
"desktop": 103,
"mobile": 160
},
{
"date": "2024-06-30",
"desktop": 446,
"mobile": 400
}
]shared/mount.ts179 lines · dependency
shared/mount.ts
import {
defineChart,
isResponsiveChartDefinition,
mountChart,
} from '@tanstack/charts'
import { tooltip } from '@tanstack/charts/tooltip'
import type {
DomChartDefinition,
ChartDefinitionOptions,
ChartValue,
ChartTooltipOptions,
} from '@tanstack/charts'
import type {
ConformanceHandle,
ConformanceInput,
ConformanceMount,
} from '../types'
import { catalogPreviewDefinition, type CatalogPreviewOptions } from './preview'
export function mountObservablePlot(
container: HTMLElement,
input: ConformanceInput,
render: (input: ConformanceInput) => HTMLElement | SVGSVGElement,
): ConformanceHandle {
let element = render(input)
container.append(element)
return {
update(nextInput) {
const nextElement = render(nextInput)
element.replaceWith(nextElement)
element = nextElement
},
destroy() {
element.remove()
},
}
}
export function tanstackMount<
TDatum,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
>(
createDefinition: (
input: ConformanceInput,
) => DomChartDefinition<TDatum, TXValue, TYValue>,
ariaLabel: string,
interactiveTooltip: true | ChartTooltipOptions<TDatum> = true,
previewOptions: CatalogPreviewOptions<TDatum, TXValue, TYValue> = {},
): TanStackConformanceCase<TDatum, TXValue, TYValue> {
const mount: ConformanceMount = (container, input) => {
const options = {
definition: withConformanceBehavior(
createDefinition(input),
input,
interactiveTooltip,
previewOptions,
),
width: input.width,
height: input.height,
ariaLabel,
} as const
const host = mountChart(container, options)
applyCatalogPreviewFocus(host, input, previewOptions)
return {
update(nextInput) {
host.update({
...options,
definition: withConformanceBehavior(
createDefinition(nextInput),
nextInput,
interactiveTooltip,
previewOptions,
),
width: nextInput.width,
height: nextInput.height,
})
applyCatalogPreviewFocus(host, nextInput, previewOptions)
},
destroy() {
host.destroy()
},
}
}
const catalogCase = Object.assign(mount, {
createDefinition,
ariaLabel,
interactiveTooltip,
})
return Object.assign(catalogCase, { mount: catalogCase })
}
export interface TanStackConformanceCase<
TDatum,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
> {
(container: HTMLElement, input: ConformanceInput): ConformanceHandle
createDefinition: (
input: ConformanceInput,
) => DomChartDefinition<TDatum, TXValue, TYValue>
ariaLabel: string
interactiveTooltip: true | ChartTooltipOptions<TDatum>
mount: ConformanceMount
}
export function tanstackCase<
TDatum,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
>(
createDefinition: (
input: ConformanceInput,
) => DomChartDefinition<TDatum, TXValue, TYValue>,
ariaLabel: string,
interactiveTooltip: true | ChartTooltipOptions<TDatum> = true,
previewOptions: CatalogPreviewOptions<TDatum, TXValue, TYValue> = {},
): TanStackConformanceCase<TDatum, TXValue, TYValue> {
return tanstackMount(
createDefinition,
ariaLabel,
interactiveTooltip,
previewOptions,
)
}
export function withConformanceBehavior<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
>(
definition: DomChartDefinition<TDatum, TXValue, TYValue>,
input: ConformanceInput,
interactiveTooltip: true | ChartTooltipOptions<TDatum>,
previewOptions: CatalogPreviewOptions<TDatum, TXValue, TYValue> = {},
): DomChartDefinition<TDatum, TXValue, TYValue> {
const presentation =
input.preview === true
? catalogPreviewDefinition(definition, previewOptions)
: definition
const behavior: ChartDefinitionOptions<TDatum, TXValue, TYValue, 'dom'> = {
svgAnimation: false,
...(input.interactive === true ||
(input.preview === true && previewOptions.focus)
? {}
: { focus: false }),
keyboard: input.interactive === true,
tooltip:
input.interactive !== true
? false
: interactiveTooltip === true
? tooltip
: { use: tooltip, ...interactiveTooltip },
}
if (isResponsiveChartDefinition(presentation)) {
return defineChart(presentation, behavior)
}
return defineChart(presentation, behavior)
}
function applyCatalogPreviewFocus<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
>(
host: ReturnType<typeof mountChart<TDatum, TXValue, TYValue>>,
input: ConformanceInput,
options: CatalogPreviewOptions<TDatum, TXValue, TYValue>,
) {
if (input.preview !== true || !options.focus) return
host.interaction.setControlledFocus(options.focus(host.getScene(), input), {
source: 'programmatic',
})
}shared/preview.ts144 lines · dependency
shared/preview.ts
import { isResponsiveChartDefinition } from '@tanstack/charts'
import type {
ChartPoint,
ChartScene,
ChartValue,
DomChartDefinition,
} from '@tanstack/charts'
import type { ConformanceInput } from '../types'
export interface CatalogPreviewOptions<
TDatum = unknown,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
> {
/** Keep the source definition's Cartesian axes and grid. */
guides?: boolean
/** Keep the source definition's color legend. */
legend?: boolean
/** Keep the source definition's authored or automatic margins. */
margin?: boolean
/** Paint one deterministic source point through the chart's focus strategy. */
focus?: (
scene: ChartScene<TDatum, TXValue, TYValue>,
input: ConformanceInput,
) => ChartPoint<TDatum, TXValue, TYValue> | null
}
export function catalogPreviewDefinition<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
>(
definition: DomChartDefinition<TDatum, TXValue, TYValue>,
options: CatalogPreviewOptions<TDatum, TXValue, TYValue> = {},
): DomChartDefinition<TDatum, TXValue, TYValue> {
if (isResponsiveChartDefinition(definition)) {
return {
...definition,
chart(context) {
const spec = definition.chart(context)
const color = previewColor(spec.color, options.legend === true)
return {
...spec,
...(options.guides === true ? {} : { guides: false }),
...(options.margin === true ? {} : { margin: 0 }),
...(color ? { color } : {}),
}
},
}
}
const color = previewColor(definition.color, options.legend === true)
return {
...definition,
...(options.guides === true ? {} : { guides: false }),
...(options.margin === true ? {} : { margin: 0 }),
...(color ? { color } : {}),
}
}
function previewColor<TColor extends { legend?: unknown }>(
color: TColor | undefined,
keepLegend: boolean,
): Omit<TColor, 'legend'> | TColor | undefined {
if (!color || keepLegend) return color
const { legend: _legend, ...withoutLegend } = color
return withoutLegend
}
export function samplePreviewData<TDatum>(
data: readonly TDatum[],
input: ConformanceInput,
limit: number,
accessors: readonly ((datum: TDatum) => number | null | undefined)[] = [],
): readonly TDatum[] {
if (input.preview !== true || data.length <= limit) return data
const selected = new Set<number>()
const slots = Math.max(2, limit - accessors.length * 2)
for (let slot = 0; slot < slots; slot += 1) {
selected.add(Math.round((slot / (slots - 1)) * (data.length - 1)))
}
for (const accessor of accessors) {
let minimumIndex = -1
let minimum = Number.POSITIVE_INFINITY
let maximumIndex = -1
let maximum = Number.NEGATIVE_INFINITY
data.forEach((datum, index) => {
const value = accessor(datum)
if (value === null || value === undefined || !Number.isFinite(value)) {
return
}
if (value < minimum) {
minimum = value
minimumIndex = index
}
if (value > maximum) {
maximum = value
maximumIndex = index
}
})
if (minimumIndex >= 0) selected.add(minimumIndex)
if (maximumIndex >= 0) selected.add(maximumIndex)
}
return data.filter((_datum, index) => selected.has(index))
}
export function samplePreviewSeries<TDatum, TSeries>(
data: readonly TDatum[],
input: ConformanceInput,
limitPerSeries: number,
series: (datum: TDatum) => TSeries,
): readonly TDatum[] {
if (input.preview !== true) return data
const indicesBySeries = new Map<TSeries, number[]>()
data.forEach((datum, index) => {
const key = series(datum)
const indices = indicesBySeries.get(key) ?? []
indices.push(index)
indicesBySeries.set(key, indices)
})
const selected = new Set<number>()
for (const indices of indicesBySeries.values()) {
if (indices.length <= limitPerSeries) {
indices.forEach((index) => selected.add(index))
continue
}
for (let slot = 0; slot < limitPerSeries; slot += 1) {
const index =
indices[
Math.round((slot / (limitPerSeries - 1)) * (indices.length - 1))
]
if (index !== undefined) selected.add(index)
}
}
return data.filter((_datum, index) => selected.has(index))
}shared/react-mount.ts57 lines · dependency
shared/react-mount.ts
import { createElement } from 'react'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import type { ForwardRefExoticComponent, RefAttributes } from 'react'
import type {
ConformanceInput,
ConformanceMount,
ConformanceTestDriver,
} from '../types'
export interface ReactConformanceProps {
input: ConformanceInput
idPrefix?: string
}
export type ReactConformanceComponent = ForwardRefExoticComponent<
ReactConformanceProps & RefAttributes<ConformanceTestDriver>
>
export function reactMount(
Component: ReactConformanceComponent,
): ConformanceMount {
return (container, input) => {
const root = createRoot(container)
let activeDriver: ConformanceTestDriver | null = null
const driver = new Proxy({} as ConformanceTestDriver, {
get(_target, property) {
const value = activeDriver?.[property as keyof ConformanceTestDriver]
return typeof value === 'function' ? value.bind(activeDriver) : value
},
})
const render = (nextInput: ConformanceInput) => {
flushSync(() => {
root.render(
createElement(Component, {
input: nextInput,
ref: (nextDriver: ConformanceTestDriver | null) => {
activeDriver = nextDriver
},
}),
)
})
}
render(input)
return {
update: render,
driver,
destroy() {
flushSync(() => {
root.unmount()
})
},
}
}
}shared/shadcn-catalog-data.ts216 lines · dependency
shared/shadcn-catalog-data.ts
export type ShadcnChartFamily =
'area' | 'bar' | 'line' | 'pie' | 'radar' | 'radial' | 'tooltip'
export interface ShadcnCatalogSpec {
name: string
family: ShadcnChartFamily
variant: string
title: string
description: string
footerNote: string
square: boolean
legend: boolean
}
export interface ShadcnMonthDatum {
month: string
desktop: number
mobile: number
tablet: number
}
export interface ShadcnSeriesDatum {
month: string
series: 'desktop' | 'mobile' | 'tablet' | 'other'
value: number
}
export interface ShadcnBrowserDatum {
browser: string
visitors: number
}
export interface ShadcnRadarDatum {
month: string
desktop: number
mobile?: number
}
export interface ShadcnActivityDatum {
date: string
activity: 'running' | 'swimming'
value: number
}
export const shadcnMonths: readonly ShadcnMonthDatum[] = [
{ month: 'January', desktop: 186, mobile: 80, tablet: 44 },
{ month: 'February', desktop: 305, mobile: 200, tablet: 72 },
{ month: 'March', desktop: 237, mobile: 120, tablet: 58 },
{ month: 'April', desktop: 73, mobile: 190, tablet: 91 },
{ month: 'May', desktop: 209, mobile: 130, tablet: 67 },
{ month: 'June', desktop: 214, mobile: 140, tablet: 82 },
]
export const shadcnSeriesRows: readonly ShadcnSeriesDatum[] =
shadcnMonths.flatMap((row) => [
{ month: row.month, series: 'desktop', value: row.desktop },
{ month: row.month, series: 'mobile', value: row.mobile },
{ month: row.month, series: 'tablet', value: row.tablet },
])
export const shadcnBrowsers: readonly ShadcnBrowserDatum[] = [
{ browser: 'chrome', visitors: 275 },
{ browser: 'safari', visitors: 200 },
{ browser: 'firefox', visitors: 187 },
{ browser: 'edge', visitors: 173 },
{ browser: 'other', visitors: 90 },
]
export const shadcnRadarDefault: readonly ShadcnRadarDatum[] = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 305 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 273 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 214 },
]
export const shadcnRadarFilled: readonly ShadcnRadarDatum[] = [
{ month: 'January', desktop: 186 },
{ month: 'February', desktop: 285 },
{ month: 'March', desktop: 237 },
{ month: 'April', desktop: 203 },
{ month: 'May', desktop: 209 },
{ month: 'June', desktop: 264 },
]
export const shadcnRadarMultiple: readonly ShadcnRadarDatum[] =
shadcnMonths.map(({ month, desktop, mobile }) => ({
month,
desktop,
mobile,
}))
export const shadcnRadarLines: readonly ShadcnRadarDatum[] = [
{ month: 'January', desktop: 186, mobile: 160 },
{ month: 'February', desktop: 185, mobile: 170 },
{ month: 'March', desktop: 207, mobile: 180 },
{ month: 'April', desktop: 173, mobile: 160 },
{ month: 'May', desktop: 160, mobile: 190 },
{ month: 'June', desktop: 174, mobile: 204 },
]
export const shadcnActivities: readonly ShadcnActivityDatum[] = [
{ date: '2024-07-15', activity: 'running', value: 450 },
{ date: '2024-07-15', activity: 'swimming', value: 300 },
{ date: '2024-07-16', activity: 'running', value: 380 },
{ date: '2024-07-16', activity: 'swimming', value: 420 },
{ date: '2024-07-17', activity: 'running', value: 520 },
{ date: '2024-07-17', activity: 'swimming', value: 120 },
{ date: '2024-07-18', activity: 'running', value: 140 },
{ date: '2024-07-18', activity: 'swimming', value: 550 },
{ date: '2024-07-19', activity: 'running', value: 600 },
{ date: '2024-07-19', activity: 'swimming', value: 350 },
{ date: '2024-07-20', activity: 'running', value: 480 },
{ date: '2024-07-20', activity: 'swimming', value: 400 },
]
export const shadcnColors = [
'var(--chart-1, var(--ts-chart-1))',
'var(--chart-2, var(--ts-chart-2))',
'var(--chart-3, var(--ts-chart-3))',
'var(--chart-4, var(--ts-chart-4))',
'var(--chart-5, var(--ts-chart-5))',
] as const
const titleOverrides: Record<string, string> = {
'chart-area-default': 'Area Chart',
'chart-area-stacked-expand': 'Area Chart - Stacked Expanded',
'chart-bar-default': 'Bar Chart',
'chart-bar-label-custom': 'Bar Chart - Custom Label',
'chart-bar-stacked': 'Bar Chart - Stacked + Legend',
'chart-line-default': 'Line Chart',
'chart-line-dots-custom': 'Line Chart - Custom Dots',
'chart-line-label-custom': 'Line Chart - Custom Label',
'chart-pie-simple': 'Pie Chart',
'chart-pie-donut-text': 'Pie Chart - Donut with Text',
'chart-pie-label-custom': 'Pie Chart - Custom Label',
'chart-radar-default': 'Radar Chart',
'chart-radar-grid-circle-fill': 'Radar Chart - Grid Circle Filled',
'chart-radar-grid-circle-no-lines': 'Radar Chart - Grid Circle - No lines',
'chart-radar-grid-fill': 'Radar Chart - Grid Filled',
'chart-radar-label-custom': 'Radar Chart - Custom Label',
'chart-radar-radius': 'Radar Chart - Radius Axis',
'chart-radial-simple': 'Radial Chart',
'chart-tooltip-indicator-line': 'Tooltip - Line Indicator',
'chart-tooltip-indicator-none': 'Tooltip - No Indicator',
'chart-tooltip-label-custom': 'Tooltip - Custom label',
'chart-tooltip-label-none': 'Tooltip - No Label',
}
export function getShadcnCatalogSpec(name: string): ShadcnCatalogSpec {
const parts = name.split('-')
const family = parts[1]
if (!isShadcnFamily(family)) {
throw new TypeError(`Unknown shadcn chart family in ${name}`)
}
const variant = parts.slice(2).join('-')
const title =
titleOverrides[name] ??
`${family === 'tooltip' ? 'Tooltip' : `${titleCase(family)} Chart`} - ${variant.split('-').map(titleCase).join(' ')}`
return {
name,
family,
variant,
title,
description:
(family === 'area' || family === 'bar' || family === 'line') &&
variant === 'interactive'
? 'Showing total visitors for the last 3 months'
: family === 'area' || family === 'radar'
? 'Showing total visitors for the last 6 months'
: family === 'tooltip'
? tooltipDescription(variant)
: 'January - June 2024',
footerNote:
family === 'area' || family === 'radar'
? 'January - June 2024'
: 'Showing total visitors for the last 6 months',
square: family === 'pie' || family === 'radar' || family === 'radial',
legend:
variant.includes('legend') ||
variant === 'icons' ||
(variant === 'stacked' && family === 'bar') ||
(family === 'area' && variant === 'interactive'),
}
}
function tooltipDescription(variant: string): string {
if (variant === 'advanced') return 'Tooltip with custom formatter and total.'
if (variant === 'default') return 'Default tooltip with ChartTooltipContent.'
if (variant === 'formatter') return 'Tooltip with custom formatter.'
if (variant === 'icons') return 'Tooltip with icons.'
if (variant === 'indicator-line') return 'Tooltip with line indicator.'
if (variant === 'indicator-none') return 'Tooltip with no indicator.'
if (variant === 'label-custom')
return 'Tooltip with custom label from chartConfig.'
if (variant === 'label-formatter') return 'Tooltip with label formatter.'
if (variant === 'label-none') return 'Tooltip with no label.'
return 'A chart tooltip.'
}
function titleCase(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1)
}
function isShadcnFamily(value: string | undefined): value is ShadcnChartFamily {
return (
value === 'area' ||
value === 'bar' ||
value === 'line' ||
value === 'pie' ||
value === 'radar' ||
value === 'radial' ||
value === 'tooltip'
)
}shared/shadcn-catalog-tanstack.tsx2,152 lines · dependency
shared/shadcn-catalog-tanstack.tsx
import { useMemo, useRef, useState } from 'react'
import {
areaY,
barX,
barY,
d3Curve,
defineChart,
dot,
group,
isResponsiveChartDefinition,
lineY,
stack,
text,
} from '@tanstack/charts'
import {
angleGrid,
focusGroupAngle,
pie,
polar,
radialArc,
radialArea,
radialBarAngle,
radialDot,
radialGrid,
radialRule,
radialText,
type PieDatum,
} from '@tanstack/charts/polar'
import { Chart } from '@tanstack/charts/react/core'
import { RendererChart as TooltipChart } from '@tanstack/charts/react/tooltip'
import { tooltip } from '@tanstack/charts/tooltip'
import { scaleBand, scaleLinear, scalePoint } from 'd3-scale'
import {
curveLinear,
curveLinearClosed,
curveMonotoneX,
curveNatural,
curveStep,
} from 'd3-shape'
import {
getShadcnCatalogSpec,
shadcnActivities,
shadcnBrowsers,
shadcnColors,
shadcnMonths,
shadcnRadarDefault,
shadcnRadarFilled,
shadcnRadarLines,
shadcnRadarMultiple,
shadcnSeriesRows,
type ShadcnActivityDatum,
type ShadcnBrowserDatum,
type ShadcnCatalogSpec,
type ShadcnMonthDatum,
type ShadcnSeriesDatum,
} from './shadcn-catalog-data'
import {
ShadcnChartCard,
ShadcnTrendFooter,
shadcnChartMount,
} from './shadcn-chart-card'
import { createShadcnSpringRenderer } from './shadcn-motion'
import interactiveAreaData from '../shadcn/area-interactive-data.json'
import { tanstackCase } from './mount'
import type { ChartPoint, ChartTooltipOptions } from '@tanstack/charts'
import type { ChartDefinition, ChartValue } from '@tanstack/charts'
import type { ConformanceInput } from '../types'
const monthSeries = ['desktop', 'mobile', 'tablet'] as const
const twoSeries = ['desktop', 'mobile'] as const
const browserNames = shadcnBrowsers.map((row) => row.browser)
const activityNames = ['running', 'swimming'] as const
const horizontalVariants = new Set(['horizontal', 'label-custom', 'mixed'])
const interactiveAreaRows = interactiveAreaData as readonly {
date: string
desktop: number
mobile: number
}[]
const interactiveBarRows: readonly ShadcnMonthDatum[] = interactiveAreaRows.map(
(row) => ({
month: row.date,
desktop: row.desktop,
mobile: row.mobile,
tablet: 0,
}),
)
const interactivePieRows: readonly ShadcnBrowserDatum[] = [
{ browser: 'january', visitors: 186 },
{ browser: 'february', visitors: 305 },
{ browser: 'march', visitors: 237 },
{ browser: 'april', visitors: 173 },
{ browser: 'may', visitors: 209 },
]
const interactiveTimeRangeOptions = [
{ value: '90d', label: 'Last 3 months' },
{ value: '30d', label: 'Last 30 days' },
{ value: '7d', label: 'Last 7 days' },
] as const
type InteractiveTimeRange = '90d' | '30d' | '7d'
type InteractiveSeries = (typeof twoSeries)[number]
interface ShadcnInteractiveState {
timeRange: InteractiveTimeRange
series: InteractiveSeries
month: string
}
type ShadcnInteractiveDefinitionFactory<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
> = (state: ShadcnInteractiveState) => ChartDefinition<TDatum, TXValue, TYValue>
type ShadcnRadialCatalogDatum =
| ShadcnBrowserDatum
| PieDatum<{ id: string; value: number }>
| { id: string; angle: number; radius: number; text: string }
| { browser: string; visitors: number; ring: string }
| {
id: string
ring: string
start: number
end: number
fill: string
}
export function createShadcnTanStackExample(name: string) {
const spec = getShadcnCatalogSpec(name)
if (spec.family === 'area') {
return createShadcnExample(
spec,
buildAreaDefinition(spec),
spec.variant === 'interactive'
? ({ timeRange }) => buildInteractiveAreaDefinition(timeRange)
: undefined,
)
}
if (spec.family === 'bar') {
return createShadcnExample<
ShadcnMonthDatum | ShadcnSeriesDatum | ShadcnBrowserDatum,
string | number,
string | number
>(
spec,
buildBarDefinition(spec),
spec.variant === 'interactive'
? ({ series }) => buildInteractiveBarDefinition(series)
: undefined,
)
}
if (spec.family === 'line') {
return createShadcnExample(
spec,
buildLineDefinition(spec),
spec.variant === 'interactive'
? ({ series }) => buildInteractiveLineDefinition(series)
: undefined,
)
}
if (spec.family === 'pie') {
return createShadcnExample(
spec,
buildPieDefinition(spec),
spec.variant === 'interactive'
? ({ month }) => buildInteractivePieDefinition(month)
: undefined,
)
}
if (spec.family === 'radar') {
return createShadcnExample(spec, buildRadarDefinition(spec))
}
if (spec.family === 'radial') {
return createShadcnExample<
ShadcnRadialCatalogDatum,
number,
string | number
>(spec, buildRadialDefinition(spec))
}
return createShadcnExample(spec, buildTooltipDefinition())
}
function createShadcnExample<
TDatum,
TXValue extends ChartValue,
TYValue extends ChartValue,
>(
spec: ShadcnCatalogSpec,
definition: ChartDefinition<TDatum, TXValue, TYValue>,
interactiveDefinitionFactory?: ShadcnInteractiveDefinitionFactory<
TDatum,
TXValue,
TYValue
>,
) {
const tooltipOptions: ChartTooltipOptions<TDatum, TXValue, TYValue> = {
className: 'sc-chart-tooltip',
anchor:
spec.family === 'tooltip'
? (_points, context) => ({
x: context.surface.width * 0.271,
y: context.surface.height * 0.554,
})
: 'group-center',
placement: spec.family === 'tooltip' ? 'bottom-right' : 'auto',
offset: spec.family === 'tooltip' ? 0 : undefined,
sort: 'color-domain',
content:
spec.family === 'tooltip'
? () => ({ rows: [] })
: (points) => shadcnTooltipContent(spec, points),
}
const chartOptions = {
svgAnimation: false,
focus:
spec.family === 'radar'
? focusGroupAngle
: spec.family === 'pie' || spec.family === 'radial'
? 'nearest'
: 'group-x',
keyboard: spec.family !== 'radial',
tooltip: { use: tooltip, ...tooltipOptions },
} as const
const applyChartOptions = (
source: ChartDefinition<TDatum, TXValue, TYValue>,
) =>
isResponsiveChartDefinition(source)
? defineChart(source, chartOptions)
: defineChart(source, chartOptions)
const interactiveDefinition = applyChartOptions(definition)
const catalogCase = tanstackCase(
() => interactiveDefinition,
`${spec.title} implemented with TanStack Charts`,
true,
{ guides: spec.family !== 'pie', margin: true },
)
function TanStackView({ input }: { input: ConformanceInput }) {
const seededTooltip = useRef(false)
const [timeRange, setTimeRange] = useState<InteractiveTimeRange>('90d')
const [activeSeries, setActiveSeries] =
useState<InteractiveSeries>('desktop')
const [activeMonth, setActiveMonth] = useState('january')
const chartDefinition = useMemo(
() =>
interactiveDefinitionFactory
? applyChartOptions(
interactiveDefinitionFactory({
timeRange,
series: activeSeries,
month: activeMonth,
}),
)
: interactiveDefinition,
[timeRange, activeSeries, activeMonth],
)
const renderer = useMemo(
() => createShadcnSpringRenderer<TDatum, TXValue, TYValue>(),
[],
)
const legend = spec.legend ? <ShadcnLegend spec={spec} /> : undefined
const interactive = spec.variant === 'interactive'
const footer =
spec.family === 'tooltip' ||
interactive ||
(spec.family === 'pie' && spec.variant === 'legend') ? undefined : (
<ShadcnTrendFooter note={spec.footerNote} />
)
const headerAction =
(spec.family === 'bar' || spec.family === 'line') && interactive ? (
<ShadcnBarMetrics active={activeSeries} onChange={setActiveSeries} />
) : interactive ? (
spec.family === 'area' ? (
<ShadcnSelectControl
ariaLabel="Select a value"
value={timeRange}
options={interactiveTimeRangeOptions}
onChange={(value) => setTimeRange(value as InteractiveTimeRange)}
/>
) : (
<ShadcnSelectControl
ariaLabel="Select a value"
value={activeMonth}
options={interactivePieRows.map((row, index) => ({
value: row.browser,
label: titleCase(row.browser),
swatch: shadcnColors[index],
}))}
onChange={setActiveMonth}
/>
)
) : undefined
return (
<ShadcnChartCard
input={input}
title={spec.title}
description={spec.description}
chartShape={spec.square ? 'square' : 'wide'}
centered={spec.square}
headerInsetBottom={
spec.family === 'radar' &&
spec.variant !== 'dots' &&
spec.variant !== 'legend'
? 16
: 0
}
chartFooter={legend}
footer={footer}
headerAction={headerAction}
variant={
spec.family === 'area' && interactive
? 'interactive-area'
: spec.family === 'bar' && interactive
? 'interactive-bar'
: spec.family === 'line' && interactive
? 'interactive-line'
: spec.family === 'pie' && interactive
? 'interactive-pie'
: 'default'
}
chartHeight={
interactive &&
(spec.family === 'area' ||
spec.family === 'bar' ||
spec.family === 'line')
? 250
: spec.legend && !spec.square
? (Math.max(1, input.width - 50) * 9) / 16 - 27
: undefined
}
chartClassName={
spec.name === 'chart-radar-label-custom' ? 'sc-chart-clip' : undefined
}
>
{({ width, height }) =>
spec.family === 'tooltip' ? (
<TooltipChart
definition={chartDefinition}
renderer={renderer}
initialWidth={width}
height={height}
ariaLabel={spec.title}
onRender={({ scene, interaction }) => {
if (seededTooltip.current) return
const point = scene.points.find(
(candidate) =>
(candidate.datum as ShadcnActivityDatum).date ===
'2024-07-16',
)
if (!point) return
seededTooltip.current = true
interaction.setControlledFocus(point, {
source: 'programmatic',
})
}}
renderTooltipBody={({ points }) => (
<ShadcnTooltipBody
points={points as readonly ChartPoint<ShadcnActivityDatum>[]}
variant={spec.variant}
/>
)}
/>
) : (
<Chart
definition={chartDefinition}
renderer={renderer}
initialWidth={width}
height={height}
ariaLabel={spec.title}
/>
)
}
</ShadcnChartCard>
)
}
return {
definition: interactiveDefinition,
catalogCase,
mount: shadcnChartMount(TanStackView),
}
}
function shadcnTooltipContent<TDatum>(
spec: ShadcnCatalogSpec,
points: readonly ChartPoint<TDatum>[],
) {
if (spec.family === 'pie' || spec.family === 'radial') {
const point = points.find((candidate) => browserMetric(candidate.datum))
const metric = point && browserMetric(point.datum)
return metric
? {
title: titleCase(metric.browser),
rows: [
{
label: 'Visitors',
value: metric.visitors.toLocaleString('en-US'),
color: point.color,
},
],
}
: { rows: [] }
}
return {
title: String(points[0]?.xValue ?? ''),
rows: points.map((point) => ({
label: titleCase(
String(
point.group ??
point.markId.replace(
/-?(bars|lines|areas|slices|values|radar)$/u,
'',
),
),
),
value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'),
color: point.color,
})),
}
}
function browserMetric(datum: unknown) {
if (!datum || typeof datum !== 'object') return undefined
const browser = Reflect.get(datum, 'browser')
const visitors = Reflect.get(datum, 'visitors')
return typeof browser === 'string' &&
typeof visitors === 'number' &&
Number.isFinite(visitors)
? { browser, visitors }
: undefined
}
function buildAreaDefinition(spec: ShadcnCatalogSpec) {
if (spec.variant === 'interactive') return buildInteractiveAreaDefinition()
const multi =
spec.variant === 'axes' ||
spec.variant === 'gradient' ||
spec.variant === 'icons' ||
spec.variant === 'interactive' ||
spec.variant === 'legend' ||
spec.variant.startsWith('stacked')
const expanded = spec.variant === 'stacked-expand'
const expandedRows = shadcnMonths.flatMap((row, index) => [
{
month: row.month,
series: 'other',
value: [45, 100, 150, 50, 100, 160][index]!,
},
{ month: row.month, series: 'mobile', value: row.mobile },
{ month: row.month, series: 'desktop', value: row.desktop },
])
const rows = expanded
? expandedRows
: multi
? shadcnSeriesRows.filter((row) => row.series !== 'tablet')
: shadcnMonths
const curve =
spec.variant === 'linear'
? curveLinear
: spec.variant === 'step'
? curveStep
: curveNatural
const gradients = spec.variant === 'gradient'
return defineChart({
marks: [
multi
? areaY(rows as readonly ShadcnSeriesDatum[], {
id: 'visitor-areas',
x: 'month',
y: 'value',
z: 'series',
color: 'series',
key: (row) => `${row.month}:${row.series}`,
layout: stack({
order: expanded
? ['other', 'mobile', 'desktop']
: ['mobile', 'desktop'],
...(expanded ? { offset: 'normalize' } : {}),
}),
curve: d3Curve(curve),
fill: expanded
? (row) =>
row.series === 'other'
? `color-mix(in oklch, ${shadcnColors[2]} 10%, transparent)`
: `color-mix(in oklch, ${row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0]} 40%, transparent)`
: gradients
? (row) => `url(#shadcn-area-${row.series})`
: undefined,
fillOpacity: expanded ? 1 : 0.4,
stroke: expanded
? (row) =>
row.series === 'other'
? shadcnColors[2]
: row.series === 'mobile'
? shadcnColors[1]
: shadcnColors[0]
: (row) =>
row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0],
strokeWidth: 1.5,
})
: areaY(rows as readonly ShadcnMonthDatum[], {
id: 'visitor-area',
x: 'month',
y: 'desktop',
curve: d3Curve(curve),
fill: shadcnColors[0],
fillOpacity: 0.4,
stroke: shadcnColors[0],
strokeWidth: 1.5,
}),
],
x: shadcnPointXAxis(),
y: {
scale:
spec.variant === 'axes'
? scaleLinear().domain([0, 600])
: expanded
? scaleLinear().domain([0, 1])
: scaleLinear,
grid: true,
axis:
spec.variant === 'axes'
? { line: false, ticks: { size: 0, values: [0, 300, 600] } }
: { line: false, ticks: false, tickLabels: false },
},
color: {
domain: expanded ? ['other', 'mobile', 'desktop'] : monthSeries,
range: expanded
? [shadcnColors[2], shadcnColors[1], shadcnColors[0]]
: shadcnColors.slice(0, 3),
},
gradients: gradients
? twoSeries.map((series, index) => ({
id: `shadcn-area-${series}`,
x1: 0,
y1: 1,
x2: 0,
y2: 0,
stops: [
{ offset: 0.05, color: shadcnColors[index], opacity: 0.08 },
{ offset: 0.95, color: shadcnColors[index], opacity: 0.8 },
],
}))
: undefined,
margin:
spec.variant === 'axes'
? { top: 12, right: 12, bottom: 28, left: 40 }
: { top: 5, right: 12, bottom: 35, left: 12 },
theme: shadcnTheme(),
})
}
function buildInteractiveAreaDefinition(
timeRange: InteractiveTimeRange = '90d',
) {
const filteredRows = filterInteractiveAreaRows(timeRange)
const rows: ShadcnSeriesDatum[] = filteredRows.flatMap((row) => [
{ month: row.date, series: 'mobile', value: row.mobile },
{ month: row.date, series: 'desktop', value: row.desktop },
])
return defineChart(({ width }) => ({
marks: [
areaY(rows, {
id: 'visitor-areas',
x: 'month',
y: 'value',
z: 'series',
color: 'series',
key: (row) => `${row.month}:${row.series}`,
layout: stack({ order: ['mobile', 'desktop'] }),
curve: d3Curve(curveNatural),
fill: (row) => `url(#shadcn-interactive-${row.series})`,
fillOpacity: 1,
stroke: (row) =>
row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0],
strokeWidth: 1,
}),
],
x: {
scale: scalePoint,
axis: {
line: false,
ticks: {
values: interactiveDateTicks(timeRange),
size: 0,
padding: 10,
format: formatMonthDay,
},
},
},
y: {
scale: scaleLinear().domain([0, 1200]),
grid: true,
axis: {
line: false,
ticks: { values: [0, 300, 600, 900, 1200], size: 0 },
tickLabels: false,
},
},
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
gradients: twoSeries.map((series, index) => ({
id: `shadcn-interactive-${series}`,
x1: 0,
y1: 1,
x2: 0,
y2: 0,
stops: [
{ offset: 0.05, color: shadcnColors[index], opacity: 0.1 },
{ offset: 0.95, color: shadcnColors[index], opacity: 0.8 },
],
})),
margin: {
top: 32,
right: width < 400 ? 14 : 5,
bottom: 35,
left: 5,
},
theme: shadcnTheme(),
}))
}
function buildBarDefinition(spec: ShadcnCatalogSpec) {
if (spec.variant === 'interactive') {
return buildInteractiveBarDefinition()
}
if (spec.variant === 'active') {
const rows = shadcnBrowsers.map((row) =>
row.browser === 'chrome'
? { ...row, visitors: 187 }
: row.browser === 'firefox'
? { ...row, visitors: 275 }
: row,
)
return defineChart({
marks: [
barY(rows, {
id: 'visitor-bars',
x: 'browser',
y: 'visitors',
color: 'browser',
fill: (row) =>
row.browser === 'firefox'
? `color-mix(in srgb, ${shadcnColors[2]} 80%, transparent)`
: shadcnColors[browserNames.indexOf(row.browser)]!,
stroke: (row) =>
row.browser === 'firefox' ? shadcnColors[2] : 'none',
strokeWidth: 2,
strokeDasharray: (row) =>
row.browser === 'firefox' ? '4 4' : 'none',
radius: 8,
}),
],
x: shadcnBrowserXAxis(),
y: { scale: scaleLinear, grid: true, axis: false },
color: { domain: browserNames, range: shadcnColors },
margin: { top: 5, right: 5, bottom: 35, left: 5 },
theme: shadcnTheme(),
})
}
if (spec.variant === 'mixed') {
return defineChart({
marks: [
barX(shadcnBrowsers, {
id: 'visitor-bars',
x: 'visitors',
y: 'browser',
color: 'browser',
radius: 5,
}),
],
x: { scale: scaleLinear, axis: false },
y: shadcnBrowserYAxis(),
color: { domain: browserNames, range: shadcnColors },
margin: { top: 5, right: 5, bottom: 5, left: 60 },
theme: shadcnTheme(),
})
}
if (spec.variant === 'label-custom') {
return defineChart({
marks: [
barX(shadcnMonths, {
id: 'visitor-bars',
x: 'desktop',
y: 'month',
fill: shadcnColors[1],
radius: 4,
}),
text(shadcnMonths, {
id: 'month-labels',
x: () => 0,
y: 'month',
text: 'month',
dx: 8,
anchor: 'start',
fill: 'var(--background)',
fontSize: 12,
}),
text(shadcnMonths, {
id: 'visitor-labels',
x: 'desktop',
y: 'month',
text: (row) => row.desktop.toLocaleString('en-US'),
dx: 8,
anchor: 'start',
fill: 'var(--foreground)',
fontSize: 12,
}),
],
x: { scale: scaleLinear, grid: true, axis: false },
y: {
scale: () => scaleBand<string>().paddingInner(0.18).paddingOuter(0.08),
axis: false,
},
margin: { top: 5, right: 36, bottom: 5, left: 0 },
theme: shadcnTheme(),
})
}
if (spec.variant === 'multiple') {
return groupedBarDefinition()
}
if (spec.variant === 'stacked') {
const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet')
return defineChart({
marks: [
barY(rows, {
id: 'visitor-bars',
x: 'month',
y: 'value',
z: 'series',
color: 'series',
key: (row) => `${row.month}:${row.series}`,
layout: stack({ order: twoSeries }),
radius: 4,
}),
],
x: shadcnXAxis(),
y: { scale: scaleLinear, grid: true, axis: false },
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: { top: 5, right: 5, bottom: 35, left: 5 },
theme: shadcnTheme(),
})
}
const negativeRows = [
{ month: 'January', desktop: 186, mobile: 0, tablet: 0 },
{ month: 'February', desktop: 205, mobile: 0, tablet: 0 },
{ month: 'March', desktop: -207, mobile: 0, tablet: 0 },
{ month: 'April', desktop: 173, mobile: 0, tablet: 0 },
{ month: 'May', desktop: -209, mobile: 0, tablet: 0 },
{ month: 'June', desktop: 214, mobile: 0, tablet: 0 },
]
const rows = spec.variant === 'negative' ? negativeRows : shadcnMonths
const horizontal = horizontalVariants.has(spec.variant)
const labels =
spec.variant === 'label' ||
spec.variant === 'label-custom' ||
spec.variant === 'negative'
const marks = horizontal
? [
barX(rows, {
id: 'visitor-bars',
x: 'desktop',
y: 'month',
fill: shadcnColors[0],
radius: 5,
}),
...(labels
? [
text(rows, {
id: 'visitor-labels',
x: 'desktop',
y: 'month',
text: (row) => row.desktop.toLocaleString('en-US'),
dx: -7,
anchor: 'end',
fill: 'var(--background)',
fontSize: 11,
fontWeight: 600,
}),
]
: []),
]
: [
barY(rows, {
id: 'visitor-bars',
x: 'month',
y: 'desktop',
fill: (row) =>
spec.variant === 'negative' && row.desktop < 0
? shadcnColors[1]
: shadcnColors[0],
radius: spec.variant === 'negative' ? 0 : 8,
}),
...(labels
? [
text(rows, {
id: 'visitor-labels',
x: 'month',
y: 'desktop',
text: (row) => row.desktop.toLocaleString('en-US'),
...(spec.variant === 'negative'
? {
text: (row: ShadcnMonthDatum) => row.month,
dy: (row: ShadcnMonthDatum) =>
row.desktop < 0 ? 14 : -10,
fill: shadcnColors[0],
fontSize: 12,
}
: {
dy: -12,
fill: 'var(--foreground)',
fontSize: 12,
}),
}),
]
: []),
]
return defineChart({
marks,
x: horizontal
? { scale: scaleLinear, axis: false }
: spec.variant === 'negative'
? {
scale: () =>
scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: false,
}
: shadcnXAxis(),
y: horizontal
? {
scale: () =>
scaleBand<string>().paddingInner(0.18).paddingOuter(0.08),
axis: { line: false, ticks: { size: 0 } },
}
: { scale: scaleLinear, grid: true, axis: false },
margin: horizontal
? { top: 5, right: 5, bottom: 5, left: 72 }
: spec.variant === 'negative'
? { top: 24, right: 5, bottom: 24, left: 5 }
: { top: labels ? 24 : 5, right: 5, bottom: 35, left: 5 },
theme: shadcnTheme(),
})
}
function buildInteractiveBarDefinition(
activeSeries: InteractiveSeries = 'desktop',
) {
return defineChart({
marks: [
barY(interactiveBarRows, {
id: 'daily-bars',
x: 'month',
y: (row) => row[activeSeries],
z: () => activeSeries,
key: 'month',
fill: activeSeries === 'desktop' ? shadcnColors[1] : shadcnColors[0],
}),
],
x: {
scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: {
line: false,
ticks: {
values: [
'2024-04-01',
'2024-04-11',
'2024-04-22',
'2024-05-03',
'2024-05-14',
'2024-05-26',
'2024-06-06',
'2024-06-17',
'2024-06-29',
],
size: 0,
padding: 10,
format: formatMonthDay,
},
},
},
y: { scale: scaleLinear, grid: true, axis: false },
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: { top: 5, right: 12, bottom: 25, left: 12 },
theme: shadcnTheme(),
})
}
function groupedBarDefinition() {
const rows = shadcnSeriesRows.filter(
(row) => row.series === 'desktop' || row.series === 'mobile',
)
const groupScale = scaleBand<string>()
.domain(twoSeries)
.paddingInner(0.08)
.paddingOuter(0)
return defineChart({
marks: [
barY(rows, {
id: 'visitor-bars',
x: 'month',
y: 'value',
z: 'series',
color: 'series',
key: (row) => `${row.month}:${row.series}`,
layout: group({ scale: groupScale }),
radius: 4,
}),
],
x: shadcnXAxis(),
y: { scale: scaleLinear, grid: true, axis: false },
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: { top: 5, right: 5, bottom: 35, left: 5 },
theme: shadcnTheme(),
})
}
function buildLineDefinition(spec: ShadcnCatalogSpec) {
if (spec.variant === 'interactive') return buildInteractiveLineDefinition()
const multiple = spec.variant === 'multiple'
const browserVariant =
spec.variant === 'dots-colors' || spec.variant === 'label-custom'
const rows = multiple
? shadcnSeriesRows.filter((row) => row.series !== 'tablet')
: browserVariant
? shadcnBrowsers
: shadcnMonths
const curve =
spec.variant === 'linear'
? curveLinear
: spec.variant === 'step'
? curveStep
: multiple
? curveMonotoneX
: curveNatural
const dots = spec.variant.includes('dots')
const labels = spec.variant.includes('label')
const dotMarks = !dots
? []
: browserVariant
? [
dot(shadcnBrowsers, {
id: 'visitor-dots',
x: 'browser',
y: 'visitors',
color: 'browser',
r: 5,
}),
]
: [
...(spec.variant === 'dots-custom'
? [
text(shadcnMonths, {
id: 'visitor-dot-stems',
x: 'month',
y: 'desktop',
text: () => '│',
fill: shadcnColors[0],
fontSize: 24,
}),
]
: []),
dot(shadcnMonths, {
id: 'visitor-dots',
x: 'month',
y: 'desktop',
r: spec.variant === 'dots-custom' ? 5 : 4,
fill:
spec.variant === 'dots-custom'
? 'var(--background)'
: shadcnColors[0],
stroke: shadcnColors[0],
strokeWidth: spec.variant === 'dots-custom' ? 2 : 1,
}),
]
const labelMarks = !labels
? []
: browserVariant
? [
text(shadcnBrowsers, {
id: 'visitor-labels',
x: 'browser',
y: 'visitors',
text: (row) => titleCase(row.browser),
dy: -12,
fill: 'var(--foreground)',
fontSize: 12,
}),
]
: [
text(shadcnMonths, {
id: 'visitor-labels',
x: 'month',
y: 'desktop',
text: (row) => row.desktop.toLocaleString('en-US'),
dy: -12,
fill: 'var(--foreground)',
fontSize: 12,
}),
]
return defineChart(({ width }) => ({
marks: [
multiple
? lineY(rows as readonly ShadcnSeriesDatum[], {
id: 'visitor-lines',
x: 'month',
y: 'value',
z: 'series',
color: 'series',
key: (row) => `${row.month}:${row.series}`,
curve: d3Curve(curve),
strokeWidth: 2,
})
: browserVariant
? lineY(rows as readonly ShadcnBrowserDatum[], {
id: 'visitor-line',
x: 'browser',
y: 'visitors',
curve: d3Curve(curve),
stroke: shadcnColors[1],
strokeWidth: 2,
})
: lineY(rows as readonly ShadcnMonthDatum[], {
id: 'visitor-line',
x: 'month',
y: 'desktop',
curve: d3Curve(curve),
stroke: shadcnColors[0],
strokeWidth: 2,
}),
...dotMarks,
...labelMarks,
],
x: browserVariant ? { scale: scalePoint, axis: false } : shadcnPointXAxis(),
y: {
scale: scaleLinear().domain([0, browserVariant ? 300 : 320]),
grid: true,
axis: {
line: false,
ticks: {
values: browserVariant
? [0, 75, 150, 225, 300]
: [0, 80, 160, 240, 320],
size: 0,
},
tickLabels: false,
},
},
color: {
domain: browserVariant ? browserNames : twoSeries,
range: browserVariant ? shadcnColors : shadcnColors.slice(0, 2),
},
margin: browserVariant
? { top: 24, right: 24, bottom: 5, left: 24 }
: {
top:
spec.variant === 'dots-custom' && width < 400
? 16
: labels
? 24
: 8,
right: 12,
bottom: 35,
left: 12,
},
theme: shadcnTheme(),
}))
}
function buildInteractiveLineDefinition(
activeSeries: InteractiveSeries = 'desktop',
) {
return defineChart(({ width }) => ({
marks: [
lineY(interactiveBarRows, {
id: 'daily-line',
x: 'month',
y: (row) => row[activeSeries],
z: () => activeSeries,
key: 'month',
curve: d3Curve(curveMonotoneX),
stroke: activeSeries === 'desktop' ? shadcnColors[0] : shadcnColors[1],
strokeWidth: 2,
}),
],
x: {
scale: scalePoint,
axis: {
line: false,
ticks: {
values: [
'2024-04-10',
'2024-04-21',
'2024-05-02',
'2024-05-13',
'2024-05-25',
'2024-06-05',
'2024-06-16',
'2024-06-29',
],
size: 0,
padding: 10,
format: formatMonthDay,
},
},
},
y: {
scale: scaleLinear().domain([0, 600]),
grid: true,
axis: {
line: false,
ticks: { values: [0, 150, 300, 450, 600], size: 0 },
tickLabels: false,
},
},
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: {
top: 5,
right: width < 400 ? 24 : 12,
bottom: 35,
left: 12,
},
theme: shadcnTheme(),
}))
}
function buildPieDefinition(spec: ShadcnCatalogSpec) {
if (spec.variant === 'interactive') return buildInteractivePieDefinition()
if (spec.variant === 'stacked') return buildStackedPieDefinition()
const arcs = pie(shadcnBrowsers, {
value: 'visitors',
startAngle: Math.PI / 2,
endAngle: (-Math.PI * 3) / 2,
})
const donut = spec.variant.includes('donut')
const active = spec.variant === 'donut-active'
const centerText = spec.variant === 'donut-text'
const labels = spec.variant.includes('label')
const externalLabels = spec.variant === 'label'
const customLabels = spec.variant === 'label-custom'
const labelList = spec.variant === 'label-list'
const separator = spec.variant === 'separator-none' ? 0 : active ? 5 : 1
const marks = [
radialArc(arcs, {
id: 'browser-slices',
key: 'browser',
innerRadius: donut ? 60 : undefined,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: separator,
}),
...(active
? [
radialArc(arcs.slice(0, 1), {
id: 'active-browser-slice',
key: 'browser',
innerRadius: 60,
outerRadius: ({ radius }) => radius + 10,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: 5,
}),
]
: []),
...(externalLabels
? [
radialRule(arcs, {
id: 'browser-leaders',
angle: 'angle',
radius1: 1,
radius2: 1,
radius2Offset: 14,
key: 'browser',
stroke: 'var(--border)',
}),
]
: []),
...(labels
? [
radialText(arcs, {
id: 'browser-labels',
angle: 'angle',
radius: externalLabels || customLabels ? 1 : 0.68,
radiusOffset: externalLabels || customLabels ? 17 : 0,
text: (row) =>
labelList
? titleCase(row.browser)
: row.visitors.toLocaleString('en-US'),
key: 'browser',
anchor: externalLabels || customLabels ? 'outside' : 'middle',
fill:
externalLabels || customLabels
? 'var(--foreground)'
: 'var(--background)',
fontSize: 12,
fontWeight: labelList ? 400 : undefined,
}),
]
: []),
...(centerText
? [
radialText([{ id: 'total', angle: 0, radius: 0, text: '1,125' }], {
id: 'visitor-total',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: -5,
fill: 'var(--foreground)',
fontSize: 30,
fontWeight: 700,
}),
radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], {
id: 'visitor-label',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: 19,
fill: 'var(--muted-foreground)',
fontSize: 14,
}),
]
: []),
]
return defineChart({
marks: [
polar({
radiusRatio: 0.78,
angle: { scale: scaleLinear().domain([0, Math.PI * 2]) },
radius: { scale: scaleLinear().domain([0, 1]) },
marks,
}),
],
color: { domain: browserNames, range: shadcnColors },
margin: 0,
})
}
function buildStackedPieDefinition() {
const desktopRows = [186, 305, 237, 173, 209].map((visitors, index) => ({
browser: ['january', 'february', 'march', 'april', 'may'][index]!,
visitors,
}))
const mobileRows = [80, 200, 120, 190, 130].map((visitors, index) => ({
browser: ['january', 'february', 'march', 'april', 'may'][index]!,
visitors,
}))
const desktopArcs = pie(desktopRows, {
value: 'visitors',
startAngle: Math.PI / 2,
endAngle: (-Math.PI * 3) / 2,
})
const mobileArcs = pie(mobileRows, {
value: 'visitors',
startAngle: Math.PI / 2,
endAngle: (-Math.PI * 3) / 2,
})
const months = desktopRows.map((row) => row.browser)
return defineChart({
marks: [
polar({
radiusRatio: 1,
angle: { scale: scaleLinear().domain([0, Math.PI * 2]) },
radius: { scale: scaleLinear().domain([0, 1]) },
marks: [
radialArc(desktopArcs, {
id: 'desktop-slices',
key: 'browser',
outerRadius: 60,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: 1,
}),
radialArc(mobileArcs, {
id: 'mobile-slices',
key: 'browser',
innerRadius: 70,
outerRadius: 90,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: 1,
}),
],
}),
],
color: { domain: months, range: shadcnColors },
margin: 0,
})
}
function buildInteractivePieDefinition(activeMonth = 'january') {
const arcs = pie(interactivePieRows, {
value: 'visitors',
startAngle: Math.PI / 2,
endAngle: (-Math.PI * 3) / 2,
})
const activeIndex = Math.max(
0,
interactivePieRows.findIndex((row) => row.browser === activeMonth),
)
const activeRow = interactivePieRows[activeIndex]!
const active = arcs.filter((row) => row.browser === activeRow.browser)
const activeColor = shadcnColors[activeIndex]!
return defineChart({
marks: [
polar({
radiusRatio: 0.78,
angle: { scale: scaleLinear().domain([0, Math.PI * 2]) },
radius: { scale: scaleLinear().domain([0, 1]) },
marks: [
radialArc(arcs, {
id: 'month-slices',
key: 'browser',
innerRadius: 60,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: 1,
}),
radialArc(active, {
id: 'active-month',
key: 'browser',
innerRadius: 60,
outerRadius: ({ radius }) => radius + 10,
fill: activeColor,
stroke: 'var(--background)',
strokeWidth: 5,
}),
radialArc(active, {
id: 'active-month-ring',
key: 'browser',
innerRadius: ({ radius }) => radius + 12,
outerRadius: ({ radius }) => radius + 25,
fill: activeColor,
stroke: 'var(--background)',
strokeWidth: 3,
}),
radialText(
[
{
id: 'total',
angle: 0,
radius: 0,
text: String(activeRow.visitors),
},
],
{
id: 'active-total',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: -5,
fill: 'var(--foreground)',
fontSize: 30,
fontWeight: 700,
},
),
radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], {
id: 'active-label',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: 20,
fill: 'var(--muted-foreground)',
fontSize: 12,
}),
],
}),
],
color: {
domain: interactivePieRows.map((row) => row.browser),
range: shadcnColors,
},
margin: 0,
})
}
function buildRadarDefinition(spec: ShadcnCatalogSpec) {
const circle = spec.variant.includes('circle')
const noGrid = spec.variant === 'grid-none'
const noSpokes =
spec.variant === 'grid-circle-no-lines' ||
spec.variant === 'grid-custom' ||
spec.variant === 'lines-only' ||
noGrid
const multiple =
spec.variant === 'multiple' ||
spec.variant === 'legend' ||
spec.variant === 'icons' ||
spec.variant === 'label-custom' ||
spec.variant === 'radius' ||
spec.variant === 'lines-only'
const filledGrid = spec.variant.includes('fill')
const rows =
spec.variant === 'lines-only'
? shadcnRadarLines
: filledGrid
? shadcnRadarFilled
: spec.variant === 'grid-circle-no-lines'
? shadcnRadarDefault.map((row) =>
row.month === 'April' ? { ...row, desktop: 203 } : row,
)
: multiple
? shadcnRadarMultiple
: shadcnRadarDefault
const months = rows.map((row) => row.month)
const radiusMax = spec.variant === 'lines-only' ? 220 : filledGrid ? 300 : 320
const gridValues = Array.from({ length: 4 }, (_, index) =>
Math.round((radiusMax * (index + 1)) / 4),
)
const customLabels = spec.variant === 'label-custom'
const radiusAxis = spec.variant === 'radius'
const dots =
spec.variant === 'dots' ||
spec.variant === 'grid-circle' ||
spec.variant === 'grid-circle-no-lines' ||
noGrid
const guides = [
...(!noGrid
? [
radialGrid({
values:
spec.variant === 'grid-custom'
? [Math.round(radiusMax * 0.947)]
: radiusAxis
? [0, 80, 160, 240, 320]
: gridValues,
shape: circle ? 'circle' : 'polygon',
stroke: filledGrid ? 'transparent' : 'var(--border)',
strokeOpacity: filledGrid ? 0 : 1,
...(filledGrid ? { fill: shadcnColors[0], fillOpacity: 0.2 } : {}),
...(radiusAxis
? {
labels: true,
labelAngle: Math.PI / 6,
labelFill: 'var(--foreground)',
labelFontSize: 12,
labelRotate: 30,
labelOffset: 1,
}
: {}),
}),
]
: []),
...(!radiusAxis
? [
angleGrid({
values: months,
labels: !customLabels,
labelOffset: 10,
labelFill: 'var(--muted-foreground)',
labelFontSize: 12,
stroke: noSpokes || filledGrid ? 'transparent' : 'var(--border)',
strokeOpacity: filledGrid ? 0 : 1,
}),
]
: []),
]
const chartSpec = (radiusRatio: number) => ({
marks: [
polar({
radiusRatio,
angle: { scale: scalePoint<string>().domain(months), wrap: true },
radius: { scale: scaleLinear().domain([0, radiusMax]) },
guides,
marks: [
radialArea(rows, {
id: 'desktop-radar',
className: 'ts-chart__radar',
angle: 'month',
radius: 'desktop',
key: 'month',
z: () => 'desktop',
curve: curveLinearClosed,
fill: shadcnColors[0],
fillOpacity:
spec.variant === 'lines-only' ? 0 : filledGrid ? 0.5 : 0.6,
stroke: shadcnColors[0],
strokeOpacity: filledGrid ? 0 : 1,
strokeWidth: spec.variant === 'lines-only' ? 2 : 1,
}),
...(multiple
? [
radialArea(rows, {
id: 'mobile-radar',
className: 'ts-chart__radar',
angle: 'month',
radius: 'mobile',
key: 'month',
z: () => 'mobile',
curve: curveLinearClosed,
fill: shadcnColors[1],
fillOpacity: spec.variant === 'lines-only' ? 0 : 1,
stroke: shadcnColors[1],
strokeWidth: spec.variant === 'lines-only' ? 2 : 1,
}),
]
: []),
...(dots
? [
radialDot(rows, {
id: 'radar-dots',
angle: 'month',
radius: 'desktop',
key: 'month',
z: () => 'desktop',
r: 4,
fill: shadcnColors[0],
}),
]
: []),
...(customLabels
? [
radialText(rows, {
id: 'radar-values',
angle: 'month',
radius: () => radiusMax,
key: 'month',
z: () => 'desktop',
text: (row) => `${row.desktop}/${row.mobile ?? 0}`,
anchor: 'outside',
radiusOffset: 15,
dy: -7,
fill: 'var(--foreground)',
fontSize: 12,
fontWeight: 500,
}),
radialText(rows, {
id: 'radar-months',
angle: 'month',
radius: () => radiusMax,
key: 'month',
z: () => 'desktop',
text: 'month',
anchor: 'outside',
radiusOffset: 15,
dy: 10,
fill: 'var(--muted-foreground)',
fontSize: 12,
}),
]
: []),
],
}),
],
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: 0,
})
return customLabels
? defineChart(({ height }) => chartSpec(height < 220 ? 0.64 : 0.76))
: defineChart(chartSpec(0.76))
}
function buildRadialDefinition(spec: ShadcnCatalogSpec) {
if (spec.variant === 'stacked') return buildStackedRadialDefinition()
if (spec.variant === 'shape') {
return buildSingleRadialDefinition({
id: 'shape',
visitors: 1_260,
endDegrees: 100,
innerRadius: 65,
outerRadius: 95,
backgroundInnerRadius: 74,
backgroundOuterRadius: 86,
cornerRadius: 0,
total: '1,260',
})
}
if (spec.variant === 'text') {
return buildSingleRadialDefinition({
id: 'text',
visitors: 200,
endDegrees: 250,
innerRadius: 80,
outerRadius: 90,
backgroundInnerRadius: 80,
backgroundOuterRadius: 90,
cornerRadius: 10,
total: '200',
})
}
const label = spec.variant === 'label'
const grid = spec.variant === 'grid'
const startAngle = rechartsPolarAngle(label ? -90 : 0)
const endAngle = rechartsPolarAngle(label ? 380 : 360)
const backgroundRows = shadcnBrowsers.map((row) => ({
...row,
background: 300,
}))
const chartSpec = (radiusFactor: number) => ({
marks: [
polar({
radiusRatio: grid ? 0.8 : 1,
startAngle,
endAngle,
angle: { scale: scaleLinear().domain([0, 300]) },
radius: {
scale: scaleBand<string>().domain(browserNames).paddingInner(0.2),
range: [
30 * radiusFactor,
(label ? 110 : grid ? 100 : 110) * radiusFactor,
],
},
guides: grid
? [
radialGrid({
values: browserNames,
shape: 'circle',
stroke: 'var(--border)',
}),
angleGrid({
values: Array.from({ length: 12 }, (_, index) => index * 25),
labels: false,
stroke: 'var(--border)',
}),
]
: [],
marks: [
...(!grid
? [
radialBarAngle(backgroundRows, {
id: 'radial-backgrounds',
angle: 'background',
radius: 'browser',
key: 'browser',
fill: 'var(--muted)',
}),
]
: []),
radialBarAngle(shadcnBrowsers, {
id: 'radial-values',
angle: 'visitors',
radius: 'browser',
key: 'browser',
fill: (row) => shadcnColors[browserNames.indexOf(row.browser)]!,
}),
],
}),
...(label
? [
polar({
startAngle,
endAngle,
angle: { scale: scaleLinear().domain([0, 300]) },
radius: {
scale: scaleLinear().domain([0, 110]),
range: [0, 110 * radiusFactor],
},
marks: [
radialText(
shadcnBrowsers.map((row, index) => ({
...row,
angle: 10,
labelRadius: 38 + index * 16,
})),
{
id: 'radial-labels',
angle: 'angle',
radius: 'labelRadius',
key: 'browser',
text: (row) => titleCase(row.browser),
fill: 'white',
fontSize: 11,
rotate: -25,
},
),
],
}),
]
: []),
],
color: { domain: browserNames, range: shadcnColors },
margin: 0,
})
return label
? defineChart(({ height }) => chartSpec(height < 220 ? 0.8 : 1))
: defineChart(chartSpec(1))
}
function buildSingleRadialDefinition(options: {
id: string
visitors: number
endDegrees: number
innerRadius: number
outerRadius: number
backgroundInnerRadius: number
backgroundOuterRadius: number
cornerRadius: number
total: string
}) {
const rows = [
{ browser: 'safari', visitors: options.visitors, ring: 'visitors' },
]
const background = pie([{ id: 'background', value: 1 }], { value: 'value' })
return defineChart({
marks: [
polar({
marks: [
radialArc(background, {
id: `${options.id}-background`,
key: 'id',
innerRadius: options.backgroundInnerRadius,
outerRadius: options.backgroundOuterRadius,
fill: 'var(--muted)',
}),
],
}),
polar({
startAngle: rechartsPolarAngle(0),
endAngle: rechartsPolarAngle(options.endDegrees),
angle: { scale: scaleLinear().domain([0, options.visitors]) },
radius: {
scale: scaleBand<string>().domain(['visitors']),
range: [options.innerRadius, options.outerRadius],
},
marks: [
radialBarAngle(rows, {
id: `${options.id}-value`,
angle: 'visitors',
radius: 'ring',
key: 'browser',
fill: shadcnColors[1],
cornerRadius: options.cornerRadius,
}),
],
}),
radialCenterLabels(options.total, 36, 0, 24),
],
margin: 0,
})
}
function buildStackedRadialDefinition() {
const rows = [
{
id: 'mobile',
ring: 'visitors',
start: 0,
end: 570,
fill: shadcnColors[1],
},
{
id: 'desktop',
ring: 'visitors',
start: 570,
end: 1_260,
fill: shadcnColors[0],
},
]
return defineChart({
marks: [
polar({
startAngle: rechartsPolarAngle(0),
endAngle: rechartsPolarAngle(180),
angle: { scale: scaleLinear().domain([0, 1_260]) },
radius: {
scale: scaleBand<string>().domain(['visitors']),
range: [80, 110],
},
marks: [
radialBarAngle(rows, {
id: 'stacked-values',
angle1: 'start',
angle2: 'end',
angle: 'end',
radius: 'ring',
key: 'id',
fill: (row) => row.fill,
cornerRadius: 5,
stroke: 'transparent',
strokeWidth: 2,
}),
],
}),
radialCenterLabels('1,830', 24, -16, 4),
],
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: 0,
})
}
function radialCenterLabels(
total: string,
fontSize: number,
totalDy: number,
labelDy: number,
) {
return polar({
angle: { scale: scaleLinear().domain([0, 1]) },
radius: { scale: scaleLinear().domain([0, 1]) },
marks: [
radialText([{ id: 'total', angle: 0, radius: 0, text: total }], {
id: `radial-total-${total}`,
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: totalDy,
fill: 'var(--foreground)',
fontSize,
fontWeight: 700,
}),
radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], {
id: `radial-label-${total}`,
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: labelDy,
fill: 'var(--muted-foreground)',
fontSize: 14,
}),
],
})
}
function rechartsPolarAngle(degrees: number) {
return ((90 - degrees) * Math.PI) / 180
}
function buildTooltipDefinition() {
return defineChart({
marks: [
barY(shadcnActivities, {
id: 'activity-bars',
x: 'date',
y: 'value',
z: 'activity',
color: 'activity',
key: (row) => `${row.date}:${row.activity}`,
layout: stack({ order: activityNames }),
radius: 4,
}),
],
x: {
scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: {
line: false,
ticks: { size: 0, padding: 10, format: formatWeekday },
},
},
y: { scale: scaleLinear().domain([0, 1_000]), axis: false },
color: { domain: activityNames, range: shadcnColors.slice(0, 2) },
margin: { top: 0, right: 7, bottom: 32, left: 7 },
theme: shadcnTheme(),
})
}
function ShadcnLegend({ spec }: { spec: ShadcnCatalogSpec }) {
const labels =
spec.family === 'pie' || spec.family === 'radial' ? browserNames : twoSeries
const items = labels.map((label, index) => (
<span className="sc-legend-item" key={label}>
{spec.variant === 'icons' ? (
<svg className="sc-legend-icon" viewBox="0 0 24 24" aria-hidden>
<path
d={
index === 0
? 'M12 3v18m-4-4 4 4 4-4M5 7h14'
: 'M12 21V3m-4 4 4-4 4 4M5 17h14'
}
/>
</svg>
) : (
<span
className="sc-legend-dot"
style={{ background: shadcnColors[index] }}
/>
)}
{label.charAt(0).toUpperCase() + label.slice(1)}
</span>
))
return spec.family === 'pie' && spec.variant === 'legend' ? (
<div className="sc-pie-legend">{items}</div>
) : (
<>{items}</>
)
}
function ShadcnTooltipBody({
variant,
points,
}: {
variant: string
points: readonly ChartPoint<ShadcnActivityDatum>[]
}) {
const ordered = [...points].sort(
(left, right) =>
activityNames.indexOf(left.datum.activity) -
activityNames.indexOf(right.datum.activity),
)
const noLabel =
variant === 'label-none' ||
variant === 'formatter' ||
variant === 'icons' ||
variant === 'advanced'
const noIndicator =
variant === 'indicator-none' ||
variant === 'label-none' ||
variant === 'formatter' ||
variant === 'icons'
const lineIndicator = variant === 'indicator-line'
const formatted = variant === 'formatter' || variant === 'advanced'
const label =
variant === 'label-formatter'
? 'July 15, 2024'
: variant === 'label-custom'
? 'Activities'
: '2024-07-16'
return (
<div
className={`sc-shadcn-tooltip${variant === 'advanced' ? ' sc-advanced-tooltip' : ''}${noIndicator ? ' sc-tooltip-no-indicator' : ''}`}
>
{noLabel ? null : <strong className="sc-tooltip-label">{label}</strong>}
{ordered.map((point, index) => (
<div className="sc-shadcn-tooltip-row" key={point.datum.activity}>
{variant === 'icons' ? (
<ShadcnActivityIcon activity={point.datum.activity} />
) : noIndicator ? null : (
<span
className={lineIndicator ? 'sc-tooltip-line' : 'sc-tooltip-dot'}
style={{ background: shadcnColors[index] }}
/>
)}
<span>{titleCase(point.datum.activity)}</span>
<b className="sc-tooltip-value">
{point.datum.value}
{formatted ? <span>kcal</span> : null}
</b>
</div>
))}
{variant === 'advanced' ? (
<div className="sc-tooltip-total">
<span>Total</span>
<b className="sc-tooltip-value">
{ordered.reduce((total, point) => total + point.datum.value, 0)}
<span>kcal</span>
</b>
</div>
) : null}
</div>
)
}
function ShadcnActivityIcon({ activity }: { activity: string }) {
return (
<svg className="sc-tooltip-icon" viewBox="0 0 24 24" aria-hidden>
{activity === 'running' ? (
<>
<path d="M4 17c3-1 4-4 4-7l3 2 2-4 3 1" />
<path d="m9 13 4 5M14 5h.01" />
</>
) : (
<>
<path d="M2 16c2-2 4 2 6 0s4 2 6 0 4 2 8 0" />
<path d="M2 20c2-2 4 2 6 0s4 2 6 0 4 2 8 0M5 12l3-3 4 3 3-4 4 4" />
</>
)}
</svg>
)
}
function ShadcnSelectControl({
ariaLabel,
value,
options,
onChange,
}: {
ariaLabel: string
value: string
options: readonly { value: string; label: string; swatch?: string }[]
onChange: (value: string) => void
}) {
const selected = options.find((option) => option.value === value)
return (
<label className="sc-select-display">
{selected?.swatch ? (
<span
className="sc-select-swatch"
style={{ background: selected.swatch }}
/>
) : null}
<select
aria-label={ariaLabel}
value={value}
onChange={(event) => onChange(event.currentTarget.value)}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m6 9 6 6 6-6"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</label>
)
}
function ShadcnBarMetrics({
active,
onChange,
}: {
active: InteractiveSeries
onChange: (series: InteractiveSeries) => void
}) {
const totals = {
desktop: interactiveBarRows.reduce((sum, row) => sum + row.desktop, 0),
mobile: interactiveBarRows.reduce((sum, row) => sum + row.mobile, 0),
}
return (
<>
{twoSeries.map((series) => (
<button
key={series}
type="button"
className="sc-bar-metric"
data-active={active === series}
aria-pressed={active === series}
onClick={() => onChange(series)}
>
<span>{titleCase(series)}</span>
<strong>{totals[series].toLocaleString('en-US')}</strong>
</button>
))}
</>
)
}
function filterInteractiveAreaRows(timeRange: InteractiveTimeRange) {
const days = timeRange === '30d' ? 30 : timeRange === '7d' ? 7 : 90
const start = new Date('2024-06-30T00:00:00Z')
start.setUTCDate(start.getUTCDate() - days)
const firstDate = start.toISOString().slice(0, 10)
return interactiveAreaRows.filter((row) => row.date >= firstDate)
}
function interactiveDateTicks(timeRange: InteractiveTimeRange) {
if (timeRange === '7d') {
return ['2024-06-23', '2024-06-25', '2024-06-27', '2024-06-29']
}
if (timeRange === '30d') {
return [
'2024-06-01',
'2024-06-08',
'2024-06-15',
'2024-06-22',
'2024-06-29',
]
}
return [
'2024-04-10',
'2024-04-21',
'2024-05-02',
'2024-05-13',
'2024-05-25',
'2024-06-05',
'2024-06-16',
'2024-06-29',
]
}
function shadcnXAxis() {
return {
scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: {
line: false,
ticks: {
size: 0,
padding: 10,
format: (value: string) => value.slice(0, 3),
},
},
}
}
function shadcnPointXAxis() {
return {
scale: scalePoint,
axis: {
line: false,
ticks: {
size: 0,
padding: 10,
format: (value: string) => value.slice(0, 3),
},
},
}
}
function shadcnBrowserXAxis() {
return {
scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: {
line: false,
ticks: {
size: 0,
padding: 10,
format: titleCase,
},
},
}
}
function shadcnBrowserYAxis() {
return {
scale: () => scaleBand<string>().paddingInner(0.18).paddingOuter(0.08),
axis: {
line: false,
ticks: { size: 0, padding: 10, format: titleCase },
},
}
}
function shadcnTheme() {
return {
foreground: 'var(--muted-foreground, var(--muted))',
grid: 'var(--border)',
background: 'transparent',
}
}
function formatWeekday(value: string) {
return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format(
new Date(value),
)
}
function formatMonthDay(value: string) {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
timeZone: 'UTC',
}).format(new Date(value))
}
function titleCase(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1)
}shared/shadcn-chart-card.tsx313 lines · dependency
shared/shadcn-chart-card.tsx
import { forwardRef, useImperativeHandle, useRef } from 'react'
import { reactMount } from './react-mount'
import type { ComponentType, ReactNode } from 'react'
import type { ConformanceInput, ConformanceTestDriver } from '../types'
import type { ReactConformanceProps } from './react-mount'
export interface ShadcnChartSize {
width: number
height: number
}
interface ShadcnChartCardProps {
input: ConformanceInput
title: string
description: string
chartShape?: 'wide' | 'square'
centered?: boolean
headerInsetBottom?: number
footer?: ReactNode
chartFooter?: ReactNode
headerAction?: ReactNode
variant?:
| 'default'
| 'interactive-area'
| 'interactive-bar'
| 'interactive-line'
| 'interactive-pie'
chartHeight?: number
chartClassName?: string
children: (size: ShadcnChartSize) => ReactNode
}
export function ShadcnChartCard({
input,
title,
description,
chartShape = 'wide',
centered = false,
headerInsetBottom = 0,
footer,
chartFooter,
headerAction,
variant = 'default',
chartHeight,
chartClassName,
children,
}: ShadcnChartCardProps) {
const cardWidth = Math.max(1, input.width)
const contentWidth = Math.max(1, cardWidth - 50)
const chartSize =
chartShape === 'square'
? {
width: Math.min(
variant === 'interactive-pie' ? 300 : 250,
contentWidth,
),
height: Math.min(
variant === 'interactive-pie' ? 300 : 250,
contentWidth,
),
}
: {
width: contentWidth,
height: chartHeight ?? (contentWidth * 9) / 16,
}
return (
<div
className="sc-example"
data-conformance-view="main"
style={{ width: input.width, height: input.height }}
>
<article className={`sc-card sc-${variant}`} style={{ width: cardWidth }}>
<header
className="sc-card-header"
style={
headerInsetBottom > 0
? { paddingBottom: headerInsetBottom }
: undefined
}
>
<div className="sc-card-heading">
<h2>{title}</h2>
<p>{description}</p>
</div>
{headerAction ? (
<div className="sc-card-action">{headerAction}</div>
) : null}
</header>
<div className={`sc-card-content${centered ? ' sc-centered' : ''}`}>
<div
className={`sc-chart${chartClassName ? ` ${chartClassName}` : ''}`}
style={chartSize}
>
{children(chartSize)}
</div>
{chartFooter ? (
<div className="sc-chart-footer">{chartFooter}</div>
) : null}
</div>
{footer ? (
<footer className={`sc-card-footer${centered ? ' sc-centered' : ''}`}>
{footer}
</footer>
) : null}
</article>
</div>
)
}
export function ShadcnTrendFooter({
note = 'Showing total visitors for the last 6 months',
}: {
centered?: boolean
note?: string
}) {
return (
<>
<div className="sc-trend">
Trending up by 5.2% this month
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m3 17 6-6 4 4 8-8" />
<path d="M14 7h7v7" />
</svg>
</div>
<div className="sc-footer-note">{note}</div>
</>
)
}
export function shadcnChartMount(
Component: ComponentType<{ input: ConformanceInput }>,
) {
const View = forwardRef<ConformanceTestDriver, ReactConformanceProps>(
function ShadcnChartView({ input }, ref) {
const rootRef = useRef<HTMLDivElement>(null)
useImperativeHandle(
ref,
() => ({
resolveTarget: () => null,
readState: () => ({}),
viewBounds(view) {
if (view && view !== 'main') return null
const bounds = rootRef.current?.getBoundingClientRect()
return bounds
? {
x: bounds.left,
y: bounds.top,
width: bounds.width,
height: bounds.height,
}
: null
},
}),
[],
)
return (
<div ref={rootRef} style={{ width: input.width, height: input.height }}>
<Component input={input} />
</div>
)
},
)
const mount = reactMount(View)
return (container: HTMLElement, input: ConformanceInput) => {
ensureShadcnChartStyles(container.ownerDocument)
return mount(container, input)
}
}
function ensureShadcnChartStyles(document: Document) {
const id = 'shadcn-chart-card-styles'
let style = document.getElementById(id)
if (!(style instanceof HTMLStyleElement)) {
style = document.createElement('style')
style.id = id
document.head.append(style)
}
if (style.textContent !== shadcnChartCardStyles) {
style.textContent = shadcnChartCardStyles
}
}
export const shadcnChartCardStyles = `
.sc-example {
--background: oklch(1 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
display: flex;
justify-content: center;
align-items: flex-start;
overflow: hidden;
color: var(--foreground);
background: var(--background);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-rendering: geometricPrecision;
}
:root[data-theme='dark'] .sc-example {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
}
.sc-example, .sc-example * { box-sizing: border-box; }
.sc-card {
display: flex;
flex-direction: column;
gap: 24px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--card);
color: var(--card-foreground);
padding: 24px 0;
}
.sc-card-header { display: grid; gap: 8px; padding: 0 24px; }
.sc-card-heading { display: grid; gap: 8px; }
.sc-card-header h2 { margin: 0; font-size: 16px; font-weight: 600; line-height: 1; letter-spacing: -0.01em; }
.sc-card-header p { margin: 0; color: var(--muted-foreground); font-size: 14px; line-height: 20px; }
.sc-card-content { display: flex; min-height: 0; flex-direction: column; padding: 0 24px; }
.sc-chart { position: relative; flex: none; }
.sc-chart > * { display: block; }
.sc-chart-clip .ts-chart { overflow: hidden !important; }
.sc-card-footer { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 0 24px; font-size: 14px; line-height: 1; }
.sc-chart-footer { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 12px; color: var(--muted-foreground); font-size: 12px; }
.sc-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.sc-legend-dot { width: 8px; height: 8px; border-radius: 2px; }
.sc-legend-icon { width: 12px; height: 12px; fill: none; stroke: var(--muted-foreground); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.sc-pie-legend { display: flex; width: 260px; flex-wrap: wrap; justify-content: center; gap: 10px 16px; }
.sc-pie-legend .sc-legend-item { flex: 0 0 70px; justify-content: center; }
.sc-centered { justify-content: center; align-items: center; text-align: center; }
.sc-trend { display: flex; align-items: center; gap: 8px; font-weight: 500; }
.sc-trend svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.sc-footer-note { color: var(--muted-foreground); }
.sc-example .ts-chart { color: var(--muted-foreground); }
.sc-example .recharts-surface { overflow: visible; }
.sc-example .ts-chart__grid line, .sc-example .ts-chart__polar-grid path, .sc-example .ts-chart__polar-grid line { stroke: var(--border); }
.sc-example .ts-chart__axes text, .sc-example .ts-chart__polar-grid text { fill: var(--muted-foreground); font-size: 12px; }
.sc-example .ts-chart-tooltip {
min-width: 128px;
padding: 7px 10px !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: var(--card) !important;
color: var(--card-foreground) !important;
box-shadow: 0 2px 6px rgb(0 0 0 / 8%);
font-size: 12px;
}
.sc-advanced-tooltip { display: grid; width: 158px; gap: 6px; }
.sc-advanced-row { display: grid; grid-template-columns: 10px 1fr auto; align-items: center; gap: 6px; }
.sc-advanced-swatch { width: 10px; height: 10px; border-radius: 2px; }
.sc-advanced-value { display: flex; align-items: baseline; gap: 2px; color: var(--foreground); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 500; font-variant-numeric: tabular-nums; }
.sc-advanced-unit { color: var(--muted-foreground); font-family: inherit; font-weight: 400; }
.sc-advanced-total { display: flex; align-items: center; margin-top: 2px; padding-top: 7px; border-top: 1px solid var(--border); color: var(--foreground); font-weight: 500; }
.sc-advanced-total .sc-advanced-value { margin-left: auto; }
.sc-shadcn-tooltip { display: grid; gap: 5px; }
.sc-tooltip-label { color: var(--foreground); font-weight: 500; }
.sc-shadcn-tooltip-row { display: grid; grid-template-columns: 9px minmax(0, 1fr) auto; align-items: center; gap: 7px; color: var(--muted-foreground); }
.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { grid-template-columns: minmax(0, 1fr) auto; }
.sc-tooltip-dot { width: 8px; height: 8px; border-radius: 2px; }
.sc-tooltip-line { width: 3px; height: 16px; border-radius: 2px; }
.sc-tooltip-icon { width: 11px; height: 11px; fill: none; stroke: var(--muted-foreground); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.sc-tooltip-value { display: flex; align-items: baseline; gap: 2px; color: var(--foreground); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 500; font-variant-numeric: tabular-nums; }
.sc-tooltip-value span { color: var(--muted-foreground); font-family: inherit; font-weight: 400; }
.sc-tooltip-total { display: flex; justify-content: space-between; margin-top: 1px; padding-top: 7px; border-top: 1px solid var(--border); color: var(--foreground); font-weight: 500; }
.sc-interactive-area { gap: 0; padding-top: 0; }
.sc-interactive-area .sc-card-header { display: flex; align-items: center; gap: 16px; padding-top: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.sc-interactive-area .sc-card-heading { flex: 1; gap: 4px; }
.sc-interactive-area .sc-card-content { padding-top: 24px; }
.sc-interactive-area .sc-chart-footer { margin-top: 8px; }
.sc-interactive-bar { gap: 0; padding: 0 0 30px; }
.sc-interactive-bar .sc-card-header { display: flex; min-height: 99px; align-items: stretch; gap: 0; padding: 0; border-bottom: 1px solid var(--border); }
.sc-interactive-bar .sc-card-heading { flex: 1; align-content: center; gap: 4px; padding: 16px 24px; }
.sc-interactive-bar .sc-card-action { display: flex; margin: 0; }
.sc-interactive-bar .sc-card-content { padding: 43px 24px 0; }
.sc-interactive-line { gap: 0; padding: 0; }
.sc-interactive-line .sc-card-header { display: flex; min-height: 99px; align-items: stretch; gap: 0; padding: 0; border-bottom: 1px solid var(--border); }
.sc-interactive-line .sc-card-heading { flex: 1; align-content: center; gap: 4px; padding: 16px 24px; }
.sc-interactive-line .sc-card-action { display: flex; margin: 0; }
.sc-interactive-line .sc-card-content { padding: 43px 24px 0; }
.sc-interactive-pie .sc-card-header { display: flex; align-items: flex-start; gap: 16px; padding-bottom: 0; }
.sc-interactive-pie .sc-card-heading { flex: 1; gap: 4px; }
.sc-interactive-pie .sc-card-content { padding-bottom: 44px; }
.sc-interactive-pie .sc-chart { transform: translateY(55px); }
.sc-interactive-pie .sc-card-action { transform: translateY(48px); }
.sc-card-action { margin-left: auto; }
.sc-select-display { position: relative; display: flex; min-width: 130px; height: 36px; align-items: center; gap: 8px; padding: 0 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--background); color: var(--foreground); font-size: 14px; box-shadow: 0 1px 2px rgb(0 0 0 / 4%); }
.sc-select-display:focus-within { outline: 2px solid var(--foreground); outline-offset: 2px; }
.sc-select-display select { min-width: 0; flex: 1; appearance: none; border: 0; outline: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
.sc-select-display svg { width: 14px; height: 14px; flex: none; color: var(--muted-foreground); pointer-events: none; }
.sc-select-swatch { width: 12px; height: 12px; flex: none; border-radius: 3px; }
.sc-bar-metric { display: grid; width: 168px; align-content: center; gap: 6px; padding: 16px 32px; border: 0; border-left: 1px solid var(--border); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.sc-bar-metric[data-active='true'] { background: var(--muted); }
.sc-bar-metric:focus-visible { outline: 2px solid var(--foreground); outline-offset: -3px; }
.sc-bar-metric span { color: var(--muted-foreground); font-size: 12px; }
.sc-bar-metric strong { font-size: 30px; line-height: 1; letter-spacing: -0.03em; }
`shared/shadcn-motion.ts21 lines · dependency
shared/shadcn-motion.ts
import { motion } from '@tanstack/charts/motion'
import type { ChartValue } from '@tanstack/charts'
import type { ChartMotionSpringTransition } from '@tanstack/charts/motion'
export const shadcnSpringTransition = {
type: 'spring',
stiffness: 170,
damping: 18,
mass: 1,
} as const satisfies ChartMotionSpringTransition
export function createShadcnSpringRenderer<
TDatum = unknown,
TXValue extends ChartValue = ChartValue,
TYValue extends ChartValue = ChartValue,
>() {
return motion<TDatum, TXValue, TYValue>({
initial: 'always',
transition: shadcnSpringTransition,
})
}types.ts378 lines · dependency
types.ts
export type ConformanceReferenceRenderer =
'observable-plot' | 'recharts' | 'echarts'
export type ConformanceRenderer = ConformanceReferenceRenderer | 'tanstack'
export type ConformanceSupport = 'native' | 'composed' | 'gap' | 'deferred'
export type ConformanceGeometryRole =
| 'arc'
| 'area'
| 'arrow'
| 'bar'
| 'cell'
| 'contour'
| 'delaunay'
| 'density'
| 'dot'
| 'frame'
| 'geo'
| 'hexagon'
| 'line'
| 'link'
| 'rect'
| 'radar'
| 'regression'
| 'rule'
| 'text'
| 'tick'
| 'vector'
| 'voronoi'
| 'waffle'
export interface ConformanceInput {
width: number
height: number
revision: number
interactive?: boolean
/** Use lower-detail geometry suited to compact catalog cards. */
preview?: boolean
/** True only for semantic browser scenarios, not catalog or visual mounts. */
behavior?: boolean
}
export interface ConformanceHandle {
update: (input: ConformanceInput) => void
driver?: ConformanceTestDriver
destroy: () => void
}
export type ConformanceMount = (
container: HTMLElement,
input: ConformanceInput,
) => ConformanceHandle
export interface ConformanceGeometryExpectation {
id?: string
view?: string
role: ConformanceGeometryRole
count: number
maxCount?: number
rendererRoles?: Partial<Record<ConformanceRenderer, ConformanceGeometryRole>>
}
export type ConformanceAxis = 'x' | 'y' | 'fx' | 'fy'
export interface ConformanceGuideExpectation {
id: string
axis:
| ConformanceAxis
| (Record<'tanstack', ConformanceAxis> &
Partial<Record<ConformanceReferenceRenderer, ConformanceAxis>>)
sequence?: readonly string[]
maxRepeat?: number
}
export type ConformanceJsonValue =
| null
| boolean
| number
| string
| readonly ConformanceJsonValue[]
| ConformanceJsonObject
export interface ConformanceJsonObject {
readonly [key: string]: ConformanceJsonValue
}
export interface ConformanceTarget {
view?: string
anchor: string
}
export type ConformanceRenderedTarget =
| {
selector: string
index?: number
role?: never
name?: never
exact?: never
root?: never
page?: never
}
| {
role: string
name?: string
exact?: boolean
index?: number
selector?: never
root?: never
page?: never
}
| {
root: true
selector?: never
role?: never
name?: never
exact?: never
index?: never
page?: never
}
| {
page: true
selector?: never
role?: never
name?: never
exact?: never
index?: never
root?: never
}
export interface ConformanceResolvedTarget {
/** Viewport-relative client coordinate used by Playwright mouse input. */
x: number
/** Viewport-relative client coordinate used by Playwright mouse input. */
y: number
/** Optional element to focus before a real Playwright keyboard action. */
focusElement?: HTMLElement | SVGElement
}
export interface ConformanceGeometryQuery {
view?: string
role: ConformanceGeometryRole
}
export interface ConformanceGeometrySample {
/** Viewport-relative client box, matching getBoundingClientRect coordinates. */
x: number
y: number
width: number
height: number
paint?: string
}
export interface ConformanceTestDriver {
/**
* Benchmark-only semantic bridge. Case metadata names anchors; each renderer
* resolves those anchors without exposing renderer-specific selectors.
*/
resolveTarget: (target: ConformanceTarget) => ConformanceResolvedTarget | null
readState: () => ConformanceJsonObject
geometry?: (
query: ConformanceGeometryQuery,
) => readonly ConformanceGeometrySample[]
/**
* Viewport-relative logical view bounds. Multi-grid renderers may expose
* independent views without separate DOM roots.
*/
viewBounds?: (view?: string) => ConformanceGeometrySample | null
settle?: () => void | Promise<void>
}
export type ConformanceStateAssertion =
| {
path: string
equals: ConformanceJsonValue
}
| {
path: string
includes: ConformanceJsonValue
}
| {
path: string
approx: number
tolerance: number
}
type ConformanceRenderedStringMatcher =
| {
equals: string | null
includes?: never
}
| {
includes: string
equals?: never
}
type ConformanceRenderedNumberMatcher =
| {
equals: number
approx?: never
tolerance?: never
atLeast?: never
atMost?: never
}
| {
approx: number
tolerance: number
equals?: never
atLeast?: never
atMost?: never
}
| {
atLeast: number
equals?: never
approx?: never
tolerance?: never
atMost?: never
}
| {
atMost: number
equals?: never
approx?: never
tolerance?: never
atLeast?: never
}
export type ConformanceRenderedAssertion =
| ({
target: ConformanceRenderedTarget
property: 'count'
} & ConformanceRenderedNumberMatcher)
| ({
target: ConformanceRenderedTarget
property: 'text'
} & ConformanceRenderedStringMatcher)
| ({
target: ConformanceRenderedTarget
property: 'attribute'
attribute: string
} & ConformanceRenderedStringMatcher)
| {
target: ConformanceRenderedTarget
property: 'visible' | 'focused'
equals: boolean
}
| ({
target: ConformanceRenderedTarget
property:
| 'scrollLeft'
| 'scrollTop'
| 'scrollWidth'
| 'scrollHeight'
| 'clientWidth'
| 'clientHeight'
| 'width'
| 'height'
} & ConformanceRenderedNumberMatcher)
| {
target: ConformanceRenderedTarget
property: 'contained'
within?: ConformanceRenderedTarget
tolerance?: number
equals: true
}
export type ConformanceInteractionStep =
| {
type: 'pointerMove'
target: ConformanceTarget
steps?: number
}
| {
type: 'pointerDown'
target: ConformanceTarget
}
| {
type: 'pointerUp'
target: ConformanceTarget
}
| {
type: 'pointerCancel'
}
| {
type: 'pointerLeave'
view?: string
}
| {
type: 'update'
revision: number
}
| {
type: 'click'
target: ConformanceTarget
}
| {
type: 'key'
key: string
target?: ConformanceTarget
}
| {
type: 'drag'
from: ConformanceTarget
to: ConformanceTarget
steps?: number
}
| {
type: 'wheel'
target: ConformanceTarget
deltaX?: number
deltaY?: number
steps?: number
deltaMode?: 'pixel' | 'line' | 'page'
}
| {
type: 'touchTap'
target: ConformanceTarget
}
| {
type: 'touchDrag'
from: ConformanceTarget
to: ConformanceTarget
steps?: number
cancel?: boolean
}
| {
type: 'wait'
durationMs: number
}
| {
type: 'assert'
assertions: readonly ConformanceStateAssertion[]
}
| {
type: 'assertRendered'
assertions: readonly ConformanceRenderedAssertion[]
}
| {
type: 'screenshot'
name: string
view?: string
}
export interface ConformanceInteractionScenario {
id: string
steps: readonly ConformanceInteractionStep[]
}
export interface ConformanceCaseMeta {
schemaVersion: 1
referenceRenderer?: ConformanceReferenceRenderer
collections?: readonly string[]
height?: number
order: number
id: string
title: string
family: string
intent: string
support: ConformanceSupport
features: readonly string[]
geometry: readonly ConformanceGeometryExpectation[]
minimumGeometrySimilarity?: number
guideAssertions?: readonly ConformanceGuideExpectation[]
interactionScenarios?: readonly ConformanceInteractionScenario[]
source: {
title: string
url: string
}
ai: {
create: string
maintain: string
}
}
export interface ConformanceImplementationModule {
mount: ConformanceMount
/** Definition-only mount used by compact generated catalog previews. */
catalogCase?: { mount: ConformanceMount }
}