All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
ScatterChart,
|
|
Scatter,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
ReferenceLine,
|
|
} from 'recharts'
|
|
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
|
|
email: string
|
|
evaluationCount: number
|
|
averageScore: number
|
|
stddev: number
|
|
deviationFromOverall: number
|
|
isOutlier: boolean
|
|
}
|
|
|
|
interface JurorConsistencyProps {
|
|
data: {
|
|
overallAverage: number
|
|
jurors: JurorMetric[]
|
|
}
|
|
}
|
|
|
|
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
|
const scatterData = data.jurors.map((j) => ({
|
|
name: j.name,
|
|
avgScore: parseFloat(j.averageScore.toFixed(2)),
|
|
stddev: parseFloat(j.stddev.toFixed(2)),
|
|
evaluations: j.evaluationCount,
|
|
isOutlier: j.isOutlier,
|
|
}))
|
|
|
|
const outlierCount = data.jurors.filter((j) => j.isOutlier).length
|
|
|
|
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>
|
|
<div className="h-[400px]">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
type="number"
|
|
dataKey="avgScore"
|
|
name="Average Score"
|
|
domain={[0, 10]}
|
|
label={{ value: 'Average Score', position: 'insideBottom', offset: -10 }}
|
|
/>
|
|
<YAxis
|
|
type="number"
|
|
dataKey="stddev"
|
|
name="Std Deviation"
|
|
label={{ value: 'Std Deviation', angle: -90, position: 'insideLeft' }}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: 'hsl(var(--card))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '6px',
|
|
}}
|
|
/>
|
|
<ReferenceLine
|
|
x={data.overallAverage}
|
|
stroke="#de0f1e"
|
|
strokeDasharray="3 3"
|
|
label={{ value: 'Avg', fill: '#de0f1e', position: 'top' }}
|
|
/>
|
|
<Scatter data={scatterData} fill="#053d57">
|
|
{scatterData.map((entry, index) => (
|
|
<circle
|
|
key={index}
|
|
r={Math.max(4, entry.evaluations)}
|
|
fill={entry.isOutlier ? '#de0f1e' : '#053d57'}
|
|
fillOpacity={0.7}
|
|
/>
|
|
))}
|
|
</Scatter>
|
|
</ScatterChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<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>
|
|
<div>
|
|
<p className="font-medium">{juror.name}</p>
|
|
<p className="text-xs text-muted-foreground">{juror.email}</p>
|
|
</div>
|
|
</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>
|
|
)
|
|
}
|