{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sparkline", "title": "Sparkline", "description": "An inline mini chart that drops naturally into text and table cells.", "dependencies": [ "roughjs", "d3-shape", "d3-scale" ], "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/sparkline.tsx", "content": "\"use client\";\n\n/**\n * Dependencies: roughjs (already installed), d3-shape, d3-scale\n * npm install d3-shape d3-scale\n */\n\nimport { useCallback, useEffect, type HTMLAttributes } from \"react\";\nimport { line as d3Line, area as d3Area, curveCatmullRom } from \"d3-shape\";\nimport { scaleLinear } from \"d3-scale\";\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 type SparklineType = \"line\" | \"area\" | \"bar\";\n\nexport interface SparklineProps\n extends HTMLAttributes, CrumbleColorProps {\n animateOnMount?: boolean;\n color?: string;\n data: number[];\n height?: number;\n id?: string;\n theme?: CrumbleTheme;\n type?: SparklineType;\n width?: number;\n}\n\nexport function Sparkline({\n animateOnMount = true,\n className,\n color,\n data,\n fill,\n height = 32,\n id,\n stroke,\n strokeMuted,\n theme: themeProp,\n type = \"line\",\n width = 80,\n ...props\n}: SparklineProps) {\n const sparkId = id ?? \"sparkline\";\n const { drawCircle, drawPath, drawRect, svgRef, theme } = useRough({\n variant: \"chart\",\n stableId: sparkId,\n theme: themeProp,\n });\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n\n const draw = useCallback(() => {\n const svg = svgRef.current;\n if (!svg || data.length < 2) return;\n\n svg.replaceChildren();\n svg.setAttribute(\"width\", String(width));\n svg.setAttribute(\"height\", String(height));\n svg.setAttribute(\"viewBox\", `0 0 ${width} ${height}`);\n\n const PAD = { top: 3, bottom: 3, left: 2, right: 2 };\n const plotW = width - PAD.left - PAD.right;\n const plotH = height - PAD.top - PAD.bottom;\n\n // d3 scales — proper domain handling including negative values\n const xScale = scaleLinear()\n .domain([0, data.length - 1])\n .range([PAD.left, PAD.left + plotW]);\n\n const [dMin, dMax] = [Math.min(...data), Math.max(...data)];\n const yScale = scaleLinear()\n .domain([dMin === dMax ? dMin - 1 : dMin, dMax])\n .range([PAD.top + plotH, PAD.top])\n .nice();\n\n const strokeColor = color ?? \"currentColor\";\n\n if (type === \"bar\") {\n const barW = Math.max(2, (plotW / data.length) * 0.7);\n const gap = plotW / data.length;\n const zeroY = yScale(0);\n\n data.forEach((v, i) => {\n const x = xScale(i) - barW / 2;\n const y = yScale(v);\n const barH = Math.abs(zeroY - y);\n const barY = v >= 0 ? y : zeroY;\n\n if (barH < 1) return;\n\n const bar = drawRect(x, barY, barW, barH, {\n fill: strokeColor,\n fillStyle: theme === \"ink\" ? \"solid\" : \"hachure\",\n fillWeight: 0.7,\n hachureGap: 4,\n seed: stableSeed(`${sparkId}-bar-${i}`),\n stroke: strokeColor,\n strokeWidth: 0.5,\n });\n if (bar) svg.appendChild(bar);\n });\n return;\n }\n\n if (type === \"area\") {\n // d3 area generator — fills between line and baseline\n const areaGen = d3Area()\n .x((_, i) => xScale(i))\n .y0(yScale(dMin < 0 ? 0 : dMin))\n .y1((v) => yScale(v))\n .curve(curveCatmullRom.alpha(0.5));\n\n const areaPath = areaGen(data);\n if (areaPath) {\n const areaNode = document.createElementNS(\n \"http://www.w3.org/2000/svg\",\n \"path\",\n );\n areaNode.setAttribute(\"d\", areaPath);\n areaNode.setAttribute(\"fill\", strokeColor);\n areaNode.setAttribute(\"opacity\", \"0.15\");\n areaNode.setAttribute(\"stroke\", \"none\");\n svg.appendChild(areaNode);\n }\n }\n\n // d3 line generator — Catmull-Rom for smooth curves\n const lineGen = d3Line()\n .x((_, i) => xScale(i))\n .y((v) => yScale(v))\n .curve(curveCatmullRom.alpha(0.5));\n\n const linePath = lineGen(data);\n if (!linePath) return;\n\n const lineNode = drawPath(linePath, {\n fill: \"none\",\n seed: stableSeed(sparkId),\n stroke: strokeColor,\n strokeWidth: theme === \"crayon\" ? 2.5 : theme === \"ink\" ? 2 : 1.5,\n }) as SVGGElement | null;\n if (!lineNode) return;\n\n // Animate on mount: stroke-dashoffset draw-on\n if (animateOnMount) {\n lineNode.querySelectorAll(\"path\").forEach((p) => {\n const len = p.getTotalLength();\n p.style.strokeDasharray = String(len);\n p.style.strokeDashoffset = String(len);\n const dur = theme === \"crayon\" ? 700 : theme === \"ink\" ? 400 : 550;\n p.style.transition = `stroke-dashoffset ${dur}ms cubic-bezier(0.16,1,0.3,1)`;\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n p.style.strokeDashoffset = \"0\";\n }),\n );\n });\n }\n\n svg.appendChild(lineNode);\n\n // End dot — marks current/latest value\n const lastX = xScale(data.length - 1);\n const lastY = yScale(data[data.length - 1]);\n const dot = drawCircle(lastX, lastY, 5, {\n fill: strokeColor,\n fillStyle: \"solid\",\n seed: stableSeed(`${sparkId}-dot`),\n stroke: strokeColor,\n strokeWidth: 0.5,\n });\n if (dot) svg.appendChild(dot);\n }, [\n animateOnMount,\n color,\n data,\n drawCircle,\n drawPath,\n drawRect,\n height,\n sparkId,\n svgRef,\n theme,\n type,\n width,\n ]);\n\n useEffect(() => {\n const rid = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(rid);\n }, [draw]);\n\n return (\n \n \n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/sparkline.tsx" } ], "type": "registry:ui" }