'use client' import { ResponsiveScatterPlot } from '@nivo/scatterplot' import type { ScatterPlotDatum, ScatterPlotNodeProps, } from '@nivo/scatterplot' import { animated } from '@react-spring/web' 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' import { nivoTheme, BRAND_DARK_BLUE, BRAND_RED } from './chart-theme' interface JurorMetric { userId: string name: string evaluationCount: number averageScore: number stddev: number deviationFromOverall: number isOutlier: boolean } interface JurorConsistencyProps { data: { overallAverage: number jurors: JurorMetric[] } } interface JurorDatum extends ScatterPlotDatum { x: number y: number name: string evaluations: number isOutlier: boolean } function CustomNode({ node, style, blendMode, isInteractive, onMouseEnter, onMouseMove, onMouseLeave, onClick, }: ScatterPlotNodeProps) { const fillColor = node.data.isOutlier ? BRAND_RED : BRAND_DARK_BLUE return ( s / 2)} fill={fillColor} fillOpacity={0.7} stroke={fillColor} strokeWidth={1} style={{ mixBlendMode: blendMode }} onMouseEnter={ isInteractive && onMouseEnter ? (event) => onMouseEnter(node, event) : undefined } onMouseMove={ isInteractive && onMouseMove ? (event) => onMouseMove(node, event) : undefined } onMouseLeave={ isInteractive && onMouseLeave ? (event) => onMouseLeave(node, event) : undefined } onClick={ isInteractive && onClick ? (event) => onClick(node, event) : undefined } /> ) } export function JurorConsistencyChart({ data }: JurorConsistencyProps) { if (!data?.jurors?.length) { return (

No juror consistency data available

) } const scatterData = [ { id: 'Jurors', data: data.jurors.map((j) => ({ x: parseFloat(j.averageScore.toFixed(2)), y: parseFloat(j.stddev.toFixed(2)), name: j.name, evaluations: j.evaluationCount, isOutlier: j.isOutlier, })), }, ] const outlierCount = data.jurors.filter((j) => j.isOutlier).length return (
{/* Scatter: Average Score vs Standard Deviation */} Juror Scoring Patterns Overall Avg: {data.overallAverage.toFixed(2)} {outlierCount > 0 && ( {outlierCount} outlier{outlierCount > 1 ? 's' : ''} )}
data={scatterData} theme={nivoTheme} colors={[BRAND_DARK_BLUE]} xScale={{ type: 'linear', min: 0, max: 10 }} yScale={{ type: 'linear', min: 0, max: 'auto' }} axisBottom={{ legend: 'Average Score', legendPosition: 'middle', legendOffset: 40, }} axisLeft={{ legend: 'Std Deviation', legendPosition: 'middle', legendOffset: -50, }} useMesh={true} nodeSize={(node) => Math.max(8, Math.min(20, node.data.evaluations * 2)) } nodeComponent={CustomNode} margin={{ top: 20, right: 20, bottom: 60, left: 60 }} tooltip={({ node }) => (
{node.data.name}
Avg Score: {node.data.x}
Std Dev: {node.data.y}
Evaluations: {node.data.evaluations}
)} markers={[ { axis: 'x', value: data.overallAverage, lineStyle: { stroke: BRAND_RED, strokeWidth: 2, strokeDasharray: '6 4', }, legend: `Avg: ${data.overallAverage.toFixed(1)}`, legendPosition: 'top', }, ]} />

Dot size represents number of evaluations. Red dots indicate outlier jurors (2+ points from mean).

{/* Juror details table */} Juror Consistency Details Juror Evaluations Avg Score Std Dev Deviation from Mean Status {data.jurors.map((juror) => (

{juror.name}

{juror.evaluationCount} {juror.averageScore.toFixed(2)} {juror.stddev.toFixed(2)} {juror.deviationFromOverall.toFixed(2)} {juror.isOutlier ? ( Outlier ) : ( Normal )}
))}
) }