Compare commits

..

3 Commits

Author SHA1 Message Date
Sascha Kühl bd164bff25 Merge remote-tracking branch 'origin/main' 2025-06-16 16:50:26 +02:00
Sascha Kühl d6ee77afaa start calculating stats 2025-06-15 22:28:17 +02:00
Sascha Kühl 2428ba1152 bullpen view progress 2025-06-15 22:27:56 +02:00
5 changed files with 232 additions and 28 deletions

View File

@ -77,46 +77,75 @@ generateItems();
</script> </script>
<template> <template>
<div>
<ion-label>hallo</ion-label>
</div>
<ion-list :inset="false"> <ion-list :inset="false">
<ion-item v-for="(bullpen, index) in items" :key="index" class="custom-item" :button="true" :detail="false"> <ion-item v-for="(bullpen, index) in items" :key="index" class="custom-item" lines="none" :button="true" :detail="false">
<!-- Top-left icon --> <!-- Top-left icon -->
<ion-icon :icon="baseballOutline" slot="start" class="item-icon"></ion-icon> <ion-icon :icon="baseballOutline" slot="start" class="item-icon"></ion-icon>
<ion-label> <ion-label>
<strong>{{formatDate(bullpen.startedAt)}}</strong> <strong>{{formatDate(bullpen.startedAt)}}</strong>
<ion-label class="item-labels"> <ion-label class="item-labels">
<!-- Horizontale Anordnung der Kreise --> <div class="stats-container">
<div class="circle-container"> <div class="stat-item" :style="{ '--progress': '57%' }">
<div class="circle-wrapper"> <div class="stat-content">
<div class="progress-circle" :class="percentageClass(57)"> <span class="percentage">57%</span>
<span>{{ (0.57 * 100).toFixed(0) }}%</span> <span class="label">Precision</span>
<div class="left-half-clipper">
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div>
</div> </div>
<label>Precision</label>
</div> </div>
<div class="circle-wrapper"> <div class="stat-item" :style="{ '--progress': '77%' }">
<div class="progress-circle" :class="percentageClass(77)"> <div class="stat-content">
<span>{{ (0.77 * 100).toFixed(0) }}%</span> <span class="percentage">77%</span>
<div class="left-half-clipper"> <span class="label">Strike</span>
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div>
</div> </div>
<label>Strike</label>
</div> </div>
<div class="circle-wrapper"> <div class="stat-item" :style="{ '--progress': '33%' }">
<div class="progress-circle" :class="percentageClass(33)"> <div class="stat-content">
<span>{{ (0.33 * 100).toFixed(0) }}%</span> <span class="percentage">33%</span>
<div class="left-half-clipper"> <span class="label">Balls</span>
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div>
</div> </div>
<label>Balls</label>
</div> </div>
<!-- <div class="stat-item" :style="{ '&#45;&#45;progress': '45%' }">-->
<!-- <div class="stat-content">-->
<!-- <span class="percentage">45%</span>-->
<!-- <span class="label">Fastballs</span>-->
<!-- </div>-->
<!-- </div>-->
</div> </div>
<!-- Horizontale Anordnung der Kreise -->
<!-- <div class="circle-container">-->
<!-- <div class="circle-wrapper">-->
<!-- <div class="progress-circle" :class="percentageClass(57)">-->
<!-- <span>{{ (0.57 * 100).toFixed(0) }}%</span>-->
<!-- <div class="left-half-clipper">-->
<!-- <div class="first50-bar"></div>-->
<!-- <div class="value-bar"></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <label>Precision</label>-->
<!-- </div>-->
<!-- <div class="circle-wrapper">-->
<!-- <div class="progress-circle" :class="percentageClass(77)">-->
<!-- <span>{{ (0.77 * 100).toFixed(0) }}%</span>-->
<!-- <div class="left-half-clipper">-->
<!-- <div class="first50-bar"></div>-->
<!-- <div class="value-bar"></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <label>Strike</label>-->
<!-- </div>-->
<!-- <div class="circle-wrapper">-->
<!-- <div class="progress-circle" :class="percentageClass(33)">-->
<!-- <span>{{ (0.33 * 100).toFixed(0) }}%</span>-->
<!-- <div class="left-half-clipper">-->
<!-- <div class="first50-bar"></div>-->
<!-- <div class="value-bar"></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <label>Balls</label>-->
<!-- </div>-->
<!-- </div>-->
</ion-label> </ion-label>
</ion-label> </ion-label>
<div class="metadata-end-wrapper" slot="end"> <div class="metadata-end-wrapper" slot="end">
@ -198,4 +227,59 @@ ion-label strong{
color: var(--ion-color-medium); color: var(--ion-color-medium);
margin-top: 8px; margin-top: 8px;
} }
.stats-container {
display: flex;
justify-content: space-between;
align-items: stretch;
width: 100%;
margin-top: 8px;
height: 70px;
}
.stat-item {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: var(--ion-color-light);
}
.stat-item::before {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: var(--progress);
background: var(--ion-color-primary);
opacity: 0.15;
z-index: 0;
}
.stat-content {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
}
.percentage {
flex: 1;
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
color: var(--ion-color-dark);
}
.label {
font-size: 12px;
color: var(--ion-color-medium);
padding-bottom: 8px;
}
</style> </style>

View File

