From aa01f4136dc6f2fd0508ab8ed8be079f72748422 Mon Sep 17 00:00:00 2001 From: bl3kunja Date: Wed, 12 Nov 2025 19:05:44 +0100 Subject: [PATCH] player stats updated badges --- locales/en.json | 2 + locales/sl.json | 2 + templates/modern_player_stats.html | 520 ++++++++++++++++++++++++++--- tv_app.py | 14 + 4 files changed, 500 insertions(+), 38 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4fad329..8529c70 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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", diff --git a/locales/sl.json b/locales/sl.json index cc40ee2..dd1a415 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -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", diff --git a/templates/modern_player_stats.html b/templates/modern_player_stats.html index f08d95a..7f3f3eb 100644 --- a/templates/modern_player_stats.html +++ b/templates/modern_player_stats.html @@ -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; + } } @@ -986,25 +1152,30 @@
-
- 🏆 -
{{ stats.total_tournaments }}
-
Tournaments
+
+
+
+
🎯
+
4T
+
{{ stats.best_4_targets_score if stats.best_4_targets_score > 0 else 0 }}
+
+
+
+
20T
+
{{ stats.best_20_targets_score if stats.best_20_targets_score > 0 else 0 }}
+
+
+
💪
+
40T
+
{{ stats.best_40_targets_score if stats.best_40_targets_score > 0 else 0 }}
+
+
+
Best Scores
- 🎖️ -
{{ stats.total_leagues }}
-
Leagues
-
-
- -
{{ stats.best_tournament_score }}
-
Best Score
-
-
- 📊 -
{{ stats.average_tournament_score|round|int if stats.average_tournament_score > 0 else 0 }}
-
Average
+ 🎯 +
0
+
Most Common
🔫 @@ -1012,25 +1183,81 @@
Total Shots
- 📉 -
{{ stats.worst_tournament_score if stats.worst_tournament_score > 0 else 0 }}
-
Worst Score
+ 🏆 +
{{ stats.total_tournaments }}
+
Tournaments
+
+
+ 👑 +
{{ stats.total_leagues|default(0) }}
+
Leagues
- +
📊 Overall Shot Accuracy
- +
+
+ +
+ + +
+
+
10
+
0
+
+
+
9
+
0
+
+
+
8
+
0
+
+
+
7
+
0
+
+
+
6
+
0
+
+
+
5
+
0
+
+
+
4
+
0
+
+
+
3
+
0
+
+
+
2
+
0
+
+
+
1
+
0
+
+
+
0
+
0
+
+
@@ -1058,13 +1285,17 @@
0
Games
+
+
0
+
Best
+
0
Average
-
0
-
Best
+
0
+
Most Common
@@ -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) ]; } diff --git a/tv_app.py b/tv_app.py index 4f49e0e..25ee86d 100644 --- a/tv_app.py +++ b/tv_app.py @@ -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)