{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bar-chart", "title": "Bar Chart", "description": "A hand-drawn bar chart with hachure-filled bars and animated mount.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json" ], "files": [ { "path": "registry/new-york/ui/bar-chart.tsx", "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, type HTMLAttributes } from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport {\n resolveRoughVars,\n stableSeed,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\n\nexport interface BarChartDataPoint {\n color?: string;\n label: string;\n value: number;\n}\n\nexport interface BarChartProps\n extends HTMLAttributes, CrumbleColorProps {\n animateOnMount?: boolean;\n axisLabel?: string;\n data: BarChartDataPoint[];\n formatValue?: (v: number) => string;\n height?: number;\n id?: string;\n showGrid?: boolean;\n showValues?: boolean;\n theme?: CrumbleTheme;\n}\n\nconst PAD = { top: 20, right: 16, bottom: 40, left: 48 };\nconst GRID_LINES = 4;\n\nexport function BarChart({\n animateOnMount = true,\n axisLabel,\n className,\n data,\n fill,\n formatValue = (v) => String(v),\n height = 240,\n id,\n showGrid = true,\n showValues = true,\n stroke,\n strokeMuted,\n theme: themeProp,\n ...props\n}: BarChartProps) {\n const containerRef = useRef(null);\n const chartId = id ?? \"bar-chart\";\n const { drawLine, drawRect, svgRef, theme } = useRough({\n variant: \"chart\",\n stableId: chartId,\n theme: themeProp,\n });\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n\n // Linear scale helper\n const scale = (\n value: number,\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number,\n ) =>\n domainMax === domainMin\n ? rangeMin\n : rangeMin +\n ((value - domainMin) / (domainMax - domainMin)) * (rangeMax - rangeMin);\n\n const draw = useCallback(() => {\n const container = containerRef.current;\n const svg = svgRef.current;\n if (!container || !svg || data.length === 0) return;\n\n svg.replaceChildren();\n\n const W = container.offsetWidth;\n const H = height;\n svg.setAttribute(\"width\", String(W));\n svg.setAttribute(\"height\", String(H));\n svg.setAttribute(\"viewBox\", `0 0 ${W} ${H}`);\n\n const plotW = W - PAD.left - PAD.right;\n const plotH = H - PAD.top - PAD.bottom;\n\n const maxVal = Math.max(...data.map((d) => d.value), 0);\n const niceMax = Math.ceil(maxVal / GRID_LINES) * GRID_LINES || 1;\n\n // Grid lines + Y axis labels\n if (showGrid) {\n for (let i = 0; i <= GRID_LINES; i++) {\n const val = (niceMax / GRID_LINES) * i;\n const y = PAD.top + plotH - scale(val, 0, niceMax, 0, plotH);\n\n const gridLine = drawLine(PAD.left, y, PAD.left + plotW, y, {\n seed: stableSeed(`${chartId}-grid-${i}`),\n stroke: \"var(--cr-stroke-muted)\",\n strokeWidth: theme === \"crayon\" ? 1.5 : 0.8,\n });\n if (gridLine) svg.appendChild(gridLine);\n\n // Y label\n const text = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"text\",\n );\n text.setAttribute(\"x\", String(PAD.left - 8));\n text.setAttribute(\"y\", String(y + 4));\n text.setAttribute(\"text-anchor\", \"end\");\n text.setAttribute(\"fill\", \"currentColor\");\n text.setAttribute(\"font-size\", \"11\");\n text.setAttribute(\"opacity\", \"0.5\");\n text.textContent = formatValue(val);\n svg.appendChild(text);\n }\n }\n\n // Axes\n const yAxis = drawLine(PAD.left, PAD.top, PAD.left, PAD.top + plotH, {\n seed: stableSeed(`${chartId}-yaxis`),\n stroke: \"var(--cr-stroke-muted)\",\n strokeWidth: theme === \"crayon\" ? 2 : 1,\n });\n const xAxis = drawLine(\n PAD.left,\n PAD.top + plotH,\n PAD.left + plotW,\n PAD.top + plotH,\n {\n seed: stableSeed(`${chartId}-xaxis`),\n stroke: \"var(--cr-stroke-muted)\",\n strokeWidth: theme === \"crayon\" ? 2 : 1,\n },\n );\n if (yAxis) svg.appendChild(yAxis);\n if (xAxis) svg.appendChild(xAxis);\n\n // Bars\n const barW = Math.max(4, (plotW / data.length) * 0.6);\n const gap = plotW / data.length;\n\n data.forEach((d, i) => {\n const barH = scale(d.value, 0, niceMax, 0, plotH);\n const x = PAD.left + gap * i + (gap - barW) / 2;\n const y = PAD.top + plotH - barH;\n\n const barNode = drawRect(x, y, barW, barH, {\n fill: d.color ?? \"currentColor\",\n fillStyle: theme === \"ink\" ? \"solid\" : \"hachure\",\n fillWeight: theme === \"pencil\" ? 0.8 : 1.2,\n hachureGap: theme === \"crayon\" ? 4 : 6,\n seed: stableSeed(`${chartId}-bar-${i}`),\n stroke: d.color ?? \"currentColor\",\n strokeWidth: theme === \"crayon\" ? 2 : theme === \"ink\" ? 1.5 : 1,\n }) as SVGGElement | null;\n if (!barNode) return;\n\n // Animate on mount: bars grow from bottom using clipPath on the group\n if (animateOnMount) {\n const clipId = `${chartId}-clip-${i}`;\n const defs =\n svg.querySelector(\"defs\") ??\n svg.insertBefore(\n document.createElementNS(\"http://www.w3.org/2000/svg\", \"defs\"),\n svg.firstChild,\n );\n\n const clipPath = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"clipPath\",\n );\n clipPath.setAttribute(\"id\", clipId);\n const clipRect = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"rect\",\n );\n clipRect.setAttribute(\"x\", String(x - 4));\n clipRect.setAttribute(\"y\", String(PAD.top + plotH));\n clipRect.setAttribute(\"width\", String(barW + 8));\n clipRect.setAttribute(\"height\", String(barH + 4));\n clipPath.appendChild(clipRect);\n defs.appendChild(clipPath);\n\n barNode.setAttribute(\"clip-path\", `url(#${clipId})`);\n\n // Animate clip rect upward\n const delay = i * 60;\n const dur = theme === \"crayon\" ? 600 : theme === \"ink\" ? 350 : 500;\n const anim = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"animate\",\n );\n anim.setAttribute(\"attributeName\", \"y\");\n anim.setAttribute(\"from\", String(PAD.top + plotH));\n anim.setAttribute(\"to\", String(y - 4));\n anim.setAttribute(\"dur\", `${dur}ms`);\n anim.setAttribute(\"begin\", `${delay}ms`);\n anim.setAttribute(\"fill\", \"freeze\");\n anim.setAttribute(\"calcMode\", \"spline\");\n anim.setAttribute(\"keySplines\", \"0.16 1 0.3 1\");\n clipRect.appendChild(anim);\n }\n\n svg.appendChild(barNode);\n\n // Value label above bar\n if (showValues && d.value > 0) {\n const label = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"text\",\n );\n label.setAttribute(\"x\", String(x + barW / 2));\n label.setAttribute(\"y\", String(y - 4));\n label.setAttribute(\"text-anchor\", \"middle\");\n label.setAttribute(\"fill\", \"currentColor\");\n label.setAttribute(\"font-size\", \"11\");\n label.setAttribute(\"opacity\", \"0.7\");\n label.textContent = formatValue(d.value);\n svg.appendChild(label);\n }\n\n // X axis label\n const xlabel = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"text\",\n );\n xlabel.setAttribute(\"x\", String(x + barW / 2));\n xlabel.setAttribute(\"y\", String(PAD.top + plotH + 16));\n xlabel.setAttribute(\"text-anchor\", \"middle\");\n xlabel.setAttribute(\"fill\", \"currentColor\");\n xlabel.setAttribute(\"font-size\", \"11\");\n xlabel.setAttribute(\"opacity\", \"0.6\");\n xlabel.textContent = d.label;\n svg.appendChild(xlabel);\n });\n\n // Optional axis label\n if (axisLabel) {\n const al = document.createElementNS(\"http://www.w3.org/2000/svg\", \"text\");\n al.setAttribute(\"x\", String(PAD.left + plotW / 2));\n al.setAttribute(\"y\", String(H - 4));\n al.setAttribute(\"text-anchor\", \"middle\");\n al.setAttribute(\"fill\", \"currentColor\");\n al.setAttribute(\"font-size\", \"11\");\n al.setAttribute(\"opacity\", \"0.5\");\n al.textContent = axisLabel;\n svg.appendChild(al);\n }\n }, [\n animateOnMount,\n axisLabel,\n chartId,\n data,\n drawLine,\n drawRect,\n formatValue,\n height,\n showGrid,\n showValues,\n svgRef,\n theme,\n ]);\n\n useEffect(() => {\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const ro = new ResizeObserver(() => draw());\n ro.observe(container);\n return () => ro.disconnect();\n }, [draw]);\n\n return (\n \n \n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/bar-chart.tsx" } ], "type": "registry:ui" }