player stats updated badges

This commit is contained in:
2025-11-12 19:05:44 +01:00
parent 43a9b63c95
commit aa01f4136d
4 changed files with 500 additions and 38 deletions
+2
View File
@@ -182,6 +182,8 @@
"average_score": "Average Score",
"highest_score": "Highest Score",
"worst_score": "Worst Score",
"hit_rate": "Hit Rate",
"most_common": "Most Common",
"completed": "Completed",
"position": "Position",
"points": "Points",
+2
View File
@@ -188,6 +188,8 @@
"average_score": "Povprečni Rezultat",
"highest_score": "Najvišji Rezultat",
"worst_score": "Najslabši Rezultat",
"hit_rate": "Stopnja Uspešnosti",
"most_common": "Najpogostejši",
"completed": "Zaključeno",
"position": "Uvrstitev",
"points": "Točke",
+482 -38
View File
@@ -73,6 +73,45 @@
font-weight: 500;
}
/* Tournament Badge Styling */
.stat-badge.tournament-badge {
display: flex;
flex-direction: column;
justify-content: center;
}
.tournament-scores {
display: flex;
gap: 10px;
margin-bottom: 8px;
justify-content: center;
}
.tournament-score-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.tournament-type {
font-size: 0.65rem;
color: #999;
font-weight: 600;
text-transform: uppercase;
}
.tournament-emoji {
font-size: 1rem;
margin-bottom: 2px;
}
.tournament-best {
font-size: 1.3rem;
font-weight: bold;
color: #28a745;
}
/* Charts Section */
.charts-section {
min-height: 450px;
@@ -625,6 +664,49 @@
max-height: 100%;
}
/* Shot Count Cards */
.shot-count-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
gap: 12px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
}
.shot-count-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 12px;
text-align: center;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90px;
}
.shot-count-card:hover {
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.15);
transform: translateY(-2px);
border-color: #28a745;
}
.shot-score {
font-size: 1.3rem;
font-weight: bold;
color: #28a745;
margin-bottom: 8px;
}
.shot-count {
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
.history-list {
flex: 1;
overflow-y: auto;
@@ -968,6 +1050,90 @@
min-height: 300px;
padding: 10px;
}
/* Responsive Shot Count Cards */
.shot-count-cards {
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 8px;
margin-top: 20px;
padding-top: 15px;
}
.shot-count-card {
min-height: 80px;
padding: 10px;
}
.shot-score {
font-size: 1.1rem;
margin-bottom: 5px;
}
.shot-count {
font-size: 1.5rem;
}
/* Responsive Overall Accuracy Dashboard */
.overall-top-section {
flex-direction: column;
gap: 15px;
margin-bottom: 15px;
}
.gauge-section {
flex: 0 0 100%;
min-height: 250px;
}
.stat-card-group {
flex: 0 0 100%;
grid-template-columns: 2fr 2fr;
gap: 10px;
}
.overall-bottom-section {
grid-template-columns: 1fr;
gap: 15px;
margin-top: 15px;
}
.overall-bar-chart,
.overall-radar-chart {
min-height: 280px;
padding: 10px;
}
.stat-card {
padding: 10px;
}
.stat-card-icon {
font-size: 1.4rem;
margin-bottom: 4px;
}
.stat-card-value {
font-size: 1.2rem;
margin-bottom: 2px;
}
.stat-card-label {
font-size: 0.65rem;
}
.gauge-stats {
gap: 15px;
padding: 8px 12px;
font-size: 0.8rem;
}
.gauge-stat-value {
font-size: 1.1rem;
}
.accuracy-gauge-wrapper {
height: 250px;
}
}
</style>
<script src="/static/js/i18n.js"></script>
@@ -986,25 +1152,30 @@
<div class="container">
<!-- Stats Overview -->
<div class="stats-badges">
<div class="stat-badge">
<span class="stat-icon">🏆</span>
<div class="stat-number">{{ stats.total_tournaments }}</div>
<div class="stat-label" data-i18n="tournament.tournaments">Tournaments</div>
<div class="stat-badge tournament-badge">
<div class="tournament-scores">
<div class="tournament-score-item">
<div class="tournament-emoji">🎯</div>
<div class="tournament-type">4T</div>
<div class="tournament-best" id="best-4">{{ stats.best_4_targets_score if stats.best_4_targets_score > 0 else 0 }}</div>
</div>
<div class="tournament-score-item">
<div class="tournament-emoji"></div>
<div class="tournament-type">20T</div>
<div class="tournament-best" id="best-20">{{ stats.best_20_targets_score if stats.best_20_targets_score > 0 else 0 }}</div>
</div>
<div class="tournament-score-item">
<div class="tournament-emoji">💪</div>
<div class="tournament-type">40T</div>
<div class="tournament-best" id="best-40">{{ stats.best_40_targets_score if stats.best_40_targets_score > 0 else 0 }}</div>
</div>
</div>
<div class="stat-label">Best Scores</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🎖️</span>
<div class="stat-number">{{ stats.total_leagues }}</div>
<div class="stat-label" data-i18n="league.league">Leagues</div>
</div>
<div class="stat-badge">
<span class="stat-icon"></span>
<div class="stat-number">{{ stats.best_tournament_score }}</div>
<div class="stat-label" data-i18n="results.best_score">Best Score</div>
</div>
<div class="stat-badge">
<span class="stat-icon">📊</span>
<div class="stat-number">{{ stats.average_tournament_score|round|int if stats.average_tournament_score > 0 else 0 }}</div>
<div class="stat-label" data-i18n="results.average_score">Average</div>
<span class="stat-icon">🎯</span>
<div class="stat-number" id="mostCommon-badge">0</div>
<div class="stat-label" data-i18n="results.most_common">Most Common</div>
</div>
<div class="stat-badge">
<span class="stat-icon">🔫</span>
@@ -1012,25 +1183,81 @@
<div class="stat-label" data-i18n="results.total_shots">Total Shots</div>
</div>
<div class="stat-badge">
<span class="stat-icon">📉</span>
<div class="stat-number">{{ stats.worst_tournament_score if stats.worst_tournament_score > 0 else 0 }}</div>
<div class="stat-label" data-i18n="results.worst_score">Worst Score</div>
<span class="stat-icon">🏆</span>
<div class="stat-number">{{ stats.total_tournaments }}</div>
<div class="stat-label" data-i18n="tournament.tournaments">Tournaments</div>
</div>
<div class="stat-badge">
<span class="stat-icon">👑</span>
<div class="stat-number">{{ stats.total_leagues|default(0) }}</div>
<div class="stat-label" data-i18n="league.league">Leagues</div>
</div>
</div>
<!-- Card 1: Overall Accuracy (Colorful - No Filters) -->
<!-- Card 1: Overall Accuracy (Bar + Pie Charts) -->
<div class="overall-accuracy-card">
<div class="panel-title">📊 <span data-i18n="analysis.shot_accuracy">Overall Shot Accuracy</span></div>
<!-- Overall Shot Accuracy Section -->
<!-- Charts Section -->
<div class="overall-content-wrapper">
<!-- Bar Chart: All Shots -->
<div class="overall-chart-left">
<canvas id="overallAccuracyChart"></canvas>
</div>
<!-- Pie/Radar Chart -->
<div class="overall-radar-right">
<canvas id="overallRadarChart"></canvas>
</div>
</div>
<!-- Shot Count Cards -->
<div class="shot-count-cards">
<div class="shot-count-card">
<div class="shot-score">10</div>
<div class="shot-count" id="count-10">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">9</div>
<div class="shot-count" id="count-9">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">8</div>
<div class="shot-count" id="count-8">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">7</div>
<div class="shot-count" id="count-7">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">6</div>
<div class="shot-count" id="count-6">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">5</div>
<div class="shot-count" id="count-5">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">4</div>
<div class="shot-count" id="count-4">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">3</div>
<div class="shot-count" id="count-3">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">2</div>
<div class="shot-count" id="count-2">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">1</div>
<div class="shot-count" id="count-1">0</div>
</div>
<div class="shot-count-card">
<div class="shot-score">0</div>
<div class="shot-count" id="count-0">0</div>
</div>
</div>
</div>
<!-- Card 2: Filtered Accuracy (Main Outer Card - White) -->
@@ -1058,13 +1285,17 @@
<div class="chart-stat-value" id="gameCount">0</div>
<div class="chart-stat-label" data-i18n="tournament.tournaments">Games</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="bestScore">0</div>
<div class="chart-stat-label" data-i18n="results.best_score">Best</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="avgScore">0</div>
<div class="chart-stat-label" data-i18n="results.average_score">Average</div>
</div>
<div class="chart-stat">
<div class="chart-stat-value" id="bestScore">0</div>
<div class="chart-stat-label" data-i18n="results.best_score">Best</div>
<div class="chart-stat-value" id="mostCommonShot">0</div>
<div class="chart-stat-label">Most Common</div>
</div>
</div>
</div>
@@ -1342,11 +1573,75 @@
}
});
// Create overall accuracy bar chart
// Create overall bar chart
createOverallAccuracyChart(totalCounts);
// Create overall radar chart
// Create overall pie chart
createOverallRadarChart(totalCounts);
// Populate shot count cards
populateShotCountCards(totalCounts);
}
// Populate shot count cards below charts
function populateShotCountCards(totalCounts) {
const scoreMap = {
10: totalCounts.tens,
9: totalCounts.nines,
8: totalCounts.eights,
7: totalCounts.sevens,
6: totalCounts.sixes,
5: totalCounts.fives,
4: totalCounts.fours,
3: totalCounts.threes,
2: totalCounts.twos,
1: totalCounts.ones,
0: totalCounts.zeros
};
// Update shot count cards
for (const [score, count] of Object.entries(scoreMap)) {
const countElement = document.getElementById(`count-${score}`);
if (countElement) {
countElement.textContent = count;
}
}
// Update top stat badges
updateTopStatsBadges(totalCounts);
}
// Update top stat badges with overall metrics
function updateTopStatsBadges(totalCounts) {
// Perfect 10s count
const totalTensBadge = document.getElementById('totalTens-badge');
if (totalTensBadge) {
totalTensBadge.textContent = totalCounts.tens;
}
// Calculate most common shot value
let mostCommonShot = 0;
let maxCount = 0;
const shotScores = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCountsArray = [
totalCounts.tens, totalCounts.nines, totalCounts.eights,
totalCounts.sevens, totalCounts.sixes, totalCounts.fives,
totalCounts.fours, totalCounts.threes, totalCounts.twos,
totalCounts.ones, totalCounts.zeros
];
for (let i = 0; i < shotScores.length; i++) {
if (shotCountsArray[i] > maxCount) {
maxCount = shotCountsArray[i];
mostCommonShot = shotScores[i];
}
}
const mostCommonBadge = document.getElementById('mostCommon-badge');
if (mostCommonBadge) {
mostCommonBadge.textContent = mostCommonShot;
}
}
// Create overall accuracy bar chart
@@ -1505,6 +1800,124 @@
});
}
// Create accuracy gauge chart (doughnut showing overall accuracy percentage)
let accuracyGaugeChartInstance = null;
function createAccuracyGaugeChart(totalCounts) {
const canvas = document.getElementById('accuracyGaugeChart');
if (!canvas) {
console.warn('Accuracy gauge chart canvas not found');
return;
}
const ctx = canvas.getContext('2d');
// Destroy existing chart if it exists
if (accuracyGaugeChartInstance) {
accuracyGaugeChartInstance.destroy();
}
// Calculate accuracy percentage (perfect shots 8-10 / total shots)
const perfectShots = totalCounts.tens + totalCounts.nines + totalCounts.eights;
const totalShots = Object.values(totalCounts).reduce((a, b) => a + b, 0);
const accuracyPercentage = totalShots > 0 ? Math.round((perfectShots / totalShots) * 100) : 0;
// Calculate consistency score (lower variance = higher consistency)
const shotValues = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCounts = [totalCounts.tens, totalCounts.nines, totalCounts.eights, totalCounts.sevens, totalCounts.sixes, totalCounts.fives, totalCounts.fours, totalCounts.threes, totalCounts.twos, totalCounts.ones, totalCounts.zeros];
let mean = 0;
if (totalShots > 0) {
mean = shotValues.reduce((sum, val, i) => sum + (val * shotCounts[i]), 0) / totalShots;
}
let variance = 0;
if (totalShots > 0) {
variance = shotValues.reduce((sum, val, i) => sum + (Math.pow(val - mean, 2) * shotCounts[i]), 0) / totalShots;
}
const consistency = Math.max(0, Math.round(100 - (variance * 5))); // Scale variance to 0-100
// Update DOM with percentages
document.getElementById('accuracyPercentage').textContent = accuracyPercentage + '%';
document.getElementById('consistencyScore').textContent = consistency;
const gaugeData = [accuracyPercentage, 100 - accuracyPercentage];
accuracyGaugeChartInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Accuracy', 'Missed'],
datasets: [{
data: gaugeData,
backgroundColor: ['#28a745', '#f0f0f0'],
borderColor: ['#28a745', '#e0e0e0'],
borderWidth: 3,
circumference: 180,
rotation: 270
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': ' + context.parsed + '%';
}
}
}
}
}
});
}
// Update stat cards with performance metrics
function updateStatCards(statsData, totalCounts) {
try {
// Count tournaments
const tournaments = statsData.tournament_history ? statsData.tournament_history.length : 0;
document.getElementById('totalTournaments').textContent = tournaments;
// Count total 10s
const totalTens = totalCounts.tens || 0;
document.getElementById('totalTens').textContent = totalTens;
// Calculate variance
const shotValues = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const shotCounts = [totalCounts.tens, totalCounts.nines, totalCounts.eights, totalCounts.sevens, totalCounts.sixes, totalCounts.fives, totalCounts.fours, totalCounts.threes, totalCounts.twos, totalCounts.ones, totalCounts.zeros];
const totalShots = shotCounts.reduce((a, b) => a + b, 0);
let mean = 0;
if (totalShots > 0) {
mean = shotValues.reduce((sum, val, i) => sum + (val * shotCounts[i]), 0) / totalShots;
}
let variance = 0;
if (totalShots > 0) {
variance = shotValues.reduce((sum, val, i) => sum + (Math.pow(val - mean, 2) * shotCounts[i]), 0) / totalShots;
}
document.getElementById('scoreVariance').textContent = variance.toFixed(2);
// Calculate range (highest - lowest non-zero score)
let minScore = null;
let maxScore = null;
for (let i = 0; i < shotValues.length; i++) {
if (shotCounts[i] > 0) {
if (maxScore === null) maxScore = shotValues[i];
minScore = shotValues[i];
}
}
const range = maxScore !== null ? (maxScore - minScore) : 0;
document.getElementById('scoreRange').textContent = range;
} catch (error) {
console.error('Error updating stat cards:', error);
}
}
// Load shot accuracy data from template context
function loadShotAccuracyData() {
try {
@@ -1672,10 +2085,41 @@
const avgScore = gameCount > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / gameCount) : 0;
const bestScore = gameCount > 0 ? Math.max(...scores) : 0;
// Calculate most common shot value
let mostCommonShot = 0;
const shotCounts = { 10: 0, 9: 0, 8: 0, 7: 0, 6: 0, 5: 0, 4: 0, 3: 0, 2: 0, 1: 0, 0: 0 };
tournaments.forEach(tournament => {
if (tournament.shot_breakdown) {
const breakdown = tournament.shot_breakdown;
shotCounts[10] += breakdown.tens || 0;
shotCounts[9] += breakdown.nines || 0;
shotCounts[8] += breakdown.eights || 0;
shotCounts[7] += breakdown.sevens || 0;
shotCounts[6] += breakdown.sixes || 0;
shotCounts[5] += breakdown.fives || 0;
shotCounts[4] += breakdown.fours || 0;
shotCounts[3] += breakdown.threes || 0;
shotCounts[2] += breakdown.twos || 0;
shotCounts[1] += breakdown.ones || 0;
shotCounts[0] += breakdown.zeros || 0;
}
});
// Find shot value with highest count
let maxCount = 0;
for (const [score, count] of Object.entries(shotCounts)) {
if (count > maxCount) {
maxCount = count;
mostCommonShot = parseInt(score);
}
}
// Update basic stats
document.getElementById('gameCount').textContent = gameCount;
document.getElementById('avgScore').textContent = avgScore;
document.getElementById('bestScore').textContent = bestScore;
document.getElementById('avgScore').textContent = avgScore;
document.getElementById('mostCommonShot').textContent = mostCommonShot;
// Update shot accuracy stats (if available in tournament data)
updateAccuracyStats(tournaments);
@@ -1767,17 +2211,17 @@
function generateColorfulArray() {
// Color mapping from green (perfect 10) to red (misses 0): labels are [10,9,8,7,6,5,4,3,2,1,0]
return [
'#2E7D32', // 10 - green (perfect)
'#388E3C', // 9 - green
'#43A047', // 8 - green
'#558B2F', // 7 - light green
'#9CCC65', // 6 - lime green
'#FDD835', // 5 - yellow
'#FBC02D', // 4 - golden yellow
'#FFA726', // 3 - orange
'#FF7043', // 2 - light orange-red
'#E53935', // 1 - red
'#C62828' // 0 - dark red (miss)
'#4A9D6F', // 10 - medium green (perfect)
'#5BA97A', // 9 - medium green
'#6CB585', // 8 - medium green
'#7DC285', // 7 - medium light green
'#8FCC7F', // 6 - medium lime green
'#D4B84D', // 5 - medium yellow
'#CDA642', // 4 - medium golden yellow
'#D99A5D', // 3 - medium orange
'#D87A6C', // 2 - medium light orange-red
'#C85C5C', // 1 - medium red
'#B84A4A' // 0 - medium dark red (miss)
];
}
+14
View File
@@ -223,6 +223,9 @@ def analyze_player_performance(player_id, archives_data):
'best_tournament_score': 0,
'worst_tournament_score': float('inf'),
'average_tournament_score': 0,
'best_4_targets_score': 0,
'best_20_targets_score': 0,
'best_40_targets_score': 0,
'total_shots_fired': 0,
'performance_trend': [],
'tournament_history': [],
@@ -264,6 +267,17 @@ def analyze_player_performance(player_id, archives_data):
player_stats['total_shots_fired'] += shots_in_tournament
# Track format-specific best scores
if tournament_type == '4_targets':
if score > player_stats['best_4_targets_score']:
player_stats['best_4_targets_score'] = score
elif tournament_type == '20_targets':
if score > player_stats['best_20_targets_score']:
player_stats['best_20_targets_score'] = score
elif tournament_type == '40_targets':
if score > player_stats['best_40_targets_score']:
player_stats['best_40_targets_score'] = score
# Calculate and aggregate shot accuracy
if tournament_type in player_stats['shot_accuracy']:
shot_accuracy = calculate_shot_accuracy(participant)