{"name":"chart","type":"registry:ui","files":[{"path":"chart.tsx","type":"registry:ui","content":"import type { ComponentType, ReactNode } from \"react\";\nimport * as echarts from \"echarts/core\";\n\nexport const THEMES = { light: \"\", dark: \".dark\" } as const;\nexport type ThemeKey = keyof typeof THEMES;\nexport const THEME_KEYS = Object.keys(THEMES) as ThemeKey[];\n\nexport type AtLeastOneThemeColor =\n  | { light: string[]; dark?: string[] }\n  | { light?: string[]; dark: string[] };\n\nexport type ChartConfig = Record<\n  string,\n  {\n    label?: ReactNode;\n    icon?: ComponentType;\n    colors?: AtLeastOneThemeColor;\n  }\n>;\n\nexport function getColorsCount(item: ChartConfig[string]): number {\n  if (!item.colors) return 1;\n  const counts = THEME_KEYS.map((theme) => item.colors?.[theme]?.length ?? 0);\n  return Math.max(...counts, 1);\n}\n\nexport function distributeColors(colors: string[], maxCount: number): string[] {\n  const available = colors.length;\n  if (available >= maxCount) return colors.slice(0, maxCount);\n\n  const result: string[] = [];\n  const baseSlots = Math.floor(maxCount / available);\n  const extraSlots = maxCount % available;\n\n  for (let i = 0; i < available; i++) {\n    const isExtra = i >= available - extraSlots;\n    const slots = baseSlots + (isExtra ? 1 : 0);\n    for (let j = 0; j < slots; j++) result.push(colors[i]);\n  }\n\n  return result;\n}\n\nexport function buildChartCss(id: string, config: ChartConfig): string {\n  const colorConfig = Object.entries(config).filter(([, item]) => item.colors);\n  if (!colorConfig.length) return \"\";\n\n  const varsFor = (theme: ThemeKey) =>\n    colorConfig\n      .flatMap(([key, item]) => {\n        const authored = item.colors?.[theme];\n        if (!authored || authored.length === 0) return [];\n        return distributeColors(authored, getColorsCount(item)).map(\n          (color, index) => `  --color-${key}-${index}: ${color};`,\n        );\n      })\n      .join(\"\\n\");\n\n  return Object.entries(THEMES)\n    .map(([theme, prefix]) => `${prefix} [data-chart=${id}] {\\n${varsFor(theme as ThemeKey)}\\n}`)\n    .join(\"\\n\");\n}\n\nlet normalizerCtx: CanvasRenderingContext2D | null = null;\nexport function normalizeColor(value: string): string {\n  const raw = value.trim();\n  if (!raw || typeof document === \"undefined\") return raw;\n\n  if (!normalizerCtx) {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = 1;\n    canvas.height = 1;\n    normalizerCtx = canvas.getContext(\"2d\", { willReadFrequently: true });\n  }\n  if (!normalizerCtx) return raw;\n\n  normalizerCtx.clearRect(0, 0, 1, 1);\n  normalizerCtx.fillStyle = \"#000\";\n  normalizerCtx.fillStyle = raw; \n  normalizerCtx.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = normalizerCtx.getImageData(0, 0, 1, 1).data;\n  return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;\n}\n\nexport function withAlpha(color: string, alpha: number): string {\n  const match = color.match(/rgba?\\(([^)]+)\\)/);\n  if (!match) return color;\n  const [r, g, b, a] = match[1].split(\",\").map((p) => p.trim());\n  const base = a === undefined ? 1 : Number.parseFloat(a) || 0;\n  return `rgba(${r}, ${g}, ${b}, ${(base * alpha).toFixed(3)})`;\n}\n\nexport type ResolvedColors = {\n  series: Record<string, string[]>; \n  tokens: {\n    mutedForeground: string;\n    border: string;\n    foreground: string;\n    background: string;\n  };\n};\n\nexport function resolveColors(\n  container: HTMLElement,\n  config: ChartConfig,\n  seriesKeys: string[],\n): ResolvedColors {\n  const computed = getComputedStyle(container);\n  const series: Record<string, string[]> = {};\n\n  for (const key of seriesKeys) {\n    const count = getColorsCount(config[key] ?? {});\n    const slots: string[] = [];\n    for (let n = 0; n < count; n++) {\n      const raw = computed.getPropertyValue(`--color-${key}-${n}`).trim();\n      slots.push(raw ? normalizeColor(raw) : \"rgba(120, 120, 120, 1)\");\n    }\n    series[key] = slots;\n  }\n\n  const probe = document.createElement(\"span\");\n  probe.style.cssText = \"position:absolute;width:0;height:0;visibility:hidden;pointer-events:none;\";\n  container.appendChild(probe);\n  const readToken = (className: string) => {\n    probe.className = className;\n    return normalizeColor(getComputedStyle(probe).color);\n  };\n  const tokens = {\n    mutedForeground: readToken(\"text-muted-foreground\"),\n    border: readToken(\"text-border\"),\n    foreground: readToken(\"text-foreground\"),\n    background: readToken(\"text-background\"),\n  };\n  container.removeChild(probe);\n\n  return { series, tokens };\n}\n\nexport function seriesPaint(slots: string[]): string | echarts.graphic.LinearGradient {\n  if (slots.length <= 1) return slots[0] ?? \"rgba(120, 120, 120, 1)\";\n  const stops = slots.map((color, i) => ({ offset: i / (slots.length - 1), color }));\n  return new echarts.graphic.LinearGradient(0, 0, 1, 0, stops);\n}\n\nexport function indicatorBackground(key: string, colorsCount: number): string {\n  if (colorsCount <= 1) return `var(--color-${key}-0)`;\n  const stops = Array.from({ length: colorsCount }, (_, i) => {\n    const offset = (i / (colorsCount - 1)) * 100;\n    return `var(--color-${key}-${i}) ${offset}%`;\n  }).join(\", \");\n  return `linear-gradient(to right, ${stops})`;\n}\n\nexport function flattenColor(color: string, base: string): string {\n  const parse = (value: string) =>\n    value\n      .match(/rgba?\\(([^)]+)\\)/)?.[1]\n      .split(\",\")\n      .map((part) => Number.parseFloat(part)) ?? [0, 0, 0, 1];\n  const [r, g, b, a = 1] = parse(color);\n  const [baseR, baseG, baseB] = parse(base);\n  const mix = (channel: number, baseChannel: number) =>\n    Math.round(channel * a + baseChannel * (1 - a));\n  return `rgb(${mix(r, baseR)}, ${mix(g, baseG)}, ${mix(b, baseB)})`;\n}\n","target":"components/ui/chart.tsx"}],"dependencies":["echarts"]}