Observer platform overhaul: Nivo charts, round-type stats, UX improvements
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m29s
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>
This commit is contained in:
@@ -1,19 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Area,
|
||||
ComposedChart,
|
||||
Bar,
|
||||
} from 'recharts'
|
||||
import { ResponsiveLine } from '@nivo/line'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { nivoTheme, BRAND_DARK_BLUE } from './chart-theme'
|
||||
|
||||
interface TimelineDataPoint {
|
||||
date: string
|
||||
@@ -26,7 +15,6 @@ interface EvaluationTimelineProps {
|
||||
}
|
||||
|
||||
export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) {
|
||||
// Format date for display
|
||||
const formattedData = data.map((d) => ({
|
||||
...d,
|
||||
dateFormatted: new Date(d.date).toLocaleDateString('en-US', {
|
||||
@@ -38,6 +26,16 @@ export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) {
|
||||
const totalEvaluations =
|
||||
data.length > 0 ? data[data.length - 1].cumulative : 0
|
||||
|
||||
const lineData = [
|
||||
{
|
||||
id: 'Cumulative Evaluations',
|
||||
data: formattedData.map((d) => ({
|
||||
x: d.dateFormatted,
|
||||
y: d.cumulative,
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -49,52 +47,55 @@ export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={formattedData}
|
||||
margin={{ top: 20, right: 30, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="dateFormatted"
|
||||
tick={{ fontSize: 12 }}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis yAxisId="left" orientation="left" stroke="#8884d8" />
|
||||
<YAxis yAxisId="right" orientation="right" stroke="#82ca9d" />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
formatter={(value: number | undefined, name: string | undefined) => [
|
||||
value ?? 0,
|
||||
(name ?? '') === 'daily' ? 'Daily' : 'Cumulative',
|
||||
]}
|
||||
labelFormatter={(label) => `Date: ${label}`}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar
|
||||
yAxisId="left"
|
||||
dataKey="daily"
|
||||
name="Daily Evaluations"
|
||||
fill="#8884d8"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Line
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="cumulative"
|
||||
name="Cumulative Total"
|
||||
stroke="#82ca9d"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
<div style={{ height: '300px' }}>
|
||||
<ResponsiveLine
|
||||
data={lineData}
|
||||
theme={nivoTheme}
|
||||
colors={[BRAND_DARK_BLUE]}
|
||||
enableArea={true}
|
||||
areaOpacity={0.1}
|
||||
areaBaselineValue={0}
|
||||
curve="monotoneX"
|
||||
pointSize={6}
|
||||
pointColor={BRAND_DARK_BLUE}
|
||||
pointBorderWidth={2}
|
||||
pointBorderColor="#ffffff"
|
||||
useMesh={true}
|
||||
enableSlices="x"
|
||||
sliceTooltip={({ slice }) => {
|
||||
const point = slice.points[0]
|
||||
const dataItem = formattedData.find(
|
||||
(d) => d.dateFormatted === point.data.xFormatted
|
||||
)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<strong>{point.data.xFormatted}</strong>
|
||||
<div>Cumulative: {point.data.yFormatted}</div>
|
||||
{dataItem && <div>Daily: {dataItem.daily}</div>}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
|
||||
axisBottom={{
|
||||
tickRotation: -45,
|
||||
legend: '',
|
||||
legendOffset: 36,
|
||||
}}
|
||||
axisLeft={{
|
||||
legend: 'Evaluations',
|
||||
legendOffset: -50,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
yScale={{ type: 'linear', min: 0, max: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user