{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "chart",
"type": "registry:ui",
"title": "Chart",
"description": "Skia-rendered charts: BarChart, LineChart, DonutChart + ChartLegend.",
"registryDependencies": [
"https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json",
"https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json",
"https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json"
],
"dependencies": [
"@shopify/react-native-skia@2.6.2"
],
"files": [
{
"path": "registry/shared/ui/chart.tsx",
"type": "registry:ui",
"target": "components/ui/chart.tsx",
"content": "import { useMemo, useState } from 'react';\nimport { useColorScheme, View, type ViewProps } from 'react-native';\nimport {\n Canvas,\n Circle,\n Path,\n RoundedRect,\n Skia,\n} from '@shopify/react-native-skia';\nimport { Text } from '@/components/ui/text';\nimport { cn } from '@/lib/utils';\n\nconst LIGHT_PALETTE = ['#18181b', '#525252', '#a3a3a3', '#d4d4d4', '#0ea5e9'];\nconst DARK_PALETTE = ['#fafafa', '#a3a3a3', '#737373', '#525252', '#38bdf8'];\n\nexport interface ChartDatum {\n label: string;\n value: number;\n /** Per-bar/slice color override. */\n color?: string;\n}\n\ninterface CommonChartProps extends ViewProps {\n className?: string;\n /** Palette override — index-mapped per datum. */\n colors?: string[];\n}\n\nfunction useChartColors(override?: string[]) {\n const dark = useColorScheme() === 'dark';\n return { palette: override ?? (dark ? DARK_PALETTE : LIGHT_PALETTE) };\n}\n\nfunction useWidth(): [\n number,\n (e: { nativeEvent: { layout: { width: number } } }) => void,\n] {\n const [w, setW] = useState(0);\n return [w, (e) => setW(e.nativeEvent.layout.width)];\n}\n\n// ---------- Bar chart ----------\n\nexport interface BarChartProps extends CommonChartProps {\n data: ChartDatum[];\n height?: number;\n barRadius?: number;\n showLabels?: boolean;\n}\n\nexport function BarChart({\n data,\n height = 160,\n barRadius = 4,\n showLabels = true,\n colors,\n className,\n ...props\n}: BarChartProps) {\n const { palette } = useChartColors(colors);\n const [width, onLayout] = useWidth();\n const max = Math.max(...data.map((d) => d.value), 1);\n\n const bars = useMemo(() => {\n if (!width || !data.length) return [];\n const slot = width / data.length;\n const barWidth = Math.min(slot * 0.55, 56);\n return data.map((d, i) => {\n const h = Math.max((d.value / max) * (height - 4), 2);\n return {\n x: slot * i + (slot - barWidth) / 2,\n y: height - h,\n width: barWidth,\n height: h,\n color: d.color ?? (colors ? palette[i % palette.length] : palette[0]),\n };\n });\n }, [width, data, height, max, palette, colors]);\n\n return (\n \n {width > 0 && (\n <>\n \n {showLabels && (\n \n {data.map((d) => (\n \n {d.label}\n \n ))}\n \n )}\n >\n )}\n \n );\n}\n\n// ---------- Line chart ----------\n\nexport interface LineChartProps extends CommonChartProps {\n data: ChartDatum[];\n height?: number;\n /** Stroke width of the line. */\n strokeWidth?: number;\n /** Show dots at each point. */\n dots?: boolean;\n showLabels?: boolean;\n}\n\nexport function LineChart({\n data,\n height = 160,\n strokeWidth = 2.5,\n dots = true,\n showLabels = true,\n colors,\n className,\n ...props\n}: LineChartProps) {\n const { palette } = useChartColors(colors);\n const [width, onLayout] = useWidth();\n const color = colors?.[0] ?? palette[0];\n const pad = 8;\n\n const { path, points } = useMemo(() => {\n if (!width || data.length < 2) {\n return {\n path: Skia.Path.Make(),\n points: [] as { x: number; y: number }[],\n };\n }\n const max = Math.max(...data.map((d) => d.value));\n const min = Math.min(...data.map((d) => d.value));\n const range = max - min || 1;\n const stepX = (width - pad * 2) / (data.length - 1);\n const pts = data.map((d, i) => ({\n x: pad + i * stepX,\n y: height - pad - ((d.value - min) / range) * (height - pad * 2),\n }));\n const b = Skia.PathBuilder.Make();\n b.moveTo(pts[0].x, pts[0].y);\n for (let i = 1; i < pts.length; i++) {\n const prev = pts[i - 1];\n const cur = pts[i];\n const midX = (prev.x + cur.x) / 2;\n b.cubicTo(midX, prev.y, midX, cur.y, cur.x, cur.y);\n }\n return { path: b.build(), points: pts };\n }, [width, data, height]);\n\n return (\n \n {width > 0 && (\n <>\n \n {showLabels && (\n \n {data.map((d) => (\n \n {d.label}\n \n ))}\n \n )}\n >\n )}\n \n );\n}\n\n// ---------- Donut chart ----------\n\nexport interface DonutChartProps extends CommonChartProps {\n data: ChartDatum[];\n size?: number;\n /** Ring thickness. */\n strokeWidth?: number;\n /** Center label — e.g. total. */\n centerLabel?: string;\n centerValue?: string;\n}\n\nexport function DonutChart({\n data,\n size = 160,\n strokeWidth = 24,\n centerLabel,\n centerValue,\n colors,\n className,\n ...props\n}: DonutChartProps) {\n const { palette } = useChartColors(colors);\n const total = data.reduce((s, d) => s + d.value, 0) || 1;\n const r = (size - strokeWidth) / 2;\n const c = size / 2;\n\n const segments = useMemo(() => {\n let acc = 0;\n return data.map((d, i) => {\n const start = acc / total;\n acc += d.value;\n const end = acc / total;\n const startAngle = start * 360 - 90;\n const sweep = (end - start) * 360 - 1;\n const path = Skia.PathBuilder.Make()\n .arcToOval(\n Skia.XYWHRect(c - r, c - r, r * 2, r * 2),\n startAngle,\n sweep,\n true\n )\n .build();\n return { path, color: d.color ?? palette[i % palette.length] };\n });\n }, [data, total, r, c, palette]);\n\n return (\n \n \n {(centerLabel || centerValue) && (\n \n {centerValue && (\n \n {centerValue}\n \n )}\n {centerLabel && (\n {centerLabel}\n )}\n \n )}\n \n );\n}\n\n// ---------- Legend ----------\n\nexport function ChartLegend({\n data,\n colors,\n className,\n ...props\n}: CommonChartProps & { data: ChartDatum[] }) {\n const { palette } = useChartColors(colors);\n return (\n \n {data.map((d, i) => (\n \n \n {d.label}\n \n ))}\n \n );\n}\n"
}
]
}