import React, { useState } from 'react'; import Papa from 'papaparse'; const StatcastPitchingTool = () => { const [pitchers, setPitchers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [sortField, setSortField] = useState('Name'); const [sortDirection, setSortDirection] = useState('asc'); const [searchQuery, setSearchQuery] = useState(''); const [roleFilter, setRoleFilter] = useState('ALL'); const REQUIRED_HEADERS = [ 'Name', 'TM', 'POS', 'IP', 'ERA', 'FIP', 'BABIP', 'K%', 'BB%', 'HR/9', 'H/9', 'LOB%', 'GO%', 'STU', 'MOV', 'CON', 'STM', 'IR', 'IRS', 'BF', 'IBB', 'WPA', 'pLi', 'GS' ]; const calculateAdvancedStats = (pitcher) => { // Identify the correct role const isSP = pitcher.POS.includes('SP'); const isCL = pitcher.POS.includes('CL'); const isRP = pitcher.POS.includes('RP') && !isCL; // Ensure RP is not mistakenly classified as CL // OOTP Stats - Using default values if missing const ERA = parseFloat(pitcher.ERA) || 4.50; const FIP = parseFloat(pitcher.FIP) || 4.50; const BABIP = parseFloat(pitcher.BABIP) || 0.300; const K_PCT = parseFloat(pitcher['K%']) || 22.0; const BB_PCT = parseFloat(pitcher['BB%']) || 8.0; const HR_9 = parseFloat(pitcher['HR/9']) || 1.2; const H_9 = parseFloat(pitcher['H/9']) || 9.0; const LOB_PCT = parseFloat(pitcher['LOB%']) || 72.0; const GB_PCT = parseFloat(pitcher['GO%']) || 42.0; const IP = parseFloat(pitcher.IP) || 0; const BF = parseFloat(pitcher.BF) || (IP * 3 + H_9 * (IP / 9)); // Approximate if missing const BB = (BB_PCT / 100) * BF; const K = (K_PCT / 100) * BF; const IBB = parseFloat(pitcher.IBB) || 0; const WPA = parseFloat(pitcher.WPA) || 0; const pLI = parseFloat(pitcher.pLi) || 1; // Note: changed from pLI to pLi const GS = parseFloat(pitcher.GS) || (isSP ? 10 : 0); const ER = (ERA / 9) * IP; const H = (H_9 / 9) * IP; // New Stats: Inherited Runners & Escape Rate const IR = parseInt(pitcher.IR) || 0; const IRS = parseInt(pitcher.IRS) || 0; // OOTP Ratings const STUFF = parseFloat(pitcher.STU) || 50; // Changed from Stuff to STU const MOVEMENT = parseFloat(pitcher.MOV) || 50; // Changed from Movement to MOV const CONTROL = parseFloat(pitcher.CON) || 50; // Changed from Control to CON const STAMINA = parseFloat(pitcher.STM) || 50; // Changed from Stamina to STM // Expected Stats const xERA = ((ERA * 0.6) + (FIP * 0.3) + (BABIP * 10 * 0.1)).toFixed(2); const xFIP = ((FIP * 0.85) + (HR_9 * 0.15)).toFixed(2); // Advanced Metrics const DOMS = ((K_PCT - BB_PCT) + (K_PCT * 1.5)).toFixed(1); const ChaseEfficiency = ((K_PCT / (K_PCT + BB_PCT)) * 100).toFixed(1); const tWHIP = (((BB - IBB) + H) / IP).toFixed(2); const HLPI = ((WPA * 50) + (LOB_PCT * 0.5) - (BB_PCT * 2)).toFixed(1); const PutawayRate = ((K / BF) * 100).toFixed(1); const SwStr = ((K / (BF - BB)) * 100).toFixed(1); const TrueKBB = ((K_PCT - BB_PCT) + (pLI * 5)).toFixed(1); const LIE = ((ERA * 0.6) + (LOB_PCT * 0.4)).toFixed(1); const ESC_PCT = IR > 0 ? ((IR - IRS) / IR * 100).toFixed(1) : 100; const K_IP = IP > 0 ? (K / IP).toFixed(2) : 0; const K_BF_PCT = ((K / BF) * 100).toFixed(1); return { ...pitcher, Role: pitcher.POS, IP, xERA, xFIP, ESC_PCT, // Escape Rate (% of inherited runners stranded) K_IP, // Strikeouts per inning pitched K_BF_PCT, // Strikeout rate per batter faced DOMS, ChaseEfficiency, tWHIP, HLPI, PutawayRate, SwStr, TrueKBB, LIE, }; }; const processCSV = async (file) => { setLoading(true); setError(''); try { const text = await file.text(); Papa.parse(text, { header: true, dynamicTyping: true, skipEmptyLines: true, transformHeader: (header) => header.trim(), complete: (results) => { const missingHeaders = REQUIRED_HEADERS.filter( header => !results.meta.fields.includes(header) ); if (missingHeaders.length > 0) { setError(`Missing required columns: ${missingHeaders.join(', ')}`); setLoading(false); return; } const qualifiedPitchers = results.data.filter(pitcher => parseFloat(pitcher.IP) >= (pitcher.POS === 'SP' ? 20 : 10) ); const processedPitchers = qualifiedPitchers.map(pitcher => calculateAdvancedStats(pitcher)); setPitchers(processedPitchers); setLoading(false); }, error: (error) => { setError(`Error processing file: ${error.message}`); setLoading(false); } }); } catch (error) { setError(`Error reading file: ${error.message}`); setLoading(false); } }; const handleSort = (field) => { const direction = field === sortField && sortDirection === 'asc' ? 'desc' : 'asc'; setSortField(field); setSortDirection(direction); const sortedPitchers = [...pitchers].sort((a, b) => { let aVal = a[field]; let bVal = b[field]; if (!isNaN(aVal) && !isNaN(bVal)) { aVal = parseFloat(aVal); bVal = parseFloat(bVal); } if (direction === 'asc') { return aVal > bVal ? 1 : -1; } return aVal < bVal ? 1 : -1; }); setPitchers(sortedPitchers); }; const exportToCSV = () => { if (filteredPitchers.length === 0) return; const headers = Object.keys(filteredPitchers[0]).join(','); const csvContent = [ headers, ...filteredPitchers.map(pitcher => Object.values(pitcher).map(val => `"${val}"`.replace(/"/g, '""') ).join(',') ) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', 'pitching_analysis.csv'); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const filteredPitchers = pitchers.filter(pitcher => { const matchesSearch = pitcher.Name?.toLowerCase().includes(searchQuery.toLowerCase()) || pitcher.TM?.toLowerCase().includes(searchQuery.toLowerCase()); const matchesRole = roleFilter === 'ALL' || pitcher.Role === roleFilter; return matchesSearch && matchesRole; }); return (
| handleSort('Name')}> Name {sortField === 'Name' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('POS')}> POS {sortField === 'POS' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('IP')}> IP {sortField === 'IP' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('xERA')}> xERA {sortField === 'xERA' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('xFIP')}> xFIP {sortField === 'xFIP' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('DOMS')}> DOMS {sortField === 'DOMS' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('ChaseEfficiency')}> ChE% {sortField === 'ChaseEfficiency' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('tWHIP')}> True WHIP {sortField === 'tWHIP' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('HLPI')}> High Lev {sortField === 'HLPI' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('PutawayRate')}> PWR% {sortField === 'PutawayRate' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('SwStr')}> SwStr% {sortField === 'SwStr' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('TrueKBB')}> tK-BB% {sortField === 'TrueKBB' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('LIE')}> LIE {sortField === 'LIE' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('ESC_PCT')}> ESC% {sortField === 'ESC_PCT' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('K_IP')}> K/IP {sortField === 'K_IP' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('K_BF_PCT')}> K/BF% {sortField === 'K_BF_PCT' && (sortDirection === 'asc' ? '↑' : '↓')} |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {pitcher.Name} | {pitcher.POS} | {pitcher.IP} | {pitcher.xERA} | {pitcher.xFIP} | {pitcher.DOMS} | {pitcher.ChaseEfficiency} | {pitcher.tWHIP} | {pitcher.HLPI} | {pitcher.PutawayRate} | {pitcher.SwStr} | {pitcher.TrueKBB} | {pitcher.LIE} | {pitcher.ESC_PCT} | {pitcher.K_IP} | {pitcher.K_BF_PCT} |