{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mini-chart",
  "title": "迷你图",
  "description": "放进卡片的极简图表：柱、折线、面积、阶梯线、进度环与环形图。无网格无纵轴，可选 11px 横轴，涨跌色随配色约定变化，尊重「减少动态效果」。",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "chart"
  ],
  "files": [
    {
      "path": "src/registry/ui/mini-chart/mini-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport {\n  Area,\n  AreaChart,\n  Bar,\n  BarChart,\n  Cell,\n  Line,\n  LineChart,\n  XAxis,\n  YAxis,\n} from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\"\nimport {\n  conventionClass,\n  formatStat,\n  toneColor,\n  trendOf,\n  type DataConvention,\n  type DataTone,\n  type StatFormatOptions,\n} from \"@/lib/data-display\"\n\nimport {\n  MiniDonut,\n  type MiniDonutProps,\n  type MiniDonutSegment,\n} from \"./mini-donut\"\nimport { MiniRing, type MiniRingProps } from \"./mini-ring\"\n\n/** 数据点：纯数字，或带标签的 `{ label, value }`（标签用于横轴与提示框）。 */\ntype MiniChartDatum = number | { label?: string; value: number }\n\ntype MiniChartType = \"bar\" | \"line\" | \"area\" | \"step\"\n\ntype MiniChartProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 数据点，按顺序排列 */\n  data: MiniChartDatum[]\n  /**\n   * 图形：`bar` 柱、`line` 折线、`area` 面积、`step` 阶梯线\n   * @default \"area\"\n   */\n  type?: MiniChartType\n  /**\n   * 色调。`\"trend\"` 按首尾两点的涨跌自动取涨色或跌色（随 `convention` 变化）\n   * @default \"primary\"\n   */\n  tone?: DataTone | \"trend\"\n  /** 涨跌配色约定，仅 `tone=\"trend\"` 时有用；不设置时沿用外层 */\n  convention?: DataConvention\n  /**\n   * 显示横轴（11px 的极简刻度，只保留首尾与间隔均匀的标签）\n   * @default false\n   */\n  showAxis?: boolean\n  /** 横轴刻度文字的格式化，参数是数据点的 `label` */\n  tickFormatter?: (label: string, index: number) => string\n  /**\n   * 悬停提示框\n   * @default true\n   */\n  showTooltip?: boolean\n  /** 提示框里数值的格式 */\n  format?: StatFormatOptions\n  /**\n   * 提示框里的系列名\n   * @default \"数值\"\n   */\n  seriesLabel?: string\n  /** 柱图里强调的那一根：下标，`\"last\"` 最后一根，`\"max\"` 最大值；其余柱变淡 */\n  highlight?: number | \"last\" | \"max\"\n  /**\n   * 柱图每根柱的底槽（淡色背景），像进度槽一样表示满值\n   * @default false\n   */\n  track?: boolean\n  /**\n   * 线宽，仅折线、面积与阶梯线\n   * @default 2\n   */\n  strokeWidth?: number\n  /** 读屏名称，描述这张图表示什么 */\n  label?: string\n}\n\nconst AXIS_TICK = { fontSize: 11, fill: \"var(--muted-foreground)\" } as const\n\n/** 极简迷你图：没有网格、没有纵轴，适合放进统计卡片；高度用 `className` 控制（默认 `h-16`）。 */\nfunction MiniChart({\n  className,\n  data,\n  type = \"area\",\n  tone = \"primary\",\n  convention,\n  showAxis = false,\n  tickFormatter,\n  showTooltip = true,\n  format,\n  seriesLabel = \"数值\",\n  highlight,\n  track = false,\n  strokeWidth = 2,\n  label,\n  ...props\n}: MiniChartProps) {\n  const ready = React.useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false,\n  )\n  const reduced = useReducedMotion()\n  const gradientId = React.useId().replaceAll(\":\", \"\")\n  const points = data.map((datum, index) =>\n    typeof datum === \"number\"\n      ? { label: String(index + 1), value: datum }\n      : { label: datum.label ?? String(index + 1), value: datum.value },\n  )\n  const first = points[0]?.value ?? 0\n  const last = points.at(-1)?.value ?? 0\n  const resolved: DataTone =\n    tone === \"trend\"\n      ? ({ up: \"up\", down: \"down\", flat: \"muted\" } as const)[\n          trendOf(last - first)\n        ]\n      : tone\n  const config = {\n    value: { label: seriesLabel, color: toneColor(resolved) },\n  } satisfies ChartConfig\n  const focus =\n    highlight === \"last\"\n      ? points.length - 1\n      : highlight === \"max\"\n        ? points.findIndex(\n            (point) => point.value === Math.max(...points.map((p) => p.value)),\n          )\n        : highlight\n\n  const margin = { top: 4, right: 2, bottom: 0, left: 2 }\n  const active = !reduced\n  const axis = showAxis ? (\n    <XAxis\n      dataKey=\"label\"\n      tickLine={false}\n      axisLine={false}\n      tickMargin={6}\n      minTickGap={12}\n      interval=\"preserveStartEnd\"\n      tick={AXIS_TICK}\n      tickFormatter={tickFormatter}\n    />\n  ) : (\n    <XAxis dataKey=\"label\" hide />\n  )\n  const tooltip = showTooltip ? (\n    <ChartTooltip\n      cursor={false}\n      content={\n        <ChartTooltipContent\n          className=\"min-w-28 shadow-sm\"\n          formatter={(value) => (\n            <>\n              <span\n                aria-hidden=\"true\"\n                className=\"size-2 shrink-0 rounded-[2px]\"\n                style={{ backgroundColor: toneColor(resolved) }}\n              />\n              <span className=\"text-muted-foreground\">{seriesLabel}</span>\n              <span className=\"ml-auto font-mono font-medium text-foreground tabular-nums\">\n                {formatStat(Number(value), format)}\n              </span>\n            </>\n          )}\n        />\n      }\n    />\n  ) : null\n  const color = \"var(--color-value)\"\n\n  return (\n    <div\n      data-slot=\"mini-chart\"\n      data-ready={ready}\n      data-type={type}\n      role=\"img\"\n      aria-label={label}\n      className={cn(\n        \"relative flex h-16 w-full min-w-0 text-xs\",\n        conventionClass(convention),\n        className,\n      )}\n      {...props}\n    >\n      <ChartContainer\n        data-slot=\"mini-chart-plot\"\n        data-ready={ready}\n        config={config}\n        initialDimension={{ width: 160, height: 64 }}\n        className=\"aspect-auto size-full justify-start opacity-0 transition-opacity duration-200 data-[ready=true]:opacity-100 motion-reduce:transition-none\"\n      >\n        {type === \"bar\" ? (\n          <BarChart data={points} margin={margin} barCategoryGap=\"22%\">\n            {axis}\n            <YAxis hide domain={[0, \"dataMax\"]} />\n            {tooltip}\n            <Bar\n              dataKey=\"value\"\n              fill={color}\n              radius={[3, 3, 0, 0]}\n              maxBarSize={14}\n              background={\n                track ? { fill: \"var(--muted)\", radius: 3 } : undefined\n              }\n              isAnimationActive={active}\n              animationDuration={600}\n            >\n              {points.map((point, index) => (\n                <Cell\n                  key={index}\n                  fill={color}\n                  fillOpacity={\n                    focus === undefined || focus === index ? 1 : 0.28\n                  }\n                />\n              ))}\n            </Bar>\n          </BarChart>\n        ) : type === \"area\" ? (\n          <AreaChart data={points} margin={margin}>\n            <defs>\n              <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop offset=\"0%\" stopColor={color} stopOpacity={0.28} />\n                <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n              </linearGradient>\n            </defs>\n            {axis}\n            <YAxis hide domain={[\"dataMin\", \"dataMax\"]} />\n            {tooltip}\n            <Area\n              dataKey=\"value\"\n              type=\"monotone\"\n              stroke={color}\n              strokeWidth={strokeWidth}\n              fill={`url(#${gradientId})`}\n              activeDot={{ r: 3, strokeWidth: 0 }}\n              isAnimationActive={active}\n              animationDuration={700}\n            />\n          </AreaChart>\n        ) : (\n          <LineChart data={points} margin={margin}>\n            {axis}\n            <YAxis hide domain={[\"dataMin\", \"dataMax\"]} />\n            {tooltip}\n            <Line\n              dataKey=\"value\"\n              type={type === \"step\" ? \"stepAfter\" : \"monotone\"}\n              stroke={color}\n              strokeWidth={strokeWidth}\n              dot={false}\n              activeDot={{ r: 3, strokeWidth: 0 }}\n              isAnimationActive={active}\n              animationDuration={700}\n            />\n          </LineChart>\n        )}\n      </ChartContainer>\n      <div\n        data-slot=\"mini-chart-placeholder\"\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 min-h-0 bg-muted/30 transition-opacity duration-200 motion-reduce:transition-none\",\n          ready && \"opacity-0\",\n        )}\n      >\n        <svg\n          className=\"absolute inset-0 size-full text-muted-foreground/20\"\n          preserveAspectRatio=\"none\"\n          viewBox=\"0 0 100 100\"\n        >\n          <path\n            d=\"M0 25H100 M0 50H100 M0 75H100\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeDasharray=\"3 3\"\n          />\n        </svg>\n      </div>\n    </div>\n  )\n}\n\nconst subscribe = () => () => {}\n\nexport { MiniChart, MiniDonut, MiniRing }\nexport type {\n  MiniChartDatum,\n  MiniChartProps,\n  MiniChartType,\n  MiniDonutProps,\n  MiniDonutSegment,\n  MiniRingProps,\n}\n",
      "type": "registry:ui",
      "target": "@ui/mini-chart/mini-chart.tsx"
    },
    {
      "path": "src/registry/ui/mini-chart/mini-ring.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  formatStat,\n  motionStyles,\n  toneColor,\n  type DataTone,\n} from \"@/lib/data-display\"\n\ntype MiniRingProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 当前值 */\n  value: number\n  /**\n   * 满值\n   * @default 100\n   */\n  max?: number\n  /**\n   * 进度弧的色调\n   * @default \"primary\"\n   */\n  tone?: DataTone\n  /**\n   * 外径（像素）\n   * @default 72\n   */\n  size?: number\n  /**\n   * 环的粗细，占外径的百分比\n   * @default 10\n   */\n  thickness?: number\n  /**\n   * 圆弧的总角度：360 是整圆，270 是下方留口的仪表\n   * @default 360\n   */\n  arc?: number\n  /** 环中央的内容，默认是取整后的百分比 */\n  children?: React.ReactNode\n  /** 读屏名称 */\n  label?: string\n}\n\n/**\n * 进度环：纯 SVG，比用图表库画一圈更轻也更清晰。\n * 服务端直接输出进度弧的最终长度，入场动画是纯 CSS，没有脚本也完整显示；系统开启「减少动态效果」时不播放。\n */\nfunction MiniRing({\n  className,\n  value,\n  max = 100,\n  tone = \"primary\",\n  size = 72,\n  thickness = 10,\n  arc = 360,\n  children,\n  label,\n  style,\n  ...props\n}: MiniRingProps) {\n  const ratio = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0\n  const sweep = Math.min(360, Math.max(30, arc)) / 360\n  // 圆从 3 点钟方向顺时针画：整圆从 12 点开始，开口仪表让缺口落在正下方\n  const rotation = sweep === 1 ? -90 : 90 + (360 * (1 - sweep)) / 2\n  const radius = 50 - thickness / 2\n  const common = {\n    cx: 50,\n    cy: 50,\n    r: radius,\n    fill: \"none\",\n    strokeWidth: thickness,\n    strokeLinecap: \"round\",\n    transform: `rotate(${rotation} 50 50)`,\n    pathLength: 1,\n  } as const\n  return (\n    <div\n      data-slot=\"mini-ring\"\n      role=\"meter\"\n      aria-label={label}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      aria-valuenow={value}\n      className={cn(\n        \"relative flex shrink-0 items-center justify-center\",\n        className,\n      )}\n      style={{ width: size, height: size, ...style }}\n      {...props}\n    >\n      {motionStyles()}\n      <svg\n        viewBox=\"0 0 100 100\"\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 size-full\"\n      >\n        <circle\n          {...common}\n          stroke=\"var(--edge)\"\n          strokeDasharray={`${sweep} 1`}\n        />\n        <circle\n          {...common}\n          data-dd-anim=\"arc\"\n          stroke={toneColor(tone)}\n          strokeDasharray={`${ratio * sweep} 1`}\n        />\n      </svg>\n      <div\n        data-slot=\"mini-ring-content\"\n        className=\"relative flex flex-col items-center text-center leading-tight tabular-nums\"\n      >\n        {children ?? (\n          <span className=\"text-sm font-semibold\">\n            {formatStat(ratio * 100, { style: \"percent\", digits: 0 })}\n          </span>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { MiniRing }\nexport type { MiniRingProps }\n",
      "type": "registry:ui",
      "target": "@ui/mini-chart/mini-ring.tsx"
    },
    {
      "path": "src/registry/ui/mini-chart/mini-donut.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { Pie, PieChart } from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\"\nimport {\n  SERIES_TONES,\n  formatStat,\n  toneColor,\n  type DataTone,\n  type StatFormatOptions,\n} from \"@/lib/data-display\"\n\ntype MiniDonutSegment = {\n  /** 分类名，同时是提示框里的名称 */\n  label: string\n  value: number\n  /** 色调，不设置时按顺序取默认色板 */\n  tone?: DataTone\n}\n\ntype MiniDonutProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 分段数据 */\n  segments: MiniDonutSegment[]\n  /**\n   * 外径（像素）\n   * @default 96\n   */\n  size?: number\n  /**\n   * 环的粗细，占半径的百分比\n   * @default 30\n   */\n  thickness?: number\n  /** 悬停提示框里数值的格式 */\n  format?: StatFormatOptions\n  /** 环中央的内容，如合计数字 */\n  children?: React.ReactNode\n  /** 读屏名称 */\n  label?: string\n}\n\n/** 环形图：几个分类的占比，中央可放合计。分段之间留 2° 的缝。 */\nfunction MiniDonut({\n  className,\n  segments,\n  size = 96,\n  thickness = 30,\n  format,\n  children,\n  label,\n  style,\n  ...props\n}: MiniDonutProps) {\n  const ready = React.useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false,\n  )\n  const reduced = useReducedMotion()\n  const config: ChartConfig = {}\n  const data = segments.map((segment, index) => {\n    const color = toneColor(\n      segment.tone ?? SERIES_TONES[index % SERIES_TONES.length] ?? \"blue\",\n    )\n    config[segment.label] = { label: segment.label, color }\n    return { label: segment.label, value: segment.value, fill: color }\n  })\n  return (\n    <div\n      data-slot=\"mini-donut\"\n      data-ready={ready}\n      role=\"img\"\n      aria-label={label}\n      className={cn(\"relative shrink-0\", className)}\n      style={{ width: size, height: size, ...style }}\n      {...props}\n    >\n      <ChartContainer\n        config={config}\n        initialDimension={{ width: size, height: size }}\n        className={cn(\n          \"aspect-auto size-full transition-opacity duration-200 motion-reduce:transition-none\",\n          !ready && \"opacity-0\",\n        )}\n      >\n        <PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>\n          <ChartTooltip\n            cursor={false}\n            content={\n              <ChartTooltipContent\n                hideLabel\n                nameKey=\"label\"\n                className=\"min-w-28 shadow-sm\"\n                formatter={(value, name, item) => (\n                  <>\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"size-2 shrink-0 rounded-[2px]\"\n                      style={{ backgroundColor: item.payload?.fill }}\n                    />\n                    <span className=\"text-muted-foreground\">{name}</span>\n                    <span className=\"ml-auto font-mono font-medium text-foreground tabular-nums\">\n                      {formatStat(Number(value), format)}\n                    </span>\n                  </>\n                )}\n              />\n            }\n          />\n          <Pie\n            data={data}\n            dataKey=\"value\"\n            nameKey=\"label\"\n            innerRadius={`${100 - thickness}%`}\n            outerRadius=\"100%\"\n            paddingAngle={2}\n            cornerRadius={3}\n            strokeWidth={0}\n            isAnimationActive={!reduced}\n            animationDuration={700}\n          />\n        </PieChart>\n      </ChartContainer>\n      <div\n        data-slot=\"mini-donut-placeholder\"\n        style={{ borderWidth: (size * thickness) / 200 }}\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-0 rounded-full border-muted/50 transition-opacity duration-200 motion-reduce:transition-none\",\n          ready && \"opacity-0\",\n        )}\n      />\n      {children ? (\n        <div\n          data-slot=\"mini-donut-content\"\n          className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center leading-tight tabular-nums\"\n        >\n          {children}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\nconst subscribe = () => () => {}\n\nexport { MiniDonut }\nexport type { MiniDonutProps, MiniDonutSegment }\n",
      "type": "registry:ui",
      "target": "@ui/mini-chart/mini-donut.tsx"
    },
    {
      "path": "src/registry/lib/data-display.ts",
      "content": "import * as React from \"react\"\n\n/**\n * 数据展示共用工具：涨跌配色约定、色调、数字与日期格式化。\n * stat-card、mini-chart、chart-card 三个条目共用这一份，所以涨跌色、数字格式只在这里定义。\n */\n\n/**\n * 涨跌配色约定：\n * - `intl`（默认）：绿涨红跌；\n * - `cn`：红涨绿跌，国内行情软件的习惯。\n */\ntype DataConvention = \"intl\" | \"cn\"\n\n// 约定只改写 `--stat-up` / `--stat-down` 两个 CSS 变量。变量沿 DOM 向下继承，\n// 所以给整块看板的外层设一次 `convention`，里面所有涨跌色（包括图表里的 SVG）都跟着变，\n// 不需要 context，服务端组件也能用。没有设置时回落到默认的绿涨红跌。\nconst CONVENTION_CLASSES: Record<DataConvention, string> = {\n  intl: \"[--stat-up:var(--flow-green,var(--primary))] [--stat-down:var(--flow-rose,var(--destructive))]\",\n  cn: \"[--stat-up:var(--flow-rose,var(--destructive))] [--stat-down:var(--flow-green,var(--primary))]\",\n}\n\n/** 把 `convention` 换成要加到根元素上的类名；不传返回 `undefined`，沿用外层的约定。 */\nfunction conventionClass(convention?: DataConvention) {\n  return convention ? CONVENTION_CLASSES[convention] : undefined\n}\n\ntype Trend = \"up\" | \"down\" | \"flat\"\n\nfunction trendOf(value: number): Trend {\n  return value > 0 ? \"up\" : value < 0 ? \"down\" : \"flat\"\n}\n\n/** 涨跌文字色（Tailwind 只认完整类名，所以三种写全）。 */\nconst TREND_TEXT: Record<Trend, string> = {\n  up: \"text-[color:var(--stat-up,var(--flow-green,var(--primary)))]\",\n  down: \"text-[color:var(--stat-down,var(--flow-rose,var(--destructive)))]\",\n  flat: \"text-muted-foreground\",\n}\n\n/**\n * 内置色调：\n * - `primary`、`foreground`、`muted`、`destructive` 直接引用主题变量，跟随颜色主题；\n * - `blue` 等多彩色调引用 `--flow-*` 变量，没有安装时回落到主题强调色；\n * - `chart-1` … `chart-5` 引用主题的图表色板；\n * - `up` / `down` 是涨跌色，随 `convention` 变化。\n */\ntype DataToneName =\n  | \"primary\"\n  | \"foreground\"\n  | \"muted\"\n  | \"destructive\"\n  | \"blue\"\n  | \"green\"\n  | \"amber\"\n  | \"rose\"\n  | \"violet\"\n  | \"cyan\"\n  | \"chart-1\"\n  | \"chart-2\"\n  | \"chart-3\"\n  | \"chart-4\"\n  | \"chart-5\"\n  | \"up\"\n  | \"down\"\n\n/** 色调：内置名，或任意 CSS 颜色（`\"var(--brand)\"`、`\"oklch(0.7 0.2 150)\"` 都可以）。 */\ntype DataTone = DataToneName | (string & {})\n\nconst TONES: Record<DataToneName, string> = {\n  primary: \"var(--primary)\",\n  foreground: \"var(--foreground)\",\n  muted: \"var(--muted-foreground)\",\n  destructive: \"var(--destructive)\",\n  blue: \"var(--flow-blue, var(--primary))\",\n  green: \"var(--flow-green, var(--primary))\",\n  amber: \"var(--flow-amber, var(--primary))\",\n  rose: \"var(--flow-rose, var(--destructive))\",\n  violet: \"var(--flow-violet, var(--primary))\",\n  cyan: \"var(--flow-cyan, var(--primary))\",\n  \"chart-1\": \"var(--chart-1)\",\n  \"chart-2\": \"var(--chart-2)\",\n  \"chart-3\": \"var(--chart-3)\",\n  \"chart-4\": \"var(--chart-4)\",\n  \"chart-5\": \"var(--chart-5)\",\n  up: \"var(--stat-up, var(--flow-green, var(--primary)))\",\n  down: \"var(--stat-down, var(--flow-rose, var(--destructive)))\",\n}\n\n/** 把色调换成可直接用于 `fill` / `stroke` / `color` 的 CSS 颜色。 */\nfunction toneColor(tone: DataTone): string {\n  return Object.hasOwn(TONES, tone) ? TONES[tone as DataToneName] : tone\n}\n\n/**\n * 多序列图表默认的色调轮换：六种互不相近的多彩色调。\n * 不把主题强调色放进来：强调色会落在蓝、绿、紫、琥珀任意一种附近，\n * 切到对应的颜色主题时会与其中一条序列撞色。单条序列想跟随主题，显式写 `tone=\"primary\"`。\n */\nconst SERIES_TONES: DataTone[] = [\n  \"blue\",\n  \"amber\",\n  \"green\",\n  \"violet\",\n  \"rose\",\n  \"cyan\",\n]\n\ntype StatFormatOptions = {\n  /**\n   * 数字样式：`number` 普通数字，`percent` 百分比（传入的 12.5 表示 12.5%），`currency` 货币\n   * @default \"number\"\n   */\n  style?: \"number\" | \"percent\" | \"currency\"\n  /**\n   * BCP 47 区域标识；`zh-CN` 压缩成「万 / 亿」，`en` 压缩成 K / M / B\n   * @default \"zh-CN\"\n   */\n  locale?: string\n  /**\n   * 压缩大数（1.2万、3.4亿）；百分比不压缩\n   * @default true\n   */\n  compact?: boolean\n  /**\n   * 货币代码，仅 `style=\"currency\"` 生效；人民币显示 ¥\n   * @default \"CNY\"\n   */\n  currency?: string\n  /**\n   * 最多保留的小数位\n   * @default 1（压缩与百分比），2（普通数字与货币）\n   */\n  digits?: number\n  /**\n   * 正数前加 `+`（零不加），用于涨跌幅\n   * @default false\n   */\n  sign?: boolean\n}\n\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * 格式化统计数字，默认 `zh-CN` 压缩：`12480` → `1.2万`，`345000000` → `3.5亿`；\n * `locale: \"en\"` 时为 `12K`、`345M`。同参数的 `Intl.NumberFormat` 会被缓存复用。\n */\nfunction formatStat(value: number, options: StatFormatOptions = {}): string {\n  const {\n    style = \"number\",\n    locale = \"zh-CN\",\n    compact = style !== \"percent\",\n    currency = \"CNY\",\n    sign = false,\n  } = options\n  const digits = options.digits ?? (compact || style === \"percent\" ? 1 : 2)\n  const key = [style, locale, compact, currency, digits, sign].join(\"|\")\n  let formatter = formatters.get(key)\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, {\n      style: style === \"number\" ? \"decimal\" : style,\n      currency: style === \"currency\" ? currency : undefined,\n      currencyDisplay: style === \"currency\" ? \"narrowSymbol\" : undefined,\n      notation: compact && style !== \"percent\" ? \"compact\" : \"standard\",\n      maximumFractionDigits: digits,\n      signDisplay: sign ? \"exceptZero\" : \"auto\",\n    })\n    formatters.set(key, formatter)\n  }\n  return formatter.format(style === \"percent\" ? value / 100 : value)\n}\n\ntype DayStyle = \"long\" | \"short\" | \"weekday\"\n\nconst dayFormatters = new Map<string, Intl.DateTimeFormat>()\n\n/**\n * 格式化日期轴文字：`long` → `2月14日`，`short` → `02/14`，`weekday` → `一`（单字星期）。\n * 日期按 UTC 解析与输出，`\"2026-02-14\"` 这类字符串在任何时区、任何环境下结果一致，不会引起水合差异。\n */\nfunction formatDay(\n  input: string | number | Date,\n  style: DayStyle = \"long\",\n  locale = \"zh-CN\",\n): string {\n  const key = `${style}|${locale}`\n  let formatter = dayFormatters.get(key)\n  if (!formatter) {\n    const options: Intl.DateTimeFormatOptions =\n      style === \"weekday\"\n        ? { weekday: \"narrow\" }\n        : style === \"short\"\n          ? { month: \"2-digit\", day: \"2-digit\" }\n          : { month: \"long\", day: \"numeric\" }\n    formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" })\n    dayFormatters.set(key, formatter)\n  }\n  return formatter.format(new Date(input))\n}\n\ntype AxisFormatOptions = Pick<\n  StatFormatOptions,\n  \"style\" | \"locale\" | \"currency\" | \"digits\"\n>\n\n// 压缩单位表：从大到小，命中第一个不超过轴最大值的单位\nconst AXIS_UNITS = {\n  zh: [\n    [1e8, \"亿\"],\n    [1e4, \"万\"],\n  ],\n  \"zh-trad\": [\n    [1e8, \"億\"],\n    [1e4, \"萬\"],\n  ],\n  en: [\n    [1e9, \"B\"],\n    [1e6, \"M\"],\n    [1e3, \"K\"],\n  ],\n} as const\n\n/**\n * 按「整条轴」统一单位的刻度格式化：先用轴的最大值选定一个单位，所有刻度都换算成它，\n * 避免 `9000` 与 `1.2万` 混在同一根轴上。例如最大值 12000 时刻度是 `0`、`0.3万`、`0.6万`、`0.9万`、`1.2万`；\n * 最大值 8000 时全部是整数（`2000`、`4000`…）。`en` 同理换成 K / M / B。\n *\n * @param max 轴上数据的最大绝对值；留 15% 余量，因为图表会把坐标轴上沿取整到比数据更大的刻度\n */\nfunction createAxisFormatter(\n  max: number,\n  options: AxisFormatOptions = {},\n): (value: number) => string {\n  const { style = \"number\", locale = \"zh-CN\", currency = \"CNY\" } = options\n  if (style === \"percent\")\n    return (value) =>\n      formatStat(value, { style: \"percent\", locale, digits: options.digits })\n  const family = /^zh-(TW|HK|MO|Hant)/i.test(locale)\n    ? \"zh-trad\"\n    : /^zh/i.test(locale)\n      ? \"zh\"\n      : \"en\"\n  const reach = Math.abs(max) * 1.15\n  const unit = AXIS_UNITS[family].find(([size]) => reach >= size)\n  const divisor = unit?.[0] ?? 1\n  const suffix = unit?.[1] ?? \"\"\n  const formatter = new Intl.NumberFormat(locale, {\n    style: style === \"currency\" ? \"currency\" : \"decimal\",\n    currency: style === \"currency\" ? currency : undefined,\n    currencyDisplay: style === \"currency\" ? \"narrowSymbol\" : undefined,\n    notation: \"standard\",\n    maximumFractionDigits: options.digits ?? 2,\n  })\n  return (value) =>\n    value === 0\n      ? formatter.format(0)\n      : `${formatter.format(value / divisor)}${suffix}`\n}\n\n// 进度条、计量条、分段条、进度环、刻度仪表的入场动画：纯 CSS，服务端直接输出最终状态，\n// 所以没有脚本时也完整显示；系统开启「减少动态效果」时不播放。\nconst MOTION_CSS = `@media (prefers-reduced-motion: no-preference) {\n@keyframes dd-reveal { from { clip-path: inset(0 100% 0 0 round 9999px) } to { clip-path: inset(0 0 0 0 round 9999px) } }\n@keyframes dd-rise { from { opacity: 0; transform: scaleY(0.35) } }\n@keyframes dd-fade { from { opacity: 0 } }\n@keyframes dd-arc { from { stroke-dasharray: 0 1 } }\n[data-dd-anim=\"reveal\"] { animation: dd-reveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"rise\"] { animation: dd-rise 0.3s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"fade\"] { animation: dd-fade 0.35s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"arc\"] { animation: dd-arc 0.9s cubic-bezier(0.22, 1, 0.36, 1) both }\n}`\n\n/**\n * 渲染上面的动画样式。React 19 会按 `href` 去重并提升到 `<head>`，\n * 同一页放多少个组件都只有一份；放在服务端组件里也能用。\n */\nfunction motionStyles() {\n  return React.createElement(\n    \"style\",\n    { href: \"data-display-motion\", precedence: \"default\" },\n    MOTION_CSS,\n  )\n}\n\nexport {\n  TREND_TEXT,\n  SERIES_TONES,\n  conventionClass,\n  createAxisFormatter,\n  formatDay,\n  formatStat,\n  motionStyles,\n  toneColor,\n  trendOf,\n}\nexport type {\n  AxisFormatOptions,\n  DataConvention,\n  DataTone,\n  DataToneName,\n  DayStyle,\n  StatFormatOptions,\n  Trend,\n}\n",
      "type": "registry:ui",
      "target": "lib/data-display.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "flow-blue": "oklch(0.56 0.2 258)",
      "flow-green": "oklch(0.62 0.16 150)",
      "flow-amber": "oklch(0.7 0.16 70)",
      "flow-rose": "oklch(0.62 0.21 5)",
      "flow-violet": "oklch(0.56 0.22 292)",
      "flow-cyan": "oklch(0.66 0.12 215)",
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "flow-blue": "oklch(0.72 0.15 255)",
      "flow-green": "oklch(0.77 0.17 152)",
      "flow-amber": "oklch(0.82 0.15 82)",
      "flow-rose": "oklch(0.74 0.16 8)",
      "flow-violet": "oklch(0.74 0.15 295)",
      "flow-cyan": "oklch(0.82 0.11 208)",
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
