web/src/features/score-analytics/README.md
Score Analytics provides a comprehensive dashboard for analyzing score data with support for single and two-score comparison modes. The architecture follows a Provider + Hook + Smart Cards pattern for clean separation of concerns and maximum code reuse.
useScoreAnalyticsQuery hook (not in components)ScoreAnalyticsProvider exposes all data via React ContextuseScoreAnalytics() hook/web/src/features/score-analytics/
├── /server/
│ └── scoreAnalyticsRouter.ts # tRPC router with 3 procedures:
│ # - getScoreIdentifiers
│ # - estimateScoreComparisonSize
│ # - getScoreComparisonAnalytics
│
├── /components/
│ ├── /cards/ # Smart cards (consume context)
│ │ ├── StatisticsCard.tsx # Summary metrics
│ │ ├── TimelineChartCard.tsx # Time series trends
│ │ ├── DistributionChartCard.tsx # Score distributions
│ │ ├── HeatmapCard.tsx # Score comparisons (heatmap/confusion matrix)
│ │ ├── EstimateLoadingCard.tsx # Loading state during estimation
│ │ └── SamplingBadge.tsx # Badge showing sampling status
│ │
│ ├── /charts/ # Reusable chart components (presentation only)
│ │ ├── ScoreDistribution*.tsx # Distribution charts (Numeric/Boolean/Categorical)
│ │ ├── ScoreTimeSeries*.tsx # Timeline charts (Numeric/Boolean/Categorical)
│ │ ├── Heatmap*.tsx # Heatmap components (Heatmap/Cell/Legend/Placeholder)
│ │ ├── MetricCard.tsx # Metric display component
│ │ ├── ScoreCombobox.tsx # Score selector dropdown
│ │ └── ObjectTypeFilter.tsx # Object type filter
│ │
│ ├── ScoreAnalyticsHeader.tsx # Header controls (score selectors, filters, date picker)
│ ├── ScoreAnalyticsDashboard.tsx # 2x2 responsive grid layout
│ └── ScoreAnalyticsProvider.tsx # Context provider (wraps hook + exposes data)
│
├── /hooks/
│ └── useScoreAnalyticsQuery.ts # Data fetching + transformation hook
│
├── /lib/ # Utility functions and transformers
│ ├── scoreAnalyticsTransformers.ts # Pure transformation functions
│ ├── analytics-url-state.ts # URL state management hook
│ ├── clickhouse-time-utils.ts # ClickHouse time interval utilities
│ ├── color-scales.ts # Color scheme generation
│ ├── heatmap-utils.ts # Heatmap data processing
│ ├── score-formatter.ts # Score value formatting
│ └── statistics-utils.ts # Statistical calculations
│
└── README.md # This file
Page (analytics.tsx)
↓
ScoreAnalyticsProvider (wraps dashboard)
↓ (runs estimate query first)
↓ api.scoreAnalytics.estimateScoreComparisonSize
↓ (then runs main query)
↓
useScoreAnalyticsQuery hook
↓ (fetches data via tRPC)
↓ api.scoreAnalytics.getScoreComparisonAnalytics
↓ (transforms using pure functions from /lib/)
↓ (returns structured data)
↓
React Context
↓
Smart Cards (consume via useScoreAnalytics)
↓
Chart Components (receive props)
/components/ScoreAnalyticsProvider.tsx)Purpose: Context provider that wraps useScoreAnalyticsQuery and exposes data to child components.
Responsibilities:
useScoreAnalyticsQuery with query parametersUsage:
<ScoreAnalyticsProvider
projectId="..."
score1={{ id: "...", name: "...", source: "...", dataType: "NUMERIC" }}
score2={...} // optional
objectType="TRACE"
startDate={new Date()}
endDate={new Date()}
interval={{ count: 1, unit: "day" }}
>
<ScoreAnalyticsDashboard />
</ScoreAnalyticsProvider>
/hooks/useScoreAnalyticsQuery.ts)Purpose: Fetches data from API and transforms it ONCE using pure functions.
Responsibilities:
api.scoreAnalytics.getScoreComparisonAnalytics)/lib/scoreAnalyticsTransformers.ts:
ScoreAnalyticsData objectKey Interfaces:
interface ScoreAnalyticsQueryParams {
projectId: string;
score1: ScoreOption;
score2?: ScoreOption;
objectType: ObjectType;
startDate: Date;
endDate: Date;
interval: { count: number; unit: string };
}
interface ScoreAnalyticsData {
statistics: { score1, score2?, comparison? };
distribution: { score1, score2?, binLabels?, categories?, etc. };
timeSeries: { numeric, categorical };
heatmapData: { cells, rowLabels, colLabels };
metadata: { mode: "single" | "two", dataType, isSameScore };
}
/components/cards/)Purpose: Self-contained components that consume context and handle their own UI logic.
Pattern:
export function ExampleCard() {
const { data, isLoading, params, colors } = useScoreAnalytics();
// Handle loading state
if (isLoading) return <LoadingState />;
// Handle empty state
if (!data) return <EmptyState />;
// Use data from context
const { statistics, metadata } = data;
// Render chart with transformed data
return <ChartComponent data={...} />;
}
All cards:
useScoreAnalytics() hook (NOT props)/components/charts/)Purpose: Pure presentation components that receive data via props.
Pattern:
interface ChartProps {
data: SomeDataType;
dataType: "NUMERIC" | "BOOLEAN" | "CATEGORICAL";
score1Name: string;
score2Name?: string;
}
export function ExampleChart({ data, dataType, score1Name, score2Name }: ChartProps) {
// Pure rendering logic only
// No data fetching, no context, no transformations
return <Recharts... />;
}
/lib/scoreAnalyticsTransformers.ts)Purpose: Pure functions for data transformation (no side effects).
Key Functions:
extractCategories() - Get unique categories from confusion matrix/stacked distributionfillDistributionBins() - Fill missing bins with zero countscalculateModeMetrics() - Calculate mode and mode percentagetransformHeatmapData() - Transform API data for heatmaps/confusion matricesgenerateBinLabels() - Generate formatted bin labels for numeric distributionsAll functions:
/lib/)Purpose: Helper functions for specific domains:
analytics-url-state.ts: Manages URL query parameters for filters and selectionsclickhouse-time-utils.ts: ClickHouse interval normalization and time bucketingcolor-scales.ts: Generates consistent color schemes for chartsheatmap-utils.ts: Heatmap-specific data processing and calculationsscore-formatter.ts: Formats score values for displaystatistics-utils.ts: Statistical calculations (correlation, Cohen's Kappa, F1, etc.)/server/scoreAnalyticsRouter.ts)Purpose: tRPC router exposing score analytics API endpoints.
Procedures:
getScoreIdentifiers: Returns all available scores in a project (name, dataType, source)estimateScoreComparisonSize: Estimates query size and determines if sampling is needed
getScoreComparisonAnalytics: Main analytics query with adaptive optimizations
ClickHouse Optimizations:
cityHash64 for consistent samplingNUMERIC: Continuous numeric scores
BOOLEAN: True/false scores
CATEGORICAL: Discrete category scores
Single Score: Analyze one score in isolation
Two Scores: Compare two scores
Same Score Twice: Edge case where same score selected twice
NewCard.tsx in /components/cards/export function NewCard() {
const { data, isLoading, params } = useScoreAnalytics();
if (isLoading) return <LoadingState />;
if (!data) return <EmptyState />;
return <NewChart data={data.something} />;
}
ScoreAnalyticsDashboard.tsxNewChart.tsx in /components/charts//lib/scoreAnalyticsTransformers.tsuseScoreAnalyticsQuery hook's useMemouseScoreAnalyticsQuery.tsuseMemoconst { data, params } = useScoreAnalytics();
const { metadata } = data;
const { mode } = metadata;
if (mode === "single") {
// Show single score UI
} else {
// Show two-score UI with tabs
}
const { metadata } = data;
const { dataType } = metadata;
if (dataType === "NUMERIC") {
return <NumericChart />;
} else if (dataType === "BOOLEAN") {
return <BooleanChart />;
} else {
return <CategoricalChart />;
}
const { colors } = useScoreAnalytics();
if (isSingleScoreColors(colors)) {
// Use colors.score
} else {
// Use colors.score1 and colors.score2
}
Page: /web/src/pages/project/[projectId]/scores/analytics.tsx
Feature Directory: /web/src/features/score-analytics/
Server (tRPC):
/web/src/features/score-analytics/server/scoreAnalyticsRouter.tsapi.scoreAnalytics.*/web/src/server/api/root.tsUtilities:
/web/src/features/score-analytics/lib/
scoreAnalyticsTransformers.ts - Pure transformation functionsanalytics-url-state.ts - URL state managementclickhouse-time-utils.ts - ClickHouse time utilitiescolor-scales.ts - Color scheme generationheatmap-utils.ts - Heatmap processingscore-formatter.ts - Score formattingstatistics-utils.ts - Statistical calculations/web/src/utils/fill-time-series-gaps.tsBackend Repositories:
/packages/shared/src/server/repositories/score-analytics.tsuseMemo with proper dependenciesBackend tests: /web/src/__tests__/server/score-comparison-analytics.servertest.ts
Test coverage:
Run tests:
pnpm --filter=web test -- --testPathPattern="score-comparison-analytics"
Cards not updating: Check that useScoreAnalytics() is called inside <ScoreAnalyticsProvider>
Data transformation issues: Check transformers in useScoreAnalyticsQuery.ts - all transformations should happen there
Type errors: Check interfaces in useScoreAnalyticsQuery.ts and ScoreAnalyticsProvider.tsx
Color issues: Check /lib/color-scales.ts and color assignment in ScoreAnalyticsProvider.tsx
Category collisions in timeline: Check namespace logic in useScoreAnalyticsQuery.ts (categorical time series transformation)