All checks were successful
Build and Push Docker Image / build (push) Successful in 12m29s
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering, unscored projects, criteria scores, activeRoundCount scoping, email privacy leaks in juror consistency + workload) Phase 2-3: Migrate all 9 chart components from Recharts to Nivo (@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot outlier coloring and pie chart label visibility bugs. Phase 4: Add round-type-aware stats (getRoundTypeStats backend + RoundTypeStatsCards component) showing appropriate metrics per round type (intake/filtering/evaluation/submission/mentoring/live/deliberation). Phase 5: UX improvements — Stage→Round terminology, clickable dashboard round links, URL-based round selection (?round=), round type indicators in selectors, accessible Toggle-based cross-round comparison, sortable project table columns (title/score/evaluations), brand score colors on dashboard bar chart with aria labels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
'use client'
|
|
|
|
import { ResponsiveBar } from '@nivo/bar'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { nivoTheme, scoreGradient } from './chart-theme'
|
|
|
|
interface ProjectRankingData {
|
|
id: string
|
|
title: string
|
|
teamName: string | null
|
|
status: string
|
|
averageScore: number | null
|
|
evaluationCount: number
|
|
}
|
|
|
|
interface ProjectRankingsProps {
|
|
data: ProjectRankingData[]
|
|
limit?: number
|
|
}
|
|
|
|
type RankingBarDatum = {
|
|
project: string
|
|
score: number
|
|
fullTitle: string
|
|
teamName: string
|
|
evaluationCount: number
|
|
}
|
|
|
|
export function ProjectRankingsChart({
|
|
data,
|
|
limit = 20,
|
|
}: ProjectRankingsProps) {
|
|
const scoredData = data.filter(
|
|
(d): d is ProjectRankingData & { averageScore: number } =>
|
|
d.averageScore !== null,
|
|
)
|
|
|
|
const averageScore =
|
|
scoredData.length > 0
|
|
? scoredData.reduce((sum, d) => sum + d.averageScore, 0) /
|
|
scoredData.length
|
|
: 0
|
|
|
|
const displayData = scoredData.slice(0, limit)
|
|
|
|
const chartData: RankingBarDatum[] = displayData.map((d) => ({
|
|
project:
|
|
d.title.length > 30 ? d.title.substring(0, 30) + '...' : d.title,
|
|
score: d.averageScore,
|
|
fullTitle: d.title,
|
|
teamName: d.teamName ?? '',
|
|
evaluationCount: d.evaluationCount,
|
|
}))
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>Project Rankings</span>
|
|
<span className="text-sm font-normal text-muted-foreground">
|
|
Top {displayData.length} of {scoredData.length} scored projects
|
|
</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div
|
|
style={{
|
|
height: `${Math.max(400, displayData.length * 30)}px`,
|
|
}}
|
|
>
|
|
<ResponsiveBar
|
|
data={chartData}
|
|
keys={['score']}
|
|
indexBy="project"
|
|
layout="horizontal"
|
|
theme={nivoTheme}
|
|
colors={(bar) => scoreGradient(bar.data.score as number)}
|
|
valueScale={{ type: 'linear', max: 10 }}
|
|
borderRadius={4}
|
|
enableLabel={true}
|
|
label={(d) => {
|
|
const v = d.value
|
|
return v != null ? Number(v).toFixed(1) : ''
|
|
}}
|
|
labelSkipWidth={30}
|
|
labelTextColor="#ffffff"
|
|
margin={{ top: 10, right: 30, bottom: 30, left: 200 }}
|
|
padding={0.2}
|
|
markers={[
|
|
{
|
|
axis: 'x',
|
|
value: averageScore,
|
|
lineStyle: {
|
|
stroke: '#6b7280',
|
|
strokeWidth: 2,
|
|
strokeDasharray: '6 4',
|
|
},
|
|
legend: `Avg: ${averageScore.toFixed(1)}`,
|
|
legendPosition: 'top',
|
|
textStyle: {
|
|
fill: '#6b7280',
|
|
fontSize: 11,
|
|
},
|
|
},
|
|
]}
|
|
tooltip={({ data: rowData }) => (
|
|
<div
|
|
style={{
|
|
background: '#ffffff',
|
|
padding: '8px 12px',
|
|
borderRadius: '8px',
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
border: '1px solid #e5e7eb',
|
|
fontSize: 12,
|
|
}}
|
|
>
|
|
<strong>{rowData.fullTitle}</strong>
|
|
{rowData.teamName && (
|
|
<>
|
|
<br />
|
|
<span style={{ color: '#6b7280' }}>
|
|
{rowData.teamName}
|
|
</span>
|
|
</>
|
|
)}
|
|
<br />
|
|
Score: {Number(rowData.score).toFixed(2)}
|
|
<br />
|
|
Evaluations: {rowData.evaluationCount}
|
|
</div>
|
|
)}
|
|
animate={true}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|