Add pitching stats
This commit is contained in:
parent
e25b7024e2
commit
69a59c7d72
@ -54,6 +54,7 @@ export interface PitchingStat {
|
||||
kPer9: number
|
||||
bbPer9: number
|
||||
kPerBB: number
|
||||
ip: number
|
||||
}
|
||||
|
||||
interface PitchingStatRaw {
|
||||
@ -170,6 +171,25 @@ export async function fetchPitchingStatsBySeasonAndTeamId(seasonNumber: number,
|
||||
return pitchingStatsResponse.stats.map(normalizePitchingStat)
|
||||
}
|
||||
|
||||
export async function fetchPitchingStatsBySeason(seasonNumber: number, isRegularSeason: boolean): Promise<PitchingStat[]> {
|
||||
// different endpoint for pre-modern stats (/plays) era
|
||||
if (seasonNumber < MODERN_STAT_ERA_START) {
|
||||
console.warn('pitchingStatsService.fetchPitchingStatsBySeason - Not looking up legacy season stats rn')
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await fetch(`${SITE_URL}/api/v3/plays/pitching?season=${seasonNumber}&group_by=player&limit=1000&s_type=${isRegularSeason ? 'regular' : 'post'}`)
|
||||
|
||||
const pitchingStatsResponse: {
|
||||
count: number
|
||||
stats: PitchingStatRaw[]
|
||||
} = await response.json()
|
||||
|
||||
if (pitchingStatsResponse.count === 0) return []
|
||||
|
||||
return pitchingStatsResponse.stats.map(normalizePitchingStat)
|
||||
}
|
||||
|
||||
async function fetchLegacyPitchingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<PitchingStat | undefined> {
|
||||
const response = await fetch(`${SITE_URL}/api/v3/pitchingstats/totals?season=${seasonNumber}&player_id=${playerId}&s_type=${isRegularSeason ? 'regular' : 'post'}`)
|
||||
|
||||
@ -250,7 +270,8 @@ export function aggregatePitchingStats(pitchingStats: PitchingStat[]): PitchingS
|
||||
woba: 0,
|
||||
kPer9: 0,
|
||||
bbPer9: 0,
|
||||
kPerBB: 0
|
||||
kPerBB: 0,
|
||||
ip: 0
|
||||
}
|
||||
|
||||
pitchingStats.forEach(stat => {
|
||||
@ -304,7 +325,8 @@ export function aggregatePitchingStats(pitchingStats: PitchingStat[]): PitchingS
|
||||
obp: 0, //obp({pa: totalStat.}),
|
||||
slg: slg(totalStatWithHit),
|
||||
ops: 0, //ops(totalStat),
|
||||
woba: woba(totalStatWithHit)
|
||||
woba: woba(totalStatWithHit),
|
||||
ip: totalStat.outs / 3
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,6 +355,7 @@ function makeModernPitchingStatFromLegacy(legacyStat: LegacyPitchingStat): Pitch
|
||||
hbp: legacyStat.hbp,
|
||||
wp: legacyStat.wp,
|
||||
balk: legacyStat.balk,
|
||||
ip: legacyStat.ip,
|
||||
|
||||
// calculated stats
|
||||
era: era({ ip: legacyStat.ip, e_run: legacyStat.erun }),
|
||||
@ -412,7 +435,8 @@ function normalizePitchingStat(rawStat: PitchingStatRaw): PitchingStat {
|
||||
woba: rawStat.woba,
|
||||
kPer9: rawStat['k/9'],
|
||||
bbPer9: rawStat['bb/9'],
|
||||
kPerBB: rawStat['k/bb']
|
||||
kPerBB: rawStat['k/bb'],
|
||||
ip: rawStat.outs / 3
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,10 +8,12 @@ export const CURRENT_SEASON = 8
|
||||
|
||||
export const MODERN_STAT_ERA_START = 8
|
||||
|
||||
export const GAMES_PER_SEASON = 72
|
||||
export const GAMES_PER_WEEK = 4
|
||||
|
||||
export const WEEKS_PER_SEASON = 18
|
||||
|
||||
export const GAMES_PER_SEASON = GAMES_PER_WEEK * WEEKS_PER_SEASON
|
||||
|
||||
// TODO: Annually update this start date and holidays with two week series or get it from the DB
|
||||
export const SEASON_START_DATE: dayjs.Dayjs = dayjs('07-31-2023', 'MM/DD/YYYY')
|
||||
export const THANKSGIVING: dayjs.Dayjs = dayjs('11-23-2023', 'MM/DD/YYYY')
|
||||
|
||||
@ -5,20 +5,21 @@
|
||||
<h1 id="season-heading">Season {{ seasonNumber }} Leaders</h1>
|
||||
<h2 id="week-num">Week {{ weekNumber }}</h2>
|
||||
<!-- TODO this will be dynamic based on batting vs pitching -->
|
||||
<h2 id="stat-heading">Batting Leaderboards - Min PA: {{ plateAppearanceMinimum }}</h2>
|
||||
<h2 id="stat-heading">{{ statType }} Leaderboards - Min {{ statMinimum }}</h2>
|
||||
<!-- <label for="seasonNumber">Select Season</label> -->
|
||||
<!-- <select name="seasonNumber" id="seasonNumber" v-model="seasonNumber">
|
||||
<option v-for="season in seasonNumbers" :key="season" :value="season">Season {{ season }}</option>
|
||||
</select> -->
|
||||
<!-- <select name="statType" id="statType" v-model="statType">
|
||||
<option key="batting" value="batting">Batting</option>
|
||||
<option key="pitching" value="pitching">Pitching</option>
|
||||
<option key="fielding" value="fielding">Fielding</option>
|
||||
</select> -->
|
||||
<!-- TODO ADD SORT FOR ASC/DESC -->
|
||||
<select name="statType" id="statType" v-model="statType">
|
||||
<option key="Batting" value="Batting">Batting</option>
|
||||
<option key="Pitching" value="Pitching">Pitching</option>
|
||||
<option key="Fielding" value="Fielding">Fielding</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="stat-listing">
|
||||
<div v-for="table in battingLeaderboardTableData" :key="table.battingStatProperty" class="col-sm-auto"
|
||||
<div v-for="table in leaderboardTableData" :key="table.statPropertyName" class="col-sm-auto"
|
||||
style="text-align: center">
|
||||
<h3><small>{{ table.title }}</small></h3>
|
||||
<div class="table-responsive-xl">
|
||||
@ -31,7 +32,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="stat in getTop10BattingStatByCategory(table.battingStatProperty)" :key="stat.player.name">
|
||||
<tr v-for="stat in getTop10StatByCategory(table.statPropertyName)" :key="stat.player.name">
|
||||
<td>
|
||||
<RouterLink
|
||||
:to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: stat.player.name } }">
|
||||
@ -44,7 +45,7 @@
|
||||
{{ stat.team.abbrev }}
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td>{{ formatNumericalStat(stat[table.battingStatProperty] as number) }}</td>
|
||||
<td>{{ formatNumericalStat(stat, table.statPropertyName, table.precision || 0) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -58,13 +59,14 @@
|
||||
import { fetchBattingStatsBySeason, type BattingStat } from '@/services/battingStatsService'
|
||||
import { type LeagueInfo, fetchCurrentLeagueInfo } from '@/services/currentService'
|
||||
import type { FieldingStat } from '@/services/fieldingStatsService'
|
||||
import type { PitchingStat } from '@/services/pitchingStatsService'
|
||||
import { CURRENT_SEASON, WEEKS_PER_SEASON } from '@/services/utilities'
|
||||
import { fetchPitchingStatsBySeason, type PitchingStat } from '@/services/pitchingStatsService'
|
||||
import { CURRENT_SEASON, GAMES_PER_WEEK, WEEKS_PER_SEASON } from '@/services/utilities'
|
||||
|
||||
interface BattingLeaderboardTableData {
|
||||
interface LeaderboardTableData<T> {
|
||||
title: string
|
||||
columnName: string
|
||||
battingStatProperty: keyof BattingStat
|
||||
statPropertyName: keyof T
|
||||
precision?: number
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -76,8 +78,8 @@ export default {
|
||||
allPlayersPitchingStats: [] as PitchingStat[],
|
||||
currentSeasonNumber: CURRENT_SEASON,
|
||||
seasonNumber: CURRENT_SEASON,
|
||||
statType: 'batting' as 'batting' | 'pitching' | 'fielding',
|
||||
weekNumber: 18
|
||||
statType: 'Batting' as 'Batting' | 'Pitching' | 'Fielding',
|
||||
weekNumber: undefined! as number
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -89,46 +91,98 @@ export default {
|
||||
// since stats are for regular season, do not use post season weeks to up PA/IP mins
|
||||
return Math.min(this.weekNumber, WEEKS_PER_SEASON)
|
||||
},
|
||||
statMinimum(): string {
|
||||
if (this.statType == 'Batting') return `PA: ${this.plateAppearanceMinimum}`
|
||||
if (this.statType == 'Pitching') return `IP: ${this.inningsPitchedMinimum}`
|
||||
return 'Coming Soon'
|
||||
},
|
||||
plateAppearanceMinimum(): number {
|
||||
const GAMES_PER_WEEK = 4
|
||||
const MIN_PLATE_APPEARANCES_PER_GAME = 3
|
||||
return this.weekNumberForCalcs * GAMES_PER_WEEK * MIN_PLATE_APPEARANCES_PER_GAME
|
||||
},
|
||||
inningsPitchedMinimum(): number {
|
||||
const MIN_IP_PER_GAME = 1
|
||||
return this.weekNumberForCalcs * GAMES_PER_WEEK * MIN_IP_PER_GAME
|
||||
},
|
||||
qualifyingBattingStats(): BattingStat[] {
|
||||
return this.allPlayersBattingStats.filter(bs => bs.pa >= this.plateAppearanceMinimum)
|
||||
},
|
||||
battingStat(): BattingStat[] {
|
||||
// concat an empty array so that the existing array is not sorted as a side effect
|
||||
return this.qualifyingBattingStats.concat([]).sort((a, b) => b.hr - a.hr).slice(0, 10)
|
||||
qualifyingPitchingStats(): PitchingStat[] {
|
||||
return this.allPlayersPitchingStats.filter(ps => ps.ip >= this.inningsPitchedMinimum)
|
||||
},
|
||||
battingLeaderboardTableData(): BattingLeaderboardTableData[] {
|
||||
leaderboardTableData(): LeaderboardTableData<BattingStat>[] | LeaderboardTableData<PitchingStat>[] | LeaderboardTableData<FieldingStat>[] {
|
||||
if (this.statType == 'Batting') {
|
||||
return this.battingLeaderboardTableData
|
||||
}
|
||||
if (this.statType == 'Pitching') {
|
||||
return this.pitchingLeaderboardTableData
|
||||
}
|
||||
|
||||
return this.fieldingLeaderboardTableData
|
||||
},
|
||||
battingLeaderboardTableData(): LeaderboardTableData<BattingStat>[] {
|
||||
return [
|
||||
{ title: 'Home Runs', columnName: 'HR', battingStatProperty: 'hr' },
|
||||
{ title: 'Batting Average', columnName: 'AVG', battingStatProperty: 'avg' },
|
||||
{ title: 'On Base Percentage', columnName: 'OBP', battingStatProperty: 'obp' },
|
||||
{ title: 'Slugging Percentage', columnName: 'SLG', battingStatProperty: 'slg' },
|
||||
{ title: 'On Base Plus Slugging', columnName: 'OPS', battingStatProperty: 'ops' },
|
||||
{ title: 'Weighted On Base Avg', columnName: 'wOBA', battingStatProperty: 'woba' },
|
||||
{ title: 'Runs Batted In', columnName: 'RBI', battingStatProperty: 'rbi' },
|
||||
{ title: 'Doubles', columnName: '2B', battingStatProperty: 'double' },
|
||||
{ title: 'Triples', columnName: '3B', battingStatProperty: 'triple' },
|
||||
{ title: 'Hits', columnName: 'H', battingStatProperty: 'hit' },
|
||||
{ title: 'Plate Appearances', columnName: 'PA', battingStatProperty: 'pa' },
|
||||
{ title: 'Ground Into Double Play', columnName: 'GIDP', battingStatProperty: 'gidp' },
|
||||
{ title: 'Walks', columnName: 'BB', battingStatProperty: 'bb' },
|
||||
{ title: 'Strikeouts', columnName: 'K', battingStatProperty: 'so' },
|
||||
{ title: 'Hit By Pitch', columnName: 'HBP', battingStatProperty: 'hbp' },
|
||||
{ title: 'Intentional Walks', columnName: 'IBB', battingStatProperty: 'ibb' },
|
||||
{ title: 'Stolen Bases', columnName: 'SB', battingStatProperty: 'sb' },
|
||||
{ title: 'Caught Stealing', columnName: 'CS', battingStatProperty: 'cs' },
|
||||
{ title: 'Runs Scored', columnName: 'R', battingStatProperty: 'run' },
|
||||
{ title: 'At Bats', columnName: 'AB', battingStatProperty: 'ab' },
|
||||
{ title: 'Home Runs', columnName: 'HR', statPropertyName: 'hr' },
|
||||
{ title: 'Batting Average', columnName: 'AVG', statPropertyName: 'avg', precision: 3 },
|
||||
{ title: 'On Base Percentage', columnName: 'OBP', statPropertyName: 'obp', precision: 3 },
|
||||
{ title: 'Slugging Percentage', columnName: 'SLG', statPropertyName: 'slg', precision: 3 },
|
||||
{ title: 'On Base Plus Slugging', columnName: 'OPS', statPropertyName: 'ops', precision: 3 },
|
||||
{ title: 'Weighted On Base Avg', columnName: 'wOBA', statPropertyName: 'woba', precision: 3 },
|
||||
{ title: 'Runs Batted In', columnName: 'RBI', statPropertyName: 'rbi' },
|
||||
{ title: 'Doubles', columnName: '2B', statPropertyName: 'double' },
|
||||
{ title: 'Triples', columnName: '3B', statPropertyName: 'triple' },
|
||||
{ title: 'Hits', columnName: 'H', statPropertyName: 'hit' },
|
||||
{ title: 'Plate Appearances', columnName: 'PA', statPropertyName: 'pa' },
|
||||
{ title: 'Ground Into Double Play', columnName: 'GIDP', statPropertyName: 'gidp' },
|
||||
{ title: 'Walks', columnName: 'BB', statPropertyName: 'bb' },
|
||||
{ title: 'Strikeouts', columnName: 'K', statPropertyName: 'so' },
|
||||
{ title: 'Hit By Pitch', columnName: 'HBP', statPropertyName: 'hbp' },
|
||||
{ title: 'Intentional Walks', columnName: 'IBB', statPropertyName: 'ibb' },
|
||||
{ title: 'Stolen Bases', columnName: 'SB', statPropertyName: 'sb' },
|
||||
{ title: 'Caught Stealing', columnName: 'CS', statPropertyName: 'cs' },
|
||||
{ title: 'Runs Scored', columnName: 'R', statPropertyName: 'run' },
|
||||
{ title: 'At Bats', columnName: 'AB', statPropertyName: 'ab' },
|
||||
// { title: 'No-Doubt Home Runs', columnName: 'NDHR', battingStatProperty: 'dong' },
|
||||
{ title: 'Ballpark Home Runs', columnName: 'BPHR', battingStatProperty: 'bphr' },
|
||||
{ title: 'Ballpark Flyouts', columnName: 'BPFO', battingStatProperty: 'bpfo' },
|
||||
{ title: 'Ballpark Singles', columnName: 'BP1B', battingStatProperty: 'bp1b' },
|
||||
{ title: 'Ballpark Lineouts', columnName: 'BPLO', battingStatProperty: 'bplo' },
|
||||
{ title: 'Ballpark Home Runs', columnName: 'BPHR', statPropertyName: 'bphr' },
|
||||
{ title: 'Ballpark Flyouts', columnName: 'BPFO', statPropertyName: 'bpfo' },
|
||||
{ title: 'Ballpark Singles', columnName: 'BP1B', statPropertyName: 'bp1b' },
|
||||
{ title: 'Ballpark Lineouts', columnName: 'BPLO', statPropertyName: 'bplo' },
|
||||
]
|
||||
},
|
||||
pitchingLeaderboardTableData(): LeaderboardTableData<PitchingStat>[] {
|
||||
return [
|
||||
// { title: 'Starter Wins', columnName: 'W', statPropertyName: 'win' },
|
||||
{ title: 'Wins', columnName: 'W', statPropertyName: 'win' },
|
||||
{ title: 'Losses', columnName: 'L', statPropertyName: 'loss' },
|
||||
{ title: 'Earned Run Average', columnName: 'ERA', statPropertyName: 'era', precision: 2 },
|
||||
{ title: 'Walks + Hits / IP', columnName: 'WHIP', statPropertyName: 'whip', precision: 3 },
|
||||
|
||||
{ title: 'Innings Pitched', columnName: 'IP', statPropertyName: 'ip', precision: 1 },
|
||||
{ title: 'Runs Allowed', columnName: 'R', statPropertyName: 'run' },
|
||||
{ title: 'Earned Runs Allowed', columnName: 'ER', statPropertyName: 'e_run' },
|
||||
{ title: 'Holds', columnName: 'HD', statPropertyName: 'hold' },
|
||||
|
||||
{ title: 'Saves', columnName: 'SV', statPropertyName: 'save' },
|
||||
{ title: 'Blown Saves', columnName: 'BSV', statPropertyName: 'bsave' },
|
||||
{ title: 'Strikeouts', columnName: 'K', statPropertyName: 'so' },
|
||||
{ title: 'Strikeouts Per Nine', columnName: 'K/9', statPropertyName: 'kPer9', precision: 2 },
|
||||
|
||||
{ title: 'Walks Allowed', columnName: 'BB', statPropertyName: 'bb' },
|
||||
{ title: 'Walks Per Nine', columnName: 'BB/9', statPropertyName: 'bbPer9', precision: 2 },
|
||||
{ title: 'Strikeouts Per Walk', columnName: 'K/BB', statPropertyName: 'kPerBB', precision: 2 },
|
||||
{ title: 'Hit By Pitches Allowed', columnName: 'HBP', statPropertyName: 'hbp' },
|
||||
|
||||
{ title: 'Hits Allowed', columnName: 'H', statPropertyName: 'hits' },
|
||||
{ title: 'Home Runs Allowed', columnName: 'HR', statPropertyName: 'hr' },
|
||||
{ title: 'Balks', columnName: 'BK', statPropertyName: 'balk' },
|
||||
{ title: 'Wild Pitches', columnName: 'WP', statPropertyName: 'wp' },
|
||||
|
||||
{ title: 'Inherited Runners', columnName: 'IR', statPropertyName: 'ir' },
|
||||
{ title: 'Inherited Runners Scored', columnName: 'IRS', statPropertyName: 'ir_sc' }
|
||||
]
|
||||
},
|
||||
fieldingLeaderboardTableData(): LeaderboardTableData<FieldingStat>[] {
|
||||
return []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -145,26 +199,59 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTop10StatByCategory(stat: keyof BattingStat | keyof PitchingStat | keyof FieldingStat): (BattingStat | PitchingStat | FieldingStat)[] {
|
||||
if (this.statType == 'Batting') {
|
||||
return this.getTop10BattingStatByCategory(stat as keyof BattingStat)
|
||||
}
|
||||
if (this.statType == 'Pitching') {
|
||||
return this.getTop10PitchingStatByCategory(stat as keyof PitchingStat)
|
||||
}
|
||||
|
||||
return [] // this.getTop10FieldingStatByCategory(stat as keyof FieldingStat)
|
||||
},
|
||||
getTop10BattingStatByCategory(stat: keyof BattingStat): BattingStat[] {
|
||||
// concat an empty array so that the existing array is not sorted as a side effect
|
||||
return this.qualifyingBattingStats.concat([]).sort((a, b) => (b[stat] as number) - (a[stat] as number)).slice(0, 10)
|
||||
},
|
||||
formatNumericalStat(stat: number): number | string {
|
||||
if (Number.isInteger(stat)) return stat
|
||||
getTop10PitchingStatByCategory(stat: keyof PitchingStat): PitchingStat[] {
|
||||
// qualifying IP exempt stats
|
||||
const ipExemptStats: (keyof PitchingStat)[] = ['win', 'loss', 'save', 'bsave', 'hold', 'so', 'bb', 'hbp', 'hits', 'hr', 'run', 'e_run', 'balk', 'wp', 'ir', 'ir_sc']
|
||||
|
||||
return stat.toFixed(3)
|
||||
// concat an empty array so that the existing array is not sorted as a side effect
|
||||
const statBase: PitchingStat[] = ipExemptStats.includes(stat) ? this.allPlayersPitchingStats.concat([]) : this.qualifyingPitchingStats.concat([])
|
||||
|
||||
const reverseSortStats = ['era', 'whip', 'bbPer9']
|
||||
const sortMultiplier = reverseSortStats.includes(stat) ? -1 : 1
|
||||
|
||||
return statBase.sort((a, b) => sortMultiplier * ((b[stat] as number) - (a[stat] as number))).slice(0, 10)
|
||||
},
|
||||
// getTop10FieldingStatByCategory(stat: keyof FieldingStat): FieldingStat[] {
|
||||
// // concat an empty array so that the existing array is not sorted as a side effect
|
||||
|
||||
// return []//this.qualifyingBattingStats.concat([]).sort((a, b) => (b[stat] as number) - (a[stat] as number)).slice(0, 10)
|
||||
// },
|
||||
formatNumericalStat(stat: BattingStat | PitchingStat | FieldingStat, property: keyof BattingStat | keyof PitchingStat | keyof FieldingStat, precision: number): number | string {
|
||||
let numericalStat: number = 0
|
||||
|
||||
if (this.statType == 'Batting') numericalStat = (stat as BattingStat)[property as keyof BattingStat] as number
|
||||
if (this.statType == 'Pitching') numericalStat = (stat as PitchingStat)[property as keyof PitchingStat] as number
|
||||
if (this.statType == 'Fielding') numericalStat = (stat as FieldingStat)[property as keyof FieldingStat] as number
|
||||
|
||||
if (Number.isInteger(numericalStat)) return numericalStat
|
||||
|
||||
return numericalStat.toFixed(precision)
|
||||
},
|
||||
async fetchData(): Promise<void> {
|
||||
const leagueInfo: LeagueInfo = await fetchCurrentLeagueInfo()
|
||||
this.weekNumber = leagueInfo.week
|
||||
|
||||
if (this.statType == 'batting') {
|
||||
if (this.statType == 'Batting') {
|
||||
this.allPlayersBattingStats = await fetchBattingStatsBySeason(this.seasonNumber, true)
|
||||
}
|
||||
if (this.statType == 'pitching') {
|
||||
this.allPlayersPitchingStats = []
|
||||
if (this.statType == 'Pitching') {
|
||||
this.allPlayersPitchingStats = await fetchPitchingStatsBySeason(this.seasonNumber, true)
|
||||
}
|
||||
if (this.statType == 'fielding') {
|
||||
if (this.statType == 'Fielding') {
|
||||
this.allPlayersFieldingStats = []
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user