From fbcbf895becb1e72c282179c160f241c25b32cbf Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 20 Feb 2026 13:42:31 +0100 Subject: [PATCH] Add defensive null guards to all chart components and analytics All 9 chart components now have early-return null/empty checks before calling .map() on data props. The diversity-metrics chart guards all nested array fields (byCountry, byCategory, byOceanIssue, byTag). Analytics backend guards p.tags in getDiversityMetrics. This prevents any "Cannot read properties of null (reading 'map')" crashes even if upstream data shapes are unexpected. Co-Authored-By: Claude Opus 4.6 --- src/components/charts/criteria-scores.tsx | 2 ++ .../charts/cross-round-comparison.tsx | 10 ++++++++++ src/components/charts/diversity-metrics.tsx | 20 +++++++++---------- src/components/charts/evaluation-timeline.tsx | 2 ++ src/components/charts/juror-consistency.tsx | 10 ++++++++++ src/components/charts/juror-workload.tsx | 2 ++ src/components/charts/project-rankings.tsx | 2 ++ src/components/charts/score-distribution.tsx | 2 ++ src/components/charts/status-breakdown.tsx | 2 ++ src/server/routers/analytics.ts | 2 +- 10 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/components/charts/criteria-scores.tsx b/src/components/charts/criteria-scores.tsx index 1f6c97b..588b105 100644 --- a/src/components/charts/criteria-scores.tsx +++ b/src/components/charts/criteria-scores.tsx @@ -23,6 +23,8 @@ type CriterionBarDatum = { } export function CriteriaScoresChart({ data }: CriteriaScoresProps) { + if (!data?.length) return null + const overallAverage = data.length > 0 ? data.reduce((sum, d) => sum + d.averageScore, 0) / data.length diff --git a/src/components/charts/cross-round-comparison.tsx b/src/components/charts/cross-round-comparison.tsx index a5e847b..7420cc4 100644 --- a/src/components/charts/cross-round-comparison.tsx +++ b/src/components/charts/cross-round-comparison.tsx @@ -21,6 +21,16 @@ interface CrossStageComparisonProps { export function CrossStageComparisonChart({ data, }: CrossStageComparisonProps) { + if (!data?.length) { + return ( + + +

No comparison data available

+
+
+ ) + } + const baseData = data.map((round) => ({ name: round.roundName.length > 20 diff --git a/src/components/charts/diversity-metrics.tsx b/src/components/charts/diversity-metrics.tsx index 6b441bd..e8d6316 100644 --- a/src/components/charts/diversity-metrics.tsx +++ b/src/components/charts/diversity-metrics.tsx @@ -39,7 +39,7 @@ function formatLabel(value: string): string { } export function DiversityMetricsChart({ data }: DiversityMetricsProps) { - if (data.total === 0) { + if (!data || data.total === 0) { return ( @@ -50,8 +50,8 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { } // Top countries for pie chart (max 10, others grouped) - const topCountries = data.byCountry.slice(0, 10) - const otherCountries = data.byCountry.slice(10) + const topCountries = (data.byCountry || []).slice(0, 10) + const otherCountries = (data.byCountry || []).slice(10) const countryPieData = otherCountries.length > 0 ? [...topCountries, { country: 'Others', @@ -67,12 +67,12 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { })) // Pre-format category and ocean issue data for display - const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({ + const formattedCategories = (data.byCategory || []).slice(0, 10).map((c) => ({ category: formatLabel(c.category), count: c.count, })) - const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({ + const formattedOceanIssues = (data.byOceanIssue || []).slice(0, 15).map((o) => ({ issue: formatLabel(o.issue), count: o.count, })) @@ -89,19 +89,19 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { -
{data.byCountry.length}
+
{(data.byCountry || []).length}

Countries Represented

-
{data.byCategory.length}
+
{(data.byCategory || []).length}

Categories

-
{data.byTag.length}
+
{(data.byTag || []).length}

Unique Tags

@@ -228,14 +228,14 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) { )} {/* Tags Cloud */} - {data.byTag.length > 0 && ( + {(data.byTag || []).length > 0 && ( Project Tags
- {data.byTag.slice(0, 30).map((tag) => ( + {(data.byTag || []).slice(0, 30).map((tag) => ( ({ ...d, dateFormatted: new Date(d.date).toLocaleDateString('en-US', { diff --git a/src/components/charts/juror-consistency.tsx b/src/components/charts/juror-consistency.tsx index e97556a..f1ee1ca 100644 --- a/src/components/charts/juror-consistency.tsx +++ b/src/components/charts/juror-consistency.tsx @@ -91,6 +91,16 @@ function CustomNode({ } export function JurorConsistencyChart({ data }: JurorConsistencyProps) { + if (!data?.jurors?.length) { + return ( + + +

No juror consistency data available

+
+
+ ) + } + const scatterData = [ { id: 'Jurors', diff --git a/src/components/charts/juror-workload.tsx b/src/components/charts/juror-workload.tsx index b682575..81282d6 100644 --- a/src/components/charts/juror-workload.tsx +++ b/src/components/charts/juror-workload.tsx @@ -25,6 +25,8 @@ type WorkloadBarDatum = { } export function JurorWorkloadChart({ data }: JurorWorkloadProps) { + if (!data?.length) return null + const totalAssigned = data.reduce((sum, d) => sum + d.assigned, 0) const totalCompleted = data.reduce((sum, d) => sum + d.completed, 0) const overallRate = diff --git a/src/components/charts/project-rankings.tsx b/src/components/charts/project-rankings.tsx index 1b60158..df02806 100644 --- a/src/components/charts/project-rankings.tsx +++ b/src/components/charts/project-rankings.tsx @@ -30,6 +30,8 @@ export function ProjectRankingsChart({ data, limit = 20, }: ProjectRankingsProps) { + if (!data?.length) return null + const scoredData = data.filter( (d): d is ProjectRankingData & { averageScore: number } => d.averageScore !== null, diff --git a/src/components/charts/score-distribution.tsx b/src/components/charts/score-distribution.tsx index 49cd6bf..f03fcd5 100644 --- a/src/components/charts/score-distribution.tsx +++ b/src/components/charts/score-distribution.tsx @@ -15,6 +15,8 @@ export function ScoreDistributionChart({ averageScore, totalScores, }: ScoreDistributionProps) { + if (!data?.length) return null + const chartData = data.map((d) => ({ score: String(d.score), count: d.count, diff --git a/src/components/charts/status-breakdown.tsx b/src/components/charts/status-breakdown.tsx index b2bf2d3..fe0da18 100644 --- a/src/components/charts/status-breakdown.tsx +++ b/src/components/charts/status-breakdown.tsx @@ -14,6 +14,8 @@ interface StatusBreakdownProps { } export function StatusBreakdownChart({ data }: StatusBreakdownProps) { + if (!data?.length) return null + const total = data.reduce((sum, item) => sum + item.count, 0) const pieData = data.map((d) => ({ diff --git a/src/server/routers/analytics.ts b/src/server/routers/analytics.ts index 5dc7258..245b1f6 100644 --- a/src/server/routers/analytics.ts +++ b/src/server/routers/analytics.ts @@ -615,7 +615,7 @@ export const analyticsRouter = router({ // By tag const tagCounts: Record = {} projects.forEach((p) => { - p.tags.forEach((tag) => { + (p.tags || []).forEach((tag) => { tagCounts[tag] = (tagCounts[tag] || 0) + 1 }) })