Batting stats on leaderboards
This commit is contained in:
parent
e73123c6a0
commit
a6791e84f2
@ -17,6 +17,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<RouterLink class="nav-link" to="/standings">Standings</RouterLink>
|
<RouterLink class="nav-link" to="/standings">Standings</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<RouterLink class="nav-link" to="/leaderboards">Leaderboards</RouterLink>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<RouterLink class="nav-link"
|
<RouterLink class="nav-link"
|
||||||
:to="{ name: 'team', params: { seasonNumber: seasonNumber(), teamAbbreviation: 'FA' } }">Free Agents
|
:to="{ name: 'team', params: { seasonNumber: seasonNumber(), teamAbbreviation: 'FA' } }">Free Agents
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="career-batting-table">
|
<tbody id="career-batting-table">
|
||||||
<tr v-for="stat in battingStats">
|
<tr v-for="stat in battingStats" :key="stat.player.bbref_id">
|
||||||
<td>
|
<td>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: stat.player.name } }">
|
:to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: stat.player.name } }">
|
||||||
@ -112,7 +112,7 @@
|
|||||||
import { aggregateBattingStats, fetchBattingStatsBySeasonAndTeamId, type BattingStat } from '@/services/battingStatsService'
|
import { aggregateBattingStats, fetchBattingStatsBySeasonAndTeamId, type BattingStat } from '@/services/battingStatsService'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TeamBattingTable",
|
name: 'TeamBattingTable',
|
||||||
props: {
|
props: {
|
||||||
seasonNumber: { type: Number, required: true },
|
seasonNumber: { type: Number, required: true },
|
||||||
teamId: { type: Number, required: true },
|
teamId: { type: Number, required: true },
|
||||||
|
|||||||
@ -31,6 +31,11 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
name: 'standings',
|
name: 'standings',
|
||||||
component: () => import('../views/StandingsView.vue')
|
component: () => import('../views/StandingsView.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/leaderboards',
|
||||||
|
name: 'leaderboards',
|
||||||
|
component: () => import('../views/LeaderboardView.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/rules',
|
path: '/rules',
|
||||||
name: 'rules',
|
name: 'rules',
|
||||||
|
|||||||
@ -64,6 +64,25 @@ interface LegacyBattingStat {
|
|||||||
bplo: number
|
bplo: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchBattingStatsBySeason(seasonNumber: number, isRegularSeason: boolean): Promise<BattingStat[]> {
|
||||||
|
// different endpoint for pre-modern stats (/plays) era
|
||||||
|
if (seasonNumber < MODERN_STAT_ERA_START) {
|
||||||
|
console.warn('battingStatsService.fetchBattingStatsBySeason - Not looking up legacy season stats rn')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${SITE_URL}/api/v3/plays/batting?season=${seasonNumber}&group_by=player&limit=1000&s_type=${isRegularSeason ? 'regular' : 'post'}`)
|
||||||
|
|
||||||
|
const battingStatsResponse: {
|
||||||
|
count: number
|
||||||
|
stats: BattingStat[]
|
||||||
|
} = await response.json()
|
||||||
|
|
||||||
|
if (battingStatsResponse.count === 0) return []
|
||||||
|
|
||||||
|
return battingStatsResponse.stats
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchBattingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<BattingStat | undefined> {
|
export async function fetchBattingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<BattingStat | undefined> {
|
||||||
// different endpoint for pre-modern stats (/plays) era
|
// different endpoint for pre-modern stats (/plays) era
|
||||||
if (seasonNumber < MODERN_STAT_ERA_START) {
|
if (seasonNumber < MODERN_STAT_ERA_START) {
|
||||||
|
|||||||
173
src/views/LeaderboardView.vue
Normal file
173
src/views/LeaderboardView.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="leaderboard-view centerDiv">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<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>
|
||||||
|
<!-- <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="batting" value="batting">Pitching</option>
|
||||||
|
<option key="batting" value="batting">Fielding</option>
|
||||||
|
</select> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" id="stat-listing">
|
||||||
|
<div v-for="table in battingLeaderboardTableData" :key="table.battingStatProperty" class="col-sm-auto"
|
||||||
|
style="text-align: center">
|
||||||
|
<h3><small>{{ table.title }}</small></h3>
|
||||||
|
<div class="table-responsive-xl">
|
||||||
|
<table style="text-align: center" class="table table-sm table-striped">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th style="min-width: 150px">Player</th>
|
||||||
|
<th style="min-width: 75px">Team</th>
|
||||||
|
<th style="min-width: 75px">{{ table.columnName }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="stat in getTop10BattingStatByCategory(table.battingStatProperty)" :key="stat.player.name">
|
||||||
|
<td>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: stat.player.name } }">
|
||||||
|
{{ stat.player.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<RouterLink v-if="stat.team.abbrev"
|
||||||
|
:to="{ name: 'team', params: { seasonNumber: seasonNumber, teamAbbreviation: stat.team.abbrev } }">
|
||||||
|
{{ stat.team.abbrev }}
|
||||||
|
</RouterLink>
|
||||||
|
</td>
|
||||||
|
<td>{{ formatNumericalStat(stat[table.battingStatProperty] as number) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
interface BattingLeaderboardTableData {
|
||||||
|
title: string
|
||||||
|
columnName: string
|
||||||
|
battingStatProperty: keyof BattingStat
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LeaderboardView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allPlayersBattingStats: [] as BattingStat[],
|
||||||
|
allPlayersFieldingStats: [] as FieldingStat[],
|
||||||
|
allPlayersPitchingStats: [] as PitchingStat[],
|
||||||
|
currentSeasonNumber: CURRENT_SEASON,
|
||||||
|
seasonNumber: CURRENT_SEASON,
|
||||||
|
statType: 'batting' as 'batting' | 'pitching' | 'fielding',
|
||||||
|
weekNumber: 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
seasonNumbers(): number[] {
|
||||||
|
if (!this.currentSeasonNumber) return []
|
||||||
|
return Array.from({ length: this.currentSeasonNumber }, (_, i) => i + 1)
|
||||||
|
},
|
||||||
|
weekNumberForCalcs(): number {
|
||||||
|
// 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)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
battingLeaderboardTableData(): BattingLeaderboardTableData[] {
|
||||||
|
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: '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' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
seasonNumber(newValue, oldValue) {
|
||||||
|
if (newValue !== oldValue)
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
statType(newValue, oldValue) {
|
||||||
|
if (newValue !== oldValue)
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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
|
||||||
|
|
||||||
|
return stat.toFixed(3)
|
||||||
|
},
|
||||||
|
async fetchData(): Promise<void> {
|
||||||
|
const leagueInfo: LeagueInfo = await fetchCurrentLeagueInfo()
|
||||||
|
this.weekNumber = leagueInfo.week
|
||||||
|
|
||||||
|
if (this.statType == 'batting') {
|
||||||
|
this.allPlayersBattingStats = await fetchBattingStatsBySeason(this.seasonNumber, true)
|
||||||
|
}
|
||||||
|
if (this.statType == 'pitching') {
|
||||||
|
this.allPlayersPitchingStats = []
|
||||||
|
}
|
||||||
|
if (this.statType == 'fielding') {
|
||||||
|
this.allPlayersFieldingStats = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user