đšī¸ Gauge Chart
The Gauge Chart is a sleek and interactive data visualization tool designed to showcase progress, performance, or ratings in a visually engaging way. With a smooth animated arc, vibrant gradient colors, and dynamic tick marks, this semi-circular chart makes tracking key metrics both intuitive and fun. Whether you're measuring customer satisfaction, performance scores, or financial health, the Gauge Chart provides a quick, at-a-glance overview with a modern and responsive design.
Preview
3.2
ExcellentFollow below steps đđģ
Install dependencies
1npm i framer-motion
Component
Create a file gauge-chart.tsx in your components folder and paste this code
1'use client';23import { motion } from 'framer-motion';4import React from 'react';56const GaugeChart = ({ score, min, max }: { score: number; min: number; max: number }) => {7// Ensure score is within bounds8const boundedScore = Math.max(min, Math.min(max, score));910// Calculate the angle (0 to 180 degrees)11const angle = ((boundedScore - min) / (max - min)) * 180;1213// Arc length calculation14const RADIUS = 80;15const arcLength = (180 / 360) * (2 * Math.PI * RADIUS);1617// Calculate score thresholds dynamically18const range = max - min;19const lowThreshold = min + range * 0.33;20const highThreshold = min + range * 0.66;2122// Generate tick marks23const numTicks = 8;24const ticks = Array.from({ length: numTicks + 1 }, (_, i) => {25const tickAngle = (i * 180) / numTicks;26const tickRadians = (tickAngle * Math.PI) / 180;27const x1 = 100 + (RADIUS - 5) * Math.cos(tickRadians - Math.PI);28const y1 = 100 + (RADIUS - 5) * Math.sin(tickRadians - Math.PI);29const x2 = 100 + (RADIUS + 5) * Math.cos(tickRadians - Math.PI);30const y2 = 100 + (RADIUS + 5) * Math.sin(tickRadians - Math.PI);31const labelX = 100 + (RADIUS + 15) * Math.cos(tickRadians - Math.PI);32const labelY = 100 + (RADIUS + 15) * Math.sin(tickRadians - Math.PI);33const value = min + (i * (max - min)) / numTicks;34return { x1, y1, x2, y2, labelX, labelY, value };35});3637// Helper function to get score status and its corresponding style38const getScoreStatus = () => {39if (boundedScore > highThreshold) {40return {41text: 'Excellent',42className:43'bg-gradient-to-r from-green-500/20 to-green-400/20 text-green-500 ring-1 ring-green-500/30 shadow-lg shadow-green-500/10'44};45}46if (boundedScore > lowThreshold) {47return {48text: 'Good',49className:50'bg-gradient-to-r from-orange-500/20 to-orange-400/20 text-orange-500 ring-1 ring-orange-500/30 shadow-lg shadow-orange-500/10'51};52}53return {54text: 'Poor',55className:56'bg-gradient-to-r from-red-500/20 to-red-400/20 text-red-500 ring-1 ring-red-500/30 shadow-lg shadow-red-500/10'57};58};5960const scoreStatus = getScoreStatus();6162return (63<div className="relative w-72 h-72 border-[4px] border-slate-600/20 rounded-full p-4 flex flex-col">64{/* SVG Gauge */}65<svg className="relative top-0 overflow-visible" viewBox="0 0 200 120">66<defs>67{/* Enhanced Gradient Definition */}68<linearGradient69id="gaugeGradient"70gradientUnits="userSpaceOnUse"71x1="20"72y1="100"73x2="180"74y2="100"75>76<stop offset="0%" stopColor="#ef4444" />77<stop offset="25%" stopColor="#f97316" />78<stop offset="50%" stopColor="#facc15" />79<stop offset="75%" stopColor="#84cc16" />80<stop offset="100%" stopColor="#22c55e" />81</linearGradient>82{/* Glow Filter */}83<filter id="glow">84<feGaussianBlur stdDeviation="2" result="coloredBlur" />85<feMerge>86<feMergeNode in="coloredBlur" />87<feMergeNode in="SourceGraphic" />88</feMerge>89</filter>90</defs>9192{/* Background Arc with shadow */}93<path94d="M20,100 A80,80 0 0,1 180,100"95fill="none"96stroke="rgba(30, 41, 59, 0.2)"97strokeWidth="8"98strokeLinecap="round"99/>100<path101d="M20,100 A80,80 0 0,1 180,100"102fill="none"103stroke="#1e293b"104strokeWidth="3"105strokeLinecap="round"106/>107108{/* Tick marks and labels */}109{ticks.map((tick, i) => (110<g key={i} className="text-slate-500">111<line112x1={tick.x1}113y1={tick.y1}114x2={tick.x2}115y2={tick.y2}116stroke="currentColor"117strokeWidth="2"118opacity="0.5"119/>120<text121x={tick.labelX}122y={tick.labelY}123textAnchor="middle"124fontSize="8"125fill="currentColor"126opacity="0.8"127>128{tick.value.toFixed(1)}129</text>130</g>131))}132133{/* Progress Arc with glow */}134<motion.path135d="M20,100 A80,80 0 0,1 180,100"136fill="none"137stroke="url(#gaugeGradient)"138strokeWidth="3"139strokeDasharray={arcLength}140strokeDashoffset={arcLength * (1 - angle / 180)}141strokeLinecap="round"142filter="url(#glow)"143initial={{ strokeDashoffset: arcLength }}144animate={{ strokeDashoffset: arcLength * (1 - angle / 180) }}145transition={{ duration: 1, ease: 'easeOut' }}146/>147</svg>148149{/* Enhanced Needle with glow */}150<div className="absolute inset-0 h-full w-full top-0 flex items-center justify-center">151<motion.div152className="w-1.5 h-20 bg-white/40 origin-bottom rounded-full mb-16"153style={{154transformOrigin: 'bottom center',155clipPath: 'polygon(50% 0, 100% 100%, 0 100%)',156background:157'radial-gradient(circle at bottom, rgba(240,240,240,0.8) 0%, rgba(240,240,240,0.2))'158}}159initial={{ rotate: -90 }}160animate={{ rotate: -90 + angle }}161transition={{ duration: 1, ease: 'easeOut' }}162/>163</div>164165{/* Enhanced Score Display */}166<div className="absolute inset-0 text-center flex flex-col items-center justify-end h-full gap-1 w-full pb-10">167<p className="text-4xl font-semibold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-slate-200 to-slate-400">168{boundedScore.toFixed(1)}169</p>170<span171className={`px-4 py-1 rounded-full text-xs font-medium backdrop-blur-sm ${scoreStatus.className}`}172>173{scoreStatus.text}174</span>175</div>176</div>177);178};179180export default GaugeChart;
Usage
1<GaugeChart score={3.1} min={1.5} max={4} />
âī¸ Got a question or feedback?
Feel free to reach out!