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
-
+
+
+
+
@@ -1058,13 +1285,17 @@
0
Games
+
-
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)