157 lines
3.8 KiB
TypeScript
157 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { Loader2 } from "lucide-react"
|
|
import { Label, Pie, PieChart } from "recharts"
|
|
|
|
import {
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
type ChartConfig,
|
|
} from "@/components/ui/chart"
|
|
|
|
interface DetectionDonutChartProps {
|
|
defectedSignboard: number
|
|
pothole: number
|
|
roadCrack: number
|
|
damagedRoadMarking: number
|
|
goodSignboard: number
|
|
isLoading?: boolean
|
|
}
|
|
|
|
const chartConfig = {
|
|
pothole: {
|
|
label: "Potholes",
|
|
color: "var(--chart-1)",
|
|
},
|
|
defectedSignboard: {
|
|
label: "Defected Signboards",
|
|
color: "var(--chart-2)",
|
|
},
|
|
roadCrack: {
|
|
label: "Road Cracks",
|
|
color: "var(--chart-3)",
|
|
},
|
|
damagedRoadMarking: {
|
|
label: "Damaged Markings",
|
|
color: "var(--chart-4)",
|
|
},
|
|
goodSignboard: {
|
|
label: "Good Signboards",
|
|
color: "var(--chart-5)",
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
export function DetectionDonutChart({
|
|
defectedSignboard,
|
|
pothole,
|
|
roadCrack,
|
|
damagedRoadMarking,
|
|
goodSignboard,
|
|
isLoading = false,
|
|
}: DetectionDonutChartProps) {
|
|
const chartData = React.useMemo(
|
|
() =>
|
|
[
|
|
{ type: "pothole", count: pothole, fill: "var(--color-pothole)" },
|
|
{
|
|
type: "defectedSignboard",
|
|
count: defectedSignboard,
|
|
fill: "var(--color-defectedSignboard)",
|
|
},
|
|
{ type: "roadCrack", count: roadCrack, fill: "var(--color-roadCrack)" },
|
|
{
|
|
type: "damagedRoadMarking",
|
|
count: damagedRoadMarking,
|
|
fill: "var(--color-damagedRoadMarking)",
|
|
},
|
|
{
|
|
type: "goodSignboard",
|
|
count: goodSignboard,
|
|
fill: "var(--color-goodSignboard)",
|
|
},
|
|
].filter((item) => item.count > 0),
|
|
[
|
|
pothole,
|
|
defectedSignboard,
|
|
roadCrack,
|
|
damagedRoadMarking,
|
|
goodSignboard,
|
|
]
|
|
)
|
|
|
|
const totalDetections = React.useMemo(() => {
|
|
return chartData.reduce((acc, curr) => acc + curr.count, 0)
|
|
}, [chartData])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="h-[250px] flex items-center justify-center">
|
|
<Loader2 className="h-8 w-8 text-primary/50 animate-spin" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (totalDetections === 0) {
|
|
return (
|
|
<div className="h-[250px] flex flex-col items-center justify-center text-muted-foreground">
|
|
<p className="text-sm">No detections found</p>
|
|
<p className="text-xs mt-1">Process videos to see data</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="mx-auto aspect-square max-h-[250px]"
|
|
>
|
|
<PieChart>
|
|
<ChartTooltip
|
|
cursor={false}
|
|
content={<ChartTooltipContent hideLabel />}
|
|
/>
|
|
<Pie
|
|
data={chartData}
|
|
dataKey="count"
|
|
nameKey="type"
|
|
innerRadius={60}
|
|
strokeWidth={5}
|
|
>
|
|
<Label
|
|
content={({ viewBox }) => {
|
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
|
return (
|
|
<text
|
|
x={viewBox.cx}
|
|
y={viewBox.cy}
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
>
|
|
<tspan
|
|
x={viewBox.cx}
|
|
y={viewBox.cy}
|
|
className="fill-foreground text-3xl font-bold"
|
|
>
|
|
{totalDetections.toLocaleString()}
|
|
</tspan>
|
|
<tspan
|
|
x={viewBox.cx}
|
|
y={(viewBox.cy || 0) + 24}
|
|
className="fill-muted-foreground"
|
|
>
|
|
Total
|
|
</tspan>
|
|
</text>
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
</Pie>
|
|
</PieChart>
|
|
</ChartContainer>
|
|
)
|
|
}
|
|
|