{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-charts",
  "type": "registry:component",
  "title": "Charts",
  "description": "Bar, stacked, line, area, pie, donut, radar, radial and sparkline charts",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-icons.json",
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/data/src/chart/animation.ts",
      "target": "@ui/gpuixui/data/chart/animation.ts",
      "type": "registry:lib",
      "content": "import { useMotionPreference } from \"../../motion/react.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ChartSeries } from \"./logic.js\";\n\nexport const ANIMATION_FRAME_MS = 16;\n\nexport const DEFAULT_ANIMATION_DURATION = 1500;\n\nexport const easeOutCubic = (progress: number): number => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  return 1 - (1 - t) ** 3;\n};\n\nexport const between = (from: number, to: number, progress: number): number => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  const start = Number.isFinite(from) ? from : 0;\n  const end = Number.isFinite(to) ? to : 0;\n  return start + (end - start) * t;\n};\n\nexport const interpolateSeries = (\n  from: ReadonlyArray<ChartSeries>,\n  to: ReadonlyArray<ChartSeries>,\n  progress: number,\n): ReadonlyArray<ChartSeries> => {\n  const t = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));\n  return Arr.map(to, (target) => {\n    const before = Option.match(\n      Arr.findFirst(from, (one) => one.key === target.key),\n      {\n        onNone: () => [] as ReadonlyArray<number>,\n        onSome: (one) => one.values,\n      },\n    );\n    return {\n      ...target,\n      values: Arr.map(target.values, (value, index) =>\n        between(before[index] ?? 0, value, t),\n      ),\n    };\n  });\n};\n\nexport const interpolateValues = (\n  from: ReadonlyArray<number>,\n  to: ReadonlyArray<number>,\n  progress: number,\n): ReadonlyArray<number> =>\n  Arr.map(to, (value, index) => between(from[index] ?? 0, value, progress));\n\nexport const scaleValue = (value: number, progress: number): number =>\n  !Number.isFinite(value) || progress >= 1\n    ? value\n    : value === 0\n      ? 0\n      : between(0, value, progress);\n\nexport const scaleValues = (\n  values: ReadonlyArray<number>,\n  progress: number,\n): ReadonlyArray<number> =>\n  Arr.map(values, (value) => scaleValue(value, progress));\n\nexport const scaleSeries = (\n  series: ReadonlyArray<ChartSeries>,\n  progress: number,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(series, (one) => ({\n    ...one,\n    values: scaleValues(one.values, progress),\n  }));\n\nexport const signatureOf = (\n  values: ReadonlyArray<ReadonlyArray<number>>,\n): string =>\n  Arr.join(\n    Arr.map(values, (one) => Arr.join(Arr.map(one, String), \",\")),\n    \"|\",\n  );\n\nconst now = (): number => performance.now();\n\nconst useTween = <A>(\n  value: A,\n  signature: string,\n  blend: (from: A, to: A, progress: number) => A,\n  animate: boolean,\n  duration: number,\n  reduced: boolean,\n): A => {\n  const durationMs = Number.isFinite(duration) ? Math.max(0, duration) : 0;\n  const running = animate && !reduced && durationMs > 0;\n  const [painted, setPainted] = useState(value);\n  const from = useRef(value);\n  useEffect(() => {\n    if (!running) {\n      from.current = value;\n      setPainted(value);\n      return;\n    }\n    const start = now();\n    let handle: ReturnType<typeof setTimeout> | undefined;\n    const tick = (): void => {\n      const t = Math.min(1, (now() - start) / durationMs);\n      setPainted(blend(from.current, value, easeOutCubic(t)));\n      if (t < 1) {\n        handle = setTimeout(tick, ANIMATION_FRAME_MS);\n      } else {\n        from.current = value;\n      }\n    };\n    handle = setTimeout(tick, 0);\n    return () => {\n      if (handle !== undefined) clearTimeout(handle);\n    };\n  }, [signature, running, durationMs]);\n  return running ? painted : value;\n};\n\nexport const useMountReveal = (animate: boolean, duration: number): number => {\n  const reduced = useMotionPreference() === \"reduced\";\n  const durationMs = Number.isFinite(duration) ? Math.max(0, duration) : 0;\n  const running = animate && !reduced && durationMs > 0;\n  const [progress, setProgress] = useState(running ? 0 : 1);\n  useEffect(() => {\n    if (!running) {\n      setProgress(1);\n      return;\n    }\n    const start = now();\n    let handle: ReturnType<typeof setTimeout> | undefined;\n    const tick = (): void => {\n      const t = Math.min(1, (now() - start) / durationMs);\n      setProgress(easeOutCubic(t));\n      if (t < 1) {\n        handle = setTimeout(tick, ANIMATION_FRAME_MS);\n      }\n    };\n    handle = setTimeout(tick, 0);\n    return () => {\n      if (handle !== undefined) clearTimeout(handle);\n    };\n  }, [running, durationMs]);\n  return running ? progress : 1;\n};\n\n/** The values a chart paints, tweened from the set it last showed. */\nexport const useAnimatedValues = (\n  values: ReadonlyArray<ReadonlyArray<number>>,\n  animate: boolean,\n  duration: number,\n): ReadonlyArray<ReadonlyArray<number>> => {\n  const reduced = useMotionPreference() === \"reduced\";\n  return useTween(\n    values,\n    signatureOf(values),\n    interpolateValues2,\n    animate,\n    duration,\n    reduced,\n  );\n};\n\n/** Two lists blended, for the values hook. */\nconst interpolateValues2 = (\n  from: ReadonlyArray<ReadonlyArray<number>>,\n  to: ReadonlyArray<ReadonlyArray<number>>,\n  progress: number,\n): ReadonlyArray<ReadonlyArray<number>> =>\n  Arr.map(to, (one, index) =>\n    interpolateValues(from[index] ?? [], one, progress),\n  );\n\n/** The series a chart paints, tweened from the set it last showed. */\nexport const useAnimatedSeries = (\n  series: ReadonlyArray<ChartSeries>,\n  animate: boolean,\n  duration: number,\n): ReadonlyArray<ChartSeries> => {\n  const reduced = useMotionPreference() === \"reduced\";\n  return useTween(\n    series,\n    signatureOf(Arr.map(series, (one) => one.values)),\n    interpolateSeries,\n    animate,\n    duration,\n    reduced,\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/chart.tsx",
      "target": "@ui/gpuixui/data/chart/chart.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { Icon, isIconName, type IconName } from \"../../icons/index.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport { themeModeOf } from \"../../tokens/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport { areaGradient } from \"./paint.js\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\nimport { listOf, recordsOf, textsOf } from \"../internal/list.js\";\nimport type { ChartBox, ChartMark, ChartSeries } from \"./logic.js\";\nimport {\n  categoriesFromData,\n  configSeriesColour,\n  seriesFromConfig,\n  type ChartConfig,\n  type ChartRow,\n} from \"./config.js\";\nimport { lineStroke } from \"./mark.js\";\nimport {\n  chartBox,\n  chartScale,\n  seriesDescription,\n  seriesName,\n} from \"./logic.js\";\nimport {\n  areaPath,\n  barMarksInRange,\n  candleAtMarks,\n  candleUp,\n  chartRange,\n  curvePath,\n  emphasisedPointMarks,\n  funnelMarks,\n  horizontalBarMarks,\n  pointMarks,\n  polygonPath,\n  radarCircleGrid,\n  radarGrid,\n  radarPoints,\n  radarSpokes,\n  radialArcs,\n  fullRing,\n  scatterMarks,\n  chartView,\n  chartWindow,\n  radarCircleGridAt,\n  radarGridAt,\n  ringBands,\n  ringLabelPoints,\n  ringOutsideLabels,\n  ringSegments,\n  scaleY,\n  seriesPoints,\n  valueLabel,\n  type Candle,\n  type ChartView,\n  type Curve,\n} from \"./geometry.js\";\nimport {\n  DEFAULT_ANIMATION_DURATION,\n  scaleSeries,\n  scaleValue,\n  useAnimatedValues,\n  useMountReveal,\n} from \"./animation.js\";\nimport type { MarkClip } from \"./mark.js\";\nimport {\n  ChartFrame,\n  ChartMask,\n  PointTooltip,\n  paletteOf,\n  seriesColourOf,\n  type LegendShape,\n  type LegendSide,\n  type TooltipIndicator,\n  type PaintContext,\n  type PaintLayer,\n  type ValueLabel,\n} from \"./frame.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { ChartLegend } from \"./legend.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { CHART_PLOT, CHART_ROOT } from \"./recipe.js\";\n\nexport {\n  CHART_AXIS_WIDTH,\n  DEFAULT_CHART_HEIGHT,\n  DEFAULT_CHART_WIDTH,\n} from \"./frame.js\";\nexport {\n  ChartLegend,\n  type ChartLegendItem,\n  type ChartLegendProps,\n} from \"./legend.js\";\nexport type { LegendShape, TooltipIndicator } from \"./frame.js\";\n\nconst DEFAULT_RING_SIZE = 160;\n\nconst DEFAULT_SPARKLINE_WIDTH = 120;\n\nconst DEFAULT_SPARKLINE_HEIGHT = 32;\n\nexport const DEFAULT_SPARKLINE_LABEL = \"Trend\";\n\nconst chosenColour = (colour: string | undefined, fallback: string): string =>\n  Option.match(Option.fromNullishOr(colour), {\n    onNone: () => fallback,\n    onSome: (value) => (Str.isNonEmpty(Str.trim(value)) ? value : fallback),\n  });\n\nconst revealOf = (reveal: number | undefined): number => reveal ?? 1;\n\nconst revealClip = (\n  reveal: number | undefined,\n  box: ChartBox,\n): MarkClip | undefined =>\n  revealOf(reveal) >= 1\n    ? undefined\n    : { x: 0, y: 0, width: box.width * revealOf(reveal), height: box.height };\n\nconst scaleCandles = (\n  candles: ReadonlyArray<Candle>,\n  progress: number,\n): ReadonlyArray<Candle> =>\n  Arr.map(candles, (candle) => ({\n    open: scaleValue(candle.open, progress),\n    high: scaleValue(candle.high, progress),\n    low: scaleValue(candle.low, progress),\n    close: scaleValue(candle.close, progress),\n  }));\n\nexport interface ChartProps extends Omit<Props, \"children\"> {\n  readonly series?: ReadonlyArray<ChartSeries>;\n  readonly labels?: ReadonlyArray<string>;\n  readonly config?: ChartConfig;\n  readonly data?: ReadonlyArray<ChartRow>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly legend?: boolean | LegendSide;\n  readonly legendShape?: LegendShape;\n  readonly axis?: boolean;\n  readonly grid?: boolean;\n  readonly xLabels?: boolean;\n  readonly ticks?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly tooltip?: boolean;\n  readonly tooltipIndicator?: TooltipIndicator;\n  readonly tooltipIcons?: ReadonlyArray<IconName>;\n  readonly labelFormat?: (label: string) => string;\n  readonly activeBand?: boolean;\n  readonly crosshair?: boolean;\n  readonly domain?: readonly [number, number];\n  readonly view?: readonly [number, number];\n  readonly zoom?: boolean;\n  readonly onViewChange?: (view: readonly [number, number]) => void;\n  readonly brush?: boolean;\n  readonly onDomainChange?: (domain: readonly [number, number]) => void;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly valueFormat?: (value: number) => string;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nconst seriesOf = (\n  series: ReadonlyArray<ChartSeries> | undefined,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(recordsOf(series), (one) => ({\n    ...one,\n    values: listOf(one.values),\n  }));\n\nconst seriesFor = (\n  series: ReadonlyArray<ChartSeries> | undefined,\n  config: ChartConfig | undefined,\n  data: ReadonlyArray<ChartRow> | undefined,\n): ReadonlyArray<ChartSeries> =>\n  config === undefined || data === undefined\n    ? seriesOf(series)\n    : seriesFromConfig(config, data);\n\nconst labelsFor = (\n  labels: ReadonlyArray<string> | undefined,\n  data: ReadonlyArray<ChartRow> | undefined,\n): ReadonlyArray<string> => {\n  const own = labelsOf(labels);\n  if (data === undefined || data.length === 0) return own;\n  const fromData = categoriesFromData(data);\n  return fromData.length === 0 ? own : fromData;\n};\n\nconst labelsOf = (\n  labels: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> => textsOf(labels);\n\nconst viewOf = (\n  labels: ReadonlyArray<string>,\n  view: readonly [number, number] | undefined,\n): ChartView | undefined =>\n  Option.getOrUndefined(chartView(labels.length, view));\n\nconst windowOf = (\n  rows: ReadonlyArray<ChartSeries>,\n  labels: ReadonlyArray<string>,\n  domain: readonly [number, number] | undefined,\n): {\n  readonly rows: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n} =>\n  Option.match(chartWindow(labels.length, domain), {\n    onNone: () => ({ rows, labels }),\n    onSome: (window) => {\n      const width = window.end - window.start;\n      return {\n        rows: Arr.map(rows, (one) => ({\n          ...one,\n          values: Arr.take(Arr.drop(one.values, window.start), width),\n        })),\n        labels: Arr.take(Arr.drop(labels, window.start), width),\n      };\n    },\n  });\n\nconst dataOf = (\n  data: ReadonlyArray<ChartDatum> | undefined,\n): ReadonlyArray<ChartDatum> =>\n  Arr.map(recordsOf(data), (one) => ({\n    ...one,\n    value: one.value,\n  }));\n\nconst layersFor = (\n  series: ReadonlyArray<ChartSeries>,\n  layers: ReadonlyArray<ReadonlyArray<ChartMark>>,\n): ReadonlyArray<PaintLayer> =>\n  Arr.flatMap(series, (one, seriesIndex) => [\n    {\n      key: one.key,\n      series: one,\n      seriesIndex,\n      marks: layers[seriesIndex] ?? [],\n    },\n  ]);\n\nconst barValueLabels =\n  (\n    format: (value: number, one: ChartSeries, category: string) => string,\n    horizontal: boolean,\n    view: ChartView | undefined,\n  ) =>\n  ({\n    series,\n    box,\n    range,\n    labels,\n    reveal,\n  }: PaintContext): ReadonlyArray<ValueLabel> => {\n    if (revealOf(reveal) < 1) return [];\n    if (series.length !== 1) return [];\n    const [layer] = horizontal\n      ? horizontalBarMarks(series, box, range, labels.length)\n      : barMarksInRange(series, box, range, labels.length, 0, view);\n    const one = series[0];\n    if (one === undefined) return [];\n    return Arr.flatMap(layer ?? [], (mark, index) => {\n      if (mark.kind !== \"rect\") return [];\n      return [\n        {\n          key: one.key,\n          x: horizontal ? mark.x + mark.width + 4 : mark.x,\n          y: horizontal\n            ? mark.y + mark.height / 2 - 8\n            : Math.max(0, mark.y - 16),\n          width: horizontal ? 40 : mark.width,\n          text: format(one.values[index] ?? 0, one, labels[index] ?? one.key),\n        },\n      ];\n    });\n  };\n\nconst pointValueLabels =\n  (format: (value: number) => string) =>\n  ({\n    series,\n    box,\n    range,\n    labels,\n    reveal,\n  }: PaintContext): ReadonlyArray<ValueLabel> => {\n    if (revealOf(reveal) < 1) return [];\n    if (series.length !== 1) return [];\n    const [one] = series;\n    if (one === undefined) return [];\n    const points = seriesPoints(one.values, box, range, labels.length);\n    return Arr.map(points, (point, index) => ({\n      key: one.key,\n      x: point.x - 24,\n      y: Math.max(0, point.y - 16),\n      width: 48,\n      text: format(one.values[index] ?? 0),\n    }));\n  };\n\nexport interface BarChartProps extends ChartProps {\n  readonly orientation?: \"vertical\" | \"horizontal\";\n  readonly radius?: number;\n  readonly valueLabels?: boolean;\n  readonly valueLabelFormat?: (\n    value: number,\n    series: ChartSeries,\n    category: string,\n  ) => string;\n}\n\nexport const BarChart = ({\n  orientation = \"vertical\",\n  radius = 3,\n  valueLabels = false,\n  valueLabelFormat,\n  valueFormat,\n  ...props\n}: BarChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  const horizontal = orientation === \"horizontal\";\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      grid={props.grid ?? !horizontal}\n      paint={({ series, labels, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return layersFor(\n          shown,\n          horizontal\n            ? horizontalBarMarks(shown, box, range, labels.length, radius)\n            : barMarksInRange(shown, box, range, labels.length, radius, view),\n        );\n      }}\n      valueLabels={\n        valueLabels\n          ? barValueLabels(\n              (value, one, category) =>\n                valueLabelFormat === undefined\n                  ? (valueFormat ?? valueLabel)(value)\n                  : valueLabelFormat(value, one, category),\n              horizontal,\n              view,\n            )\n          : undefined\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface StackedBarProps extends ChartProps {\n  readonly radius?: number;\n  readonly percent?: boolean;\n}\n\nconst percentOf = (\n  series: ReadonlyArray<ChartSeries>,\n  categories: number,\n): ReadonlyArray<ChartSeries> =>\n  Arr.map(series, (one) => ({\n    ...one,\n    values: Arr.map(Arr.take(one.values, categories), (value, index) => {\n      const total = Arr.reduce(\n        series,\n        0,\n        (sum, entry) => sum + Math.max(0, entry.values[index] ?? 0),\n      );\n      return total <= 0 ? 0 : (Math.max(0, value) / total) * 100;\n    }),\n  }));\n\nexport const StackedBar = ({\n  radius = 3,\n  percent = false,\n  ...props\n}: StackedBarProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const labels = windowed.labels;\n  const source = windowed.rows;\n  const rows = percent ? percentOf(source, labels.length) : source;\n  const range = percent\n    ? { min: 0, max: 100 }\n    : chartRange(rows, labels.length, true);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return layersFor(\n          shown,\n          stackedLayers(shown, box, range, slots.length, radius, view),\n        );\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nconst stackedLayers = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: { readonly min: number; readonly max: number },\n  categories: number,\n  radius: number,\n  view?: ChartView,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const span = view === undefined ? categories : view.end - view.start;\n  const offset = view === undefined ? 0 : view.start;\n  const slot = box.width / Math.max(1, span);\n  const width = series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / 1);\n  const [, levels] = Arr.mapAccum(series, [] as Array<number>, (stack, one) => {\n    const next = Arr.makeBy(Math.max(0, categories), (index) => {\n      const sum = (stack[index] ?? 0) + Math.max(0, one.values[index] ?? 0);\n      return Number.isFinite(sum) ? sum : (stack[index] ?? 0);\n    });\n    return [next, next];\n  });\n  return Arr.map(series, (_, seriesIndex) => {\n    const upper = levels[seriesIndex] ?? [];\n    const lower = seriesIndex === 0 ? [] : (levels[seriesIndex - 1] ?? []);\n    return Arr.flatMap(upper, (value, index) => {\n      const top = scaleY(value, box, range);\n      const bottom = scaleY(lower[index] ?? 0, box, range);\n      const height = Math.abs(bottom - top);\n      if (height < 0.5) return [];\n      return [\n        {\n          kind: \"rect\" as const,\n          x: (index - offset) * slot + (slot - width) / 2,\n          y: Math.min(top, bottom),\n          width,\n          height,\n          rx: Math.min(radius, width / 2, height / 2),\n        },\n      ];\n    });\n  });\n};\n\nexport interface ComposedChartProps extends ChartProps {\n  readonly lineKeys?: ReadonlyArray<string>;\n  readonly radius?: number;\n}\n\nexport const ComposedChart = ({\n  lineKeys = [],\n  radius = 3,\n  ...props\n}: ComposedChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  const isLine = (key: string): boolean =>\n    Arr.some(lineKeys, (candidate) => candidate === key);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        const clip = revealClip(reveal, box);\n        const bars = Arr.filter(shown, (one) => !isLine(one.key));\n        const barMarks = barMarksInRange(\n          bars,\n          box,\n          range,\n          slots.length,\n          radius,\n          view,\n        );\n        return Arr.flatten([\n          Arr.map(bars, (one, barIndex) => ({\n            key: one.key,\n            series: one,\n            seriesIndex: Arr.findFirstIndex(\n              series,\n              (entry) => entry.key === one.key,\n            ).pipe(Option.getOrElse(() => barIndex)),\n            marks: barMarks[barIndex] ?? [],\n          })),\n          Arr.flatMap(shown, (one, seriesIndex) => {\n            if (!isLine(one.key)) return [];\n            const points = seriesPoints(one.values, box, range, slots.length);\n            return [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(curvePath(points, \"linear\"), {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\" as const, d, stroke: lineStroke(2) },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ];\n          }),\n        ]);\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface LineChartProps extends ChartProps {\n  readonly curve?: Curve;\n  readonly dots?: boolean;\n  readonly dashed?: boolean;\n  readonly lineWidth?: number;\n  readonly showLabels?: boolean;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n  readonly pointTooltip?: boolean;\n}\n\nexport interface ScatterChartProps extends ChartProps {\n  readonly radius?: number;\n}\n\nexport const ScatterChart = ({ radius = 3, ...props }: ScatterChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ series, labels: slots, box, reveal }) => {\n        const progress = revealOf(reveal);\n        return progress <= 0\n          ? layersFor(\n              series,\n              Arr.map(series, () => []),\n            )\n          : layersFor(\n              series,\n              Arr.map(series, (one) =>\n                scatterMarks(\n                  one.values,\n                  box,\n                  range,\n                  slots.length,\n                  radius * progress,\n                ),\n              ),\n            );\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface CandleChartProps extends ChartProps {\n  readonly candles: ReadonlyArray<Candle>;\n  readonly width?: number;\n  readonly upColor?: string;\n  readonly downColor?: string;\n}\n\nexport const CandleChart = ({\n  candles,\n  width = 6,\n  upColor,\n  downColor,\n  ...props\n}: CandleChartProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(\n    [\n      {\n        key: \"close\",\n        label: \"Close\",\n        values: Arr.map(candles, (one) => one.close),\n      },\n    ],\n    labels.length,\n  );\n  const up = upColor ?? colors.success ?? \"#22c55e\";\n  const down = downColor ?? colors.destructive ?? \"#ef4444\";\n  const tweened = useAnimatedValues(\n    [\n      Arr.map(candles, (one) => one.open),\n      Arr.map(candles, (one) => one.high),\n      Arr.map(candles, (one) => one.low),\n      Arr.map(candles, (one) => one.close),\n    ],\n    props.animate ?? true,\n    props.animationDuration ?? DEFAULT_ANIMATION_DURATION,\n  );\n  const shownCandles = Arr.map(candles, (candle, index) => ({\n    open: tweened[0]?.[index] ?? candle.open,\n    high: tweened[1]?.[index] ?? candle.high,\n    low: tweened[2]?.[index] ?? candle.low,\n    close: tweened[3]?.[index] ?? candle.close,\n  }));\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      paint={({ box, reveal }) => [\n        {\n          key: \"candles\",\n          series: { key: \"candles\", label: \"Candles\", values: [] },\n          seriesIndex: 0,\n          marks:\n            revealOf(reveal) <= 0\n              ? []\n              : Arr.flatMap(\n                  scaleCandles(shownCandles, revealOf(reveal)),\n                  (candle, index) =>\n                    Arr.map(\n                      candleAtMarks(\n                        candle,\n                        index,\n                        labels.length,\n                        box,\n                        range,\n                        width,\n                      ),\n                      (mark) => ({\n                        ...mark,\n                        fill: { color: candleUp(candle) ? up : down },\n                      }),\n                    ),\n                ),\n        },\n      ]}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nconst FUNNEL_CHAR_WIDTH = 7;\n\nexport interface FunnelChartProps extends ChartProps {\n  readonly stages: ReadonlyArray<number>;\n  readonly stageLabels?: ReadonlyArray<string>;\n}\n\nexport const FunnelChart = ({\n  stages,\n  stageLabels,\n  ...props\n}: FunnelChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = stageLabels ?? labelsFor(props.labels, props.data);\n  const range = chartRange(\n    [{ key: \"stage\", label: \"Stage\", values: stages }],\n    labels.length,\n  );\n  const tweened =\n    useAnimatedValues(\n      [stages],\n      props.animate ?? true,\n      props.animationDuration ?? DEFAULT_ANIMATION_DURATION,\n    )[0] ?? stages;\n  const theme = useTheme();\n  return (\n    <ChartFrame\n      axis={false}\n      grid={false}\n      xLabels={false}\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      range={range}\n      overlay={({ box }) => (\n        <>\n          {Arr.map(labels, (label, index) => {\n            const text = `${label} ${stages[index] ?? \"\"}`;\n            const bar = ((stages[index] ?? 0) / (stages[0] ?? 1)) * box.width;\n            const inside = bar >= text.length * FUNNEL_CHAR_WIDTH + 16;\n            return (\n              <text\n                key={label}\n                style={{\n                  ...textStyle(theme, \"label\"),\n                  position: \"absolute\",\n                  top: ((index + 0.4) * box.height) / labels.length - 8,\n                  ...(inside\n                    ? { left: 0, width: box.width, textAlign: \"center\" }\n                    : { left: (box.width + bar) / 2 + 8 }),\n                  color: inside\n                    ? themeColors(theme).primaryForeground\n                    : themeColors(theme).foreground,\n                }}\n              >\n                {text}\n              </text>\n            );\n          })}\n        </>\n      )}\n      paint={({ box, reveal }) => {\n        const shown = Arr.map(tweened, (value) =>\n          scaleValue(value, revealOf(reveal)),\n        );\n        return [\n          {\n            key: \"funnel\",\n            series: { key: \"funnel\", label: \"Funnel\", values: shown },\n            seriesIndex: 0,\n            marks: funnelMarks(shown, box),\n          },\n        ];\n      }}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport const LineChart = ({\n  curve = \"linear\",\n  dots = false,\n  dashed = false,\n  lineWidth = 2,\n  showLabels = false,\n  dotIcons,\n  pointTooltip = false,\n  ...props\n}: LineChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const rows = windowed.rows;\n  const labels = windowed.labels;\n  const range = chartRange(rows, labels.length);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      paint={({ series, labels, box, hovered, reveal }) => {\n        const clip = revealClip(reveal, box);\n        return Arr.flatMap(series, (one, seriesIndex) => {\n          const points = seriesPoints(\n            one.values,\n            box,\n            range,\n            labels.length,\n            view,\n          );\n          const path = curvePath(points, curve);\n          const line: PaintLayer = {\n            key: one.key,\n            series: one,\n            seriesIndex,\n            ...(clip === undefined ? {} : { clip }),\n            marks: Option.match(path, {\n              onNone: () => [],\n              onSome: (d): ReadonlyArray<ChartMark> => [\n                { kind: \"path\", d, stroke: lineStroke(lineWidth, dashed) },\n              ],\n            }),\n          };\n          return dots\n            ? [\n                {\n                  ...line,\n                  kind: \"dots\",\n                  decorative: true,\n                  marks: emphasisedPointMarks(\n                    points,\n                    lineWidth + 1.5,\n                    Option.getOrUndefined(hovered ?? Option.none()),\n                  ),\n                },\n                line,\n              ]\n            : [line];\n        });\n      }}\n      valueLabels={\n        showLabels\n          ? pointValueLabels(props.valueFormat ?? valueLabel)\n          : undefined\n      }\n      overlay={\n        dotIcons === undefined && !pointTooltip\n          ? undefined\n          : ({ series, labels, box, range, palette, origin, reveal }) => {\n              const zones = pointTooltip\n                ? Arr.flatMap(series, (one, seriesIndex) =>\n                    Arr.map(\n                      seriesPoints(one.values, box, range, labels.length, view),\n                      (point, index) => ({\n                        key: one.key,\n                        x: point.x,\n                        y: point.y,\n                        category: labels[index] ?? \"\",\n                        label: seriesName(one.label, one.key),\n                        value: (props.valueFormat ?? valueLabel)(\n                          one.values[index] ?? 0,\n                        ),\n                        colour: seriesColourOf(palette, one, seriesIndex),\n                      }),\n                    ),\n                  )\n                : [];\n              return (\n                <>\n                  {pointTooltip ? (\n                    <PointTooltip\n                      zones={zones}\n                      box={box}\n                      origin={origin}\n                      testId={props.testId}\n                    />\n                  ) : null}\n                  {dotIcons === undefined || revealOf(reveal) < 1\n                    ? null\n                    : Arr.flatMap(series, (one, seriesIndex) =>\n                        Arr.map(\n                          seriesPoints(one.values, box, range, labels.length),\n                          (point, index) => {\n                            const name = dotIcons[index];\n                            return name === undefined ||\n                              !isIconName(name) ? null : (\n                              <div\n                                key={`${one.key}-icon-${index}`}\n                                style={{\n                                  position: \"absolute\",\n                                  left: point.x - 7,\n                                  top: point.y - 7,\n                                }}\n                              >\n                                <Icon\n                                  color={seriesColourOf(\n                                    palette,\n                                    one,\n                                    seriesIndex,\n                                  )}\n                                  name={name}\n                                  size={14}\n                                  testId={\n                                    props.testId === undefined\n                                      ? undefined\n                                      : `${props.testId}-dot-${one.key}-${index}`\n                                  }\n                                />\n                              </div>\n                            );\n                          },\n                        ),\n                      )}\n                </>\n              );\n            }\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface AreaChartProps extends ChartProps {\n  readonly curve?: Curve;\n  readonly stacked?: boolean;\n  readonly gradient?: boolean;\n  readonly fillOpacity?: number;\n  readonly dots?: boolean;\n  readonly lineWidth?: number;\n  readonly showLabels?: boolean;\n  readonly pointTooltip?: boolean;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n}\n\nexport const AreaChart = ({\n  curve = \"natural\",\n  stacked = false,\n  gradient = true,\n  fillOpacity = 0.4,\n  dots = false,\n  lineWidth = 1,\n  showLabels = false,\n  pointTooltip = false,\n  dotIcons,\n  ...props\n}: AreaChartProps) => {\n  const windowed = windowOf(\n    seriesFor(props.series, props.config, props.data),\n    labelsFor(props.labels, props.data),\n    props.domain,\n  );\n  const labels = windowed.labels;\n  const source = windowed.rows;\n  const rows = stacked ? cumulativeSeries(source) : source;\n  const range = chartRange(source, labels.length, stacked);\n  const view = viewOf(labels, props.view);\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      domainLabels={labelsFor(props.labels, props.data)}\n      view={props.view}\n      range={range}\n      gradients={({ series, palette }) =>\n        gradient\n          ? Arr.map(series, (one, index) =>\n              areaGradient(\n                one.key,\n                seriesColourOf(palette, one, index),\n                fillOpacity * 2,\n              ),\n            )\n          : []\n      }\n      paint={({ series, labels, box, hovered, reveal }) => {\n        const clip = revealClip(reveal, box);\n        return Arr.flatMap(series, (one, seriesIndex) => {\n          const points = seriesPoints(\n            one.values,\n            box,\n            range,\n            labels.length,\n            view,\n          );\n          const fill = areaPath(points, box, range, curve);\n          const fade = `chart-area-${one.key}`;\n          const line = curvePath(points, curve);\n          return Arr.flatten([\n            [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                opacity: gradient ? 1 : fillOpacity,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(fill, {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    gradient\n                      ? {\n                          kind: \"path\",\n                          d,\n                          fill: { color: \"#000000\", gradient: fade },\n                        }\n                      : { kind: \"path\", d },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ],\n            [\n              {\n                key: one.key,\n                series: one,\n                seriesIndex,\n                kind: \"line\",\n                decorative: true,\n                ...(clip === undefined ? {} : { clip }),\n                marks: Option.match(line, {\n                  onNone: () => [],\n                  onSome: (d): ReadonlyArray<ChartMark> => [\n                    { kind: \"path\", d, stroke: lineStroke(lineWidth) },\n                  ],\n                }),\n              } satisfies PaintLayer,\n            ],\n            dots\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"dots\",\n                    decorative: true,\n                    ...(clip === undefined ? {} : { clip }),\n                    marks: emphasisedPointMarks(\n                      points,\n                      lineWidth + 1.5,\n                      Option.getOrUndefined(hovered ?? Option.none()),\n                    ),\n                  } satisfies PaintLayer,\n                ]\n              : [],\n          ]);\n        });\n      }}\n      valueLabels={\n        showLabels\n          ? pointValueLabels(props.valueFormat ?? valueLabel)\n          : undefined\n      }\n      overlay={\n        pointTooltip || dotIcons !== undefined\n          ? ({\n              series,\n              labels: slots,\n              box,\n              range: window,\n              palette,\n              origin,\n              reveal,\n            }) => (\n              <>\n                {pointTooltip ? (\n                  <PointTooltip\n                    zones={Arr.flatMap(series, (one, seriesIndex) =>\n                      Arr.map(\n                        seriesPoints(\n                          one.values,\n                          box,\n                          window,\n                          slots.length,\n                          view,\n                        ),\n                        (point, index) => ({\n                          key: one.key,\n                          x: point.x,\n                          y: point.y,\n                          category: slots[index] ?? \"\",\n                          label: seriesName(one.label, one.key),\n                          value: (props.valueFormat ?? valueLabel)(\n                            one.values[index] ?? 0,\n                          ),\n                          colour: seriesColourOf(palette, one, seriesIndex),\n                        }),\n                      ),\n                    )}\n                    box={box}\n                    origin={origin}\n                    testId={props.testId}\n                  />\n                ) : null}\n                {dotIcons === undefined || revealOf(reveal) < 1\n                  ? null\n                  : Arr.flatMap(series, (one, seriesIndex) =>\n                      Arr.map(\n                        seriesPoints(\n                          one.values,\n                          box,\n                          window,\n                          slots.length,\n                          view,\n                        ),\n                        (point, index) => {\n                          const name = dotIcons[index];\n                          return name === undefined ||\n                            !isIconName(name) ? null : (\n                            <div\n                              key={`${one.key}-icon-${index}`}\n                              style={{\n                                position: \"absolute\",\n                                left: point.x - 7,\n                                top: point.y - 7,\n                              }}\n                            >\n                              <Icon\n                                color={seriesColourOf(\n                                  palette,\n                                  one,\n                                  seriesIndex,\n                                )}\n                                name={name}\n                                size={14}\n                                testId={\n                                  props.testId === undefined\n                                    ? undefined\n                                    : `${props.testId}-dot-${one.key}-${index}`\n                                }\n                              />\n                            </div>\n                          );\n                        },\n                      ),\n                    )}\n              </>\n            )\n          : undefined\n      }\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nconst cumulativeSeries = (\n  series: ReadonlyArray<ChartSeries>,\n): ReadonlyArray<ChartSeries> => {\n  const [, running] = Arr.mapAccum(\n    series,\n    [] as ReadonlyArray<number>,\n    (stack, one) => {\n      const next = Arr.map(\n        one.values,\n        (value, index) => (stack[index] ?? 0) + Math.max(0, value),\n      );\n      return [next, next] as const;\n    },\n  );\n  return Arr.map(series, (one, index) => ({\n    ...one,\n    values: running[index] ?? [],\n  }));\n};\n\nconst LegendFor = ({\n  series,\n  colors,\n  config,\n  shape,\n  testId,\n}: {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly config?: ChartConfig;\n  readonly shape?: LegendShape;\n  readonly testId?: string;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const mode = themeModeOf(theme);\n  return (\n    <ChartLegend\n      shape={shape}\n      testId={testId}\n      items={Arr.map(series, (one, index) => ({\n        key: one.key,\n        label: one.label,\n        color: Option.getOrElse(configSeriesColour(config, one.key, mode), () =>\n          seriesColourOf(palette, one, index),\n        ),\n      }))}\n    />\n  );\n};\n\nexport interface ChartDatum {\n  readonly key: string;\n  readonly label: string;\n  readonly value: number;\n}\n\nexport interface PieChartProps extends Omit<Props, \"children\"> {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size?: number;\n  readonly innerRadius?: number;\n  readonly padAngle?: number;\n  readonly legend?: boolean;\n  readonly legendValues?: boolean;\n  readonly colors?: ReadonlyArray<string>;\n  readonly valueFormat?: (value: number) => string;\n  readonly centerLabel?: string;\n  readonly showLabels?: boolean;\n  readonly labelPosition?: \"inside\" | \"outside\";\n  readonly labelFormat?: (datum: ChartDatum, share: number) => string;\n  readonly activeIndex?: number;\n  readonly stacked?: boolean;\n  readonly onSliceActivate?: (datum: ChartDatum, index: number) => void;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nconst Ring = ({\n  sweep,\n  data,\n  size,\n  innerRadius,\n  padAngle,\n  colors,\n  centerLabel,\n  showLabels = false,\n  labelPosition = \"inside\",\n  labelFormat,\n  activeIndex,\n  stacked = false,\n  onSliceActivate,\n  testId,\n}: {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size: number;\n  readonly innerRadius: number;\n  readonly padAngle: number;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly sweep: number;\n  readonly centerLabel: string | undefined;\n  readonly showLabels?: boolean;\n  readonly labelPosition?: \"inside\" | \"outside\";\n  readonly labelFormat?: (datum: ChartDatum, share: number) => string;\n  readonly activeIndex?: number;\n  readonly stacked?: boolean;\n  readonly onSliceActivate?: (datum: ChartDatum, index: number) => void;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const box = chartBox(size, size, {\n    width: DEFAULT_RING_SIZE,\n    height: DEFAULT_RING_SIZE,\n  });\n  const slices = Arr.filter(data, (one) => one !== null && one !== undefined);\n  const values = Arr.map(slices, (one) => one.value);\n  const total = Arr.reduce(\n    values,\n    0,\n    (sum, value) => sum + Math.max(0, Number.isFinite(value) ? value : 0),\n  );\n  const segments = stacked\n    ? ringBands(values, box, innerRadius, padAngle, activeIndex)\n    : ringSegments(values, box, innerRadius, padAngle, activeIndex, sweep);\n  const labelPoints = ringLabelPoints(values, box, innerRadius);\n  const outsideLabels = ringOutsideLabels(values, box);\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-plot`}\n      style={{\n        ...recipeStyle(CHART_PLOT, theme),\n        width: box.width,\n        height: box.height,\n      }}\n    >\n      {Arr.map(slices, (one, index) => (\n        <ChartMask\n          key={one.key}\n          chartTestId={testId}\n          layer={{\n            key: one.key,\n            series: { key: one.key, label: one.label, values: [one.value] },\n            seriesIndex: index,\n            marks: segments[index] ?? [],\n          }}\n          label={seriesName(one.label, one.key)}\n          description={seriesDescription([one.value])}\n          colour={seriesColourOf(\n            palette,\n            { key: one.key, label: one.label, values: [] },\n            index,\n          )}\n          box={box}\n        />\n      ))}\n      {onSliceActivate === undefined\n        ? undefined\n        : Arr.map(slices, (one, index) => {\n            const point = labelPoints[index];\n            if (point === undefined || Option.isNone(point)) return null;\n            return (\n              <div\n                key={`${one.key}-hit`}\n                testId={\n                  testId === undefined\n                    ? undefined\n                    : `${testId}-slice-${one.key}`\n                }\n                onClick={() => onSliceActivate(one, index)}\n                style={{\n                  position: \"absolute\",\n                  left: point.value.x - 16,\n                  top: point.value.y - 16,\n                  width: 32,\n                  height: 32,\n                  borderRadius: 16,\n                }}\n              />\n            );\n          })}\n      {showLabels && labelPosition === \"outside\" ? (\n        <ChartMask\n          chartTestId={testId}\n          layer={{\n            key: \"leaders\",\n            series: { key: \"leaders\", label: \"leaders\", values: [] },\n            seriesIndex: 0,\n            decorative: true,\n            opacity: 0.8,\n            marks: Arr.flatMap(outsideLabels, (label) =>\n              Option.match(label, {\n                onNone: () => [],\n                onSome: (found): ReadonlyArray<ChartMark> => [\n                  { kind: \"path\", d: found.leader, stroke: lineStroke(1) },\n                ],\n              }),\n            ),\n          }}\n          label=\"\"\n          description=\"\"\n          colour={themeColors(theme).border ?? \"#000000\"}\n          box={box}\n        />\n      ) : undefined}\n      {showLabels && labelPosition === \"outside\"\n        ? Arr.map(slices, (one, index) => {\n            const label = outsideLabels[index];\n            if (label === undefined || Option.isNone(label)) return null;\n            const share = total <= 0 ? 0 : Math.max(0, one.value) / total;\n            const words =\n              labelFormat === undefined\n                ? seriesName(one.label, one.key)\n                : labelFormat(one, share);\n            const anchor = label.value.point;\n            return (\n              <text\n                key={`${one.key}-outside`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: label.value.right ? anchor.x + 4 : anchor.x - 84,\n                  top: anchor.y - 8,\n                  width: 80,\n                  textAlign: label.value.right ? \"left\" : \"right\",\n                  color: themeColors(theme).mutedForeground,\n                }}\n              >\n                {words}\n              </text>\n            );\n          })\n        : undefined}\n      {showLabels && labelPosition === \"inside\"\n        ? Arr.map(slices, (one, index) => {\n            const point = labelPoints[index];\n            if (point === undefined || Option.isNone(point)) return null;\n            const share = total <= 0 ? 0 : Math.max(0, one.value) / total;\n            const words =\n              labelFormat === undefined\n                ? seriesName(one.label, one.key)\n                : labelFormat(one, share);\n            return (\n              <text\n                key={`${one.key}-label`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: point.value.x - 30,\n                  top: point.value.y - 8,\n                  width: 60,\n                  textAlign: \"center\",\n                  color: themeColors(theme).background,\n                }}\n              >\n                {words}\n              </text>\n            );\n          })\n        : undefined}\n      {centerLabel === undefined ||\n      centerLabel.trim().length === 0 ? undefined : (\n        <text\n          style={{\n            ...textStyle(theme, \"label\"),\n            position: \"absolute\",\n            left: 0,\n            top: box.height / 2 - 10,\n            width: box.width,\n            textAlign: \"center\",\n            color: themeColors(theme).foreground,\n          }}\n        >\n          {centerLabel}\n        </text>\n      )}\n    </div>\n  );\n};\n\nconst RingLegend = ({\n  data,\n  colors,\n  values,\n  format,\n  testId,\n}: {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly colors: ReadonlyArray<string> | undefined;\n  readonly values: boolean;\n  readonly format: (value: number) => string;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  return (\n    <ChartLegend\n      testId={testId}\n      items={Arr.map(data, (one, index) => ({\n        key: one.key,\n        label: values\n          ? `${seriesName(one.label, one.key)} · ${format(one.value)}`\n          : one.label,\n        color: seriesColourOf(\n          palette,\n          { key: one.key, label: one.label, values: [] },\n          index,\n        ),\n      }))}\n    />\n  );\n};\n\nexport const PieChart = ({\n  data,\n  size = DEFAULT_RING_SIZE,\n  innerRadius = 0,\n  padAngle = 0,\n  legend = true,\n  legendValues = false,\n  colors,\n  valueFormat,\n  centerLabel,\n  showLabels = false,\n  labelPosition = \"inside\",\n  labelFormat,\n  activeIndex,\n  stacked = false,\n  onSliceActivate,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  className,\n  testId,\n  style,\n  ...rest\n}: PieChartProps) => {\n  const theme = useTheme();\n  const rows = dataOf(data);\n  const animated = useAnimatedValues(\n    Arr.map(rows, (one) => [one.value]),\n    animate,\n    animationDuration,\n  );\n  const reveal = useMountReveal(animate, animationDuration);\n  const shown = Arr.map(rows, (one, index) => ({\n    ...one,\n    value: stacked\n      ? scaleValue(animated[index]?.[0] ?? one.value, reveal)\n      : (animated[index]?.[0] ?? one.value),\n  }));\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <Ring\n        data={shown}\n        sweep={stacked ? 1 : reveal}\n        size={size}\n        innerRadius={innerRadius}\n        padAngle={padAngle}\n        colors={colors}\n        centerLabel={centerLabel}\n        showLabels={showLabels && reveal >= 1}\n        labelPosition={labelPosition}\n        labelFormat={labelFormat}\n        activeIndex={activeIndex}\n        stacked={stacked}\n        onSliceActivate={onSliceActivate}\n        testId={testId}\n      />\n      {legend ? (\n        <RingLegend\n          data={rows}\n          colors={colors}\n          values={legendValues}\n          format={valueFormat ?? valueLabel}\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport type DonutChartProps = PieChartProps;\n\nexport const DonutChart = ({ innerRadius = 0.62, ...props }: PieChartProps) => (\n  <PieChart {...props} innerRadius={innerRadius} />\n);\n\nconst circleAxes = (shape: \"polygon\" | \"circle\", axes: number): number =>\n  shape === \"circle\" ? 4 : axes;\n\nexport interface RadarChartProps extends ChartProps {\n  readonly levels?: number;\n  readonly dots?: boolean;\n  readonly fillOpacity?: number;\n  readonly webs?: boolean;\n  readonly gridShape?: \"polygon\" | \"circle\";\n  readonly gridFill?: boolean;\n  readonly fill?: boolean;\n  readonly radius?: number;\n  readonly gridAt?: ReadonlyArray<number>;\n  readonly dotIcons?: ReadonlyArray<IconName>;\n  readonly outline?: boolean;\n  readonly axisLabelFormat?: (label: string, index: number) => string;\n  readonly pointTooltip?: boolean;\n}\n\nconst RADAR_LABEL_GAP = 14;\n\nexport const RadarChart = ({\n  levels = 4,\n  dots = false,\n  outline = false,\n  fillOpacity = 0.6,\n  webs = true,\n  gridShape = \"polygon\",\n  gridFill = false,\n  fill = true,\n  radius,\n  gridAt,\n  dotIcons,\n  axisLabelFormat,\n  pointTooltip = false,\n  ...props\n}: RadarChartProps) => {\n  const rows = seriesFor(props.series, props.config, props.data);\n  const labels = labelsFor(props.labels, props.data);\n  const axes = Math.max(3, labels.length);\n  const max = chartScale(rows, axes);\n  const range = { min: 0, max };\n  const theme = useTheme();\n  return (\n    <ChartFrame\n      {...props}\n      series={rows}\n      labels={labels}\n      axis={false}\n      grid={false}\n      xLabels={false}\n      range={range}\n      paint={({ series, box, reveal }) => {\n        const shown = scaleSeries(series, revealOf(reveal));\n        return Arr.flatMap(shown, (one, seriesIndex) => {\n          const points = radarPoints(one.values, axes, box, max, radius);\n          const shape = polygonPath(points);\n          return Arr.flatten([\n            webs\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"web\",\n                    decorative: true,\n                    opacity: 0.6,\n                    marks: Arr.map(\n                      [\n                        ...(gridAt === undefined\n                          ? gridShape === \"circle\"\n                            ? radarCircleGrid(levels, box, radius)\n                            : radarGrid(levels, axes, box, max, radius)\n                          : gridShape === \"circle\"\n                            ? radarCircleGridAt(gridAt, box, radius)\n                            : radarGridAt(gridAt, axes, box, max, radius)),\n                        ...radarSpokes(\n                          circleAxes(gridShape, axes),\n                          box,\n                          max,\n                          radius,\n                        ),\n                      ],\n                      (d): ChartMark => ({\n                        kind: \"path\",\n                        d,\n                        stroke: {\n                          ...lineStroke(1),\n                          color: themeColors(theme).border ?? \"#000000\",\n                        },\n                      }),\n                    ),\n                  } satisfies PaintLayer,\n                  ...(gridFill\n                    ? [\n                        {\n                          key: one.key,\n                          series: one,\n                          seriesIndex,\n                          kind: \"web-fill\",\n                          decorative: true,\n                          opacity: 0.06,\n                          marks: Arr.map(\n                            radarGrid(levels, axes, box, max, radius),\n                            (d): ChartMark => ({ kind: \"path\", d }),\n                          ),\n                        } satisfies PaintLayer,\n                      ]\n                    : []),\n                ]\n              : [],\n            [\n              ...(fill\n                ? [\n                    {\n                      key: one.key,\n                      series: one,\n                      seriesIndex,\n                      opacity: fillOpacity,\n                      marks: Option.match(shape, {\n                        onNone: () => [],\n                        onSome: (d): ReadonlyArray<ChartMark> => [\n                          { kind: \"path\" as const, d },\n                        ],\n                      }),\n                    } satisfies PaintLayer,\n                  ]\n                : []),\n              ...(outline || !fill\n                ? [\n                    {\n                      key: one.key,\n                      series: one,\n                      seriesIndex,\n                      kind: \"line\",\n                      decorative: true,\n                      marks: Option.match(shape, {\n                        onNone: () => [],\n                        onSome: (d): ReadonlyArray<ChartMark> => [\n                          { kind: \"path\", d, stroke: lineStroke(2) },\n                        ],\n                      }),\n                    } satisfies PaintLayer,\n                  ]\n                : []),\n            ],\n            dots\n              ? [\n                  {\n                    key: one.key,\n                    series: one,\n                    seriesIndex,\n                    kind: \"dots\",\n                    decorative: true,\n                    marks: pointMarks(points, 3),\n                  } satisfies PaintLayer,\n                ]\n              : [],\n          ]);\n        });\n      }}\n      overlay={({ box, series, palette, origin, reveal }) => (\n        <>\n          {Arr.map(labels, (label, index) => {\n            const angle = -Math.PI / 2 + (index / axes) * Math.PI * 2;\n            const reach =\n              (radius === undefined\n                ? Math.min(box.width, box.height) / 2 - 4\n                : radius) + RADAR_LABEL_GAP;\n            const x = box.width / 2 + reach * Math.cos(angle);\n            const y = box.height / 2 + reach * Math.sin(angle);\n            return (\n              <text\n                key={`axis-${index}`}\n                style={{\n                  ...textStyle(theme, \"caption\"),\n                  position: \"absolute\",\n                  left: x - 40,\n                  top: y - 8,\n                  width: 80,\n                  textAlign: \"center\",\n                  color: themeColors(theme).mutedForeground,\n                }}\n              >\n                {axisLabelFormat === undefined\n                  ? label\n                  : axisLabelFormat(label, index)}\n              </text>\n            );\n          })}\n          {pointTooltip && revealOf(reveal) >= 1 ? (\n            <PointTooltip\n              zones={Arr.flatMap(series, (one, seriesIndex) =>\n                Arr.map(\n                  radarPoints(one.values, axes, box, max, radius),\n                  (point, index) => ({\n                    key: one.key,\n                    x: point.x,\n                    y: point.y,\n                    category: labels[index] ?? \"\",\n                    label: seriesName(one.label, one.key),\n                    value: (props.valueFormat ?? valueLabel)(\n                      one.values[index] ?? 0,\n                    ),\n                    colour: seriesColourOf(palette, one, seriesIndex),\n                  }),\n                ),\n              )}\n              box={box}\n              origin={origin}\n              testId={props.testId}\n            />\n          ) : undefined}\n          {dotIcons === undefined || revealOf(reveal) < 1\n            ? undefined\n            : Arr.flatMap(series, (one, seriesIndex) =>\n                Arr.map(\n                  radarPoints(one.values, axes, box, max, radius),\n                  (point, index) => {\n                    const name = dotIcons[index];\n                    return name === undefined || !isIconName(name) ? null : (\n                      <div\n                        key={`${one.key}-icon-${index}`}\n                        style={{\n                          position: \"absolute\",\n                          left: point.x - 7,\n                          top: point.y - 7,\n                        }}\n                      >\n                        <Icon\n                          color={seriesColourOf(palette, one, seriesIndex)}\n                          name={name}\n                          size={14}\n                          testId={\n                            props.testId === undefined\n                              ? undefined\n                              : `${props.testId}-dot-${one.key}-${index}`\n                          }\n                        />\n                      </div>\n                    );\n                  },\n                ),\n              )}\n        </>\n      )}\n      legendNode={() => (\n        <LegendFor\n          series={rows}\n          colors={props.colors}\n          config={props.config}\n          shape={props.legendShape}\n          testId={\n            props.testId === undefined ? undefined : `${props.testId}-legend`\n          }\n        />\n      )}\n    />\n  );\n};\n\nexport interface RadialChartProps extends Omit<Props, \"children\"> {\n  readonly data: ReadonlyArray<ChartDatum>;\n  readonly size?: number;\n  readonly max?: number;\n  readonly thickness?: number;\n  readonly gap?: number;\n  readonly track?: boolean;\n  readonly grid?: boolean;\n  readonly rounded?: boolean;\n  readonly showLabels?: boolean;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly legend?: boolean;\n  readonly colors?: ReadonlyArray<string>;\n  readonly valueFormat?: (value: number) => string;\n  readonly centerLabel?: string;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const RadialChart = ({\n  data,\n  size = DEFAULT_RING_SIZE,\n  max,\n  thickness = 10,\n  gap = 4,\n  track = false,\n  grid = false,\n  rounded = false,\n  showLabels = false,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  legend = true,\n  colors,\n  valueFormat,\n  centerLabel,\n  className,\n  testId,\n  style,\n  ...rest\n}: RadialChartProps) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const rows = dataOf(data);\n  const animated = useAnimatedValues(\n    Arr.map(rows, (one) => [one.value]),\n    animate,\n    animationDuration,\n  );\n  const reveal = useMountReveal(animate, animationDuration);\n  const shown = Arr.map(rows, (one, index) => ({\n    ...one,\n    value: scaleValue(animated[index]?.[0] ?? one.value, reveal),\n  }));\n  const box = chartBox(size, size, {\n    width: DEFAULT_RING_SIZE,\n    height: DEFAULT_RING_SIZE,\n  });\n  const ceiling = Option.match(Option.fromNullishOr(max), {\n    onNone: () =>\n      Arr.reduce(rows, 0, (most, one) =>\n        Math.max(most, Math.max(0, one.value)),\n      ) || 1,\n    onSome: (value) => (Number.isFinite(value) && value > 0 ? value : 1),\n  });\n  const arcs = Arr.map(\n    radialArcs(\n      Arr.map(shown, (one) => one.value),\n      box,\n      ceiling,\n      thickness,\n      gap,\n    ),\n    (marks, index) => {\n      if (!rounded) return marks;\n      const value = shown[index]?.value ?? 0;\n      const span =\n        ceiling <= 0 ? 0 : (Math.max(0, value) / ceiling) * Math.PI * 2;\n      if (span <= 0 || span >= Math.PI * 2 - 1e-9) return marks;\n      const outer =\n        Math.min(box.width, box.height) / 2 - 1 - index * (thickness + gap);\n      const mid = outer - thickness / 2;\n      const cap = (angle: number): ChartMark => ({\n        kind: \"circle\",\n        cx: box.width / 2 + mid * Math.cos(angle),\n        cy: box.height / 2 + mid * Math.sin(angle),\n        r: thickness / 2,\n      });\n      return [...marks, cap(-Math.PI / 2), cap(-Math.PI / 2 + span)];\n    },\n  );\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-plot`}\n        style={{\n          ...recipeStyle(CHART_PLOT, theme),\n          width: box.width,\n          height: box.height,\n        }}\n      >\n        {grid ? (\n          <ChartMask\n            chartTestId={testId}\n            layer={{\n              key: \"grid\",\n              series: { key: \"grid\", label: \"grid\", values: [] },\n              seriesIndex: 0,\n              decorative: true,\n              opacity: 0.6,\n              marks: Arr.makeBy(4, (index) => ({\n                kind: \"path\" as const,\n                d: fullRing(\n                  box.width / 2,\n                  box.height / 2,\n                  ((Math.min(box.width, box.height) / 2 - 1) * (index + 1)) / 4,\n                  ((Math.min(box.width, box.height) / 2 - 1) * (index + 1)) /\n                    4 -\n                    1,\n                ),\n                stroke: lineStroke(1),\n              })),\n            }}\n            label=\"\"\n            description=\"\"\n            colour={themeColors(theme).border ?? \"#000000\"}\n            box={box}\n          />\n        ) : undefined}\n        {track\n          ? Arr.map(rows, (one, index) => (\n              <ChartMask\n                key={`${one.key}-track`}\n                chartTestId={testId}\n                layer={{\n                  key: one.key,\n                  series: { key: one.key, label: one.label, values: [] },\n                  seriesIndex: index,\n                  kind: \"track\",\n                  decorative: true,\n                  opacity: 0.18,\n                  marks: Arr.makeBy(1, () => {\n                    const outer =\n                      Math.min(box.width, box.height) / 2 -\n                      1 -\n                      index * (thickness + gap);\n                    return {\n                      kind: \"path\" as const,\n                      d: fullRing(\n                        box.width / 2,\n                        box.height / 2,\n                        Math.max(0, outer),\n                        Math.max(0, outer - thickness),\n                      ),\n                    };\n                  }),\n                }}\n                label=\"\"\n                description=\"\"\n                colour={themeColors(theme).muted ?? \"#000000\"}\n                box={box}\n              />\n            ))\n          : undefined}\n        {Arr.map(rows, (one, index) => (\n          <ChartMask\n            key={one.key}\n            chartTestId={testId}\n            layer={{\n              key: one.key,\n              series: { key: one.key, label: one.label, values: [one.value] },\n              seriesIndex: index,\n              marks: arcs[index] ?? [],\n            }}\n            label={seriesName(one.label, one.key)}\n            description={seriesDescription([one.value])}\n            colour={seriesColourOf(\n              palette,\n              { key: one.key, label: one.label, values: [] },\n              index,\n            )}\n            box={box}\n          />\n        ))}\n        {showLabels && reveal >= 1\n          ? Arr.map(rows, (one, index) => {\n              const span =\n                ceiling <= 0\n                  ? 0\n                  : (Math.max(0, one.value) / ceiling) * Math.PI * 2;\n              if (span <= 0) return null;\n              const outer =\n                Math.min(box.width, box.height) / 2 -\n                1 -\n                index * (thickness + gap);\n              const mid = outer - thickness / 2;\n              const angle = -Math.PI / 2 + span;\n              return (\n                <text\n                  key={`${one.key}-value`}\n                  testId={\n                    testId === undefined\n                      ? undefined\n                      : `${testId}-value-${one.key}`\n                  }\n                  style={{\n                    ...textStyle(theme, \"caption\"),\n                    position: \"absolute\",\n                    left: box.width / 2 + mid * Math.cos(angle) + 6,\n                    top: box.height / 2 + mid * Math.sin(angle) - 8,\n                    width: 48,\n                    color: themeColors(theme).mutedForeground,\n                  }}\n                >\n                  {(valueFormat ?? valueLabel)(one.value)}\n                </text>\n              );\n            })\n          : undefined}\n        {centerLabel === undefined ||\n        centerLabel.trim().length === 0 ? undefined : (\n          <text\n            style={{\n              ...textStyle(theme, \"label\"),\n              position: \"absolute\",\n              left: 0,\n              top: box.height / 2 - 10,\n              width: box.width,\n              textAlign: \"center\",\n              color: themeColors(theme).foreground,\n            }}\n          >\n            {centerLabel}\n          </text>\n        )}\n      </div>\n      {legend ? (\n        <RingLegend\n          data={rows}\n          colors={colors}\n          values\n          format={valueFormat ?? valueLabel}\n          testId={testId === undefined ? undefined : `${testId}-legend`}\n        />\n      ) : undefined}\n    </div>\n  );\n};\n\nexport interface SparklineProps extends Omit<Props, \"children\"> {\n  readonly values: ReadonlyArray<number>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly color?: string;\n  readonly curve?: Curve;\n  /** The line is filled down to the floor. Defaults to `false`. */\n  readonly filled?: boolean;\n  /** A data change tweens the line's values. Defaults to `false`. */\n  readonly animate?: boolean;\n  /** How long a data change takes, in milliseconds. Defaults to `320`. */\n  readonly animationDuration?: number;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const Sparkline = ({\n  values,\n  width = DEFAULT_SPARKLINE_WIDTH,\n  height = DEFAULT_SPARKLINE_HEIGHT,\n  color,\n  curve = \"smooth\",\n  filled = false,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  className,\n  testId,\n  style,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: SparklineProps) => {\n  const theme = useTheme();\n  const box = chartBox(width, height, {\n    width: DEFAULT_SPARKLINE_WIDTH,\n    height: DEFAULT_SPARKLINE_HEIGHT,\n  });\n  const points = Arr.filter(listOf(values), Number.isFinite);\n  const tweened = useAnimatedValues([points], animate, animationDuration);\n  const shownPoints = tweened[0] ?? points;\n  const reveal = useMountReveal(animate, animationDuration);\n  const clip =\n    reveal >= 1\n      ? undefined\n      : { x: 0, y: 0, width: box.width * reveal, height: box.height };\n  const series: ChartSeries = {\n    key: \"sparkline\",\n    label: \"sparkline\",\n    values: shownPoints,\n  };\n  const range = chartRange([series]);\n  const path = curvePath(\n    seriesPoints(shownPoints, box, range, shownPoints.length),\n    curve,\n  );\n  const area = filled\n    ? areaPath(\n        seriesPoints(shownPoints, box, range, shownPoints.length),\n        box,\n        range,\n        curve,\n      )\n    : Option.none();\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      <div\n        style={{\n          ...recipeStyle(CHART_PLOT, theme),\n          width: box.width,\n          height: box.height,\n        }}\n      >\n        {Option.isNone(area) ? undefined : (\n          <ChartMask\n            chartTestId={testId}\n            layer={{\n              key: \"sparkline\",\n              series,\n              seriesIndex: 0,\n              kind: \"area\",\n              decorative: true,\n              opacity: 0.2,\n              ...(clip === undefined ? {} : { clip }),\n              marks: [{ kind: \"path\", d: Option.getOrElse(area, () => \"\") }],\n            }}\n            label=\"\"\n            description=\"\"\n            colour={chosenColour(\n              color,\n              paletteOf(theme, undefined)[0] ?? \"#000000\",\n            )}\n            box={box}\n          />\n        )}\n        <ChartMask\n          chartTestId={testId}\n          layer={{\n            key: \"sparkline\",\n            series,\n            seriesIndex: 0,\n            ...(clip === undefined ? {} : { clip }),\n            marks: Option.match(path, {\n              onNone: () => [],\n              onSome: (d): ReadonlyArray<ChartMark> => [\n                { kind: \"path\", d, stroke: lineStroke(1.5) },\n              ],\n            }),\n          }}\n          label={spokenText(\n            Option.fromNullishOr(ariaLabel),\n            DEFAULT_SPARKLINE_LABEL,\n          )}\n          description={seriesDescription(shownPoints)}\n          colour={chosenColour(\n            color,\n            paletteOf(theme, undefined)[0] ?? \"#000000\",\n          )}\n          box={box}\n        />\n      </div>\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/config.ts",
      "target": "@ui/gpuixui/data/chart/config.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport type { IconName } from \"../../icons/index.js\";\n\nexport type ChartMode = \"light\" | \"dark\";\n\nexport interface ChartConfigEntry {\n  readonly label?: string;\n\n  readonly icon?: IconName;\n\n  readonly color?: string;\n\n  readonly theme?: Partial<Record<ChartMode, string>>;\n}\n\nexport type ChartConfig = Record<string, ChartConfigEntry | undefined>;\n\nexport const configColour = (\n  entry: ChartConfigEntry | undefined,\n  mode: ChartMode,\n): Option.Option<string> => {\n  if (entry === undefined) return Option.none();\n  const themed = Option.fromNullishOr(entry.theme?.[mode]);\n  const chosen = Option.match(themed, {\n    onNone: () => Option.fromNullishOr(entry.color),\n    onSome: (colour) => Option.some(colour),\n  });\n  return Option.filter(chosen, (colour) => colour.trim().length > 0);\n};\n\nexport const configLabel = (\n  key: string,\n  entry: ChartConfigEntry | undefined,\n): string => {\n  const label = entry?.label;\n  return Option.match(Option.fromNullishOr(label), {\n    onNone: () => key,\n    onSome: (value) => (value.trim().length === 0 ? key : value),\n  });\n};\n\nexport const configIcon = (\n  entry: ChartConfigEntry | undefined,\n): Option.Option<IconName> => Option.fromNullishOr(entry?.icon);\n\nexport const configEntries = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<readonly [string, ChartConfigEntry]> =>\n  Option.match(Option.fromNullishOr(config), {\n    onNone: () => [],\n    onSome: (table) =>\n      Arr.map(Object.keys(table), (key) => {\n        const entry = table[key];\n\n        return [key, entry ?? {}] as const;\n      }),\n  });\n\nexport const configKeys = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<string> => Arr.map(configEntries(config), ([key]) => key);\n\nexport type ChartRow = Record<string, unknown>;\n\nexport const datumNumber = (\n  datum: ChartRow | undefined,\n  field: string,\n): Option.Option<number> => {\n  if (datum === undefined) return Option.none();\n  const value = datum[field];\n  return typeof value === \"number\" && Number.isFinite(value)\n    ? Option.some(value)\n    : Option.none<number>();\n};\n\nexport const seriesFromConfig = (\n  config: ChartConfig | undefined,\n  data: ReadonlyArray<ChartRow>,\n): ReadonlyArray<{\n  readonly key: string;\n  readonly label: string;\n  readonly values: ReadonlyArray<number>;\n}> =>\n  Arr.map(configEntries(config), ([key, entry]) => ({\n    key,\n    label: configLabel(key, entry),\n    values: Arr.map(data, (datum) =>\n      Option.getOrElse(datumNumber(datum, key), () => 0),\n    ),\n  }));\n\nexport const configSeriesColour = (\n  config: ChartConfig | undefined,\n  key: string,\n  mode: ChartMode,\n): Option.Option<string> =>\n  Option.flatMap(Option.fromNullishOr(config), (table) =>\n    configColour(table[key], mode),\n  );\n\nexport const categoriesFromData = (\n  data: ReadonlyArray<ChartRow>,\n): ReadonlyArray<string> => {\n  const first = data[0];\n  if (first === undefined) return [];\n  const field = Arr.findFirst(Object.keys(first), (key) =>\n    Arr.some(data, (datum) => typeof datum[key] === \"string\"),\n  );\n  return Option.match(field, {\n    onNone: () => [],\n    onSome: (name) =>\n      Arr.map(data, (datum) => {\n        const value = datum[name];\n        return typeof value === \"string\" ? value : \"\";\n      }),\n  });\n};\n\nexport const configColours = (\n  config: ChartConfig | undefined,\n  mode: ChartMode,\n): ReadonlyArray<string> =>\n  Arr.map(\n    Arr.filter(configEntries(config), ([, entry]) =>\n      Option.isSome(configColour(entry, mode)),\n    ),\n    ([, entry]) => Option.getOrElse(configColour(entry, mode), () => \"\"),\n  );\n\nexport const configOf = (config: ChartConfig | undefined): ChartConfig =>\n  Option.getOrElse(Option.fromNullishOr(config), () => ({}));\n\nexport const configKeySet = (\n  config: ChartConfig | undefined,\n): ReadonlyArray<string> => Object.keys(configOf(config));\n\nexport const configEntryOf = (\n  config: ChartConfig | undefined,\n  key: string,\n): Option.Option<ChartConfigEntry> =>\n  Option.fromNullishOr(configOf(config)[key]).pipe(\n    Option.map((entry) => entry ?? {}),\n  );\n"
    },
    {
      "path": "packages/data/src/chart/frame.tsx",
      "target": "@ui/gpuixui/data/chart/frame.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport type { Props } from \"@gpuix/react\";\nimport { Icon, isIconName, type IconName } from \"../../icons/index.js\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { seriesColors, themeModeOf } from \"../../tokens/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useRef, useState, type ReactNode } from \"react\";\nimport {\n  chartBox,\n  chartMarkup,\n  seriesDescription,\n  seriesName,\n  tickLabel,\n  type ChartBox,\n  type ChartMark,\n  type ChartSeries,\n} from \"./logic.js\";\nimport type { MarkClip } from \"./mark.js\";\nimport {\n  chartRange,\n  chartView,\n  chartWindow,\n  rangeTicks,\n  valueLabel,\n  type ChartRange,\n  type ChartView,\n  type Point,\n} from \"./geometry.js\";\nimport {\n  DEFAULT_ANIMATION_DURATION,\n  useAnimatedSeries,\n  useMountReveal,\n} from \"./animation.js\";\nimport { ChartPlot } from \"./plot.js\";\nimport type { ChartRender } from \"./surface.js\";\nimport type { ChartGradient } from \"./svg.js\";\nimport { configSeriesColour, type ChartConfig } from \"./config.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport {\n  CHART_AXIS,\n  CHART_AXIS_LABEL,\n  CHART_BRUSH,\n  CHART_BRUSH_BAND,\n  CHART_BRUSH_HANDLE,\n  CHART_BRUSH_TRACK,\n  CHART_CATEGORIES,\n  CHART_CATEGORY,\n  CHART_CATEGORY_LABEL,\n  CHART_PLOT,\n  CHART_ROOT,\n  CHART_TOOLTIP,\n  CHART_TOOLTIP_HEAD,\n  CHART_TOOLTIP_ROW,\n  CHART_TOOLTIP_VALUE,\n  CHART_TOOLTIP_ZONE,\n  CHART_VALUE_LABEL,\n} from \"./recipe.js\";\n\nexport const DEFAULT_CHART_WIDTH = 320;\n\nexport const DEFAULT_CHART_HEIGHT = 160;\n\nexport const CHART_AXIS_WIDTH = 36;\n\nexport interface PaintLayer {\n  readonly key: string;\n  readonly series: ChartSeries;\n  readonly seriesIndex: number;\n  readonly marks: ReadonlyArray<ChartMark>;\n  readonly opacity?: number;\n  readonly kind?: string;\n  readonly decorative?: boolean;\n  readonly clip?: MarkClip;\n}\n\nexport interface PointZone {\n  readonly key: string;\n  readonly x: number;\n  readonly y: number;\n  readonly category: string;\n  readonly label: string;\n  readonly value: string;\n  readonly colour: string;\n}\n\nexport interface ValueLabel {\n  readonly key: string;\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly text: string;\n}\n\nexport type LegendSide = \"top\" | \"bottom\";\n\nexport type LegendShape = \"square\" | \"dot\" | \"line\";\n\nexport type TooltipIndicator = \"dot\" | \"line\" | \"none\" | \"icon\";\n\nexport interface PaintContext {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly box: ChartBox;\n  readonly range: ChartRange;\n  readonly palette: ReadonlyArray<string>;\n  readonly origin: Option.Option<Point>;\n  readonly view?: ChartView;\n  readonly hovered?: Option.Option<number>;\n  readonly reveal?: number;\n}\n\nexport interface ChartFrameProps extends Omit<Props, \"children\"> {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly width?: number;\n  readonly height?: number;\n  readonly legend?: boolean | LegendSide;\n  readonly legendShape?: LegendShape;\n  readonly axis?: boolean;\n  readonly grid?: boolean;\n  readonly xLabels?: boolean;\n  readonly ticks?: number;\n  readonly colors?: ReadonlyArray<string>;\n  readonly tooltip?: boolean;\n  readonly tooltipIndicator?: TooltipIndicator;\n  readonly tooltipIcons?: ReadonlyArray<IconName>;\n  readonly labelFormat?: (label: string) => string;\n  readonly activeBand?: boolean;\n  readonly crosshair?: boolean;\n  readonly domain?: readonly [number, number];\n  readonly view?: readonly [number, number];\n  readonly zoom?: boolean;\n  readonly onViewChange?: (view: readonly [number, number]) => void;\n  readonly brush?: boolean;\n  readonly onDomainChange?: (domain: readonly [number, number]) => void;\n  readonly domainLabels?: ReadonlyArray<string>;\n  readonly animate?: boolean;\n  readonly animationDuration?: number;\n  readonly valueFormat?: (value: number) => string;\n  readonly valueLabels?: (context: PaintContext) => ReadonlyArray<ValueLabel>;\n  readonly range?: ChartRange;\n  readonly paint: (context: PaintContext) => ReadonlyArray<PaintLayer>;\n  readonly gradients?: (context: PaintContext) => ReadonlyArray<ChartGradient>;\n  readonly render?: ChartRender;\n  readonly config?: ChartConfig;\n  readonly legendNode?: (side: LegendSide) => ReactNode;\n  readonly overlay?: (context: PaintContext) => ReactNode;\n  readonly className?: string;\n  readonly testId?: string;\n}\n\nexport const ChartMask = ({\n  chartTestId,\n  layer,\n  label,\n  description,\n  colour,\n  box,\n}: {\n  readonly chartTestId: string | undefined;\n  readonly layer: PaintLayer;\n  readonly label: string;\n  readonly description: string;\n  readonly colour: string;\n  readonly box: ChartBox;\n}) => {\n  const decorative = layer.decorative === true;\n  return (\n    <svg\n      source={chartMarkup(layer.marks, box, layer.clip)}\n      role={decorative ? undefined : \"img\"}\n      aria-label={decorative ? undefined : label}\n      aria-description={decorative ? undefined : description}\n      testId={\n        chartTestId === undefined\n          ? undefined\n          : layer.kind === undefined\n            ? `${chartTestId}-layer-${layer.key}`\n            : `${chartTestId}-layer-${layer.key}-${layer.kind}`\n      }\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: box.width,\n        height: box.height,\n        color: colour,\n        opacity: layer.opacity,\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n};\n\nconst THIN_FROM = 12;\n\nconst CATEGORY_CHAR_WIDTH = 7;\n\nexport const labelStride = (\n  labels: ReadonlyArray<string>,\n  slot: number,\n): number => {\n  if (labels.length <= THIN_FROM) return 1;\n  const longest = Arr.reduce(labels, 0, (most, one) =>\n    Math.max(most, one.length),\n  );\n  return Math.max(\n    1,\n    Math.ceil(((longest + 2) * CATEGORY_CHAR_WIDTH) / Math.max(1, slot)),\n  );\n};\n\nconst thinned = (\n  labels: ReadonlyArray<string>,\n  stride: number,\n): ReadonlyArray<{ readonly label: string; readonly index: number }> =>\n  Arr.filter(\n    Arr.map(labels, (label, index) => ({ label, index })),\n    ({ index }) => index % stride === 0,\n  );\n\nexport const paletteOf = (\n  theme: ReturnType<typeof useTheme>,\n  colors: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> => {\n  const chosen = Arr.filter(colors ?? [], (colour) => colour.trim().length > 0);\n  return chosen.length > 0 ? chosen : seriesColors(theme);\n};\n\nexport const seriesColourOf = (\n  palette: ReadonlyArray<string>,\n  series: ChartSeries,\n  index: number,\n): string =>\n  Option.match(\n    Option.filter(\n      Option.fromNullishOr(series.color),\n      (colour) => colour.trim().length > 0,\n    ),\n    {\n      onNone: () => palette[index % Math.max(1, palette.length)] ?? \"#000000\",\n      onSome: (colour) => colour,\n    },\n  );\n\nconst tooltipIcon = (\n  icons: ReadonlyArray<IconName> | undefined,\n  index: number,\n): IconName => {\n  const name = icons?.[index];\n  return name !== undefined && isIconName(name) ? name : \"circle\";\n};\n\nconst pointerIndex = (\n  windowX: number | undefined,\n  origin: Option.Option<Point>,\n  box: ChartBox,\n  count: number,\n): number =>\n  Option.match(origin, {\n    onNone: () => 0,\n    onSome: (base) => {\n      const x = windowX ?? base.x;\n      return Math.max(\n        0,\n        Math.min(count, ((x - base.x) / Math.max(1, box.width)) * count),\n      );\n    },\n  });\n\nconst slotsOf = (\n  series: ReadonlyArray<ChartSeries>,\n  labels: ReadonlyArray<string>,\n): number =>\n  labels.length > 0\n    ? labels.length\n    : Math.max(\n        1,\n        Arr.reduce(series, 0, (most, one) => Math.max(most, one.values.length)),\n      );\n\nexport const ChartFrame = ({\n  series,\n  labels,\n  width = DEFAULT_CHART_WIDTH,\n  height = DEFAULT_CHART_HEIGHT,\n  legend,\n  axis = true,\n  grid = true,\n  xLabels = true,\n  ticks = 4,\n  colors,\n  tooltip = true,\n  tooltipIndicator = \"dot\",\n  tooltipIcons,\n  labelFormat,\n  activeBand = false,\n  crosshair = false,\n  domain,\n  view,\n  zoom = false,\n  onViewChange,\n  brush = false,\n  onDomainChange,\n  domainLabels,\n  animate = true,\n  animationDuration = DEFAULT_ANIMATION_DURATION,\n  valueFormat,\n  valueLabels,\n  range,\n  paint,\n  gradients,\n  render = \"document\",\n  config,\n  legendNode,\n  overlay,\n  className,\n  testId,\n  style,\n  ...rest\n}: ChartFrameProps) => {\n  const theme = useTheme();\n  const palette = paletteOf(theme, colors);\n  const mode = themeModeOf(theme);\n  const colourOf = (series: ChartSeries, index: number): string =>\n    Option.getOrElse(configSeriesColour(config, series.key, mode), () =>\n      seriesColourOf(palette, series, index),\n    );\n  const box = chartBox(width, height, {\n    width: DEFAULT_CHART_WIDTH,\n    height: DEFAULT_CHART_HEIGHT,\n  });\n  const targetRows = Arr.map(series, (one) => ({\n    ...one,\n    values: Arr.take(one.values, Math.max(1, labels.length)),\n  }));\n  const rows = useAnimatedSeries(targetRows, animate, animationDuration);\n  const reveal = useMountReveal(animate, animationDuration);\n  const categories = labels;\n  const shownView = Option.getOrUndefined(chartView(categories.length, view));\n  const labelView = shownView ?? {\n    start: 0,\n    end: Math.max(1, categories.length),\n  };\n  const measured = range ?? chartRange(targetRows, categories.length);\n  const tickValues = rangeTicks(measured, ticks);\n  const tickStep =\n    tickValues.length > 1\n      ? (measured.max - measured.min) / (tickValues.length - 1)\n      : measured.max;\n  const showLegend = legend === undefined ? rows.length > 1 : legend !== false;\n  const side: LegendSide = legend === \"top\" ? \"top\" : \"bottom\";\n  const slot = box.width / slotsOf(rows, categories);\n  const stride = labelStride(categories, slot);\n  const [hovered, setHovered] = useState<Option.Option<number>>(Option.none());\n  const [pointer, setPointer] = useState<Option.Option<Point>>(Option.none());\n  const selectFrom = useRef<Option.Option<number>>(Option.none());\n  const [selectTo, setSelectTo] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n  const [origin, setOrigin] = useState<Option.Option<Point>>(Option.none());\n  const format = valueFormat ?? valueLabel;\n  const context: PaintContext = {\n    series: rows,\n    labels: categories,\n    box,\n    range: measured,\n    palette,\n    origin,\n    view: shownView,\n    hovered,\n    reveal,\n  };\n  const layers = paint(context);\n  const legendBody =\n    legendNode === undefined || !showLegend ? undefined : legendNode(side);\n  return (\n    <div\n      {...rest}\n      role=\"group\"\n      testId={testId}\n      style={mergeStyles(recipeStyle(cn(CHART_ROOT, className), theme), style)}\n    >\n      {side === \"top\" ? legendBody : undefined}\n      <div style={{ display: \"flex\", flexDirection: \"row\" }}>\n        {axis ? (\n          <div\n            testId={testId === undefined ? undefined : `${testId}-axis`}\n            style={{\n              ...recipeStyle(CHART_AXIS, theme),\n              width: CHART_AXIS_WIDTH,\n              height: box.height,\n            }}\n          >\n            {Arr.map(Arr.reverse(tickValues), (value) => (\n              <text\n                key={`tick-${value}`}\n                testId={\n                  testId === undefined ? undefined : `${testId}-tick-${value}`\n                }\n                style={{\n                  ...recipeStyle(CHART_AXIS_LABEL, theme),\n                  textAlign: \"right\",\n                }}\n              >\n                {tickLabel(value, tickStep)}\n              </text>\n            ))}\n          </div>\n        ) : undefined}\n        <div\n          testId={testId === undefined ? undefined : `${testId}-plot`}\n          onLayout={(event) =>\n            setOrigin(\n              Option.all([\n                Option.fromNullishOr(event.x),\n                Option.fromNullishOr(event.y),\n              ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n            )\n          }\n          style={{\n            ...recipeStyle(CHART_PLOT, theme),\n            width: box.width,\n            height: box.height,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              width: box.width,\n              height: box.height,\n              overflow: \"hidden\",\n            }}\n          >\n            {activeBand\n              ? Option.match(hovered, {\n                  onNone: () => null,\n                  onSome: (index) => (\n                    <div\n                      style={{\n                        position: \"absolute\",\n                        left: slot * index,\n                        top: 0,\n                        width: slot,\n                        height: box.height,\n                        backgroundColor: themeColors(theme).muted,\n                        opacity: 0.5,\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  ),\n                })\n              : undefined}\n            {crosshair\n              ? Option.match(hovered, {\n                  onNone: () => null,\n                  onSome: (index) => (\n                    <div\n                      testId={\n                        testId === undefined ? undefined : `${testId}-crosshair`\n                      }\n                      style={{\n                        position: \"absolute\",\n                        left: slot * index + slot / 2,\n                        top: 0,\n                        width: 1,\n                        height: box.height,\n                        backgroundColor:\n                          themeColors(theme).borderStrong ??\n                          themeColors(theme).border,\n                        opacity: 0.8,\n                        pointerEvents: \"none\",\n                      }}\n                    />\n                  ),\n                })\n              : undefined}\n            <ChartPlot\n              box={box}\n              layers={layers}\n              colours={Arr.map(layers, (layer) =>\n                colourOf(layer.series, layer.seriesIndex),\n              )}\n              gridColour={themeColors(theme).border ?? \"#000000\"}\n              grid={\n                grid\n                  ? {\n                      key: \"grid\",\n                      kind: \"grid\",\n                      series: { key: \"grid\", label: \"grid\", values: [] },\n                      seriesIndex: 0,\n                      decorative: true,\n                      opacity: 0.55,\n                      marks: Arr.map(tickValues, (value) => ({\n                        kind: \"rect\" as const,\n                        x: 0,\n                        y:\n                          box.height -\n                          ((value - measured.min) /\n                            Math.max(1e-9, measured.max - measured.min)) *\n                            box.height,\n                        width: box.width,\n                        height: 1,\n                      })),\n                    }\n                  : undefined\n              }\n              render={render}\n              gradients={gradients === undefined ? [] : gradients(context)}\n              testId={testId}\n              labels={Arr.map(layers, (layer) =>\n                seriesName(layer.series.label, layer.series.key),\n              )}\n              descriptions={Arr.map(layers, (layer) =>\n                seriesDescription(layer.series.values, categories),\n              )}\n            />\n          </div>\n          {valueLabels === undefined ? undefined : (\n            <ValueLabels\n              labels={valueLabels(context)}\n              theme={theme}\n              testId={testId}\n            />\n          )}\n          {zoom ? (\n            <div\n              testId={testId === undefined ? undefined : `${testId}-gesture`}\n              onMouseDown={(event) => {\n                const at = pointerIndex(\n                  event.x,\n                  origin,\n                  box,\n                  categories.length,\n                );\n                selectFrom.current = Option.some(at);\n                setSelectTo(Option.some(at));\n              }}\n              onMouseMove={(event) => {\n                const index = pointerIndex(\n                  event.x,\n                  origin,\n                  box,\n                  categories.length,\n                );\n                setHovered(\n                  Option.some(\n                    Math.max(\n                      0,\n                      Math.min(\n                        Math.max(0, categories.length - 1),\n                        Math.floor(index),\n                      ),\n                    ),\n                  ),\n                );\n                setPointer(\n                  Option.all([\n                    Option.fromNullishOr(event.x),\n                    Option.fromNullishOr(event.y),\n                  ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n                );\n                if (Option.isSome(selectFrom.current)) {\n                  setSelectTo(Option.some(index));\n                }\n              }}\n              onMouseUp={() => {\n                Option.match(selectFrom.current, {\n                  onNone: () => {},\n                  onSome: (from) =>\n                    Option.match(selectTo, {\n                      onNone: () => {},\n                      onSome: (to) => {\n                        if (Math.abs(to - from) >= 0.5) {\n                          onViewChange?.([\n                            Math.min(from, to),\n                            Math.max(from, to),\n                          ]);\n                        }\n                      },\n                    }),\n                });\n                selectFrom.current = Option.none();\n                setSelectTo(Option.none());\n              }}\n              onScroll={(event) => {\n                const delta = event.deltaY ?? 0;\n                if (delta === 0) return;\n                const span = labelView.end - labelView.start;\n                const factor = Math.max(\n                  0.25,\n                  Math.min(4, Math.exp(delta * 0.01)),\n                );\n                const centre = labelView.start + span / 2;\n                const next = span * factor;\n                onViewChange?.([\n                  Math.max(\n                    0,\n                    Math.min(categories.length - 1, centre - next / 2),\n                  ),\n                  Math.min(\n                    categories.length,\n                    Math.max(centre - next / 2 + 1, centre + next / 2),\n                  ),\n                ]);\n              }}\n              onMouseLeave={() => {\n                setHovered(Option.none());\n                setPointer(Option.none());\n                selectFrom.current = Option.none();\n                setSelectTo(Option.none());\n              }}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: box.width,\n                height: box.height,\n              }}\n            >\n              {Option.match(selectFrom.current, {\n                onNone: () => null,\n                onSome: (from) =>\n                  Option.match(selectTo, {\n                    onNone: () => null,\n                    onSome: (to) => {\n                      const low = Math.min(from, to);\n                      const high = Math.max(from, to);\n                      const width = box.width / Math.max(1, categories.length);\n                      return (\n                        <div\n                          style={{\n                            position: \"absolute\",\n                            left: low * width,\n                            top: 0,\n                            width: Math.max(1, (high - low) * width),\n                            height: box.height,\n                            backgroundColor: themeColors(theme).accent,\n                            opacity: 0.25,\n                            pointerEvents: \"none\",\n                          }}\n                        />\n                      );\n                    },\n                  }),\n              })}\n            </div>\n          ) : tooltip && rows.length > 0 && categories.length > 0 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: box.width,\n                height: box.height,\n                display: \"flex\",\n                flexDirection: \"row\",\n              }}\n            >\n              {Arr.makeBy(categories.length, (index) => (\n                <div\n                  key={`zone-${index}`}\n                  testId={\n                    testId === undefined ? undefined : `${testId}-zone-${index}`\n                  }\n                  onMouseEnter={() => setHovered(Option.some(index))}\n                  onMouseMove={(event) =>\n                    setPointer(\n                      Option.all([\n                        Option.fromNullishOr(event.x),\n                        Option.fromNullishOr(event.y),\n                      ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n                    )\n                  }\n                  onMouseLeave={() => {\n                    setHovered(Option.none());\n                    setPointer(Option.none());\n                  }}\n                  style={{\n                    ...recipeStyle(CHART_TOOLTIP_ZONE, theme),\n                    width: slot,\n                    height: box.height,\n                  }}\n                />\n              ))}\n            </div>\n          ) : undefined}\n          {tooltip\n            ? Option.match(hovered, {\n                onNone: () => null,\n                onSome: (index) => (\n                  <ChartTooltip\n                    series={targetRows}\n                    labels={categories}\n                    palette={palette}\n                    theme={theme}\n                    index={index}\n                    slot={slot}\n                    box={box}\n                    pointer={Option.flatMap(pointer, (at) =>\n                      Option.map(origin, (base) => ({\n                        x: at.x - base.x,\n                        y: at.y - base.y,\n                      })),\n                    )}\n                    format={format}\n                    indicator={tooltipIndicator}\n                    tooltipIcons={tooltipIcons}\n                    labelFormat={labelFormat}\n                    testId={testId}\n                  />\n                ),\n              })\n            : undefined}\n          {overlay === undefined ? undefined : overlay(context)}\n        </div>\n      </div>\n      {xLabels && view !== undefined ? (\n        <div\n          testId={testId === undefined ? undefined : `${testId}-categories`}\n          style={{\n            position: \"relative\",\n            height: 16,\n            width: box.width,\n            marginLeft: axis ? CHART_AXIS_WIDTH : 0,\n            overflow: \"hidden\",\n          }}\n        >\n          {Arr.map(categories, (label, index) => {\n            const width =\n              box.width / Math.max(1, labelView.end - labelView.start);\n            return (\n              <text\n                key={`category-${index}`}\n                testId={\n                  testId === undefined ? undefined : `${testId}-label-${index}`\n                }\n                style={{\n                  ...recipeStyle(CHART_CATEGORY_LABEL, theme),\n                  position: \"absolute\",\n                  left: (index - labelView.start) * width,\n                  top: 0,\n                  width,\n                  textAlign: \"center\",\n                }}\n              >\n                {label}\n              </text>\n            );\n          })}\n        </div>\n      ) : xLabels ? (\n        <div\n          testId={testId === undefined ? undefined : `${testId}-categories`}\n          style={{\n            ...recipeStyle(CHART_CATEGORIES, theme),\n            paddingLeft: axis ? CHART_AXIS_WIDTH : 0,\n          }}\n        >\n          {Arr.map(thinned(categories, stride), ({ label, index }) => (\n            <div\n              key={`category-${index}`}\n              testId={\n                testId === undefined ? undefined : `${testId}-label-${index}`\n              }\n              style={{\n                ...recipeStyle(CHART_CATEGORY, theme),\n                width: slot * stride,\n              }}\n            >\n              <text\n                style={{\n                  ...recipeStyle(CHART_CATEGORY_LABEL, theme),\n                  width: slot * stride,\n                  textAlign: \"center\",\n                }}\n              >\n                {label}\n              </text>\n            </div>\n          ))}\n        </div>\n      ) : undefined}\n      {zoom ? (\n        <ChartZoom\n          length={categories.length}\n          view={shownView}\n          theme={theme}\n          onViewChange={onViewChange}\n          width={box.width}\n          padding={axis ? CHART_AXIS_WIDTH : 0}\n          testId={testId}\n        />\n      ) : undefined}\n      {brush ? (\n        <ChartBrush\n          labels={domainLabels ?? categories}\n          domain={domain}\n          theme={theme}\n          onDomainChange={onDomainChange}\n          width={box.width}\n          padding={axis ? CHART_AXIS_WIDTH : 0}\n          testId={testId}\n        />\n      ) : undefined}\n      {side === \"bottom\" ? legendBody : undefined}\n    </div>\n  );\n};\n\nexport const PointTooltip = ({\n  zones,\n  box,\n  origin,\n  testId,\n}: {\n  readonly zones: ReadonlyArray<PointZone>;\n  readonly box: ChartBox;\n  readonly origin: Option.Option<Point>;\n  readonly testId: string | undefined;\n}) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const [active, setActive] = useState<Option.Option<number>>(Option.none());\n  const [pointer, setPointer] = useState<Option.Option<Point>>(Option.none());\n  const width = 128;\n  return (\n    <>\n      {Arr.map(zones, (zone, index) => (\n        <div\n          key={`${zone.key}-${index}`}\n          testId={\n            testId === undefined\n              ? undefined\n              : `${testId}-point-${zone.key}-${index}`\n          }\n          onMouseEnter={() => setActive(Option.some(index))}\n          onMouseMove={(event) =>\n            setPointer(\n              Option.all([\n                Option.fromNullishOr(event.x),\n                Option.fromNullishOr(event.y),\n                origin,\n              ]).pipe(\n                Option.map(([x, y, base]) => ({\n                  x: x - base.x,\n                  y: y - base.y,\n                })),\n              ),\n            )\n          }\n          onMouseLeave={() => {\n            setActive(Option.none());\n            setPointer(Option.none());\n          }}\n          style={{\n            position: \"absolute\",\n            left: zone.x - 9,\n            top: zone.y - 9,\n            width: 18,\n            height: 18,\n            borderRadius: 9,\n          }}\n        />\n      ))}\n      {Option.match(active, {\n        onNone: () => null,\n        onSome: (index) => {\n          const zone = zones[index];\n          if (zone === undefined) return null;\n          return (\n            <div\n              testId={testId === undefined ? undefined : `${testId}-point-tip`}\n              style={{\n                ...recipeStyle(CHART_TOOLTIP, theme),\n                position: \"absolute\",\n                left: Option.match(pointer, {\n                  onNone: () => Math.max(0, zone.x + 10),\n                  onSome: (at) =>\n                    Math.max(0, Math.min(box.width - 8, at.x + 12)),\n                }),\n                top: Option.match(pointer, {\n                  onNone: () => Math.max(0, zone.y - 40),\n                  onSome: (at) => Math.max(0, at.y + 12),\n                }),\n                width,\n                pointerEvents: \"none\",\n              }}\n            >\n              <text\n                style={{\n                  ...recipeStyle(CHART_TOOLTIP_HEAD, theme),\n                  ...textStyle(theme, \"caption\"),\n                  color: colors.mutedForeground,\n                }}\n              >\n                {zone.category}\n              </text>\n              <div style={recipeStyle(CHART_TOOLTIP_ROW, theme)}>\n                <div\n                  style={{\n                    width: 6,\n                    height: 6,\n                    borderRadius: 3,\n                    backgroundColor: zone.colour,\n                  }}\n                />\n                <text\n                  style={{\n                    ...textStyle(theme, \"caption\"),\n                    color: colors.mutedForeground,\n                  }}\n                >\n                  {zone.label}\n                </text>\n                <text\n                  style={{\n                    ...recipeStyle(CHART_TOOLTIP_VALUE, theme),\n                    ...textStyle(theme, \"caption\"),\n                    color: colors.foreground,\n                  }}\n                >\n                  {zone.value}\n                </text>\n              </div>\n            </div>\n          );\n        },\n      })}\n    </>\n  );\n};\n\n/** The zoom strip: drag to pan, wheel to zoom, and reset to show everything. */\nconst ChartZoom = ({\n  length,\n  view,\n  theme,\n  onViewChange,\n  width,\n  padding,\n  testId,\n}: {\n  readonly length: number;\n  readonly view: ChartView | undefined;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly onViewChange:\n    | ((view: readonly [number, number]) => void)\n    | undefined;\n  readonly width: number;\n  readonly padding: number;\n  readonly testId: string | undefined;\n}) => {\n  const span = view === undefined ? Math.max(1, length) : view.end - view.start;\n  const start = view === undefined ? 0 : view.start;\n  const [anchor, setAnchor] = useState<\n    Option.Option<{ readonly x: number; readonly start: number }>\n  >(Option.none());\n  const apply = (nextStart: number, nextSpan: number): void => {\n    const spanNow = Math.max(1, Math.min(length, nextSpan));\n    const startNow = Math.max(0, Math.min(length - spanNow, nextStart));\n    onViewChange?.([startNow, startNow + spanNow]);\n  };\n  return (\n    <div style={{ marginLeft: padding, width }}>\n      <div\n        testId={testId === undefined ? undefined : `${testId}-zoom`}\n        onMouseDown={(event) =>\n          setAnchor(\n            Option.map(Option.fromNullishOr(event.x), (x) => ({\n              x,\n              start,\n            })),\n          )\n        }\n        onMouseLeave={() => setAnchor(Option.none())}\n        onMouseMove={(event) => {\n          if (Option.isNone(anchor)) return;\n          Option.match(anchor, {\n            onNone: () => {},\n            onSome: (base) => {\n              const delta = ((event.x ?? base.x) - base.x) / Math.max(1, width);\n              apply(base.start - delta * span, span);\n            },\n          });\n        }}\n        onMouseUp={() => setAnchor(Option.none())}\n        onScroll={(event) => {\n          const delta = event.deltaY ?? 0;\n          if (delta === 0) return;\n          const factor = Math.max(0.25, Math.min(4, Math.exp(delta * 0.01)));\n          const centre = start + span / 2;\n          const nextSpan = span * factor;\n          apply(centre - nextSpan / 2, nextSpan);\n        }}\n        style={{\n          position: \"relative\",\n          width,\n          height: 16,\n          backgroundColor: themeColors(theme).muted,\n          borderRadius: 3,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            top: 0,\n            bottom: 0,\n            left: (start / Math.max(1, length)) * width,\n            width: (span / Math.max(1, length)) * width,\n            backgroundColor: themeColors(theme).accent,\n            opacity: 0.4,\n            pointerEvents: \"none\",\n          }}\n        />\n      </div>\n      <div\n        role=\"button\"\n        testId={testId === undefined ? undefined : `${testId}-zoom-reset`}\n        onClick={() => {\n          onViewChange?.([0, Math.max(1, length)]);\n        }}\n        style={{\n          marginTop: 2,\n          fontSize: 11,\n          color: themeColors(theme).mutedForeground,\n        }}\n      >\n        Reset\n      </div>\n    </div>\n  );\n};\n\nconst ChartBrush = ({\n  labels,\n  domain,\n  theme,\n  onDomainChange,\n  width,\n  padding,\n  testId,\n}: {\n  readonly labels: ReadonlyArray<string>;\n  readonly domain: readonly [number, number] | undefined;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly onDomainChange:\n    | ((domain: readonly [number, number]) => void)\n    | undefined;\n  readonly width: number;\n  readonly padding: number;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  const [origin, setOrigin] = useState<Option.Option<Point>>(Option.none());\n  const drag = useRef<Option.Option<number>>(Option.none());\n  const [edge, setEdge] = useState<Option.Option<number>>(Option.none());\n  const [, redraw] = useState(0);\n  const count = Math.max(1, labels.length);\n  const committed = Option.getOrElse(\n    chartWindow(labels.length, domain),\n    () => ({\n      start: 0,\n      end: Math.max(1, labels.length),\n    }),\n  );\n  const selected = Option.match(drag.current, {\n    onNone: () => committed,\n    onSome: (start) =>\n      Option.match(edge, {\n        onNone: () => ({ start, end: start + 1 }),\n        onSome: (end) => ({\n          start: Math.min(start, end),\n          end: Math.max(start, end) + 1,\n        }),\n      }),\n  });\n  const indexAt = (windowX: number): number =>\n    Option.match(origin, {\n      onNone: () => 0,\n      onSome: (base) => {\n        const ratio = (windowX - base.x) / Math.max(1, width);\n        return Math.max(0, Math.min(count - 1, Math.floor(ratio * count)));\n      },\n    });\n  const report = (start: number, end: number): void => {\n    onDomainChange?.([Math.min(start, end), Math.max(start, end) + 1]);\n  };\n  const left = (selected.start / count) * width;\n  const selectedWidth = Math.max(\n    2,\n    ((selected.end - selected.start) / count) * width,\n  );\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-brush`}\n      onLayout={(event) =>\n        setOrigin(\n          Option.all([\n            Option.fromNullishOr(event.x),\n            Option.fromNullishOr(event.y),\n          ]).pipe(Option.map(([x, y]) => ({ x, y }))),\n        )\n      }\n      onMouseDown={(event) => {\n        const at = indexAt(event.x ?? 0);\n        drag.current = Option.some(at);\n        setEdge(Option.some(at));\n        redraw((tick) => tick + 1);\n        report(at, at);\n      }}\n      onMouseMove={(event) => {\n        if (Option.isNone(drag.current)) return;\n        const at = indexAt(event.x ?? 0);\n        setEdge(Option.some(at));\n        Option.match(drag.current, {\n          onNone: () => {},\n          onSome: (start) => report(start, at),\n        });\n      }}\n      onMouseUp={() => {\n        drag.current = Option.none();\n        setEdge(Option.none());\n        redraw((tick) => tick + 1);\n      }}\n      onMouseLeave={() => {\n        drag.current = Option.none();\n        setEdge(Option.none());\n        redraw((tick) => tick + 1);\n      }}\n      style={{\n        ...recipeStyle(CHART_BRUSH, theme),\n        width,\n        height: 16,\n        marginLeft: padding,\n      }}\n    >\n      <div style={recipeStyle(CHART_BRUSH_TRACK, theme)}>\n        <div\n          testId={\n            testId === undefined ? undefined : `${testId}-brush-selection`\n          }\n          style={{\n            ...recipeStyle(CHART_BRUSH_BAND, theme),\n            position: \"absolute\",\n            left,\n            width: selectedWidth,\n            backgroundColor: colors.accent,\n          }}\n        />\n        <div\n          style={{\n            ...recipeStyle(CHART_BRUSH_HANDLE, theme),\n            position: \"absolute\",\n            left,\n            backgroundColor: colors.primary,\n          }}\n        />\n        <div\n          style={{\n            ...recipeStyle(CHART_BRUSH_HANDLE, theme),\n            position: \"absolute\",\n            left: left + selectedWidth - 4,\n            backgroundColor: colors.primary,\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\nconst ChartTooltip = ({\n  series,\n  labels,\n  palette,\n  theme,\n  index,\n  slot,\n  box,\n  pointer,\n  format,\n  indicator,\n  tooltipIcons,\n  labelFormat,\n  testId,\n}: {\n  readonly series: ReadonlyArray<ChartSeries>;\n  readonly labels: ReadonlyArray<string>;\n  readonly palette: ReadonlyArray<string>;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly index: number;\n  readonly slot: number;\n  readonly box: ChartBox;\n  /** The pointer, in plot coordinates, when the runtime reported one. */\n  readonly pointer: Option.Option<Point>;\n  readonly format: (value: number) => string;\n  readonly indicator: TooltipIndicator;\n  readonly tooltipIcons: ReadonlyArray<IconName> | undefined;\n  readonly labelFormat: ((label: string) => string) | undefined;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  const width = 132;\n  const column = slot * index + slot / 2 - width / 2;\n  const left = Option.match(pointer, {\n    onNone: () => Math.max(0, Math.min(box.width - width, column)),\n    onSome: (at) => Math.max(0, Math.min(box.width - width, at.x + 12)),\n  });\n  const top = Option.match(pointer, {\n    onNone: () => 0,\n    onSome: (at) => Math.max(0, Math.min(box.height - 64, at.y + 12)),\n  });\n  return (\n    <div\n      testId={testId === undefined ? undefined : `${testId}-tooltip`}\n      style={{\n        ...recipeStyle(CHART_TOOLTIP, theme),\n        position: \"absolute\",\n        left,\n        top,\n        width,\n        pointerEvents: \"none\",\n      }}\n    >\n      <text\n        style={{\n          ...recipeStyle(CHART_TOOLTIP_HEAD, theme),\n          ...textStyle(theme, \"caption\"),\n          color: colors.mutedForeground,\n        }}\n      >\n        {Option.match(Option.fromNullishOr(labels[index]), {\n          onNone: () => \"\",\n          onSome: (label) =>\n            labelFormat === undefined ? label : labelFormat(label),\n        })}\n      </text>\n      {Arr.map(series, (one, seriesIndex) => (\n        <div\n          key={one.key}\n          testId={\n            testId === undefined ? undefined : `${testId}-tooltip-${one.key}`\n          }\n          style={recipeStyle(CHART_TOOLTIP_ROW, theme)}\n        >\n          {indicator === \"none\" ? undefined : indicator === \"icon\" ? (\n            <Icon\n              color={seriesColourOf(palette, one, seriesIndex)}\n              name={tooltipIcon(tooltipIcons, seriesIndex)}\n              size={12}\n              testId={\n                testId === undefined\n                  ? undefined\n                  : `${testId}-tooltip-icon-${one.key}`\n              }\n            />\n          ) : (\n            <div\n              testId={\n                testId === undefined\n                  ? undefined\n                  : `${testId}-tooltip-indicator-${one.key}`\n              }\n              style={{\n                width: indicator === \"line\" ? 10 : 6,\n                height: indicator === \"line\" ? 2 : 6,\n                borderRadius: indicator === \"line\" ? 1 : 3,\n                backgroundColor: seriesColourOf(palette, one, seriesIndex),\n              }}\n            />\n          )}\n          <text\n            style={{\n              ...textStyle(theme, \"caption\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {seriesName(one.label, one.key)}\n          </text>\n          <text\n            style={{\n              ...recipeStyle(CHART_TOOLTIP_VALUE, theme),\n              ...textStyle(theme, \"caption\"),\n              color: colors.foreground,\n            }}\n          >\n            {format(one.values[index] ?? 0)}\n          </text>\n        </div>\n      ))}\n    </div>\n  );\n};\n\n/** The values a bar chart paints above its bars. */\nconst ValueLabels = ({\n  labels,\n  theme,\n  testId,\n}: {\n  readonly labels: ReadonlyArray<ValueLabel>;\n  readonly theme: ReturnType<typeof useTheme>;\n  readonly testId: string | undefined;\n}) => {\n  const colors = themeColors(theme);\n  return Arr.map(labels, (label) => (\n    <text\n      key={`${label.key}-${label.x}`}\n      testId={testId === undefined ? undefined : `${testId}-value-${label.key}`}\n      style={{\n        ...recipeStyle(CHART_VALUE_LABEL, theme),\n        position: \"absolute\",\n        left: label.x,\n        top: label.y,\n        width: label.width,\n        textAlign: \"center\",\n        color: colors.mutedForeground,\n      }}\n    >\n      {label.text}\n    </text>\n  ));\n};\n"
    },
    {
      "path": "packages/data/src/chart/geometry.ts",
      "target": "@ui/gpuixui/data/chart/geometry.ts",
      "type": "registry:lib",
      "content": "import { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport type { ChartBox, ChartMark, ChartSeries } from \"./logic.js\";\n\nexport interface Point {\n  readonly x: number;\n  readonly y: number;\n}\n\nexport type Curve = \"linear\" | \"smooth\" | \"natural\" | \"step\";\n\nexport interface ChartRange {\n  readonly min: number;\n  readonly max: number;\n}\n\nexport const finite = (value: number): number =>\n  Number.isFinite(value) ? value : 0;\n\nconst slotCount = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number => {\n  if (categories !== undefined && Number.isFinite(categories)) {\n    return Math.max(0, Math.floor(categories));\n  }\n  return Arr.reduce(series, 0, (most, one) =>\n    Math.max(most, one.values.length),\n  );\n};\n\nconst paintedValues = (\n  values: ReadonlyArray<number>,\n  categories?: number,\n): ReadonlyArray<number> =>\n  categories === undefined || !Number.isFinite(categories)\n    ? values\n    : Arr.take(values, Math.max(0, Math.floor(categories)));\n\nexport const niceBound = (raw: number): number => {\n  const value = Math.abs(finite(raw));\n  if (value === 0) return 0;\n  const magnitude = 10 ** Math.floor(Math.log10(value));\n  const step = value / magnitude;\n  const nice = step <= 1 ? 1 : step <= 2 ? 2 : step <= 5 ? 5 : 10;\n  return (raw < 0 ? -1 : 1) * nice * magnitude;\n};\n\nexport const chartRange = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n  stacked = false,\n): ChartRange => {\n  const values = Arr.flatten(\n    Arr.map(series, (one) => paintedValues(one.values, categories)),\n  );\n  const totals =\n    stacked && series.length > 1\n      ? [\n          Arr.reduce(\n            Arr.map(\n              Arr.makeBy(slotCount(series, categories), (index) => index),\n              (index) =>\n                Arr.reduce(\n                  series,\n                  0,\n                  (sum, one) =>\n                    sum + Math.max(0, finite(one.values[index] ?? 0)),\n                ),\n            ),\n            0,\n            Math.max,\n          ),\n        ]\n      : [];\n  const highest = (input: ReadonlyArray<number>): number =>\n    Arr.reduce(input, 0, (most, value) => Math.max(most, value));\n  const max = niceBound(Math.max(highest(values), highest(totals), 0));\n  const min = niceBound(\n    Arr.reduce(values, 0, (least, value) => Math.min(least, value)),\n  );\n  return { min: Math.min(0, min), max: Math.max(max > 0 ? max : 1, 0) };\n};\n\nexport const scaleY = (\n  value: number,\n  box: ChartBox,\n  range: ChartRange,\n): number => {\n  const span = range.max - range.min;\n  if (span <= 0) return box.height;\n  return box.height - ((finite(value) - range.min) / span) * box.height;\n};\n\nexport const scaleX = (\n  value: number,\n  box: ChartBox,\n  range: ChartRange,\n): number => {\n  const span = range.max - range.min;\n  if (span <= 0) return 0;\n  return ((finite(value) - range.min) / span) * box.width;\n};\n\nexport const zeroY = (box: ChartBox, range: ChartRange): number =>\n  scaleY(0, box, range);\n\nexport const seriesPoints = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  view?: ChartView,\n): ReadonlyArray<Point> => {\n  const painted = paintedValues(values, categories);\n  const count = slotCount(\n    [{ key: \"\", label: \"\", values: painted }],\n    categories ?? painted.length,\n  );\n  const span = view === undefined ? count : view.end - view.start;\n  const slot = span <= 0 ? 0 : box.width / span;\n  const offset = view === undefined ? 0 : view.start;\n  return Arr.map(painted, (value, index) => ({\n    x: (index + 0.5 - offset) * slot,\n    y: scaleY(value, box, range),\n  }));\n};\n\nconst at = (value: number): string =>\n  `${Math.round((Number.isFinite(value) ? value : 0) * 100) / 100}`;\n\nconst pair = (point: Point): string => `${at(point.x)} ${at(point.y)}`;\n\nconst monotoneTangents = (\n  points: ReadonlyArray<Point>,\n): ReadonlyArray<number> => {\n  const count = points.length;\n  if (count < 2) return Arr.map(points, () => 0);\n  const run: number[] = [];\n  const slope: number[] = [];\n  for (let index = 0; index < count - 1; index += 1) {\n    const from = points[index];\n    const to = points[index + 1];\n    if (from === undefined || to === undefined) continue;\n    const step = to.x - from.x;\n    run[index] = step;\n    slope[index] = step === 0 ? 0 : (to.y - from.y) / step;\n  }\n  const tangent: number[] = Arr.map(points, () => 0);\n  tangent[0] = slope[0] ?? 0;\n  tangent[count - 1] = slope[count - 2] ?? 0;\n  for (let index = 1; index < count - 1; index += 1) {\n    const before = slope[index - 1] ?? 0;\n    const after = slope[index] ?? 0;\n    if (before * after <= 0) {\n      tangent[index] = 0;\n      continue;\n    }\n    const left = 2 * (run[index] ?? 1) + (run[index - 1] ?? 1);\n    const right = (run[index] ?? 1) + 2 * (run[index - 1] ?? 1);\n    tangent[index] = (left + right) / (left / before + right / after);\n  }\n  return tangent;\n};\n\nconst naturalControls = (\n  values: ReadonlyArray<number>,\n): readonly [ReadonlyArray<number>, ReadonlyArray<number>] => {\n  const n = values.length - 1;\n  const value = (index: number): number => values[index] ?? 0;\n  const a: Array<number> = [0];\n  const b: Array<number> = [2];\n  const r: Array<number> = [value(0) + 2 * value(1)];\n  for (let i = 1; i < n - 1; i += 1) {\n    a[i] = 1;\n    b[i] = 4;\n    r[i] = 4 * value(i) + 2 * value(i + 1);\n  }\n  a[n - 1] = 2;\n  b[n - 1] = 7;\n  r[n - 1] = 8 * value(n - 1) + value(n);\n  for (let i = 1; i < n; i += 1) {\n    const m = (a[i] ?? 0) / (b[i - 1] ?? 1);\n    b[i] = (b[i] ?? 0) - m;\n    r[i] = (r[i] ?? 0) - m * (r[i - 1] ?? 0);\n  }\n  const first: Array<number> = [];\n  first[n - 1] = (r[n - 1] ?? 0) / (b[n - 1] ?? 1);\n  for (let i = n - 2; i >= 0; i -= 1) {\n    first[i] = ((r[i] ?? 0) - (first[i + 1] ?? 0)) / (b[i] ?? 1);\n  }\n  const second: Array<number> = [];\n  second[n - 1] = (value(n) + (first[n - 1] ?? 0)) / 2;\n  for (let i = 0; i < n - 1; i += 1) {\n    second[i] = 2 * value(i + 1) - (first[i + 1] ?? 0);\n  }\n  return [first, second];\n};\n\nexport const curvePath = (\n  points: ReadonlyArray<Point>,\n  curve: Curve,\n): Option.Option<string> => {\n  const first = Arr.head(points);\n  if (Option.isNone(first)) return Option.none();\n  if (points.length === 1) {\n    return Option.some(`M${pair(first.value)}h1`);\n  }\n  const [head, ...rest] = points;\n  if (head === undefined) return Option.none();\n  const tangents = monotoneTangents(points);\n  const [x1, x2] = naturalControls(Arr.map(points, (point) => point.x));\n  const [y1, y2] = naturalControls(Arr.map(points, (point) => point.y));\n  const body = Arr.map(rest, (point, index) => {\n    const previous = points[index];\n    if (previous === undefined) return \"\";\n    return Match.value(curve).pipe(\n      Match.when(\"step\", () => `H${at(point.x)}V${at(point.y)}`),\n      Match.when(\"natural\", () =>\n        points.length < 3\n          ? `L${pair(point)}`\n          : `C${pair({ x: x1[index] ?? point.x, y: y1[index] ?? point.y })} ${pair(\n              {\n                x: x2[index] ?? point.x,\n                y: y2[index] ?? point.y,\n              },\n            )} ${pair(point)}`,\n      ),\n      Match.when(\"smooth\", () => {\n        const h = (point.x - previous.x) / 3;\n        return `C${pair({\n          x: previous.x + h,\n          y: previous.y + (tangents[index] ?? 0) * h,\n        })} ${pair({\n          x: point.x - h,\n          y: point.y - (tangents[index + 1] ?? 0) * h,\n        })} ${pair(point)}`;\n      }),\n      Match.when(\"linear\", () => `L${pair(point)}`),\n      Match.exhaustive,\n    );\n  });\n  return Option.some(`M${pair(head)}${Arr.join(body, \"\")}`);\n};\n\nexport const areaPath = (\n  points: ReadonlyArray<Point>,\n  box: ChartBox,\n  range: ChartRange,\n  curve: Curve,\n): Option.Option<string> =>\n  Option.map(curvePath(points, curve), (line) => {\n    const last = Arr.last(points);\n    const first = Arr.head(points);\n    if (Option.isNone(last) || Option.isNone(first)) return line;\n    const floor = zeroY(box, range);\n    return `${line}L${at(last.value.x)} ${at(floor)}L${at(first.value.x)} ${at(floor)}Z`;\n  });\n\nexport const clipArea = (\n  points: ReadonlyArray<Point>,\n  cut: number,\n  floor: number,\n): Option.Option<string> => {\n  const kept: Array<Point> = [];\n  Arr.forEach(points, (point, index) => {\n    const previous = points[index - 1];\n    if (previous !== undefined) {\n      const above = previous.y <= cut;\n      const below = point.y <= cut;\n      if (above !== below) {\n        const t = (cut - previous.y) / (point.y - previous.y);\n        kept.push({\n          x: previous.x + (point.x - previous.x) * t,\n          y: cut,\n        });\n      }\n    }\n    if (point.y <= cut) kept.push(point);\n  });\n  if (kept.length === 0) return Option.none();\n  const first = Arr.head(kept);\n  const last = Arr.last(kept);\n  if (Option.isNone(first) || Option.isNone(last)) return Option.none();\n  return Option.some(\n    `M${pair(first.value)}${Arr.join(\n      Arr.map(Arr.drop(kept, 1), (point) => `L${pair(point)}`),\n      \"\",\n    )}L${at(last.value.x)} ${at(floor)}L${at(first.value.x)} ${at(floor)}Z`,\n  );\n};\n\nexport const areaBands = (\n  points: ReadonlyArray<Point>,\n  box: ChartBox,\n  range: ChartRange,\n  bands: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(6, Math.floor(bands)));\n  const top = 0;\n  const floor = zeroY(box, range);\n  const span = floor - top;\n  if (span <= 0) return [];\n  return Arr.flatMap(\n    Arr.makeBy(steps, (index) => index + 1),\n    (index) =>\n      Option.match(clipArea(points, top + (span * index) / steps, floor), {\n        onNone: () => [],\n        onSome: (d): ReadonlyArray<string> => [d],\n      }),\n  );\n};\n\nexport const pointMarks = (\n  points: ReadonlyArray<Point>,\n  radius: number,\n): ReadonlyArray<ChartMark> =>\n  Arr.map(\n    points,\n    (point): ChartMark => ({\n      kind: \"circle\",\n      cx: point.x,\n      cy: point.y,\n      r: Math.max(1, radius),\n    }),\n  );\n\nexport const emphasisedPointMarks = (\n  points: ReadonlyArray<Point>,\n  radius: number,\n  hovered: number | undefined,\n): ReadonlyArray<ChartMark> =>\n  Arr.map(\n    points,\n    (point, index): ChartMark => ({\n      kind: \"circle\",\n      cx: point.x,\n      cy: point.y,\n      r: Math.max(1, index === hovered ? radius * 2 : radius),\n    }),\n  );\n\nexport const barMarksInRange = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 0,\n  view?: ChartView,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = slotCount(series, categories);\n  if (count === 0) return Arr.map(series, () => []);\n  const span = view === undefined ? count : view.end - view.start;\n  const slot = box.width / Math.max(1, span);\n  const offset = view === undefined ? 0 : view.start;\n  const width =\n    series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / series.length);\n  const zero = zeroY(box, range);\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.flatMap(\n      Arr.map(paintedValues(one.values, categories), (value, index) => ({\n        value,\n        x: (index + 0.1 - offset) * slot + seriesIndex * width,\n      })),\n      (cell: {\n        readonly value: number;\n        readonly x: number;\n      }): ReadonlyArray<ChartMark> => {\n        const y = scaleY(cell.value, box, range);\n        const height = Math.abs(y - zero);\n        if (!Number.isFinite(cell.value) || height < 0.5) return [];\n        return [\n          {\n            kind: \"rect\" as const,\n            x: cell.x,\n            y: Math.min(y, zero),\n            width,\n            height,\n            rx: Math.min(radius, width / 2, height / 2),\n          },\n        ];\n      },\n    ),\n  );\n};\n\nexport const horizontalBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 0,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = slotCount(series, categories);\n  if (count === 0) return Arr.map(series, () => []);\n  const slot = box.height / count;\n  const height =\n    series.length === 0 ? 0 : Math.max(1, (slot * 0.8) / series.length);\n  const zero = scaleX(0, box, range);\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.flatMap(\n      Arr.map(paintedValues(one.values, categories), (value, index) => ({\n        value,\n        y: index * slot + slot * 0.1 + seriesIndex * height,\n      })),\n      (cell: {\n        readonly value: number;\n        readonly y: number;\n      }): ReadonlyArray<ChartMark> => {\n        const x = scaleX(cell.value, box, range);\n        const barWidth = Math.abs(x - zero);\n        if (!Number.isFinite(cell.value) || barWidth < 0.5) return [];\n        return [\n          {\n            kind: \"rect\" as const,\n            x: Math.min(x, zero),\n            y: cell.y,\n            width: barWidth,\n            height,\n            rx: Math.min(radius, barWidth / 2, height / 2),\n          },\n        ];\n      },\n    ),\n  );\n};\n\nconst clampRadius = (reach: number | undefined, fallback: number): number =>\n  reach === undefined || !Number.isFinite(reach)\n    ? fallback\n    : Math.max(0, Math.min(fallback * 2 + 4, reach));\n\nconst radarPoint = (\n  value: number,\n  index: number,\n  axes: number,\n  box: ChartBox,\n  max: number,\n  outerRadius?: number,\n): Point => {\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const radius = clampRadius(\n    outerRadius,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  const angle = -Math.PI / 2 + (index / Math.max(1, axes)) * Math.PI * 2;\n  const reach = max <= 0 ? 0 : (Math.max(0, finite(value)) / max) * radius;\n  return {\n    x: cx + reach * Math.cos(angle),\n    y: cy + reach * Math.sin(angle),\n  };\n};\n\nexport const radarPoints = (\n  values: ReadonlyArray<number>,\n  axes: number,\n  box: ChartBox,\n  max: number,\n  radius?: number,\n): ReadonlyArray<Point> =>\n  Arr.makeBy(axes, (index) =>\n    radarPoint(values[index] ?? 0, index, axes, box, max, radius),\n  );\n\nexport const polygonPath = (\n  points: ReadonlyArray<Point>,\n): Option.Option<string> =>\n  Option.map(Arr.head(points), (first) => {\n    return `M${pair(first)}${Arr.join(\n      Arr.map(Arr.drop(points, 1), (point) => `L${pair(point)}`),\n      \"\",\n    )}Z`;\n  });\n\nexport const radarGrid = (\n  levels: number,\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(8, Math.floor(levels)));\n  return Arr.flatMap(\n    Arr.makeBy(steps, (index) => index + 1),\n    (level) =>\n      Option.match(\n        polygonPath(\n          Arr.makeBy(axes, (axis) =>\n            radarPoint((max * level) / steps, axis, axes, box, max, radius),\n          ),\n        ),\n        {\n          onNone: () => [],\n          onSome: (d): ReadonlyArray<string> => [d],\n        },\n      ),\n  );\n};\n\nexport const radarGridAt = (\n  fractions: ReadonlyArray<number>,\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> =>\n  Arr.flatMap(fractions, (fraction) => {\n    const share = Math.max(0, Math.min(1, fraction));\n    if (share <= 0) return [];\n    const reach =\n      clampRadius(\n        radius,\n        Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n      ) * share;\n    return Option.match(\n      polygonPath(\n        Arr.makeBy(axes, (axis) =>\n          radarPoint(max, axis, axes, box, max, reach),\n        ),\n      ),\n      {\n        onNone: () => [],\n        onSome: (d): ReadonlyArray<string> => [d],\n      },\n    );\n  });\n\nexport const radarCircleGridAt = (\n  fractions: ReadonlyArray<number>,\n  box: ChartBox,\n  radius?: number,\n): ReadonlyArray<string> => {\n  const reach = clampRadius(\n    radius,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  return Arr.flatMap(fractions, (fraction) => {\n    const share = Math.max(0, Math.min(1, fraction));\n    return share <= 0\n      ? []\n      : [circlePath(box.width / 2, box.height / 2, reach * share)];\n  });\n};\n\nexport const radarSpokes = (\n  axes: number,\n  box: ChartBox,\n  max = 1,\n  radius?: number,\n): ReadonlyArray<string> =>\n  Arr.makeBy(axes, (axis) => {\n    const rim = radarPoint(max, axis, axes, box, max, radius);\n    return `M${pair({ x: box.width / 2, y: box.height / 2 })}L${pair(rim)}`;\n  });\n\nexport const circlePath = (cx: number, cy: number, radius: number): string =>\n  `M${at(cx - radius)} ${at(cy)}A${at(radius)} ${at(radius)} 0 1 0 ${at(cx + radius)} ${at(cy)}A${at(radius)} ${at(radius)} 0 1 0 ${at(cx - radius)} ${at(cy)}Z`;\n\nexport const radarCircleGrid = (\n  levels: number,\n  box: ChartBox,\n  reach?: number,\n): ReadonlyArray<string> => {\n  const steps = Math.max(1, Math.min(8, Math.floor(levels)));\n  const radius = clampRadius(\n    reach,\n    Math.max(0, Math.min(box.width, box.height) / 2 - 4),\n  );\n  return Arr.makeBy(steps, (index) =>\n    circlePath(box.width / 2, box.height / 2, (radius * (index + 1)) / steps),\n  );\n};\n\nexport const fullRing = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n): string =>\n  `${ringArc(cx, cy, outer, inner, -Math.PI / 2, 0)}${ringArc(cx, cy, outer, inner, 0, (Math.PI * 3) / 2)}`;\n\nexport const ringArc = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n  start: number,\n  end: number,\n): string => {\n  const point = (radius: number, angle: number): string =>\n    `${at(cx + radius * Math.cos(angle))} ${at(cy + radius * Math.sin(angle))}`;\n  const span = Math.abs(end - start);\n  const large = span > Math.PI ? 1 : 0;\n  return [\n    `M${point(outer, start)}`,\n    `A${at(outer)} ${at(outer)} 0 ${large} 1 ${point(outer, end)}`,\n    `L${point(inner, end)}`,\n    `A${at(inner)} ${at(inner)} 0 ${large} 0 ${point(inner, start)}`,\n    \"Z\",\n  ].join(\"\");\n};\n\nexport const radialArcs = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  max: number,\n  thickness = 10,\n  gap = 4,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = Math.max(0, Math.min(box.width, box.height) / 2 - 1);\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  return Arr.map(values, (value, index) => {\n    const ringOuter = outer - index * (thickness + gap);\n    const ringInner = ringOuter - thickness;\n    if (ringOuter <= 0 || ringInner < 0) return [];\n    const share = max <= 0 ? 0 : Math.max(0, finite(value)) / max;\n    const span = share * full;\n    if (span <= 0) return [];\n    if (span >= full - 1e-9) {\n      return [\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, ringOuter, ringInner, start, start + Math.PI),\n        },\n        {\n          kind: \"path\" as const,\n          d: ringArc(\n            cx,\n            cy,\n            ringOuter,\n            ringInner,\n            start + Math.PI,\n            start + full,\n          ),\n        },\n      ];\n    }\n    return [\n      {\n        kind: \"path\" as const,\n        d: ringArc(cx, cy, ringOuter, ringInner, start, start + span),\n      },\n    ];\n  });\n};\n\nconst ACTIVE_GROW = 8;\nconst ACTIVE_BAND_INNER = 10;\nconst ACTIVE_BAND_OUTER = 20;\n\nconst gappedArc = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n  from: number,\n  to: number,\n  width: number,\n): string => {\n  const half = width / 2;\n  const point = (radius: number, angle: number): string =>\n    `${at(cx + radius * Math.cos(angle))} ${at(cy + radius * Math.sin(angle))}`;\n  const inset = (radius: number) => Math.asin(Math.min(1, half / radius));\n  const large = (a: number, b: number) => (b - a > Math.PI ? 1 : 0);\n  const rimFrom = from + inset(outer);\n  const rimTo = to - inset(outer);\n  const rim = `M${point(outer, rimFrom)}A${at(outer)} ${at(outer)} 0 ${large(rimFrom, rimTo)} 1 ${point(outer, rimTo)}`;\n  if (inner > half) {\n    const hubFrom = from + inset(inner);\n    const hubTo = to - inset(inner);\n    return `${rim}L${point(inner, hubTo)}A${at(inner)} ${at(inner)} 0 ${large(hubFrom, hubTo)} 0 ${point(inner, hubFrom)}Z`;\n  }\n  const apex = half / Math.max(1e-6, Math.sin((to - from) / 2));\n  return `${rim}L${point(Math.min(apex, outer / 2), (from + to) / 2)}Z`;\n};\n\nexport const ringSegments = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n  padAngle = 0,\n  emphasis?: number,\n  sweep = 1,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2 - (emphasis === undefined ? 0 : ACTIVE_BAND_OUTER);\n  const inner = outer * Math.min(0.95, Math.max(0, innerRatio));\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const gap = Math.min(\n    Math.max(0, padAngle),\n    (full * 0.4) / Math.max(1, drawable.length),\n  );\n  const [, segments] = Arr.mapAccum(drawable, start, (angle, value, index) => {\n    if (value <= 0) return [angle, []];\n    const span = (value / total) * full * sweep;\n    const from = angle + gap / 2;\n    const to = angle + span - gap / 2;\n    if (to <= from) return [angle + span, []];\n    const active = index === emphasis;\n    const rim = active ? outer + ACTIVE_GROW : outer;\n    if (span >= full - 1e-9) {\n      return [\n        angle + full,\n        [\n          {\n            kind: \"path\" as const,\n            d: ringArc(cx, cy, rim, inner, from, from + Math.PI),\n          },\n          {\n            kind: \"path\" as const,\n            d: ringArc(cx, cy, rim, inner, from + Math.PI, from + full),\n          },\n        ],\n      ];\n    }\n    return [\n      angle + span,\n      [\n        {\n          kind: \"path\" as const,\n          d: gappedArc(cx, cy, rim, inner, angle, angle + span, gap * outer),\n        },\n        ...(active\n          ? [\n              {\n                kind: \"path\" as const,\n                d: ringArc(\n                  cx,\n                  cy,\n                  outer + ACTIVE_BAND_OUTER,\n                  outer + ACTIVE_BAND_INNER,\n                  from,\n                  to,\n                ),\n              },\n            ]\n          : []),\n      ],\n    ];\n  });\n  return segments;\n};\n\nexport const ringLabelPoints = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n): ReadonlyArray<Option.Option<Point>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  const size = Math.min(box.width, box.height);\n  const outer = size / 2;\n  const reach = (outer + outer * innerRatio) / 2;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const [, points] = Arr.mapAccum(drawable, start, (angle, value) => {\n    if (total <= 0 || value <= 0) return [angle, Option.none()];\n    const span = (value / total) * full;\n    const middle = angle + span / 2;\n    return [\n      angle + span,\n      Option.some({\n        x: box.width / 2 + reach * Math.cos(middle),\n        y: box.height / 2 + reach * Math.sin(middle),\n      }),\n    ];\n  });\n  return points;\n};\n\nexport interface RingLabel {\n  readonly point: Point;\n  readonly right: boolean;\n  readonly leader: string;\n}\n\nexport const ringOutsideLabels = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<Option.Option<RingLabel>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  const size = Math.min(box.width, box.height);\n  const outer = size / 2;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const step = 14;\n  const [, labels] = Arr.mapAccum(drawable, start, (angle, value) => {\n    if (total <= 0 || value <= 0) return [angle, Option.none()];\n    const span = (value / total) * full;\n    const middle = angle + span / 2;\n    const right = Math.cos(middle) >= 0;\n    const rimX = box.width / 2 + (outer - 2) * Math.cos(middle);\n    const rimY = box.height / 2 + (outer - 2) * Math.sin(middle);\n    const elbowX = box.width / 2 + (outer + step) * Math.cos(middle);\n    const elbowY = box.height / 2 + (outer + step) * Math.sin(middle);\n    const anchorX = elbowX + (right ? step : -step);\n    return [\n      angle + span,\n      Option.some({\n        point: { x: anchorX, y: elbowY },\n        right,\n        leader: `M${at(rimX)} ${at(rimY)}L${at(elbowX)} ${at(elbowY)}L${at(anchorX)} ${at(elbowY)}`,\n      } satisfies RingLabel),\n    ];\n  });\n  return labels;\n};\n\nexport interface ChartView {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport const chartView = (\n  length: number,\n  view: readonly [number, number] | undefined,\n): Option.Option<ChartView> => {\n  if (view === undefined || length <= 0) return Option.none();\n  const [left, right] = view;\n  if (!Number.isFinite(left) || !Number.isFinite(right)) return Option.none();\n  const low = Math.min(left, right);\n  const high = Math.max(left, right);\n  const end = Math.min(length, Math.max(low + 1, high));\n  const start = Math.max(0, Math.min(end - 1, low));\n  return Option.some({ start, end });\n};\n\nexport interface ChartWindow {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport const chartWindow = (\n  length: number,\n  domain: readonly [number, number] | undefined,\n): Option.Option<ChartWindow> => {\n  if (domain === undefined || length <= 0) return Option.none();\n  const [left, right] = domain;\n  if (!Number.isFinite(left) || !Number.isFinite(right)) return Option.none();\n  const edgeLow = Math.min(left, right);\n  const edgeHigh = Math.max(left, right);\n  if (edgeHigh <= 0 || edgeLow >= length) return Option.none();\n  const low = Math.max(\n    0,\n    Math.min(length - 1, Math.floor(Math.min(left, right))),\n  );\n  const high = Math.max(\n    low + 1,\n    Math.min(length, Math.ceil(Math.max(left, right))),\n  );\n  if (high <= 0 || low >= length) return Option.none();\n  return Option.some({ start: low, end: high });\n};\n\nexport const ringBands = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  innerRatio = 0,\n  padAngle = 0,\n  emphasis?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, (value) => Math.max(0, finite(value)));\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2;\n  const inner = outer * Math.min(0.95, Math.max(0, innerRatio));\n  const bands = Math.max(1, drawable.length);\n  const band = (outer - inner) / bands;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n  const gap = Math.min(\n    Math.max(0, padAngle),\n    (full * 0.4) / Math.max(1, drawable.length),\n  );\n  return Arr.map(drawable, (value, index) => {\n    if (value <= 0) return [];\n    const rim = outer - index * band + (index === emphasis ? 4 : 0);\n    const floor = Math.max(0, rim - band);\n    const span = (value / total) * full;\n    const from = start + gap / 2;\n    const to = start + span - gap / 2;\n    if (to <= from) return [];\n    if (span >= full - 1e-9) {\n      return [\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, rim, floor, from, from + Math.PI),\n        },\n        {\n          kind: \"path\" as const,\n          d: ringArc(cx, cy, rim, floor, from + Math.PI, from + full),\n        },\n      ];\n    }\n    return [\n      { kind: \"path\" as const, d: ringArc(cx, cy, rim, floor, from, to) },\n    ];\n  });\n};\n\nexport const valueLabel = (value: number): string =>\n  Number.isFinite(value) ? `${Math.round(value * 10) / 10}` : \"0\";\n\nexport const rangeTicks = (\n  range: ChartRange,\n  count = 4,\n): ReadonlyArray<number> => {\n  const steps = Number.isFinite(count)\n    ? Math.min(20, Math.max(1, Math.floor(count)))\n    : 4;\n  const span = range.max - range.min;\n  return Arr.makeBy(steps + 1, (index) => range.min + (span / steps) * index);\n};\n\nexport const pinnedRange = (\n  range: readonly [number, number] | undefined,\n): Option.Option<ChartRange> => {\n  if (range === undefined) return Option.none();\n  const [low, high] = range;\n  if (!Number.isFinite(low) || !Number.isFinite(high)) return Option.none();\n  const min = Math.min(low, high);\n  const max = Math.max(low, high);\n  if (max - min <= 0) return Option.none();\n  return Option.some({ min, max });\n};\n\nexport const chartRangeWith = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n  pinned: readonly [number, number] | undefined = undefined,\n  stacked = false,\n): ChartRange =>\n  Option.getOrElse(pinnedRange(pinned), () =>\n    chartRange(series, categories, stacked),\n  );\n\nexport const referenceLines = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n): ReadonlyArray<{ readonly y: number; readonly value: number }> =>\n  Arr.map(Arr.filter(values, Number.isFinite), (value) => ({\n    y: scaleY(value, box, range),\n    value,\n  }));\n\nexport const verticalGrid = (\n  count: number,\n  box: ChartBox,\n): ReadonlyArray<{ readonly x: number; readonly width: number }> => {\n  const steps = Number.isFinite(count)\n    ? Math.min(64, Math.max(0, Math.floor(count)))\n    : 0;\n  if (steps === 0) return [];\n  const slot = box.width / steps;\n  return Arr.map(\n    Arr.makeBy(steps, (index) => index),\n    (index) => ({\n      x: index * slot,\n      width: Math.max(1, slot),\n    }),\n  );\n};\n\nexport const scatterMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  radius = 3,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const slot = box.width / count;\n  return Arr.map(\n    Arr.filter(\n      Arr.map(paintedValues(values, categories), (value, index) => ({\n        value,\n        x: (index + 0.5) * slot,\n      })),\n      (cell) => Number.isFinite(cell.value),\n    ),\n    (cell): ChartMark => ({\n      kind: \"circle\",\n      cx: cell.x,\n      cy: Math.max(0, Math.min(box.height, scaleY(cell.value, box, range))),\n      r: Math.max(0.01, radius),\n    }),\n  );\n};\n\nexport interface Candle {\n  readonly open: number;\n  readonly high: number;\n  /** The lowest price the period reached. */\n  readonly low: number;\n  /** The price the period closed at. */\n  readonly close: number;\n}\n\n/** A candle's direction: an up day or a down day. */\nexport const candleUp = (candle: Candle): boolean => candle.close > candle.open;\n\nexport const candleMarks = (\n  candle: Candle,\n  x: number,\n  width: number,\n  box: ChartBox,\n  range: ChartRange,\n): ReadonlyArray<ChartMark> => {\n  const numbers = [candle.open, candle.high, candle.low, candle.close];\n  if (Arr.some(numbers, (value) => !Number.isFinite(value))) return [];\n  const high = scaleY(candle.high, box, range);\n  const low = scaleY(candle.low, box, range);\n  const open = scaleY(candle.open, box, range);\n  const close = scaleY(candle.close, box, range);\n  const top = Math.min(open, close);\n  const bodyHeight = Math.max(1, Math.abs(close - open));\n  return [\n    {\n      kind: \"rect\",\n      x: x + width / 2 - 0.5,\n      y: Math.min(high, low),\n      width: 1,\n      height: Math.abs(low - high),\n    },\n    {\n      kind: \"rect\",\n      x,\n      y: top,\n      width,\n      height: bodyHeight,\n    },\n  ];\n};\n\nexport const candleAtMarks = (\n  candle: Candle,\n  index: number,\n  categories: number,\n  box: ChartBox,\n  range: ChartRange,\n  width = 6,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const slot = box.width / count;\n  const bodyWidth = Math.max(1, Math.min(width, slot * 0.6));\n  return candleMarks(\n    candle,\n    (index + 0.5) * slot - bodyWidth / 2,\n    bodyWidth,\n    box,\n    range,\n  );\n};\n\nexport const candleSeriesMarks = (\n  candles: ReadonlyArray<Candle>,\n  box: ChartBox,\n  range: ChartRange,\n  categories?: number,\n  width = 6,\n): ReadonlyArray<ChartMark> => {\n  const count = slotCount([], categories);\n  if (count === 0) return [];\n  const painted = Arr.take(candles, count);\n  const slot = box.width / count;\n  const bodyWidth = Math.max(1, Math.min(width, slot * 0.6));\n  return Arr.flatMap(painted, (candle, index) =>\n    candleMarks(\n      candle,\n      (index + 0.5) * slot - bodyWidth / 2,\n      bodyWidth,\n      box,\n      range,\n    ),\n  );\n};\n\nexport const funnelMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ChartMark> => {\n  const drawable = Arr.map(values, (value) =>\n    Number.isFinite(value) && value > 0 ? value : 0,\n  );\n  const first = Arr.head(drawable);\n  if (Option.isNone(first) || first.value <= 0) return [];\n  const widest = first.value;\n  const count = drawable.length;\n  const slot = box.height / count;\n  return Arr.map(\n    Arr.filter(\n      Arr.map(drawable, (value, index) => ({ value, index })),\n      (cell) => cell.value > 0,\n    ),\n    (cell): ChartMark => ({\n      kind: \"rect\",\n      x: (box.width - (cell.value / widest) * box.width) / 2,\n      y: cell.index * slot,\n      width: (cell.value / widest) * box.width,\n      height: Math.max(1, slot * 0.8),\n      rx: 6,\n    }),\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/legend.tsx",
      "target": "@ui/gpuixui/data/chart/legend.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport type { Props } from \"@gpuix/react\";\nimport { iconStepValue } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { CHART_LEGEND, CHART_LEGEND_ITEM, CHART_SWATCH } from \"./recipe.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { seriesName } from \"./logic.js\";\nimport type { LegendShape } from \"./frame.js\";\n\nexport interface ChartLegendItem {\n  readonly key: string;\n\n  readonly label: string;\n\n  readonly color: string;\n\n  readonly value?: string;\n}\n\nexport type { LegendShape, TooltipIndicator } from \"./frame.js\";\n\nexport interface ChartLegendProps extends Omit<Props, \"children\"> {\n  readonly items: ReadonlyArray<ChartLegendItem>;\n\n  readonly shape?: LegendShape;\n  readonly testId?: string;\n}\n\nexport const ChartLegend = ({\n  items,\n  shape = \"square\",\n  testId,\n  style,\n  ...rest\n}: ChartLegendProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const rows = recordsOf(items);\n  return (\n    <div\n      {...rest}\n      testId={testId}\n      style={mergeStyles(recipeStyle(CHART_LEGEND, theme), style)}\n    >\n      {Arr.map(rows, (item) => (\n        <div key={item.key} style={recipeStyle(CHART_LEGEND_ITEM, theme)}>\n          <div\n            testId={\n              testId === undefined ? undefined : `${testId}-swatch-${item.key}`\n            }\n            style={{\n              ...recipeStyle(CHART_SWATCH, theme),\n              width:\n                shape === \"line\"\n                  ? iconStepValue(theme, \"xs\") * 2\n                  : iconStepValue(theme, \"xs\"),\n              height: shape === \"line\" ? 2 : iconStepValue(theme, \"xs\"),\n              borderRadius:\n                shape === \"dot\"\n                  ? iconStepValue(theme, \"xs\") / 2\n                  : shape === \"line\"\n                    ? 1\n                    : undefined,\n              backgroundColor: item.color,\n            }}\n          />\n          <text\n            style={{\n              ...textStyle(theme, \"caption\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {item.value === undefined\n              ? seriesName(item.label, item.key)\n              : `${seriesName(item.label, item.key)} ${item.value}`}\n          </text>\n        </div>\n      ))}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/chart/logic.ts",
      "target": "@ui/gpuixui/data/chart/logic.ts",
      "type": "registry:lib",
      "content": "import { spokenText } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport { flatMark, marksMarkup, num, type ChartMark } from \"./mark.js\";\n\nexport { type ChartMark } from \"./mark.js\";\n\nexport interface ChartSeries {\n  readonly key: string;\n  readonly label: string;\n  readonly values: ReadonlyArray<number>;\n  readonly color?: string;\n}\n\nexport interface ChartBox {\n  readonly width: number;\n  readonly height: number;\n}\n\nconst paintableSide = (side: number, fallback: number): number =>\n  Number.isFinite(side) && side > 0\n    ? side\n    : Math.max(1, Number.isFinite(fallback) && fallback > 0 ? fallback : 1);\n\nexport const chartBox = (\n  width: number,\n  height: number,\n  fallback: ChartBox,\n): ChartBox => ({\n  width: paintableSide(width, fallback.width),\n  height: paintableSide(height, fallback.height),\n});\n\nexport const drawableValue = (value: number): number =>\n  Number.isFinite(value) && value > 0 ? value : 0;\n\nconst paintedSlots = (categories?: number): Option.Option<number> =>\n  Option.map(\n    Option.filter(Option.fromNullishOr(categories), Number.isFinite),\n    (count) => Math.max(0, Math.floor(count)),\n  );\n\nconst slotIndexes = (count: number): ReadonlyArray<number> =>\n  count <= 0 ? [] : Arr.makeBy(count, (index) => index);\n\nconst paintedValues = (\n  values: ReadonlyArray<number>,\n  categories?: number,\n): ReadonlyArray<number> =>\n  Option.match(paintedSlots(categories), {\n    onNone: () => values,\n    onSome: (count) => Arr.take(values, count),\n  });\n\nexport const seriesMax = (series: ChartSeries, categories?: number): number =>\n  Arr.reduce(paintedValues(series.values, categories), 0, (max, value) =>\n    Math.max(max, drawableValue(value)),\n  );\n\nexport const chartMaxOf = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  Arr.reduce(series, 0, (max, one) =>\n    Math.max(max, seriesMax(one, categories)),\n  );\n\nconst NICE_STEPS = [1, 1.2, 1.6, 2, 2.4, 3, 4, 5, 6, 8, 10] as const;\n\nexport const niceCeiling = (raw: number): number => {\n  const value = drawableValue(raw);\n  if (value === 0) return 1;\n  const magnitude = 10 ** Math.floor(Math.log10(value));\n  const step = value / magnitude;\n  const nice = Option.getOrElse(\n    Arr.findFirst(NICE_STEPS, (candidate) => step <= candidate),\n    () => 10,\n  );\n  return nice * magnitude;\n};\n\nexport const chartScale = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number => niceCeiling(chartMaxOf(series, categories));\n\nconst categoryCount = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  Option.getOrElse(paintedSlots(categories), () =>\n    Arr.reduce(series, 0, (most, one) => Math.max(most, one.values.length)),\n  );\n\nexport const stackedScale = (\n  series: ReadonlyArray<ChartSeries>,\n  categories?: number,\n): number =>\n  niceCeiling(\n    Arr.reduce(\n      Arr.map(slotIndexes(categoryCount(series, categories)), (index) =>\n        Arr.reduce(\n          series,\n          0,\n          (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n        ),\n      ),\n      0,\n      Math.max,\n    ),\n  );\n\nexport const AXIS_STEP_LIMIT = 20;\n\nconst axisSteps = (count: number): number =>\n  Number.isFinite(count)\n    ? Math.min(AXIS_STEP_LIMIT, Math.max(1, Math.floor(count)))\n    : 4;\n\nexport const axisTicks = (scale: number, count = 4): ReadonlyArray<number> => {\n  const steps = axisSteps(count);\n  return Arr.makeBy(steps + 1, (index) => (scale / steps) * index);\n};\n\nexport const tickLabel = (tick: number, step: number): string => {\n  if (!Number.isFinite(tick)) return \"0\";\n  if (!Number.isFinite(step) || step <= 0) return `${tick}`;\n  const decimals = Math.max(0, Math.ceil(-Math.log10(step)) + 1);\n  if (decimals > 20) return tick === 0 ? \"0\" : tick.toExponential(2);\n  return `${Number(tick.toFixed(decimals))}`;\n};\n\nexport interface BarSlot {\n  readonly slot: number;\n  readonly barWidth: number;\n}\n\nconst slotCount = (count: number): number => Math.max(1, Math.floor(count));\n\nexport const barSlot = (count: number, width: number, share = 0.6): BarSlot => {\n  const slot = Math.max(0, width) / slotCount(count);\n  return { slot, barWidth: slot * Math.min(1, Math.max(0, share)) };\n};\n\nconst barHeight = (value: number, box: ChartBox, scale: number): number => {\n  if (scale <= 0) return 0;\n  const raw = (drawableValue(value) / scale) * Math.max(0, box.height);\n  return Math.min(Math.max(0, box.height), raw);\n};\n\nexport const barMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  scale: number,\n  categories: number = values.length,\n): ReadonlyArray<ChartMark> => {\n  const { slot, barWidth } = barSlot(categories, box.width);\n  return Arr.map(\n    Arr.filter(\n      Arr.map(paintedValues(values, categories), (value, index) => ({\n        index,\n        height: barHeight(value, box, scale),\n      })),\n      (one) => one.height > 0,\n    ),\n    (one): ChartMark => ({\n      kind: \"rect\",\n      x: one.index * slot + (slot - barWidth) / 2,\n      y: box.height - one.height,\n      width: barWidth,\n      height: one.height,\n    }),\n  );\n};\n\nexport const groupedBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  scale: number,\n  categories?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = categoryCount(series, categories);\n  const { slot } = barSlot(count, box.width);\n  const width = series.length === 0 ? 0 : (slot * 0.8) / series.length;\n  return Arr.map(series, (one, seriesIndex) =>\n    Arr.map(\n      Arr.filter(\n        Arr.map(paintedValues(one.values, count), (value, index) => ({\n          index,\n          height: barHeight(value, box, scale),\n        })),\n        (candidate) => candidate.height > 0,\n      ),\n      (candidate): ChartMark => ({\n        kind: \"rect\",\n        x: candidate.index * slot + slot * 0.1 + seriesIndex * width,\n        y: box.height - candidate.height,\n        width,\n        height: candidate.height,\n      }),\n    ),\n  );\n};\n\nexport const stackedBarMarks = (\n  series: ReadonlyArray<ChartSeries>,\n  box: ChartBox,\n  scale: number,\n  categories?: number,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const count = categoryCount(series, categories);\n  const totals = Arr.map(slotIndexes(count), (index) =>\n    Arr.reduce(\n      series,\n      0,\n      (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n    ),\n  );\n  const { slot, barWidth } = barSlot(count, box.width, 0.7);\n  return Arr.map(series, (_, seriesIndex) =>\n    Arr.map(\n      Arr.filter(\n        Arr.map(totals, (_, index) => {\n          const below = Arr.reduce(\n            Arr.take(series, seriesIndex),\n            0,\n            (sum, one) => sum + drawableValue(one.values[index] ?? 0),\n          );\n          const value = drawableValue(series[seriesIndex]?.values[index] ?? 0);\n          const height = barHeight(value, box, scale);\n          return {\n            index,\n            height,\n            base: barHeight(below + value, box, scale),\n          };\n        }),\n        (candidate) => candidate.height > 0,\n      ),\n      (candidate): ChartMark => ({\n        kind: \"rect\",\n        x: candidate.index * slot + (slot - barWidth) / 2,\n        y: box.height - candidate.base,\n        width: barWidth,\n        height: candidate.height,\n      }),\n    ),\n  );\n};\n\nexport const linePath = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n  scale: number,\n  categories: number = values.length,\n): Option.Option<string> => {\n  const painted = paintedValues(values, categories);\n  if (painted.length === 0) return Option.none();\n  const { slot } = barSlot(\n    Option.getOrElse(paintedSlots(categories), () => values.length),\n    box.width,\n  );\n  const points = Arr.map(painted, (value, index) => ({\n    x: index * slot + slot / 2,\n    y: box.height - barHeight(value, box, scale),\n  }));\n  const path = Arr.join(\n    Arr.map(\n      points,\n      (point, index) =>\n        `${index === 0 ? \"M\" : \"L\"}${num(point.x)} ${num(point.y)}`,\n    ),\n    \"\",\n  );\n  return Option.some(painted.length === 1 ? `${path}h1` : path);\n};\n\nconst arcPath = (\n  cx: number,\n  cy: number,\n  outer: number,\n  inner: number,\n  start: number,\n  end: number,\n): string => {\n  const point = (radius: number, angle: number): string =>\n    `${num(cx + radius * Math.cos(angle))} ${num(cy + radius * Math.sin(angle))}`;\n  const span = end - start;\n  const large = span > Math.PI ? 1 : 0;\n  return [\n    `M${point(outer, start)}`,\n    `A${num(outer)} ${num(outer)} 0 ${large} 1 ${point(outer, end)}`,\n    `L${point(inner, end)}`,\n    `A${num(inner)} ${num(inner)} 0 ${large} 0 ${point(inner, start)}`,\n    \"Z\",\n  ].join(\"\");\n};\n\nexport const donutSegments = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ReadonlyArray<ChartMark>> => {\n  const drawable = Arr.map(values, drawableValue);\n  const total = Arr.reduce(drawable, 0, (sum, value) => sum + value);\n  if (total <= 0) return Arr.map(values, () => []);\n  const size = Math.min(box.width, box.height);\n  const cx = box.width / 2;\n  const cy = box.height / 2;\n  const outer = size / 2;\n  const inner = outer * 0.6;\n  const start = -Math.PI / 2;\n  const full = Math.PI * 2;\n\n  const [, segments] = Arr.mapAccum(\n    drawable,\n    start,\n    (angle, value): readonly [number, ReadonlyArray<ChartMark>] => {\n      if (value <= 0) return [angle, []];\n      const span = (value / total) * full;\n      if (span >= full - 1e-9) {\n        return [\n          angle + full,\n          [\n            {\n              kind: \"path\",\n              d: arcPath(cx, cy, outer, inner, angle, angle + Math.PI),\n            },\n            {\n              kind: \"path\",\n              d: arcPath(cx, cy, outer, inner, angle + Math.PI, angle + full),\n            },\n          ],\n        ];\n      }\n      return [\n        angle + span,\n        [\n          {\n            kind: \"path\",\n            d: arcPath(cx, cy, outer, inner, angle, angle + span),\n          },\n        ],\n      ];\n    },\n  );\n  return segments;\n};\n\nexport const donutMarks = (\n  values: ReadonlyArray<number>,\n  box: ChartBox,\n): ReadonlyArray<ChartMark> => Arr.flatten(donutSegments(values, box));\n\nexport const seriesName = (label: string, key: string): string =>\n  spokenText(Option.fromNullishOr(label), key);\n\nexport const seriesDescription = (\n  values: ReadonlyArray<number>,\n  labels: ReadonlyArray<string> = [],\n): string => {\n  const count = labels.length === 0 ? values.length : labels.length;\n  return Arr.join(\n    Arr.map(Arr.take(values, count), (value, index) => {\n      const readout = Number.isFinite(value)\n        ? `${Math.round(value * 100) / 100}`\n        : \"no value\";\n      const label = labels[index];\n      // an absent label already follows.\n      return P.isString(label) && label.trim().length > 0\n        ? `${label} ${readout}`\n        : readout;\n    }),\n    \", \",\n  );\n};\n\nexport const chartMarkup = (\n  marks: ReadonlyArray<ChartMark>,\n  box: ChartBox,\n  clip?: ChartMark[\"clip\"],\n): string => {\n  const flat = Arr.map(marks, (mark) => {\n    const shape = flatMark(mark);\n    return {\n      ...shape,\n      ...(clip === undefined ? {} : { clip }),\n      // A stroked line stays unfilled: filling it paints a wedge across its chord.\n      fill:\n        shape.fill === undefined && shape.stroke === undefined\n          ? { color: \"#000\" }\n          : shape.fill,\n    };\n  });\n  const painted = marksMarkup(flat);\n  const defs = painted.defs.length === 0 ? \"\" : `<defs>${painted.defs}</defs>`;\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${num(box.width)}\" height=\"${num(box.height)}\" viewBox=\"0 0 ${num(box.width)} ${num(box.height)}\">${defs}${painted.body}</svg>`;\n};\n\n/** The colour a series paints: the categorical scale, cycling. */\nexport const seriesColour = (\n  palette: ReadonlyArray<string>,\n  index: number,\n): string => {\n  if (palette.length === 0) return \"#000000\";\n  const at =\n    ((Math.floor(index) % palette.length) + palette.length) % palette.length;\n  return palette[at] ?? \"#000000\";\n};\n"
    },
    {
      "path": "packages/data/src/chart/mark.ts",
      "target": "@ui/gpuixui/data/chart/mark.ts",
      "type": "registry:lib",
      "content": "import { Match } from \"effect\";\nimport * as Arr from \"effect/Array\";\n\nexport interface MarkFill {\n  readonly color: string;\n  readonly opacity?: number;\n  readonly gradient?: string;\n}\n\nexport interface MarkStroke {\n  readonly color: string;\n  readonly width: number;\n  readonly opacity?: number;\n  readonly dash?: string;\n}\n\nexport interface MarkClip {\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nexport interface MarkPaint {\n  readonly fill?: MarkFill;\n  readonly stroke?: MarkStroke;\n  readonly clip?: MarkClip;\n}\n\nexport interface RectMark extends MarkPaint {\n  readonly kind: \"rect\";\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n  readonly rx?: number;\n}\n\nexport interface PathMark extends MarkPaint {\n  readonly kind: \"path\";\n  readonly d: string;\n}\n\n/** A circle, with its paint. */\nexport interface CircleMark extends MarkPaint {\n  readonly kind: \"circle\";\n  readonly cx: number;\n  readonly cy: number;\n  readonly r: number;\n}\n\n/** An ellipse, with its paint. */\nexport interface EllipseMark extends MarkPaint {\n  readonly kind: \"ellipse\";\n  readonly cx: number;\n  readonly cy: number;\n  readonly rx: number;\n  readonly ry: number;\n}\n\nexport type ChartMark = RectMark | PathMark | CircleMark | EllipseMark;\n\nexport const num = (value: number): string =>\n  Number.isFinite(value) ? `${Math.round(value * 100) / 100}` : \"0\";\n\n/** The paint attributes a mark carries, as the markup spells them. */\nconst paintAttributes = (mark: ChartMark): string => {\n  const parts: Array<string> = [];\n  parts.push(\n    mark.fill === undefined\n      ? 'fill=\"none\"'\n      : `fill=\"${\n          mark.fill.gradient === undefined\n            ? mark.fill.color\n            : `url(#${mark.fill.gradient})`\n        }\"`,\n  );\n  if (mark.fill?.opacity !== undefined && mark.fill.opacity < 1) {\n    parts.push(`fill-opacity=\"${num(mark.fill.opacity)}\"`);\n  }\n  if (mark.stroke !== undefined) {\n    parts.push(`stroke=\"${mark.stroke.color}\"`);\n    parts.push(`stroke-width=\"${num(mark.stroke.width)}\"`);\n    if (mark.stroke.opacity !== undefined && mark.stroke.opacity < 1) {\n      parts.push(`stroke-opacity=\"${num(mark.stroke.opacity)}\"`);\n    }\n    if (mark.stroke.dash !== undefined) {\n      parts.push(`stroke-dasharray=\"${mark.stroke.dash}\"`);\n    }\n    parts.push('stroke-linecap=\"round\"');\n    parts.push('stroke-linejoin=\"round\"');\n  }\n  return parts.join(\" \");\n};\n\nexport const markMarkup = (mark: ChartMark): string => {\n  const paint = paintAttributes(mark);\n  return Match.value(mark).pipe(\n    Match.when({ kind: \"rect\" }, (shape) => {\n      const radius = shape.rx === undefined ? \"\" : ` rx=\"${num(shape.rx)}\"`;\n      return `<rect x=\"${num(shape.x)}\" y=\"${num(shape.y)}\" width=\"${num(shape.width)}\" height=\"${num(shape.height)}\"${radius} ${paint}/>`;\n    }),\n    Match.when(\n      { kind: \"circle\" },\n      (shape) =>\n        `<circle cx=\"${num(shape.cx)}\" cy=\"${num(shape.cy)}\" r=\"${num(shape.r)}\" ${paint}/>`,\n    ),\n    Match.when(\n      { kind: \"ellipse\" },\n      (shape) =>\n        `<ellipse cx=\"${num(shape.cx)}\" cy=\"${num(shape.cy)}\" rx=\"${num(shape.rx)}\" ry=\"${num(shape.ry)}\" ${paint}/>`,\n    ),\n    Match.when({ kind: \"path\" }, (shape) => `<path d=\"${shape.d}\" ${paint}/>`),\n    Match.exhaustive,\n  );\n};\n\nexport const marksMarkup = (\n  marks: ReadonlyArray<ChartMark>,\n): { readonly defs: string; readonly body: string } => {\n  const indexed: ReadonlyArray<{\n    readonly mark: ChartMark;\n    readonly index: number;\n  }> = Arr.map(marks, (mark, index) => ({ mark, index }));\n  const defs = Arr.join(\n    Arr.flatMap(indexed, (entry) =>\n      entry.mark.clip === undefined\n        ? []\n        : [\n            `<clipPath id=\"chart-clip-${entry.index}\"><rect x=\"${num(entry.mark.clip.x)}\" y=\"${num(entry.mark.clip.y)}\" width=\"${num(entry.mark.clip.width)}\" height=\"${num(entry.mark.clip.height)}\"/></clipPath>`,\n          ],\n    ),\n    \"\",\n  );\n  const body = Arr.join(\n    Arr.map(marks, (mark, index) =>\n      mark.clip === undefined\n        ? markMarkup(mark)\n        : `<g clip-path=\"url(#chart-clip-${index})\">${markMarkup(mark)}</g>`,\n    ),\n    \"\",\n  );\n  return { defs, body };\n};\n\nexport const flatMark = (mark: ChartMark): ChartMark => ({\n  ...mark,\n  fill:\n    mark.fill === undefined ? undefined : { ...mark.fill, gradient: undefined },\n});\n\nexport const FALLBACK_MARK_COLOUR = \"#000000\";\n\nexport const lineStroke = (width: number, dashed = false): MarkStroke => ({\n  color: FALLBACK_MARK_COLOUR,\n  width,\n  dash: dashed ? `${num(width * 2)} ${num(width * 1.6)}` : undefined,\n});\n\nexport const solidFill = (color: string, opacity?: number): MarkFill => ({\n  color,\n  opacity,\n});\n"
    },
    {
      "path": "packages/data/src/chart/paint.ts",
      "target": "@ui/gpuixui/data/chart/paint.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport type { ChartBox } from \"./logic.js\";\nimport type { ChartMark } from \"./mark.js\";\nimport type { ChartDocument, ChartGradient } from \"./svg.js\";\nimport type { PaintLayer } from \"./frame.js\";\n\nexport interface LayerPaint {\n  readonly layer: PaintLayer;\n  /** The colour the layer's marks paint in. */\n  readonly colour: string;\n  /** The layer's own alpha, when it has one. */\n  readonly opacity?: number;\n}\n\nexport const layerMarks = (paint: LayerPaint): ReadonlyArray<ChartMark> =>\n  Arr.map(paint.layer.marks, (mark): ChartMark => {\n    const alpha = paint.opacity;\n    const clip =\n      paint.layer.clip === undefined ? {} : { clip: paint.layer.clip };\n    if (mark.fill === undefined && mark.stroke === undefined) {\n      // the geometry placed is never colourless.\n      return {\n        ...mark,\n        ...clip,\n        fill: { color: paint.colour, opacity: alpha },\n      };\n    }\n    return {\n      ...mark,\n      ...clip,\n      fill:\n        mark.fill === undefined\n          ? undefined\n          : {\n              ...mark.fill,\n              color:\n                mark.fill.color === \"#000000\" ? paint.colour : mark.fill.color,\n              opacity: mark.fill.opacity ?? alpha,\n            },\n      stroke:\n        mark.stroke === undefined\n          ? undefined\n          : {\n              ...mark.stroke,\n              color:\n                mark.stroke.color === \"#000000\"\n                  ? paint.colour\n                  : mark.stroke.color,\n            },\n    };\n  });\n\nexport const gridLayer = (\n  box: ChartBox,\n  tickValues: ReadonlyArray<number>,\n  range: { readonly min: number; readonly max: number },\n  vertical: ReadonlyArray<{ readonly x: number; readonly width: number }>,\n): PaintLayer => ({\n  key: \"grid\",\n  kind: \"grid\",\n  series: { key: \"grid\", label: \"grid\", values: [] },\n  seriesIndex: 0,\n  decorative: true,\n  opacity: 0.55,\n  marks: [\n    ...Arr.map(tickValues, (value) => ({\n      kind: \"rect\" as const,\n      x: 0,\n      y:\n        box.height -\n        ((value - range.min) / Math.max(1e-9, range.max - range.min)) *\n          box.height,\n      width: box.width,\n      height: 1,\n    })),\n    ...Arr.map(vertical, (rule) => ({\n      kind: \"rect\" as const,\n      x: rule.x,\n      y: 0,\n      width: rule.width,\n      height: box.height,\n    })),\n  ],\n});\n\nexport const chartDocument = (\n  box: ChartBox,\n  layers: ReadonlyArray<LayerPaint>,\n  gradients: ReadonlyArray<ChartGradient> = [],\n): ChartDocument => ({\n  box,\n  gradients,\n  marks: Arr.flatten(Arr.map(layers, (paint) => layerMarks(paint))),\n});\n\nexport const areaGradient = (\n  key: string,\n  colour: string,\n  opacity: number,\n): ChartGradient => ({\n  id: `chart-area-${key}`,\n  from: colour,\n  to: colour,\n  fromOpacity: opacity,\n  toOpacity: 0.1,\n});\n"
    },
    {
      "path": "packages/data/src/chart/plot.tsx",
      "target": "@ui/gpuixui/data/chart/plot.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\n\nimport * as Arr from \"effect/Array\";\nimport { chartDocument, type LayerPaint } from \"./paint.js\";\nimport { ChartMask } from \"./frame.js\";\nimport { ChartSurface, type ChartRender } from \"./surface.js\";\nimport type { ChartGradient } from \"./svg.js\";\nimport type { ChartBox, ChartMark } from \"./logic.js\";\nimport type { PaintLayer } from \"./frame.js\";\n\nexport interface ChartPlotProps {\n  readonly box: ChartBox;\n  readonly layers: ReadonlyArray<PaintLayer>;\n  readonly colours: ReadonlyArray<string>;\n  readonly grid?: PaintLayer;\n  readonly gridColour?: string;\n  readonly gradients?: ReadonlyArray<ChartGradient>;\n  readonly render?: ChartRender;\n  readonly testId?: string;\n  readonly labels?: ReadonlyArray<string>;\n  /** The values each layer's image describes. */\n  readonly descriptions?: ReadonlyArray<string>;\n}\n\nexport const ChartPlot = ({\n  box,\n  layers,\n  colours,\n  grid,\n  gridColour = \"#000000\",\n  gradients = [],\n  render = \"document\",\n  testId,\n  labels = [],\n  descriptions = [],\n}: ChartPlotProps) => {\n  const paints: ReadonlyArray<LayerPaint> = [\n    ...(grid === undefined\n      ? []\n      : [\n          {\n            layer: grid,\n            colour: gridColour,\n            opacity: grid.opacity,\n          },\n        ]),\n    ...Arr.map(layers, (layer, index) => ({\n      layer,\n      colour: colours[index] ?? \"#000000\",\n      opacity: layer.opacity,\n    })),\n  ];\n  if (render === \"mask\") {\n    return (\n      <>\n        {Arr.map(paints, (paint, index) => {\n          // of its own — the grid names nothing, so its slot is empty.\n          const named = grid === undefined || index > 0;\n          return (\n            <ChartMask\n              key={`${paint.layer.key}-${index}`}\n              chartTestId={testId}\n              layer={paint.layer}\n              label={named ? (labels[index - 1] ?? \"\") : \"\"}\n              description={named ? (descriptions[index - 1] ?? \"\") : \"\"}\n              colour={paint.colour}\n              box={box}\n            />\n          );\n        })}\n      </>\n    );\n  }\n  const document = chartDocument(box, paints, gradients);\n  return (\n    <>\n      <ChartSurface\n        document={document}\n        testId={testId === undefined ? undefined : `${testId}-document`}\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          pointerEvents: \"none\",\n        }}\n      />\n      {Arr.map(layers, (layer, index) => (\n        <ChartSeriesName\n          key={`${layer.key}-${layer.kind ?? \"main\"}-name`}\n          chartTestId={testId}\n          layer={layer}\n          label={labels[index] ?? \"\"}\n          description={descriptions[index] ?? \"\"}\n          colour={colours[index] ?? \"#000000\"}\n          box={box}\n        />\n      ))}\n    </>\n  );\n};\n\nexport const ChartSeriesName = ({\n  chartTestId,\n  layer,\n  label,\n  description,\n  colour,\n  box,\n}: {\n  readonly chartTestId: string | undefined;\n  readonly layer: PaintLayer;\n  readonly label: string;\n  readonly description: string;\n  readonly colour: string;\n  readonly box: ChartBox;\n}) => {\n  const decorative = layer.decorative === true;\n  return (\n    <div\n      role={decorative ? undefined : \"img\"}\n      aria-label={decorative ? undefined : label}\n      aria-description={decorative ? undefined : description}\n      testId={\n        chartTestId === undefined\n          ? undefined\n          : layer.kind === undefined\n            ? `${chartTestId}-layer-${layer.key}`\n            : `${chartTestId}-layer-${layer.key}-${layer.kind}`\n      }\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: box.width,\n        height: box.height,\n        color: colour,\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n};\n\n/** The marks a layer paints, for a caller that reads them directly. */\nexport const layerMarkList = (\n  layers: ReadonlyArray<PaintLayer>,\n): ReadonlyArray<ChartMark> =>\n  Arr.flatten(Arr.map(layers, (layer) => layer.marks));\n"
    },
    {
      "path": "packages/data/src/chart/recipe.ts",
      "target": "@ui/gpuixui/data/chart/recipe.ts",
      "type": "registry:lib",
      "content": "export const CHART_ROOT = \"flex flex-col gap-2\";\n\nexport const CHART_PLOT = \"relative shrink-0\";\n\nexport const CHART_AXIS = \"flex flex-col justify-between shrink-0\";\n\nexport const CHART_AXIS_LABEL = \"text-xs text-muted-foreground\";\n\nexport const CHART_CATEGORIES = \"flex flex-row shrink-0\";\n\nexport const CHART_CATEGORY =\n  \"flex flex-row justify-center overflow-hidden text-xs text-muted-foreground\";\n\nexport const CHART_CATEGORY_LABEL =\n  \"text-xs text-muted-foreground text-ellipsis whitespace-nowrap\";\n\n/** The legend: a row of swatches and labels. */\nexport const CHART_LEGEND =\n  \"flex flex-row flex-wrap items-center justify-center gap-3\";\n\nexport const CHART_LEGEND_ITEM = \"flex flex-row items-center gap-1.5\";\n\nexport const CHART_SWATCH = \"rounded-sm shrink-0\";\n\nexport const CHART_TOOLTIP_ZONE = \"shrink-0\";\n\nexport const CHART_TOOLTIP =\n  \"flex flex-col gap-1 rounded-md border border-border bg-popover p-2\";\n\nexport const CHART_TOOLTIP_HEAD = \"text-xs\";\n\nexport const CHART_TOOLTIP_ROW = \"flex flex-row items-center gap-1\";\n\nexport const CHART_TOOLTIP_VALUE = \"text-xs\";\n\nexport const CHART_VALUE_LABEL = \"text-xs\";\n\nexport const CHART_BRUSH = \"shrink-0\";\n\nexport const CHART_BRUSH_TRACK =\n  \"relative h-4 overflow-hidden rounded-sm bg-muted pointer-events-none\";\n\nexport const CHART_BRUSH_BAND =\n  \"top-0 bottom-0 rounded-sm opacity-40 pointer-events-none\";\n\nexport const CHART_BRUSH_HANDLE =\n  \"top-0 bottom-0 w-1 rounded-sm pointer-events-none\";\n"
    },
    {
      "path": "packages/data/src/chart/surface.tsx",
      "target": "@ui/gpuixui/data/chart/surface.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { ImgInstance, Props } from \"@gpuix/react\";\nimport { Effect } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\nimport { useLayoutEffect, useRef } from \"react\";\nimport type { ChartBox } from \"./logic.js\";\nimport { chartSvg, type ChartDocument } from \"./svg.js\";\n\nexport type ChartRender = \"document\" | \"mask\";\n\nexport class ChartDecodeError extends Schema.TaggedError<ChartDecodeError>()(\n  \"ChartDecodeError\",\n  { reason: Schema.String },\n  { description: \"The chart's SVG could not be decoded by the runtime.\" },\n) {}\n\nexport const documentDataUrl = (markup: string): string =>\n  `data:image/svg+xml;base64,${base64(markup)}`;\n\nconst base64 = (value: string): string => {\n  if (typeof Buffer !== \"undefined\") {\n    return Buffer.from(value, \"utf8\").toString(\"base64\");\n  }\n  const bytes = new TextEncoder().encode(value);\n  return btoa(\n    Arr.join(\n      Arr.map(Array.from(bytes), (byte) => String.fromCharCode(byte)),\n      \"\",\n    ),\n  );\n};\n\nexport const pushDocument = (\n  element: ImgInstance | null,\n  markup: string,\n): Effect.Effect<void, ChartDecodeError> =>\n  element === null\n    ? Effect.void\n    : Effect.try({\n        try: () => element.setImage(Buffer.from(markup, \"utf8\")),\n        catch: (cause) => new ChartDecodeError({ reason: String(cause) }),\n      });\n\nexport interface ChartSurfaceProps extends Omit<Props, \"children\"> {\n  readonly document: ChartDocument;\n\n  readonly markup?: string;\n\n  readonly render?: ChartRender;\n\n  readonly label?: string;\n\n  readonly description?: string;\n\n  readonly testId?: string;\n\n  readonly onDecodeError?: (failure: ChartDecodeError) => void;\n}\n\nexport const ChartSurface = ({\n  document,\n  markup,\n  render = \"document\",\n  label,\n  description,\n  testId,\n  onDecodeError,\n  style,\n  ...rest\n}: ChartSurfaceProps) => {\n  const instance = useRef<ImgInstance | null>(null);\n\n  const drawn = markup ?? chartSvg(document);\n  const box: ChartBox = document.box;\n  const source = render === \"document\" ? documentDataUrl(drawn) : undefined;\n\n  useLayoutEffect(() => {\n    if (render !== \"document\") return;\n    Effect.runSync(\n      pushDocument(instance.current, drawn).pipe(\n        Effect.catchTag(\"ChartDecodeError\", (failure) =>\n          Effect.sync(() => onDecodeError?.(failure)),\n        ),\n      ),\n    );\n  }, [drawn, render, onDecodeError]);\n\n  return (\n    <img\n      {...rest}\n      ref={instance}\n      src={source}\n      role=\"none\"\n      aria-label={label}\n      aria-description={description}\n      testId={testId}\n      style={{\n        width: box.width,\n        height: box.height,\n        ...style,\n      }}\n    />\n  );\n};\n\nexport const ChartMaskSurface = ({\n  document,\n  label,\n  description,\n  testId,\n  style,\n  ...rest\n}: Omit<ChartSurfaceProps, \"render\">) => {\n  const box = document.box;\n  return (\n    <svg\n      {...rest}\n      source={chartSvg(document)}\n      role={label === undefined ? undefined : \"img\"}\n      aria-label={label}\n      aria-description={description}\n      testId={testId}\n      style={{\n        width: box.width,\n        height: box.height,\n        ...style,\n      }}\n    />\n  );\n};\n\nexport const rendersDocument = (render: ChartRender | undefined): boolean =>\n  Option.getOrElse(Option.fromNullishOr(render), () => \"document\" as const) ===\n  \"document\";\n"
    },
    {
      "path": "packages/data/src/chart/svg.ts",
      "target": "@ui/gpuixui/data/chart/svg.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport type { ChartBox } from \"./logic.js\";\nimport type { ChartMark } from \"./mark.js\";\nimport { marksMarkup, num } from \"./mark.js\";\n\nexport interface ChartGradient {\n  readonly id: string;\n  readonly from: string;\n  readonly to: string;\n  readonly direction?: \"vertical\" | \"horizontal\";\n  readonly fromOpacity?: number;\n  readonly toOpacity?: number;\n}\n\nexport interface ChartDocument {\n  /** The plot's box, in pixels. */\n  readonly box: ChartBox;\n  /** The marks, back to front. */\n  readonly marks: ReadonlyArray<ChartMark>;\n  /** The gradients the marks refer to. */\n  readonly gradients?: ReadonlyArray<ChartGradient>;\n  readonly scale?: number;\n}\n\nexport const gradientMarkup = (gradient: ChartGradient): string => {\n  const horizontal = gradient.direction === \"horizontal\";\n  const x1 = horizontal ? \"0%\" : \"0%\";\n  const x2 = horizontal ? \"100%\" : \"0%\";\n  const y1 = horizontal ? \"0%\" : \"0%\";\n  const y2 = horizontal ? \"0%\" : \"100%\";\n  const stops = [\n    `<stop offset=\"0%\" stop-color=\"${gradient.from}\"${\n      gradient.fromOpacity === undefined\n        ? \"\"\n        : ` stop-opacity=\"${num(gradient.fromOpacity)}\"`\n    }/>`,\n    `<stop offset=\"100%\" stop-color=\"${gradient.to}\"${\n      gradient.toOpacity === undefined\n        ? \"\"\n        : ` stop-opacity=\"${num(gradient.toOpacity)}\"`\n    }/>`,\n  ].join(\"\");\n  return `<linearGradient id=\"${gradient.id}\" x1=\"${x1}\" y1=\"${y1}\" x2=\"${x2}\" y2=\"${y2}\">${stops}</linearGradient>`;\n};\n\nexport const defsMarkup = (\n  gradients: ReadonlyArray<ChartGradient>,\n  clips = \"\",\n): string =>\n  gradients.length === 0 && clips.length === 0\n    ? \"\"\n    : `<defs>${Arr.join(Arr.map(gradients, gradientMarkup), \"\")}${clips}</defs>`;\n\nexport const chartSvg = (document: ChartDocument): string => {\n  const scale =\n    document.scale === undefined || !Number.isFinite(document.scale)\n      ? 1\n      : Math.max(0.1, document.scale);\n  const width = document.box.width * scale;\n  const height = document.box.height * scale;\n  const viewBox = `0 0 ${num(document.box.width)} ${num(document.box.height)}`;\n  const marks = marksMarkup(document.marks);\n  return [\n    `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${num(width)}\" height=\"${num(height)}\" viewBox=\"${viewBox}\">`,\n    defsMarkup(document.gradients ?? [], marks.defs),\n    marks.body,\n    \"</svg>\",\n  ].join(\"\");\n};\n"
    },
    {
      "path": "packages/data/src/internal/list.ts",
      "target": "@ui/gpuixui/data/internal/list.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport * as Str from \"effect/String\";\n\nexport const listOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n): ReadonlyArray<A> => (Arr.isArray(value) ? value : []);\n\nexport const recordsOf = <A extends { readonly key: string }>(\n  value: ReadonlyArray<A> | undefined,\n): ReadonlyArray<A> =>\n  Arr.filter(\n    listOf(value),\n    (entry) => P.isObject(entry) && P.isString(entry.key),\n  );\n\nexport const textsOf = (\n  value: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> =>\n  Arr.map(listOf(value), (entry) => (P.isString(entry) ? entry : \"\"));\n\nconst namesRow = (value: unknown): boolean =>\n  P.isString(value) && Str.isNonEmpty(Str.trim(value));\n\nexport const rowsOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n  keyOf: (row: A) => string,\n): ReadonlyArray<A> =>\n  Arr.filter(listOf(value), (row) => P.isObject(row) && namesRow(keyOf(row)));\n"
    },
    {
      "path": "packages/data/src/recipe.ts",
      "target": "@ui/gpuixui/data/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(256);\n"
    },
    {
      "path": "packages/data/src/theme.ts",
      "target": "@ui/gpuixui/data/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  controlFocusRing,\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n} from \"../tokens/index.js\";\nexport type { Theme };\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    }
  ],
  "docs": "[Charts](https://gpuixui.sudokw.com/data-display/charts) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/data-display/charts",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}