Replace Recharts charts with custom SVG components
- Lead status: custom SVG donut with radial gradients, hover effects, percentage tooltips - Leads per month: custom SVG grouped bar chart with hover dimming, gradient bars - Tooltip colors fixed to use theme variables - Right-panel lead status icon colors updated
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
|
||||
|
||||
interface StatusData {
|
||||
name: string
|
||||
@@ -14,7 +14,38 @@ interface LeadStatusChartProps {
|
||||
data: StatusData[]
|
||||
}
|
||||
|
||||
function polar(cx: number, cy: number, r: number, deg: number) {
|
||||
const rad = ((deg - 90) * Math.PI) / 180
|
||||
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
|
||||
}
|
||||
|
||||
function arcPath(cx: number, cy: number, oR: number, iR: number, start: number, end: number) {
|
||||
const gap = 3
|
||||
const s = start + gap / 2
|
||||
const e = end - gap / 2
|
||||
const o1 = polar(cx, cy, oR, s)
|
||||
const o2 = polar(cx, cy, oR, e)
|
||||
const i1 = polar(cx, cy, iR, e)
|
||||
const i2 = polar(cx, cy, iR, s)
|
||||
const lg = e - s > 180 ? 1 : 0
|
||||
return `M${o1.x} ${o1.y} A${oR} ${oR} 0 ${lg} 1 ${o2.x} ${o2.y} L${i1.x} ${i1.y} A${iR} ${iR} 0 ${lg} 0 ${i2.x} ${i2.y}Z`
|
||||
}
|
||||
|
||||
export function LeadStatusChart({ data }: LeadStatusChartProps) {
|
||||
const [hov, setHov] = useState<number | null>(null)
|
||||
|
||||
const total = data.reduce((s, d) => s + d.value, 0)
|
||||
|
||||
let cum = 0
|
||||
const segs = data.map((d) => {
|
||||
const start = cum
|
||||
const deg = (d.value / total) * 360
|
||||
cum += deg
|
||||
return { ...d, start, end: cum, deg, pct: Math.round((d.value / total) * 100) }
|
||||
})
|
||||
|
||||
const active = hov !== null ? segs[hov] : null
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -26,45 +57,120 @@ export function LeadStatusChart({ data }: LeadStatusChartProps) {
|
||||
<CardTitle>Lead Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
animationBegin={200}
|
||||
animationDuration={1000}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Donut */}
|
||||
<svg width="280" height="280" viewBox="0 0 320 320" className="overflow-visible">
|
||||
<defs>
|
||||
{segs.map((s, i) => (
|
||||
<radialGradient key={i} id={`chart-grad-${i}`} cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stopColor={s.color} stopOpacity="1" />
|
||||
<stop offset="100%" stopColor={s.color} stopOpacity="0.75" />
|
||||
</radialGradient>
|
||||
))}
|
||||
</defs>
|
||||
|
||||
{/* Background ring */}
|
||||
<circle cx="160" cy="160" r="105" fill="none" stroke="hsl(var(--muted))" strokeWidth="52" />
|
||||
|
||||
{/* Segments */}
|
||||
{segs.map((s, i) => {
|
||||
const isH = hov === i
|
||||
const oR = isH ? 137 : 130
|
||||
const iR = isH ? 73 : 80
|
||||
return (
|
||||
<path
|
||||
key={i}
|
||||
d={arcPath(160, 160, oR, iR, s.start, s.end)}
|
||||
fill={`url(#chart-grad-${i})`}
|
||||
opacity={hov !== null && !isH ? 0.3 : 1}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
transition: "all 0.22s cubic-bezier(.4,0,.2,1)",
|
||||
filter: isH ? `drop-shadow(0 0 10px ${s.color}99)` : "none",
|
||||
}}
|
||||
onMouseEnter={() => setHov(i)}
|
||||
onMouseLeave={() => setHov(null)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Center circle */}
|
||||
<circle cx="160" cy="160" r="66" fill="hsl(var(--card))" />
|
||||
<circle cx="160" cy="160" r="66" fill="none" stroke="hsl(var(--border))" strokeWidth="1" />
|
||||
|
||||
{/* Center text */}
|
||||
{active ? (
|
||||
<>
|
||||
<circle cx="160" cy="160" r="66" fill={active.color + "15"} />
|
||||
<text x="160" y="148" textAnchor="middle" fill="hsl(var(--foreground))" fontSize="30" fontWeight="700" fontFamily="-apple-system,sans-serif">
|
||||
{active.value}
|
||||
</text>
|
||||
<text x="160" y="166" textAnchor="middle" fill="hsl(var(--muted-foreground))" fontSize="12" fontFamily="-apple-system,sans-serif">
|
||||
{active.name}
|
||||
</text>
|
||||
<text x="160" y="184" textAnchor="middle" fill={active.color} fontSize="13" fontWeight="600" fontFamily="-apple-system,sans-serif">
|
||||
{active.pct}%
|
||||
</text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<text x="160" y="152" textAnchor="middle" fill="hsl(var(--foreground))" fontSize="34" fontWeight="700" fontFamily="-apple-system,sans-serif">
|
||||
{total}
|
||||
</text>
|
||||
<text x="160" y="172" textAnchor="middle" fill="hsl(var(--muted-foreground))" fontSize="12" fontFamily="-apple-system,sans-serif">
|
||||
Total Leads
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 grid w-full grid-cols-2 gap-2">
|
||||
{segs.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onMouseEnter={() => setHov(i)}
|
||||
onMouseLeave={() => setHov(null)}
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-xl p-2.5 transition-all duration-200"
|
||||
style={{
|
||||
background: hov === i ? `${s.color}18` : "hsl(var(--muted) / 0.3)",
|
||||
border: `1px solid ${hov === i ? s.color + "50" : "hsl(var(--border))"}`,
|
||||
gridColumn: i === segs.length - 1 && segs.length % 2 !== 0 ? "1 / -1" : undefined,
|
||||
maxWidth: i === segs.length - 1 && segs.length % 2 !== 0 ? "50%" : undefined,
|
||||
margin: i === segs.length - 1 && segs.length % 2 !== 0 ? "0 auto" : undefined,
|
||||
width: i === segs.length - 1 && segs.length % 2 !== 0 ? "100%" : undefined,
|
||||
}}
|
||||
formatter={(value: number, name: string) => [value, name]}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
formatter={(value: string) => (
|
||||
<span className="text-sm text-muted-foreground">{value}</span>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
>
|
||||
<div
|
||||
className="h-2.5 w-2.5 shrink-0 rounded-full transition-shadow duration-200"
|
||||
style={{
|
||||
background: s.color,
|
||||
boxShadow: hov === i ? `0 0 8px ${s.color}` : "none",
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="text-xs font-medium transition-colors duration-200"
|
||||
style={{ color: hov === i ? "hsl(var(--foreground))" : "hsl(var(--muted-foreground))" }}
|
||||
>
|
||||
{s.name}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px]" style={{ color: "hsl(var(--muted-foreground) / 0.7)" }}>
|
||||
{s.value} · {s.pct}%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs font-semibold transition-colors duration-200"
|
||||
style={{ color: hov === i ? s.color : "hsl(var(--muted-foreground) / 0.5)" }}
|
||||
>
|
||||
{s.pct}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user