Add batting stats
This commit is contained in:
parent
ddf166638f
commit
4232dfb4b8
@ -52,7 +52,7 @@ export async function fetchPlayersByTeam(seasonNumber: number, teamId: number):
|
||||
return playersResponse.players
|
||||
}
|
||||
|
||||
export async function fetchPlayerByName(seasonNumber: number, playerName: string): Promise<Player> {
|
||||
export async function fetchPlayerByName(seasonNumber: number, playerName: string): Promise<Player | undefined> {
|
||||
const response = await fetch(`${SITE_URL}/api/v3/players?season=${seasonNumber}&name=${playerName}`)
|
||||
|
||||
const playersResponse: {
|
||||
@ -60,8 +60,10 @@ export async function fetchPlayerByName(seasonNumber: number, playerName: string
|
||||
players: Player[]
|
||||
} = await response.json()
|
||||
|
||||
if (playersResponse.count !== 1) {
|
||||
throw new Error('playersServices.fetchPlayerByName - Expected one player, return contained none or many')
|
||||
if (playersResponse.count === 0) return undefined
|
||||
|
||||
if (playersResponse.count > 1) {
|
||||
throw new Error(`playersServices.fetchPlayerByName - Return contained more than one player for name: ${playerName} in season number ${seasonNumber}`)
|
||||
}
|
||||
|
||||
return playersResponse.players[0]
|
||||
|
||||
331
src/services/statsService.ts
Normal file
331
src/services/statsService.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import type { Game, Team } from './apiResponseTypes'
|
||||
import type { Player } from './playersService'
|
||||
import { MODERN_STAT_ERA_START, SITE_URL } from './utilities'
|
||||
|
||||
// TODO make a stats object that has properties for current regular season, current post season,
|
||||
// last 4 games, historical seasons, career totals
|
||||
// could split into batting/pitching/fielding so only necessary ones are called to save time
|
||||
|
||||
export interface BattingStat {
|
||||
player: Player
|
||||
team: Team
|
||||
game: Game | 'TOT'
|
||||
// stats below
|
||||
pa: number
|
||||
ab: number
|
||||
run: number
|
||||
hit: number
|
||||
rbi: number
|
||||
double: number
|
||||
triple: number
|
||||
hr: number
|
||||
bb: number
|
||||
so: number
|
||||
hbp: number
|
||||
sac: number
|
||||
ibb: number
|
||||
gidp: number
|
||||
sb: number
|
||||
cs: number
|
||||
bphr: number
|
||||
bpfo: number
|
||||
bp1b: number
|
||||
bplo: number
|
||||
wpa: number
|
||||
avg: number
|
||||
obp: number
|
||||
slg: number
|
||||
ops: number
|
||||
woba: number
|
||||
}
|
||||
|
||||
interface LegacyBattingStat {
|
||||
player: Player
|
||||
team: Team
|
||||
pa: number,
|
||||
ab: number,
|
||||
run: number,
|
||||
hit: number,
|
||||
rbi: number,
|
||||
double: number,
|
||||
triple: number,
|
||||
hr: number,
|
||||
bb: number,
|
||||
so: number,
|
||||
hbp: number,
|
||||
sac: number,
|
||||
ibb: number,
|
||||
gidp: number,
|
||||
sb: number,
|
||||
cs: number,
|
||||
bphr: number,
|
||||
bpfo: number,
|
||||
bp1b: number,
|
||||
bplo: number
|
||||
}
|
||||
|
||||
export interface PitchingStat {
|
||||
player: Player
|
||||
team: Team
|
||||
game: Game | 'TOT'
|
||||
// stats below
|
||||
tbf: number
|
||||
outs: number
|
||||
games: number
|
||||
gs: number
|
||||
win: number
|
||||
loss: number
|
||||
hold: number
|
||||
save: number
|
||||
bsave: number
|
||||
ir: number
|
||||
ir_sc: number
|
||||
ab: number
|
||||
run: number
|
||||
e_run: number
|
||||
hits: number
|
||||
double: number
|
||||
triple: number
|
||||
hr: number
|
||||
bb: number
|
||||
so: number
|
||||
hbp: number
|
||||
sac: number
|
||||
ibb: number
|
||||
gidp: number
|
||||
sb: number
|
||||
cs: number
|
||||
bphr: number
|
||||
bpfo: number
|
||||
bp1b: number
|
||||
bplo: number
|
||||
wp: number
|
||||
balk: number
|
||||
wpa: number
|
||||
era: number
|
||||
whip: number
|
||||
avg: number
|
||||
obp: number
|
||||
slg: number
|
||||
ops: number
|
||||
woba: number
|
||||
"k/9": number
|
||||
"bb/9": number
|
||||
"k/bb": number
|
||||
}
|
||||
|
||||
export interface FieldingStat {
|
||||
player: Player
|
||||
team: Team
|
||||
pos: 'P' | 'C' | '1B' | '2B' | '3B' | 'SS' | 'LF' | 'CF' | 'RF' | 'TOT'
|
||||
xCheckCount: number
|
||||
hit: number
|
||||
error: number
|
||||
stolenBaseCheckCount: number
|
||||
stolenBaseCount: number
|
||||
caughtStealingCount: number
|
||||
passedBallCount: number
|
||||
winProbabilityAdded: number
|
||||
weightedFieldingPercent: number
|
||||
caughtStealingPercent: number | ''
|
||||
}
|
||||
|
||||
export interface FieldingStatRaw {
|
||||
player: Player
|
||||
team: Team
|
||||
pos: 'P' | 'C' | '1B' | '2B' | '3B' | 'SS' | 'LF' | 'CF' | 'RF' | 'TOT'
|
||||
"x-ch": number
|
||||
hit: number
|
||||
error: number
|
||||
"sb-ch": number
|
||||
sb: number
|
||||
cs: number
|
||||
pb: number
|
||||
wpa: number
|
||||
"wf%": number
|
||||
"cs%": number | ''
|
||||
}
|
||||
|
||||
export async function fetchBattingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<BattingStat | undefined> {
|
||||
// different endpoint for pre-modern stats (/plays) era
|
||||
if (seasonNumber < MODERN_STAT_ERA_START) {
|
||||
return await fetchLegacyBattingStatsBySeasonAndPlayerId(seasonNumber, playerId, isRegularSeason)
|
||||
}
|
||||
|
||||
const response = await fetch(`${SITE_URL}/api/v3/plays/batting?season=${seasonNumber}&player_id=${playerId}&s_type=${isRegularSeason ? 'regular' : 'post'}`)
|
||||
|
||||
const battingStatsResponse: {
|
||||
count: number
|
||||
stats: BattingStat[]
|
||||
} = await response.json()
|
||||
|
||||
if (battingStatsResponse.count === 0) return undefined
|
||||
|
||||
if (battingStatsResponse.count > 1) {
|
||||
throw new Error('statsService.fetchBattingStatsBySeasonAndPlayerId - Expected one stat line for season/player, return contained many')
|
||||
}
|
||||
|
||||
return battingStatsResponse.stats[0]
|
||||
}
|
||||
|
||||
async function fetchLegacyBattingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<BattingStat | undefined> {
|
||||
const response = await fetch(`${SITE_URL}/api/v3/battingstats/totals?season=${seasonNumber}&player_id=${playerId}&s_type=${isRegularSeason ? 'regular' : 'post'}`)
|
||||
|
||||
const legacyBattingStatsResponse: {
|
||||
count: number
|
||||
stats: LegacyBattingStat[]
|
||||
} = await response.json()
|
||||
|
||||
if (legacyBattingStatsResponse.count === 0) return undefined
|
||||
|
||||
if (legacyBattingStatsResponse.count > 1) {
|
||||
throw new Error('statsService.fetchLegacyBattingStatsBySeasonAndPlayerId - Expected one stat line for season/player, return contained many')
|
||||
}
|
||||
|
||||
return makeModernBattingStatFromLegacy(legacyBattingStatsResponse.stats[0])
|
||||
}
|
||||
|
||||
export async function fetchBattingStatsForLastFourGamesBySeasonAndPlayerId(seasonNumber: number, playerId: number): Promise<BattingStat[]> {
|
||||
const response = await fetch(`${SITE_URL}/api/v3/plays/batting?season=${seasonNumber}&player_id=${playerId}&group_by=playergame&limit=4&sort=newest`)
|
||||
|
||||
const battingStatsResponse: {
|
||||
count: number
|
||||
stats: BattingStat[]
|
||||
} = await response.json()
|
||||
|
||||
if (battingStatsResponse.count > 4) {
|
||||
throw new Error(`statsService.fetchBattingStatsForLastFourBySeasonAndPlayerId - Expected at most 4 games, return contained ${battingStatsResponse.count}`)
|
||||
}
|
||||
|
||||
return battingStatsResponse.stats
|
||||
}
|
||||
|
||||
export function aggregateBattingStats(battingStats: BattingStat[]): BattingStat {
|
||||
let totalStat: BattingStat = {
|
||||
player: battingStats[0].player,
|
||||
team: battingStats[0].team,
|
||||
game: 'TOT',
|
||||
// stats below
|
||||
pa: 0,
|
||||
ab: 0,
|
||||
run: 0,
|
||||
hit: 0,
|
||||
rbi: 0,
|
||||
double: 0,
|
||||
triple: 0,
|
||||
hr: 0,
|
||||
bb: 0,
|
||||
so: 0,
|
||||
hbp: 0,
|
||||
sac: 0,
|
||||
ibb: 0,
|
||||
gidp: 0,
|
||||
sb: 0,
|
||||
cs: 0,
|
||||
bphr: 0,
|
||||
bpfo: 0,
|
||||
bp1b: 0,
|
||||
bplo: 0,
|
||||
wpa: 0,
|
||||
avg: 0,
|
||||
obp: 0,
|
||||
slg: 0,
|
||||
ops: 0,
|
||||
woba: 0
|
||||
}
|
||||
|
||||
battingStats.forEach(stat => {
|
||||
totalStat.pa += stat.pa
|
||||
totalStat.ab += stat.ab
|
||||
totalStat.run += stat.run
|
||||
totalStat.hit += stat.hit
|
||||
totalStat.rbi += stat.rbi
|
||||
totalStat.double += stat.double
|
||||
totalStat.triple += stat.triple
|
||||
totalStat.hr += stat.hr
|
||||
totalStat.bb += stat.bb
|
||||
totalStat.so += stat.so
|
||||
totalStat.hbp += stat.hbp
|
||||
totalStat.sac += stat.sac
|
||||
totalStat.ibb += stat.ibb
|
||||
totalStat.gidp += stat.gidp
|
||||
totalStat.sb += stat.sb
|
||||
totalStat.cs += stat.cs
|
||||
totalStat.bphr += stat.bphr
|
||||
totalStat.bpfo += stat.bpfo
|
||||
totalStat.bp1b += stat.bp1b
|
||||
totalStat.bplo += stat.bplo
|
||||
})
|
||||
|
||||
return {
|
||||
...totalStat,
|
||||
avg: avg(totalStat),
|
||||
obp: obp(totalStat),
|
||||
slg: slg(totalStat),
|
||||
ops: ops(totalStat),
|
||||
woba: woba(totalStat)
|
||||
}
|
||||
}
|
||||
|
||||
function makeModernBattingStatFromLegacy(legacyStat: LegacyBattingStat): BattingStat {
|
||||
return {
|
||||
player: legacyStat.player,
|
||||
team: legacyStat.team,
|
||||
game: 'TOT',
|
||||
// stats below
|
||||
pa: legacyStat.pa,
|
||||
ab: legacyStat.ab,
|
||||
run: legacyStat.run,
|
||||
hit: legacyStat.hit,
|
||||
rbi: legacyStat.rbi,
|
||||
double: legacyStat.double,
|
||||
triple: legacyStat.triple,
|
||||
hr: legacyStat.hr,
|
||||
bb: legacyStat.bb,
|
||||
so: legacyStat.so,
|
||||
hbp: legacyStat.hbp,
|
||||
sac: legacyStat.sac,
|
||||
ibb: legacyStat.ibb,
|
||||
gidp: legacyStat.gidp,
|
||||
sb: legacyStat.sb,
|
||||
cs: legacyStat.cs,
|
||||
bphr: legacyStat.bphr,
|
||||
bpfo: legacyStat.bpfo,
|
||||
bp1b: legacyStat.bp1b,
|
||||
bplo: legacyStat.bplo,
|
||||
wpa: 0,
|
||||
avg: avg(legacyStat),
|
||||
obp: obp(legacyStat),
|
||||
slg: slg(legacyStat),
|
||||
ops: ops(legacyStat),
|
||||
woba: woba(legacyStat)
|
||||
}
|
||||
}
|
||||
|
||||
function avg(stat: LegacyBattingStat): number {
|
||||
if (stat.ab === 0) return 0
|
||||
|
||||
return stat.hit / stat.ab
|
||||
}
|
||||
|
||||
function obp(stat: LegacyBattingStat): number {
|
||||
if (stat.pa === 0) return 0
|
||||
|
||||
return (stat.hit + stat.bb + stat.ibb + stat.hbp) / stat.pa
|
||||
}
|
||||
function slg(stat: LegacyBattingStat): number {
|
||||
if (stat.ab === 0) return 0
|
||||
|
||||
return (stat.hit + stat.double + (stat.triple * 2) + (stat.hr * 3)) / stat.ab
|
||||
}
|
||||
function ops(stat: LegacyBattingStat): number {
|
||||
return obp(stat) + slg(stat)
|
||||
}
|
||||
function woba(stat: LegacyBattingStat): number {
|
||||
const numerator = (.69 * stat.bb) + (.72 * stat.hbp) + (.89 * (stat.hit - stat.double - stat.triple - stat.hr)) + (1.27 * stat.double) + (1.62 * stat.triple) + (2.1 * stat.hr)
|
||||
const denominator = stat.ab + stat.bb - stat.ibb + stat.sac + stat.hbp
|
||||
|
||||
if (denominator === 0) return 0
|
||||
|
||||
return numerator / denominator
|
||||
}
|
||||
@ -1,3 +1,9 @@
|
||||
export const SITE_URL = 'https://sba.manticorum.com'
|
||||
|
||||
export const CURRENT_SEASON = 8
|
||||
|
||||
export const MODERN_STAT_ERA_START = 8
|
||||
|
||||
// a type guard to tell typescript that undefined has been filtered and to only consider an array
|
||||
// of the expected types (no undefineds) after filtering
|
||||
export const isNotUndefined = <S>(value: S | undefined): value is S => value != undefined
|
||||
@ -5,10 +5,10 @@
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h1 id="player-name">{{ playerName }}{{ injuryReturnDate }}</h1>
|
||||
<h2 id="player-wara">{{ player?.wara }} sWAR</h2>
|
||||
<h2 v-if="isCurrentPlayer" id="player-wara">{{ player?.wara }} sWAR</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div v-if="isCurrentPlayer" class="col-sm-1">
|
||||
<RouterLink v-if="teamAbbreviation && teamThumbnail"
|
||||
:to="{ name: 'team', params: { seasonNumber: seasonNumber, teamAbbreviation: teamAbbreviation } }">
|
||||
<img id="thumbnail" height="125" style="float:right; vertical-align:middle; max-height:100%;"
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div v-if="isCurrentPlayer" class="row">
|
||||
<div class="col-sm-auto">
|
||||
<img style="max-height:485px; max-width: 100%;" id="team-image" :src="playerImageUrl" :alt="playerName">
|
||||
<p><a id="bbref-link" target="_blank" :href="baseballReferenceUrl">Baseball Reference Page</a></p>
|
||||
@ -31,8 +31,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Summary -->
|
||||
<div class="row" id="batter-summary">
|
||||
<!-- Batter Summary -->
|
||||
<div v-if="currentSeasonBatting" class="row" id="batter-summary">
|
||||
<div class="col-sm-12">
|
||||
<h3>Summary</h3>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="table-responsive-xl" style="max-width:45rem">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Summary</th>
|
||||
<th>AB</th>
|
||||
<th>H</th>
|
||||
<th>HR</th>
|
||||
<th>BA</th>
|
||||
<th>R</th>
|
||||
<th>RBI</th>
|
||||
<th>SB</th>
|
||||
<th>OBP</th>
|
||||
<th>SLG</th>
|
||||
<th>OPS</th>
|
||||
<th>wOBA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="batter-summary-table">
|
||||
<tr>
|
||||
<td>Season {{ currentSeasonBatting.player.season }}</td>
|
||||
<td>{{ currentSeasonBatting.ab }}</td>
|
||||
<td>{{ currentSeasonBatting.hit }}</td>
|
||||
<td>{{ currentSeasonBatting.hr }}</td>
|
||||
<td>{{ currentSeasonBatting.avg.toFixed(3) }}</td>
|
||||
<td>{{ currentSeasonBatting.run }}</td>
|
||||
<td>{{ currentSeasonBatting.rbi }}</td>
|
||||
<td>{{ currentSeasonBatting.sb }}</td>
|
||||
<td>{{ currentSeasonBatting.obp.toFixed(3) }}</td>
|
||||
<td>{{ currentSeasonBatting.slg.toFixed(3) }}</td>
|
||||
<td>{{ currentSeasonBatting.ops.toFixed(3) }}</td>
|
||||
<td>{{ currentSeasonBatting.woba.toFixed(3) }}</td>
|
||||
</tr>
|
||||
<tr v-if="currentPostSeasonBatting">
|
||||
<td>S{{ currentPostSeasonBatting.player.season }} / Playoffs</td>
|
||||
<td>{{ currentPostSeasonBatting.ab }}</td>
|
||||
<td>{{ currentPostSeasonBatting.hit }}</td>
|
||||
<td>{{ currentPostSeasonBatting.hr }}</td>
|
||||
<td>{{ currentPostSeasonBatting.avg.toFixed(3) }}</td>
|
||||
<td>{{ currentPostSeasonBatting.run }}</td>
|
||||
<td>{{ currentPostSeasonBatting.rbi }}</td>
|
||||
<td>{{ currentPostSeasonBatting.sb }}</td>
|
||||
<td>{{ currentPostSeasonBatting.obp.toFixed(3) }}</td>
|
||||
<td>{{ currentPostSeasonBatting.slg.toFixed(3) }}</td>
|
||||
<td>{{ currentPostSeasonBatting.ops.toFixed(3) }}</td>
|
||||
<td>{{ currentPostSeasonBatting.woba.toFixed(3) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot v-if="careerBattingStat" id="batter-summary-footer">
|
||||
<tr>
|
||||
<th>Career</th>
|
||||
<th>{{ careerBattingStat.ab }}</th>
|
||||
<th>{{ careerBattingStat.hit }}</th>
|
||||
<th>{{ careerBattingStat.hr }}</th>
|
||||
<th>{{ careerBattingStat.avg.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.run }}</th>
|
||||
<th>{{ careerBattingStat.rbi }}</th>
|
||||
<th>{{ careerBattingStat.sb }}</th>
|
||||
<th>{{ careerBattingStat.obp.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.slg.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.ops.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.woba.toFixed(3) }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="table-responsive-xl" style="max-width:20rem">
|
||||
<table class="table table-sm table-striped">
|
||||
@ -53,22 +124,195 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Last 4 Games -->
|
||||
<div class="col-small-12">
|
||||
<h3>Last 4 Games</h3>
|
||||
<div class="table-responsive-xl">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>PA</th>
|
||||
<th>AB</th>
|
||||
<th>R</th>
|
||||
<th>H</th>
|
||||
<th>2B</th>
|
||||
<th>3B</th>
|
||||
<th>HR</th>
|
||||
<th>RBI</th>
|
||||
<th>SB</th>
|
||||
<th>CS</th>
|
||||
<th>BB</th>
|
||||
<th>SO</th>
|
||||
<th>BPHR</th>
|
||||
<th>BPFO</th>
|
||||
<th>BP1B</th>
|
||||
<th>BPLO</th>
|
||||
<th>GIDP</th>
|
||||
<th>HBP</th>
|
||||
<th>SAC</th>
|
||||
<th>IBB</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="last4-batting">
|
||||
<tr v-for="gameStat in last4Games">
|
||||
<td>{{ makeWxGyFromGame(gameStat.game) }}</td>
|
||||
<td>{{ gameStat.pa }}</td>
|
||||
<td>{{ gameStat.ab }}</td>
|
||||
<td>{{ gameStat.run }}</td>
|
||||
<td>{{ gameStat.hit }}</td>
|
||||
<td>{{ gameStat.double }}</td>
|
||||
<td>{{ gameStat.triple }}</td>
|
||||
<td>{{ gameStat.hr }}</td>
|
||||
<td>{{ gameStat.rbi }}</td>
|
||||
<td>{{ gameStat.sb }}</td>
|
||||
<td>{{ gameStat.cs }}</td>
|
||||
<td>{{ gameStat.bb }}</td>
|
||||
<td>{{ gameStat.so }}</td>
|
||||
<td>{{ gameStat.bphr }}</td>
|
||||
<td>{{ gameStat.bpfo }}</td>
|
||||
<td>{{ gameStat.bp1b }}</td>
|
||||
<td>{{ gameStat.bplo }}</td>
|
||||
<td>{{ gameStat.gidp }}</td>
|
||||
<td>{{ gameStat.hbp }}</td>
|
||||
<td>{{ gameStat.sac }}</td>
|
||||
<td>{{ gameStat.ibb }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Career Batting -->
|
||||
<div class="row" id="career-batting-row">
|
||||
<div class="col-sm-12">
|
||||
<h3>Batting Stats</h3>
|
||||
<div class="table-responsive-xl">
|
||||
<table class="table table-sm table-striped" id="career-batting">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Season</th>
|
||||
<th>PA</th>
|
||||
<th>AB</th>
|
||||
<th>R</th>
|
||||
<th>H</th>
|
||||
<th>2B</th>
|
||||
<th>3B</th>
|
||||
<th>HR</th>
|
||||
<th>RBI</th>
|
||||
<th>SB</th>
|
||||
<th>CS</th>
|
||||
<th>BB</th>
|
||||
<th>SO</th>
|
||||
<th>BA</th>
|
||||
<th>OBP</th>
|
||||
<th>SLG</th>
|
||||
<th>OPS</th>
|
||||
<th>wOBA</th>
|
||||
<th>K%</th>
|
||||
<th>BPHR</th>
|
||||
<th>BPFO</th>
|
||||
<th>BP1B</th>
|
||||
<th>BPLO</th>
|
||||
<th>GIDP</th>
|
||||
<th>HBP</th>
|
||||
<th>SAC</th>
|
||||
<th>IBB</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="career-batting-table">
|
||||
<tr v-for="stat in sortedRegularAndPostSeasonBatting">
|
||||
<td>S{{ stat.seasonNumber }}{{ stat.isRegularSeason ? '' : ' / Playoffs' }}</td>
|
||||
<td>{{ stat.pa }}</td>
|
||||
<td>{{ stat.ab }}</td>
|
||||
<td>{{ stat.run }}</td>
|
||||
<td>{{ stat.hit }}</td>
|
||||
<td>{{ stat.double }}</td>
|
||||
<td>{{ stat.triple }}</td>
|
||||
<td>{{ stat.hr }}</td>
|
||||
<td>{{ stat.rbi }}</td>
|
||||
<td>{{ stat.sb }}</td>
|
||||
<td>{{ stat.cs }}</td>
|
||||
<td>{{ stat.bb }}</td>
|
||||
<td>{{ stat.so }}</td>
|
||||
<td>{{ stat.avg.toFixed(3) }}</td>
|
||||
<td>{{ stat.obp.toFixed(3) }}</td>
|
||||
<td>{{ stat.slg.toFixed(3) }}</td>
|
||||
<td>{{ stat.ops.toFixed(3) }}</td>
|
||||
<td>{{ stat.woba.toFixed(3) }}</td>
|
||||
<td>{{ calculateStrikeoutPercent(stat) }}</td>
|
||||
<td>{{ stat.bphr }}</td>
|
||||
<td>{{ stat.bpfo }}</td>
|
||||
<td>{{ stat.bp1b }}</td>
|
||||
<td>{{ stat.bplo }}</td>
|
||||
<td>{{ stat.gidp }}</td>
|
||||
<td>{{ stat.hbp }}</td>
|
||||
<td>{{ stat.sac }}</td>
|
||||
<td>{{ stat.ibb }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr v-if="careerBattingStat" id="career-batting-footer">
|
||||
<th>Career</th>
|
||||
<th>{{ careerBattingStat.pa }}</th>
|
||||
<th>{{ careerBattingStat.ab }}</th>
|
||||
<th>{{ careerBattingStat.run }}</th>
|
||||
<th>{{ careerBattingStat.hit }}</th>
|
||||
<th>{{ careerBattingStat.double }}</th>
|
||||
<th>{{ careerBattingStat.triple }}</th>
|
||||
<th>{{ careerBattingStat.hr }}</th>
|
||||
<th>{{ careerBattingStat.rbi }}</th>
|
||||
<th>{{ careerBattingStat.sb }}</th>
|
||||
<th>{{ careerBattingStat.cs }}</th>
|
||||
<th>{{ careerBattingStat.bb }}</th>
|
||||
<th>{{ careerBattingStat.so }}</th>
|
||||
<th>{{ careerBattingStat.avg.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.obp.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.slg.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.ops.toFixed(3) }}</th>
|
||||
<th>{{ careerBattingStat.woba.toFixed(3) }}</th>
|
||||
<th>{{ calculateStrikeoutPercent(careerBattingStat) }}</th>
|
||||
<th>{{ careerBattingStat.bphr }}</th>
|
||||
<th>{{ careerBattingStat.bpfo }}</th>
|
||||
<th>{{ careerBattingStat.bp1b }}</th>
|
||||
<th>{{ careerBattingStat.bplo }}</th>
|
||||
<th>{{ careerBattingStat.gidp }}</th>
|
||||
<th>{{ careerBattingStat.hbp }}</th>
|
||||
<th>{{ careerBattingStat.sac }}</th>
|
||||
<th>{{ careerBattingStat.ibb }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Game } from '@/services/apiResponseTypes'
|
||||
import { isDiscordAuthenticated } from '@/services/authenticationService'
|
||||
import { fetchLast2DecisionsByPlayerId, type Decision } from '@/services/decisionsService'
|
||||
import { type Player, fetchPlayerByName } from '@/services/playersService'
|
||||
import { fetchBattingStatsBySeasonAndPlayerId, fetchBattingStatsForLastFourGamesBySeasonAndPlayerId, aggregateBattingStats, type BattingStat } from '@/services/statsService'
|
||||
import { CURRENT_SEASON, isNotUndefined } from '@/services/utilities'
|
||||
|
||||
interface BattingStatWithSeason extends BattingStat {
|
||||
seasonNumber: number
|
||||
isRegularSeason: boolean
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "TeamView",
|
||||
name: "PlayerView",
|
||||
data() {
|
||||
return {
|
||||
player: undefined as Player | undefined,
|
||||
last2Decisions: [] as Decision[]
|
||||
last2Decisions: [] as Decision[],
|
||||
regularSeasonBattingStats: [] as BattingStat[],
|
||||
postSeasonBattingStats: [] as BattingStat[],
|
||||
last4Games: [] as BattingStat[]
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@ -76,6 +320,16 @@ export default {
|
||||
playerName: { type: String, required: true }
|
||||
},
|
||||
computed: {
|
||||
playerSeasonNumber(): number | undefined {
|
||||
return this.player?.season
|
||||
},
|
||||
isCurrentPlayer(): boolean {
|
||||
return this.seasonNumber === this.playerSeasonNumber
|
||||
},
|
||||
// TODO use to determine order of stats to display
|
||||
isBatter(): boolean {
|
||||
return !this.player?.pos_1.includes('P')
|
||||
},
|
||||
isAuthenticated(): boolean {
|
||||
return isDiscordAuthenticated()
|
||||
},
|
||||
@ -108,6 +362,47 @@ export default {
|
||||
injuryRating(): string | undefined {
|
||||
return this.player?.injury_rating
|
||||
},
|
||||
currentSeasonBatting(): BattingStat | undefined {
|
||||
if (!this.regularSeasonBattingStats.length) return undefined
|
||||
return this.regularSeasonBattingStats.find(stat => stat.player.season === CURRENT_SEASON)
|
||||
},
|
||||
currentPostSeasonBatting(): BattingStat | undefined {
|
||||
if (!this.postSeasonBattingStats.length) return undefined
|
||||
return this.postSeasonBattingStats.find(stat => stat.player.season === CURRENT_SEASON)
|
||||
},
|
||||
careerBattingStat(): BattingStat | undefined {
|
||||
if (this.regularSeasonBattingStats.length > 0) {
|
||||
// old site behavior just summed regular season stats for the career line total
|
||||
return aggregateBattingStats(this.regularSeasonBattingStats)
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
sortedRegularAndPostSeasonBatting(): BattingStatWithSeason[] {
|
||||
let seasonStats: BattingStatWithSeason[] = this.regularSeasonBattingStats.map(stat => {
|
||||
return {
|
||||
...stat,
|
||||
seasonNumber: stat.player.season,
|
||||
isRegularSeason: true
|
||||
}
|
||||
})
|
||||
// TODO: here would be where you could filter out postseason stats if desired (like Josef requested)
|
||||
seasonStats = seasonStats.concat(this.postSeasonBattingStats.map(stat => {
|
||||
return {
|
||||
...stat,
|
||||
seasonNumber: stat.player.season,
|
||||
isRegularSeason: false
|
||||
}
|
||||
}))
|
||||
|
||||
return seasonStats.sort((s1, s2) => {
|
||||
return s1.seasonNumber - s2.seasonNumber === 0
|
||||
? s1.isRegularSeason
|
||||
? -1
|
||||
: 1
|
||||
: s1.seasonNumber - s2.seasonNumber
|
||||
})
|
||||
},
|
||||
lastAppearance(): string | undefined {
|
||||
if (!this.last2Decisions?.length) return undefined
|
||||
if (this.last2Decisions.length <= 0) return undefined
|
||||
@ -134,11 +429,35 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async fetchData(): Promise<void> {
|
||||
this.player = await fetchPlayerByName(this.seasonNumber, this.playerName)
|
||||
this.last2Decisions = await fetchLast2DecisionsByPlayerId(this.seasonNumber, this.player?.id)
|
||||
this.player = await this.tryFetchPlayerByNameForAnySeason(this.seasonNumber, this.playerName)
|
||||
if (!this.player) return
|
||||
|
||||
this.last2Decisions = await fetchLast2DecisionsByPlayerId(this.seasonNumber, this.player.id)
|
||||
this.last4Games = await fetchBattingStatsForLastFourGamesBySeasonAndPlayerId(this.seasonNumber, this.player.id)
|
||||
|
||||
// TODO: this should change, either with an api that can take a player name for every season, a way
|
||||
// to get multiple seasons stats at once, or a players ids across all seasons at once
|
||||
const playerSeasons = await Promise.all(Array.from(Array(CURRENT_SEASON), (element, index) => index + 1).map(seasonNumber => fetchPlayerByName(seasonNumber, this.player!.name)))
|
||||
this.regularSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, true)))).filter(isNotUndefined)
|
||||
this.postSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, false)))).filter(isNotUndefined)
|
||||
},
|
||||
async tryFetchPlayerByNameForAnySeason(seasonNumber: number, playerName: string): Promise<Player | undefined> {
|
||||
do {
|
||||
const player: Player | undefined = await fetchPlayerByName(seasonNumber, playerName)
|
||||
if (player) return player
|
||||
seasonNumber--
|
||||
} while (seasonNumber > 0)
|
||||
},
|
||||
formatDecisionToAppearance(decision: Decision): string {
|
||||
return `${decision.rest_ip.toFixed(1)}IP w${decision.week}g${decision.game_num}`
|
||||
},
|
||||
makeWxGyFromGame(game: Game | 'TOT'): string {
|
||||
if (game === 'TOT') return 'TOT'
|
||||
return `w${game.week}g${game.game_num}`
|
||||
},
|
||||
calculateStrikeoutPercent(stat: BattingStat): string {
|
||||
if (!stat.pa) return 'N/A'
|
||||
return (stat.so * 100 / stat.pa).toFixed(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user