Files
MOPC-Portal/src/components/charts/juror-consistency.tsx

148 lines
4.8 KiB
TypeScript
Raw Normal View History

'use client'
import { ScatterChart } from '@tremor/react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AlertTriangle } from 'lucide-react'
interface JurorMetric {
userId: string
name: string
evaluationCount: number
averageScore: number
stddev: number
deviationFromOverall: number
isOutlier: boolean
}
interface JurorConsistencyProps {
data: {
overallAverage: number
jurors: JurorMetric[]
}
}
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
if (!data?.jurors?.length) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">No juror consistency data available</p>
</CardContent>
</Card>
)
}
const outlierCount = data.jurors.filter((j) => j.isOutlier).length
const scatterData = data.jurors.map((j) => ({
'Average Score': parseFloat(j.averageScore.toFixed(2)),
'Std Deviation': parseFloat(j.stddev.toFixed(2)),
category: j.isOutlier ? 'Outlier' : 'Normal',
name: j.name,
evaluations: j.evaluationCount,
size: Math.max(8, Math.min(20, j.evaluationCount * 2)),
}))
return (
<div className="space-y-6">
{/* Scatter: Average Score vs Standard Deviation */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Juror Scoring Patterns</span>
<span className="text-sm font-normal text-muted-foreground">
Overall Avg: {data.overallAverage.toFixed(2)}
{outlierCount > 0 && (
<Badge variant="destructive" className="ml-2">
{outlierCount} outlier{outlierCount > 1 ? 's' : ''}
</Badge>
)}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<ScatterChart
data={scatterData}
x="Average Score"
y="Std Deviation"
category="category"
size="size"
colors={['blue', 'rose']}
className="h-[400px]"
/>
<p className="text-xs text-muted-foreground mt-2 text-center">
Dot size represents number of evaluations. Red dots indicate outlier
jurors (2+ points from mean).
</p>
</CardContent>
</Card>
{/* Juror details table */}
<Card>
<CardHeader>
<CardTitle>Juror Consistency Details</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Juror</TableHead>
<TableHead className="text-right">Evaluations</TableHead>
<TableHead className="text-right">Avg Score</TableHead>
<TableHead className="text-right">Std Dev</TableHead>
<TableHead className="text-right">
Deviation from Mean
</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.jurors.map((juror) => (
<TableRow
key={juror.userId}
className={juror.isOutlier ? 'bg-destructive/5' : ''}
>
<TableCell>
<p className="font-medium">{juror.name}</p>
</TableCell>
<TableCell className="text-right tabular-nums">
{juror.evaluationCount}
</TableCell>
<TableCell className="text-right tabular-nums">
{juror.averageScore.toFixed(2)}
</TableCell>
<TableCell className="text-right tabular-nums">
{juror.stddev.toFixed(2)}
</TableCell>
<TableCell className="text-right tabular-nums">
{juror.deviationFromOverall.toFixed(2)}
</TableCell>
<TableCell className="text-center">
{juror.isOutlier ? (
<Badge variant="destructive" className="gap-1">
<AlertTriangle className="h-3 w-3" />
Outlier
</Badge>
) : (
<Badge variant="secondary">Normal</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}