@ -0,0 +1,38 @@
// Migration 2: Remove columns from Users
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
return Promise.all([
queryInterface.addColumn('BullpenSessions','precisionRate', {
type: Sequelize.INTEGER,
allowNull: false
}),
queryInterface.addColumn('BullpenSessions','strikeRate', {
type: Sequelize.INTEGER,
allowNull: false
}),
queryInterface.addColumn('BullpenSessions','ballRate', {
type: Sequelize.INTEGER,
allowNull: false
}),
queryInterface.addColumn('BullpenSessions','fastballRate', {
type: Sequelize.INTEGER,
allowNull: false
}),
queryInterface.addColumn('BullpenSessions','offSpeedRate', {
type: Sequelize.INTEGER,
allowNull: false
}),
]);
},
async down(queryInterface, Sequelize) {
return Promise.all([
queryInterface.removeColumn('BullpenSessions', 'precisionRate'),
queryInterface.removeColumn('BullpenSessions', 'strikeRate'),
queryInterface.removeColumn('BullpenSessions', 'ballRate'),
queryInterface.removeColumn('BullpenSessions', 'fastballRate'),
queryInterface.removeColumn('BullpenSessions', 'offSpeedRate')
]);
}
};

View File

@ -25,6 +25,26 @@ module.exports = (sequelize, DataTypes) => {
finishedAt: { finishedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false, allowNull: false,
},
precisionRate: {
type: DataTypes.INTEGER,
allowNull: false,
},
strikeRate: {
type: DataTypes.INTEGER,
allowNull: false,
},
ballRate: {
type: DataTypes.INTEGER,
allowNull: false,
},
fastballRate: {
type: DataTypes.INTEGER,
allowNull: false,
},
offSpeedRate: {
type: DataTypes.INTEGER,
allowNull: false,
} }
}, { }, {
sequelize, sequelize,

View File

@ -10,6 +10,53 @@ const shouldMatch = (percentage) => {
return Math.random() * 100 < percentage; return Math.random() * 100 < percentage;
} }
const calculateRates = (pitches) => {
const bullpenPitches = pitches.reduce((acc, pitch) => {
const { bullpenSessionId } = pitch;
if (!acc[bullpenSessionId]) {
acc[bullpenSessionId] = [];
}
acc[bullpenSessionId].push(pitch);
return acc;
}, {});
const totalPitches = bullpenPitches.length;
if (totalPitches === 0) return {
precisionRate: 0,
strikeRate: 0,
ballRate: 0,
fastBallRate: 0,
offSpeedRate: 0
};
const counts = bullpenPitches.reduce((acc, pitch) => {
// Check if it's a strike (hitArea 1-9)
const isStrike = pitch.hitArea >= 1 && pitch.hitArea <= 9;
// Update counts based on conditions
acc.precision += pitch.aimedArea === pitch.hitArea ? 1 : 0;
acc.strikes += isStrike ? 1 : 0;
if (isStrike) {
if (pitch.pitchTypeId === 1) {
acc.fastballs += 1;
} else {
acc.offspeed += 1;
}
}
return acc;
}, { precision: 0, strikes: 0, fastballs: 0, offspeed: 0 });
return {
precisionRate: (counts.precision / totalPitches) * 100,
strikeRate: (counts.strikes / totalPitches) * 100,
ballRate: ((totalPitches - counts.strikes) / totalPitches) * 100,
fastBallRate: (counts.fastballs / totalPitches) * 100,
offSpeedRate: (counts.offspeed / totalPitches) * 100
};
}
const createBullpens = async (queryInterface, demoPlayer) => { const createBullpens = async (queryInterface, demoPlayer) => {
const startDate = new Date(); const startDate = new Date();
// const endDate = new Date(new Date().setTime(startDate.getTime() + (10 * 60000))); // const endDate = new Date(new Date().setTime(startDate.getTime() + (10 * 60000)));
@ -22,7 +69,12 @@ const createBullpens = async (queryInterface, demoPlayer) => {
startedAt: startedAt, startedAt: startedAt,
finishedAt: new Date(new Date().setTime(startedAt.getTime() + (10 * 60000))), finishedAt: new Date(new Date().setTime(startedAt.getTime() + (10 * 60000))),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
precisionRate: 0,
strikeRate: 0,
ballRate: 0,
fastBallRate: 0,
offSpeedRate: 0
} }
}) })
await queryInterface.bulkInsert('BullpenSessions', bullpens); await queryInterface.bulkInsert('BullpenSessions', bullpens);
@ -64,6 +116,11 @@ const createBullpens = async (queryInterface, demoPlayer) => {
}); });
await queryInterface.bulkInsert('Pitches', pitches.flat()); await queryInterface.bulkInsert('Pitches', pitches.flat());
const rates = calculateRates(pitches);
await queryInterface.update(null, 'BullpenSessions', bullpens.map(bullpen => {}))
} }
/** @type {import('sequelize-cli').Migration} */ /** @type {import('sequelize-cli').Migration} */

View File

@ -2,6 +2,11 @@ const bullpenSession = {
pitcherId: 1, pitcherId: 1,
startedAt: new Date(2025, 3, 22, 16, 5, 13), startedAt: new Date(2025, 3, 22, 16, 5, 13),
finishedAt: new Date(2025, 3, 22, 16, 23, 45), finishedAt: new Date(2025, 3, 22, 16, 23, 45),
precisionRate: 33,
strikeRate: 24,
ballRate: 76,
fastballRate: 55,
offSpeedRate: 78,
pitches: [{ pitches: [{
pitchTypeId: 1, pitchTypeId: 1,
pitchTime: new Date(2025, 3, 22, 16, 7, 21), pitchTime: new Date(2025, 3, 22, 16, 7, 21),