Spaces:
Running
Running
AgentGraph
/
frontend
/src
/components
/features
/dashboard
/visualizations
/RiskDistributionPieChart.tsx
| import React, { useMemo } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { AlertTriangle, BarChart3 } from "lucide-react"; | |
| import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; | |
| interface RiskTypeStats { | |
| [riskType: string]: { | |
| count: number; | |
| percentage: number; | |
| affectedEntities: Set<string>; | |
| avgSeverity: number; | |
| }; | |
| } | |
| interface RiskDistributionPieChartProps { | |
| riskTypeStats: RiskTypeStats; | |
| totalFailures: number; | |
| onRiskTypeClick: (riskType: string, stats: any) => void; | |
| } | |
| interface PieChartData { | |
| name: string; | |
| value: number; | |
| percentage: number; | |
| color: string; | |
| severity: number; | |
| affectedEntities: number; | |
| } | |
| export function RiskDistributionPieChart({ | |
| riskTypeStats, | |
| totalFailures, | |
| onRiskTypeClick, | |
| }: RiskDistributionPieChartProps) { | |
| const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null); | |
| // Add rotation animation styles | |
| React.useEffect(() => { | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| .rotating-chart { | |
| animation: rotate 20s linear infinite; | |
| transform-origin: center; | |
| } | |
| .rotating-chart:hover { | |
| animation-play-state: paused; | |
| } | |
| @keyframes rotate { | |
| from { | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| return () => { | |
| document.head.removeChild(style); | |
| }; | |
| }, []); | |
| // Transform data for pie chart | |
| const chartData = useMemo<PieChartData[]>(() => { | |
| const colors = [ | |
| "#ef4444", // red-500 - High severity | |
| "#f97316", // orange-500 - Medium-high severity | |
| "#eab308", // yellow-500 - Medium severity | |
| "#3b82f6", // blue-500 - Medium-low severity | |
| "#8b5cf6", // purple-500 - Low severity | |
| "#10b981", // emerald-500 - Very low severity | |
| ]; | |
| // Guard against undefined or empty riskTypeStats | |
| if (!riskTypeStats || typeof riskTypeStats !== "object") { | |
| return []; | |
| } | |
| return Object.entries(riskTypeStats) | |
| .filter( | |
| ([riskType, stats]) => riskType && stats && typeof stats === "object" | |
| ) | |
| .map(([riskType, stats], index) => ({ | |
| name: riskType.replace(/_/g, " "), | |
| value: stats.count || 0, | |
| percentage: stats.percentage || 0, | |
| color: colors[index % colors.length] || "#6b7280", | |
| severity: stats.avgSeverity || 0, | |
| affectedEntities: stats.affectedEntities?.size || 0, | |
| })); | |
| }, [riskTypeStats]); | |
| // Handle pie chart segment click | |
| const handleSegmentClick = (data: PieChartData) => { | |
| // Guard against undefined data | |
| if (!data || !data.name) { | |
| return; | |
| } | |
| const originalRiskType = Object.keys(riskTypeStats).find( | |
| (key) => key.replace(/_/g, " ") === data.name | |
| ); | |
| if (originalRiskType) { | |
| onRiskTypeClick(originalRiskType, riskTypeStats[originalRiskType]); | |
| } | |
| }; | |
| if (totalFailures === 0) { | |
| return ( | |
| <Card className="glass-card h-full flex flex-col"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <AlertTriangle className="h-5 w-5" /> | |
| Risk Distribution | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="flex-1 flex items-center justify-center"> | |
| <div className="text-center py-8 text-muted-foreground"> | |
| <BarChart3 className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <p>No risk data available</p> | |
| <p className="text-sm"> | |
| Upload traces with failures to see distribution | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| return ( | |
| <Card className="glass-card shadow-sm h-full flex flex-col"> | |
| <CardHeader className="pb-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <AlertTriangle className="h-5 w-5 text-muted-foreground" /> | |
| <CardTitle className="text-lg font-semibold"> | |
| Risk Distribution | |
| </CardTitle> | |
| </div> | |
| </div> | |
| <p className="text-sm text-muted-foreground mt-2"> | |
| Click segments for detailed analysis | |
| </p> | |
| </CardHeader> | |
| <CardContent className="flex-1"> | |
| {/* Pie Chart with legend */} | |
| <div className="space-y-4"> | |
| {/* Rotating Pie Chart */} | |
| <div id="risk-distribution-chart" className="h-80 w-full relative"> | |
| {/* Glass overlay effect */} | |
| <div className="absolute inset-0 bg-gradient-to-br from-white/5 via-transparent to-white/5 rounded-lg pointer-events-none" /> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <PieChart className="rotating-chart"> | |
| <Pie | |
| data={chartData} | |
| cx="50%" | |
| cy="50%" | |
| innerRadius={70} | |
| outerRadius={120} | |
| paddingAngle={2} | |
| dataKey="value" | |
| cursor="pointer" | |
| onClick={handleSegmentClick} | |
| > | |
| {chartData.map((entry, index) => ( | |
| <Cell | |
| key={`cell-${index}`} | |
| fill={entry.color} | |
| stroke={ | |
| hoveredIndex === index | |
| ? "rgba(255,255,255,0.4)" | |
| : "rgba(255,255,255,0.1)" | |
| } | |
| strokeWidth={hoveredIndex === index ? 2 : 1} | |
| className="transition-all duration-200" | |
| style={{ | |
| filter: | |
| hoveredIndex === index | |
| ? "brightness(1.1) drop-shadow(0 0 8px rgba(255,255,255,0.3))" | |
| : "none", | |
| }} | |
| onMouseEnter={() => setHoveredIndex(index)} | |
| onMouseLeave={() => setHoveredIndex(null)} | |
| /> | |
| ))} | |
| </Pie> | |
| </PieChart> | |
| </ResponsiveContainer> | |
| </div> | |
| {/* Legend Table */} | |
| <div className="grid grid-cols-1 gap-2"> | |
| {chartData.map((entry, index) => ( | |
| <div | |
| key={index} | |
| className={`flex items-center justify-between p-3 rounded-lg border transition-all cursor-pointer ${ | |
| hoveredIndex === index | |
| ? "bg-gradient-to-r from-white/20 to-white/10 border-white/30 shadow-lg scale-[1.02]" | |
| : "bg-gradient-to-r from-white/10 to-transparent border-white/10 hover:bg-white/5" | |
| }`} | |
| onClick={() => handleSegmentClick(entry)} | |
| onMouseEnter={() => setHoveredIndex(index)} | |
| onMouseLeave={() => setHoveredIndex(null)} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div | |
| className="w-4 h-4 rounded-full border border-white/20" | |
| style={{ backgroundColor: entry.color }} | |
| /> | |
| <span className="text-sm font-medium text-foreground"> | |
| {entry.name} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-4 text-sm text-muted-foreground"> | |
| <span className="font-medium">{entry.value} failures</span> | |
| <span className="text-xs bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded"> | |
| {entry.percentage}% | |
| </span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |