{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "radial-chart", "title": "Radial Chart", "description": "An animated radial progress chart with percentage display and center label.", "dependencies": [ "recharts", "lucide-react" ], "registryDependencies": [ "card", "chart" ], "files": [ { "path": "registry/new-york/radial-chart/radial-chart.tsx", "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport {\n Label,\n PolarGrid,\n PolarRadiusAxis,\n RadialBar,\n RadialBarChart,\n} from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { ChartConfig, ChartContainer } from \"@/components/ui/chart\"\nimport { TrendingUp } from \"lucide-react\"\n\nexport interface RadialChartData {\n category: string\n value: number\n percentage: number\n fill?: string\n}\n\nexport interface RadialChartProps {\n title?: string\n description?: string\n data: RadialChartData\n chartConfig: ChartConfig\n centerLabel?: string\n footerContent?: {\n mainText?: string\n subText?: string\n showTrending?: boolean\n trendingColor?: string\n trendingIcon?: React.ReactNode\n }\n innerRadius?: number\n outerRadius?: number\n cornerRadius?: number\n startAngle?: number\n className?: string\n numberSize?: 'sm' | 'md' | 'lg' | 'xl'\n labelSize?: 'xs' | 'sm' | 'md'\n animationDuration?: number\n centerOffset?: { x?: number; y?: number }\n}\n\nexport const RadialChartShapeComponent = ({\n title,\n description,\n data,\n centerLabel,\n footerContent,\n chartConfig,\n innerRadius = 80,\n outerRadius = 130,\n cornerRadius = 50,\n startAngle = 90,\n className = \"\",\n numberSize = 'lg',\n labelSize = 'sm',\n animationDuration = 2.5,\n centerOffset = {}\n}: RadialChartProps) => {\n const targetEndAngle = startAngle + (data.percentage * 3.6) // 360 độ tương ứng với 100%\n const chartRef = useRef(null)\n const [isChartVisible, setIsChartVisible] = useState(false)\n const [currentEndAngle, setCurrentEndAngle] = useState(startAngle) // Start from startAngle\n const [currentPercentage, setCurrentPercentage] = useState(0) // For synced counting\n const animationRef = useRef(0)\n\n // Intersection Observer to trigger animation when chart is visible\n useEffect(() => {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setIsChartVisible(true)\n observer.disconnect() // Only trigger once\n }\n },\n { threshold: 0.3 } // Trigger when 30% of chart is visible\n )\n\n if (chartRef.current) {\n observer.observe(chartRef.current)\n }\n\n return () => observer.disconnect()\n }, [chartRef])\n\n // Animate RadialBar rotation and percentage counting together\n useEffect(() => {\n if (!isChartVisible) return\n\n const startTime = Date.now()\n const duration = animationDuration * 1000 // Convert to milliseconds\n const startEndAngle = startAngle\n const endEndAngle = targetEndAngle\n const startPercentage = 0\n const endPercentage = data.percentage\n\n const animate = () => {\n const elapsed = Date.now() - startTime\n const progress = Math.min(elapsed / duration, 1)\n\n // Easing function for smooth animation (easeOutQuart)\n const easeOutQuart = 1 - Math.pow(1 - progress, 4)\n\n // Calculate current values\n const newEndAngle = startEndAngle + (endEndAngle - startEndAngle) * easeOutQuart\n const newPercentage = startPercentage + (endPercentage - startPercentage) * easeOutQuart\n\n setCurrentEndAngle(newEndAngle)\n setCurrentPercentage(Math.round(newPercentage))\n\n if (progress < 1) {\n animationRef.current = requestAnimationFrame(animate)\n } else {\n setCurrentEndAngle(endEndAngle)\n setCurrentPercentage(endPercentage)\n }\n }\n\n // Small delay to ensure chart is rendered\n const timeoutId = setTimeout(() => {\n animationRef.current = requestAnimationFrame(animate)\n }, 100)\n\n return () => {\n clearTimeout(timeoutId)\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current)\n }\n }\n }, [isChartVisible, targetEndAngle, startAngle, data.percentage, animationDuration])\n\n // Create chart data with current animated values\n const chartData = [{\n ...data,\n percentage: currentPercentage\n }]\n\n // Dynamic sizing based on props\n const getNumberSizeClass = (size: string) => {\n const sizeMap = {\n 'sm': 'text-2xl',\n 'md': 'text-3xl',\n 'lg': 'text-4xl',\n 'xl': 'text-5xl'\n }\n return sizeMap[size as keyof typeof sizeMap] || sizeMap.lg\n }\n\n const getLabelSizeClass = (size: string) => {\n const sizeMap = {\n 'xs': 'text-xs',\n 'sm': 'text-sm',\n 'md': 'text-base'\n }\n return sizeMap[size as keyof typeof sizeMap] || sizeMap.sm\n }\n\n // Calculate dynamic dimensions based on content and radius\n const calculateDimensions = () => {\n const maxDigits = data.percentage.toString().length + 1 // +1 for % symbol\n const baseWidth = Math.max(120, maxDigits * 20)\n const baseHeight = 80\n\n // Scale based on inner radius for better fit\n const scale = innerRadius / 80 // 80 is our base inner radius\n\n return {\n width: Math.round(baseWidth * scale),\n height: Math.round(baseHeight * scale)\n }\n }\n\n return (\n \n \n {title}\n {description && {description}}\n \n\n \n
\n \n \n \n\n \n\n \n {\n if (viewBox && \"cx\" in viewBox && \"cy\" in viewBox) {\n const dimensions = calculateDimensions()\n const centerX = (viewBox.cx || 0) + (centerOffset.x || 0)\n const centerY = (viewBox.cy || 0) + (centerOffset.y || 0)\n\n return (\n \n \n \n \n {currentPercentage}%\n
\n\n \n {centerLabel}\n \n \n \n \n )\n }\n }}\n />\n \n \n \n \n
\n\n {footerContent && (\n \n
\n \n {footerContent.mainText}\n \n {footerContent.showTrending && footerContent.trendingIcon && (\n \n {footerContent.trendingIcon}\n \n )}\n
\n\n
\n {footerContent.subText}\n
\n
\n )}\n
\n )\n}\n\n\n// ========================================================================================\n// Radial Chart Example\n// ========================================================================================\nexport const RadialChartExample = () => {\n const data = {\n category: \"re_engagement\",\n value: 450,\n percentage: 67,\n fill: \"var(--chart-3)\",\n reEngagedUsers: 450,\n totalReEngagements: 673\n }\n\n const chartConfig = {\n re_engagement: {\n label: \"Re-engagement Rate\",\n color: \"var(--chart-3)\",\n },\n } satisfies ChartConfig\n\n return (\n \n }}\n />\n )\n}\n", "type": "registry:component" } ], "type": "registry:block" }