TanStack
Catalog

shadcn Bar Chart - Mixed

bar

3,924 lines · 10 files · 110.1 kB

cases/149-shadcn-bar-mixed/tanstack.ts7 lines · entry
cases/149-shadcn-bar-mixed/tanstack.ts
import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack'

const example = createShadcnTanStackExample('chart-bar-mixed')

export const shadcnDefinition = example.definition
export const catalogCase = example.catalogCase
export const mount = example.mount
shadcn/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 }
}