Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
Some checks failed
Build and Push Docker Image / build (push) Failing after 23s
Some checks failed
Build and Push Docker Image / build (push) Failing after 23s
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart) - Remove @nivo/*, @react-spring/web dependencies (45 packages removed) - Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed - Add new /observer/projects page with search, filters, sorting, pagination, CSV export - Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export - Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files) - Update loading skeletons to match new layouts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { ResponsivePie } from '@nivo/pie'
|
||||
import { ResponsiveBar } from '@nivo/bar'
|
||||
import { DonutChart, BarChart } from '@tremor/react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { nivoTheme, BRAND_COLORS } from './chart-theme'
|
||||
import { BRAND_COLORS } from './chart-theme'
|
||||
|
||||
interface DiversityData {
|
||||
total: number
|
||||
@@ -49,10 +48,10 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Top countries for pie chart (max 10, others grouped)
|
||||
// Top countries for donut chart (max 10, others grouped)
|
||||
const topCountries = (data.byCountry || []).slice(0, 10)
|
||||
const otherCountries = (data.byCountry || []).slice(10)
|
||||
const countryPieData = otherCountries.length > 0
|
||||
const countryData = otherCountries.length > 0
|
||||
? [...topCountries, {
|
||||
country: 'Others',
|
||||
count: otherCountries.reduce((sum, c) => sum + c.count, 0),
|
||||
@@ -60,21 +59,19 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
}]
|
||||
: topCountries
|
||||
|
||||
const nivoPieData = countryPieData.map((c) => ({
|
||||
id: c.country === 'Others' ? 'Others' : c.country.toUpperCase(),
|
||||
label: getCountryName(c.country),
|
||||
const donutData = countryData.map((c) => ({
|
||||
name: getCountryName(c.country),
|
||||
value: c.count,
|
||||
}))
|
||||
|
||||
// Pre-format category and ocean issue data for display
|
||||
const formattedCategories = (data.byCategory || []).slice(0, 10).map((c) => ({
|
||||
const categoryData = (data.byCategory || []).slice(0, 10).map((c) => ({
|
||||
category: formatLabel(c.category),
|
||||
count: c.count,
|
||||
Count: c.count,
|
||||
}))
|
||||
|
||||
const formattedOceanIssues = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
|
||||
const oceanIssueData = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
|
||||
issue: formatLabel(o.issue),
|
||||
count: o.count,
|
||||
Count: o.count,
|
||||
}))
|
||||
|
||||
return (
|
||||
@@ -114,45 +111,17 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
<CardTitle>Geographic Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: '400px' }}>
|
||||
{nivoPieData.length > 0 ? <ResponsivePie
|
||||
data={nivoPieData}
|
||||
theme={nivoTheme}
|
||||
colors={[...BRAND_COLORS]}
|
||||
innerRadius={0.4}
|
||||
padAngle={0.5}
|
||||
cornerRadius={3}
|
||||
activeOuterRadiusOffset={8}
|
||||
margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
|
||||
enableArcLinkLabels={true}
|
||||
arcLinkLabelsSkipAngle={10}
|
||||
arcLinkLabelsTextColor="#374151"
|
||||
arcLinkLabelsThickness={2}
|
||||
arcLinkLabelsColor={{ from: 'color' }}
|
||||
enableArcLabels={true}
|
||||
arcLabelsSkipAngle={10}
|
||||
arcLabelsTextColor={{ from: 'color', modifiers: [['darker', 2]] }}
|
||||
legends={[
|
||||
{
|
||||
anchor: 'bottom',
|
||||
direction: 'row',
|
||||
justify: false,
|
||||
translateX: 0,
|
||||
translateY: 56,
|
||||
itemsSpacing: 0,
|
||||
itemWidth: 100,
|
||||
itemHeight: 18,
|
||||
itemTextColor: '#374151',
|
||||
itemDirection: 'left-to-right',
|
||||
itemOpacity: 1,
|
||||
symbolSize: 12,
|
||||
symbolShape: 'circle',
|
||||
},
|
||||
]}
|
||||
/> : (
|
||||
<p className="text-muted-foreground text-center py-8">No geographic data</p>
|
||||
)}
|
||||
</div>
|
||||
{donutData.length > 0 ? (
|
||||
<DonutChart
|
||||
data={donutData}
|
||||
category="value"
|
||||
index="name"
|
||||
colors={[...BRAND_COLORS] as string[]}
|
||||
className="h-[400px]"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-8">No geographic data</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -162,29 +131,17 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
<CardTitle>Competition Categories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formattedCategories.length > 0 ? (
|
||||
<div style={{ height: '400px' }}>
|
||||
<ResponsiveBar
|
||||
data={formattedCategories}
|
||||
theme={nivoTheme}
|
||||
keys={['count']}
|
||||
indexBy="category"
|
||||
layout="horizontal"
|
||||
colors={[BRAND_COLORS[0]]}
|
||||
borderRadius={4}
|
||||
margin={{ top: 10, right: 30, bottom: 10, left: 120 }}
|
||||
padding={0.3}
|
||||
enableLabel={true}
|
||||
labelTextColor="#ffffff"
|
||||
enableGridX={true}
|
||||
enableGridY={false}
|
||||
axisBottom={null}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{categoryData.length > 0 ? (
|
||||
<BarChart
|
||||
data={categoryData}
|
||||
index="category"
|
||||
categories={['Count']}
|
||||
colors={[BRAND_COLORS[0]] as string[]}
|
||||
layout="horizontal"
|
||||
yAxisWidth={120}
|
||||
showLegend={false}
|
||||
className="h-[400px]"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-8">No category data</p>
|
||||
)}
|
||||
@@ -193,38 +150,22 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
|
||||
</div>
|
||||
|
||||
{/* Ocean Issues */}
|
||||
{formattedOceanIssues.length > 0 && (
|
||||
{oceanIssueData.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ocean Issues Addressed</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ height: '400px' }}>
|
||||
<ResponsiveBar
|
||||
data={formattedOceanIssues}
|
||||
theme={nivoTheme}
|
||||
keys={['count']}
|
||||
indexBy="issue"
|
||||
layout="vertical"
|
||||
colors={[BRAND_COLORS[2]]}
|
||||
borderRadius={4}
|
||||
margin={{ top: 20, right: 30, bottom: 80, left: 40 }}
|
||||
padding={0.3}
|
||||
enableLabel={true}
|
||||
labelTextColor="#ffffff"
|
||||
enableGridX={false}
|
||||
enableGridY={true}
|
||||
axisBottom={{
|
||||
tickSize: 0,
|
||||
tickPadding: 8,
|
||||
tickRotation: -35,
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<BarChart
|
||||
data={oceanIssueData}
|
||||
index="issue"
|
||||
categories={['Count']}
|
||||
colors={[BRAND_COLORS[2]] as string[]}
|
||||
showLegend={false}
|
||||
yAxisWidth={40}
|
||||
className="h-[400px]"
|
||||
rotateLabelX={{ angle: -35, xAxisHeight: 80 }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user