NBA_PREDICTOR / web /src /pages /Analytics.jsx
jashdoshi77's picture
ANALYSTICS PAGE MORE GRAPHS
737cf65
import { useState, useEffect } from 'react'
import {
LineChart, Line, BarChart, Bar, PieChart, Pie, Cell,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
ScatterChart, Scatter, ZAxis
} from 'recharts'
// API Base URL
const API_BASE = import.meta.env.DEV ? 'http://localhost:8000' : '';
function Analytics() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchAnalytics()
}, [])
const fetchAnalytics = async () => {
try {
const response = await fetch(`${API_BASE}/api/analytics`)
const result = await response.json()
setData(result)
} catch (err) {
console.error('Failed to fetch analytics:', err)
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
<p className="loading-text">Loading analytics...</p>
</div>
)
}
// Chart colors
const COLORS = ['#4ade80', '#facc15', '#f87171', '#60a5fa', '#a78bfa']
const GRADIENT_COLORS = {
primary: '#4ade80',
secondary: '#60a5fa',
accent: '#a78bfa'
}
// Data with fallbacks
const accuracyTrend = data?.accuracy_trend || []
const teamAccuracy = data?.team_accuracy || []
const confidenceDistribution = data?.confidence_distribution || []
const calibrationData = data?.calibration || []
const overallStats = data?.overall || { total_predictions: 0, correct: 0, accuracy: 0, avg_confidence: 0 }
// New advanced data
const eloScatter = data?.elo_scatter || []
const radarData = data?.radar_data || []
const homeAwaySplit = data?.home_away_split || { home: { accuracy: 0 }, away: { accuracy: 0 } }
const streakData = data?.streak_data || []
const currentStreak = data?.current_streak || { count: 0, type: 'W' }
const topMatchups = data?.top_matchups || []
// Custom tooltip styles
const tooltipStyle = {
background: 'rgba(0,0,0,0.9)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '8px',
padding: '8px 12px'
}
return (
<div className="animate-fadeIn">
<div className="page-header">
<h1 className="page-title">Analytics Dashboard</h1>
<p className="page-description">
Advanced model performance and NBA statistics
</p>
</div>
{/* Stats Overview */}
<div className="stats-grid" style={{ marginBottom: 'var(--space-6)' }}>
<div className="stat-card">
<div className="stat-value accent">{overallStats.total_predictions}</div>
<div className="stat-label">Total Predictions</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent-success)' }}>
{overallStats.correct}
</div>
<div className="stat-label">Correct Predictions</div>
</div>
<div className="stat-card">
<div className="stat-value accent">{overallStats.accuracy}%</div>
<div className="stat-label">Overall Accuracy</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{
color: currentStreak.type === 'W' ? '#4ade80' : '#f87171'
}}>
{currentStreak.count}{currentStreak.type}
</div>
<div className="stat-label">Current Streak</div>
</div>
</div>
{/* Prediction Streak Timeline */}
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<div className="card-header">
<span className="card-title">Prediction Streak Timeline</span>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
Last {streakData.length} predictions
</span>
</div>
<div style={{
display: 'flex',
gap: '4px',
padding: 'var(--space-4)',
flexWrap: 'wrap',
justifyContent: 'center'
}}>
{streakData.map((item, idx) => (
<div
key={idx}
title={`${item.matchup} - ${item.date}`}
style={{
width: '32px',
height: '32px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: '700',
fontSize: '0.75rem',
background: item.result === 'W'
? 'linear-gradient(135deg, #22c55e, #16a34a)'
: 'linear-gradient(135deg, #ef4444, #dc2626)',
color: 'white',
cursor: 'pointer',
transition: 'transform 0.2s',
}}
onMouseEnter={(e) => e.target.style.transform = 'scale(1.2)'}
onMouseLeave={(e) => e.target.style.transform = 'scale(1)'}
>
{item.result}
</div>
))}
</div>
</div>
{/* Charts Grid - Row 1 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-6)', marginBottom: 'var(--space-6)' }}>
{/* Radar Chart - Model Dimensions */}
<div className="card">
<div className="card-header">
<span className="card-title">Model Performance Radar</span>
</div>
<div style={{ height: '300px' }}>
<ResponsiveContainer width="100%" height="100%">
<RadarChart cx="50%" cy="50%" outerRadius="70%" data={radarData}>
<PolarGrid stroke="rgba(255,255,255,0.1)" />
<PolarAngleAxis
dataKey="dimension"
tick={{ fill: 'rgba(255,255,255,0.7)', fontSize: 11 }}
/>
<PolarRadiusAxis
angle={90}
domain={[0, 100]}
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 10 }}
/>
<Radar
name="Accuracy %"
dataKey="value"
stroke="#4ade80"
fill="#4ade80"
fillOpacity={0.3}
strokeWidth={2}
/>
<Tooltip contentStyle={tooltipStyle} />
</RadarChart>
</ResponsiveContainer>
</div>
</div>
{/* ELO vs Win% Scatter */}
<div className="card">
<div className="card-header">
<span className="card-title">ELO Rating vs Win %</span>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
Bubble size = Games played
</span>
</div>
<div style={{ height: '300px' }}>
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
type="number"
dataKey="elo"
name="ELO"
domain={['dataMin - 50', 'dataMax + 50']}
stroke="rgba(255,255,255,0.5)"
fontSize={11}
label={{ value: 'ELO Rating', position: 'bottom', fill: 'rgba(255,255,255,0.5)', fontSize: 11 }}
/>
<YAxis
type="number"
dataKey="winPct"
name="Win %"
domain={[20, 80]}
stroke="rgba(255,255,255,0.5)"
fontSize={11}
/>
<ZAxis
type="number"
dataKey="gamesPlayed"
range={[50, 400]}
name="Games"
/>
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
contentStyle={tooltipStyle}
formatter={(value, name) => [value, name]}
labelFormatter={(label) => `Team: ${eloScatter.find(t => t.elo === label)?.team || ''}`}
/>
<Scatter
name="Teams"
data={eloScatter}
fill="#60a5fa"
>
{eloScatter.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.conference === 'East' ? '#60a5fa' : '#f97316'}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
gap: 'var(--space-4)',
padding: 'var(--space-2)',
fontSize: '0.75rem'
}}>
<span><span style={{ color: '#60a5fa' }}></span> East</span>
<span><span style={{ color: '#f97316' }}></span> West</span>
</div>
</div>
</div>
{/* Charts Grid - Row 2 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-6)', marginBottom: 'var(--space-6)' }}>
{/* Accuracy Trend */}
<div className="card">
<div className="card-header">
<span className="card-title">Accuracy Trend (7 Days)</span>
</div>
<div style={{ height: '280px' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={accuracyTrend}>
<defs>
<linearGradient id="colorAccuracy" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={GRADIENT_COLORS.primary} stopOpacity={0.3} />
<stop offset="95%" stopColor={GRADIENT_COLORS.primary} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis dataKey="date" stroke="rgba(255,255,255,0.5)" fontSize={11} />
<YAxis domain={[0, 100]} stroke="rgba(255,255,255,0.5)" fontSize={11} />
<Tooltip contentStyle={tooltipStyle} />
<Area
type="monotone"
dataKey="accuracy"
stroke={GRADIENT_COLORS.primary}
strokeWidth={2}
fill="url(#colorAccuracy)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Home vs Away Performance */}
<div className="card">
<div className="card-header">
<span className="card-title">Home vs Away Accuracy</span>
</div>
<div style={{ height: '280px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ display: 'flex', gap: 'var(--space-8)', alignItems: 'flex-end' }}>
{/* Home Bar */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '80px',
height: `${homeAwaySplit.home?.accuracy * 2.5}px`,
background: 'linear-gradient(180deg, #4ade80, #22c55e)',
borderRadius: '8px 8px 0 0',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
paddingTop: 'var(--space-2)',
minHeight: '40px'
}}>
<span style={{ fontWeight: '700', fontSize: '1.25rem' }}>
{homeAwaySplit.home?.accuracy}%
</span>
</div>
<div style={{
marginTop: 'var(--space-2)',
fontWeight: '600',
color: 'var(--text-secondary)'
}}>
🏠 Home
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{homeAwaySplit.home?.correct}/{homeAwaySplit.home?.total}
</div>
</div>
{/* Away Bar */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '80px',
height: `${homeAwaySplit.away?.accuracy * 2.5}px`,
background: 'linear-gradient(180deg, #60a5fa, #3b82f6)',
borderRadius: '8px 8px 0 0',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
paddingTop: 'var(--space-2)',
minHeight: '40px'
}}>
<span style={{ fontWeight: '700', fontSize: '1.25rem' }}>
{homeAwaySplit.away?.accuracy}%
</span>
</div>
<div style={{
marginTop: 'var(--space-2)',
fontWeight: '600',
color: 'var(--text-secondary)'
}}>
✈️ Away
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{homeAwaySplit.away?.correct}/{homeAwaySplit.away?.total}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Mini Heatmap - Top Matchups */}
{topMatchups.length > 0 && (
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<div className="card-header">
<span className="card-title">Top Team Matchup Predictions</span>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
Home team win probability
</span>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '8px',
padding: 'var(--space-4)'
}}>
{topMatchups.map((matchup, idx) => {
const prob = matchup.homeWinProb
const hue = prob > 50 ? 120 : 0 // Green for >50%, Red for <50%
const saturation = Math.abs(prob - 50) * 2 // More intense as further from 50
const lightness = 35
return (
<div
key={idx}
style={{
background: `hsl(${hue}, ${saturation}%, ${lightness}%)`,
borderRadius: '8px',
padding: 'var(--space-3)',
textAlign: 'center',
transition: 'transform 0.2s',
cursor: 'pointer'
}}
onMouseEnter={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseLeave={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
<div style={{ fontSize: '0.75rem', marginBottom: '4px', opacity: 0.8 }}>
{matchup.away} @ {matchup.home}
</div>
<div style={{ fontSize: '1.25rem', fontWeight: '700' }}>
{prob}%
</div>
</div>
)
})}
</div>
</div>
)}
{/* Charts Grid - Row 3 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-6)', marginBottom: 'var(--space-6)' }}>
{/* Confidence Distribution */}
<div className="card">
<div className="card-header">
<span className="card-title">Confidence Distribution</span>
</div>
<div style={{ height: '280px' }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={confidenceDistribution}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={90}
paddingAngle={5}
dataKey="value"
label={({ name, percent }) => `${(percent * 100).toFixed(0)}%`}
labelLine={false}
>
{confidenceDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip contentStyle={tooltipStyle} />
<Legend
verticalAlign="bottom"
height={36}
formatter={(value) => <span style={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.75rem' }}>{value}</span>}
/>
</PieChart>
</ResponsiveContainer>
</div>
</div>
{/* Calibration Chart */}
<div className="card">
<div className="card-header">
<span className="card-title">Prediction Calibration</span>
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
Predicted vs Actual Win %
</span>
</div>
<div style={{ height: '280px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={calibrationData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="predicted"
stroke="rgba(255,255,255,0.5)"
fontSize={11}
/>
<YAxis
stroke="rgba(255,255,255,0.5)"
fontSize={11}
domain={[50, 90]}
/>
<Tooltip contentStyle={tooltipStyle} />
<Legend />
<Line
type="monotone"
dataKey="predicted"
stroke="rgba(255,255,255,0.3)"
strokeDasharray="5 5"
name="Perfect"
dot={false}
/>
<Line
type="monotone"
dataKey="actual"
stroke={GRADIENT_COLORS.accent}
strokeWidth={2}
name="Actual"
dot={{ fill: GRADIENT_COLORS.accent, r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Team Accuracy Bar */}
<div className="card" style={{ marginBottom: 'var(--space-6)' }}>
<div className="card-header">
<span className="card-title">Accuracy by Team</span>
</div>
<div style={{ height: '300px' }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={teamAccuracy} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis type="number" domain={[0, 100]} stroke="rgba(255,255,255,0.5)" fontSize={11} />
<YAxis dataKey="team" type="category" stroke="rgba(255,255,255,0.5)" fontSize={11} width={40} />
<Tooltip
contentStyle={tooltipStyle}
formatter={(value) => [`${value}%`, 'Accuracy']}
/>
<Bar dataKey="accuracy" fill={GRADIENT_COLORS.secondary} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Recent Predictions Table */}
<div className="card">
<div className="card-header">
<span className="card-title">Recent Predictions</span>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
<th style={tableHeaderStyle}>Date</th>
<th style={tableHeaderStyle}>Matchup</th>
<th style={tableHeaderStyle}>Prediction</th>
<th style={tableHeaderStyle}>Confidence</th>
<th style={tableHeaderStyle}>Result</th>
</tr>
</thead>
<tbody>
{(data?.recent_predictions || []).map((pred, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<td style={tableCellStyle}>{pred.date}</td>
<td style={tableCellStyle}>{pred.matchup}</td>
<td style={tableCellStyle}><strong>{pred.prediction}</strong></td>
<td style={tableCellStyle}>
<span style={{
color: pred.confidence > 70 ? '#4ade80' : pred.confidence > 60 ? '#facc15' : '#f87171'
}}>
{pred.confidence}%
</span>
</td>
<td style={tableCellStyle}>
<span className={`badge ${pred.correct ? 'badge-success' : 'badge-danger'}`}>
{pred.correct ? '✓' : '✗'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
const tableHeaderStyle = {
textAlign: 'left',
padding: 'var(--space-3) var(--space-4)',
fontSize: '0.75rem',
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: '0.05em',
color: 'var(--text-muted)'
}
const tableCellStyle = {
padding: 'var(--space-3) var(--space-4)',
fontSize: '0.875rem'
}
export default Analytics