From a876c121ef35519f7730b2ef368baf4048b3a783 Mon Sep 17 00:00:00 2001 From: bl3kunja Date: Wed, 12 Nov 2025 13:00:52 +0100 Subject: [PATCH] Add shot accuracy data calculation and visualization to player stats page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement calculate_shot_accuracy() function in tv_app.py to extract individual shot values from tournament participant data - Aggregate shot accuracy data (0-10) by tournament type (4_targets, 20_targets, 40_targets) in analyze_player_performance() - Update modern_player_stats.html to load shot accuracy data directly from template context instead of API - Add tournament type mapping between display names (40 Targets) and backend keys (40_targets) - Implement CSS-based bar chart visualization that displays shot distribution with proper color gradients - Remove unused async loadShotAccuracyData() API fetch and replace with direct template data access - Data is now properly aggregated across all tournaments for each player and format type 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- templates/modern_player_analysis.html | 59 +---- templates/modern_player_stats.html | 354 ++++++++++++++++++-------- tv_app.py | 94 ++++++- 3 files changed, 322 insertions(+), 185 deletions(-) diff --git a/templates/modern_player_analysis.html b/templates/modern_player_analysis.html index acbe1d4..b6aeeb3 100644 --- a/templates/modern_player_analysis.html +++ b/templates/modern_player_analysis.html @@ -41,64 +41,7 @@ color: #333; } - /* Standardized Navigation Bar */ - .navbar { - background: white; - color: black; - padding: 15px 25px; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 2px solid #ccc; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - } - - .navbar-title { - font-size: 1.8rem; - font-weight: bold; - color: #333; - } - - .navbar-controls { - display: flex; - gap: 12px; - align-items: center; - } - - .nav-btn { - background: #f8f9fa; - border: 2px solid #e9ecef; - cursor: pointer; - padding: 12px 20px; - border-radius: 8px; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - color: #333; - text-decoration: none; - font-weight: bold; - font-size: 0.9rem; - } - - .nav-btn:hover { - background: #e9ecef; - border-color: #007bff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - transform: translateY(-1px); - color: #007bff; - } - - .nav-btn.active { - background: #007bff; - border-color: #0056b3; - color: white; - } - - .nav-btn.active:hover { - background: #0056b3; - color: white; - } - - /* Standardized Container */ +/* Standardized Container */ .container { max-width: 1400px; margin: 0 auto; diff --git a/templates/modern_player_stats.html b/templates/modern_player_stats.html index 5d5b563..52ae65e 100644 --- a/templates/modern_player_stats.html +++ b/templates/modern_player_stats.html @@ -25,63 +25,6 @@ color: #333; } - /* Enhanced Navigation Bar */ - .navbar { - background: white; - color: black; - padding: 15px 25px; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 2px solid #ccc; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - } - - .navbar-title { - font-size: 1.8rem; - font-weight: bold; - color: #333; - } - - .navbar-controls { - display: flex; - gap: 12px; - align-items: center; - } - - .nav-btn { - background: #f8f9fa; - border: 2px solid #e9ecef; - cursor: pointer; - padding: 12px 20px; - border-radius: 8px; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - color: #333; - text-decoration: none; - font-weight: bold; - font-size: 0.9rem; - } - - .nav-btn:hover { - background: #e9ecef; - border-color: #007bff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - transform: translateY(-1px); - color: #007bff; - } - - .nav-btn.active { - background: #007bff; - border-color: #0056b3; - color: white; - } - - .nav-btn.active:hover { - background: #0056b3; - color: white; - } - /* Standardized Container */ .container { max-width: 1400px; @@ -321,6 +264,67 @@ font-weight: 500; } + .accuracy-chart-container { + background: white; + border-radius: 12px; + border: 1px solid #e9ecef; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .accuracy-chart-container h4 { + margin: 0 0 15px 0; + color: #333; + font-size: 0.95rem; + font-weight: 600; + } + + .accuracy-bar-chart { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 200px; + gap: 6px; + padding: 10px 0; + border-bottom: 2px solid #e9ecef; + } + + .accuracy-bar { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; + max-width: 35px; + } + + .accuracy-bar-value { + width: 100%; + border-radius: 4px 4px 0 0; + transition: all 0.3s ease; + cursor: pointer; + min-height: 5px; + } + + .accuracy-bar-value:hover { + opacity: 0.8; + box-shadow: 0 -2px 8px rgba(0,0,0,0.15); + } + + .accuracy-bar-label { + font-size: 0.8rem; + font-weight: 600; + color: #333; + text-align: center; + } + + .accuracy-bar-count { + font-size: 0.7rem; + color: #666; + font-weight: 500; + } + .chart-container { position: relative; flex: 1; @@ -447,6 +451,44 @@ border-color: rgba(33, 150, 243, 0.2); } + /* Clickable History Item Links */ + .history-item-link { + display: block; + cursor: pointer; + transition: all 0.3s ease; + color: inherit; + } + + .history-item-link .history-item { + cursor: pointer; + } + + .history-item-link:hover .history-item { + transform: translateY(-4px); + box-shadow: 0 12px 35px rgba(33, 150, 243, 0.35); + background: linear-gradient(135deg, #c5dff8 0%, #a5d6fd 100%); + border-color: rgba(33, 150, 243, 0.4); + } + + /* Clickable League History Item Links */ + .league-history-item-link { + display: block; + cursor: pointer; + transition: all 0.3s ease; + color: inherit; + } + + .league-history-item-link .league-history-item { + cursor: pointer; + } + + .league-history-item-link:hover .league-history-item { + transform: translateY(-4px); + box-shadow: 0 12px 35px rgba(156, 39, 176, 0.35); + background: linear-gradient(135deg, #d1a4e0 0%, #c878d8 100%); + border-color: rgba(156, 39, 176, 0.3); + } + .history-info { flex: 1; } @@ -462,6 +504,9 @@ font-size: 0.75rem; font-weight: bold; color: #0d47a1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .history-score { @@ -742,6 +787,12 @@ + +
+

Shot Accuracy Distribution

+
+
+
@@ -805,20 +856,17 @@
🎯 Tournament History
{% if stats.tournament_history %}
- {% for tournament in stats.tournament_history[:10] %} -
-
-
{{ tournament.date[:10] if tournament.date != 'Unknown' else (translations.analysis.unknown_date if translations else 'Unknown Date') }}
-
{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} {{ translations.results.shots if translations else 'shots' }}
+ {% for tournament in stats.tournament_history %} + +
+
+
{{ tournament.date[:10] if tournament.date != 'Unknown' else (translations.analysis.unknown_date if translations else 'Unknown Date') }}
+
{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} {{ translations.results.shots if translations else 'shots' }}
+
+
{{ tournament.score }}
-
{{ tournament.score }}
-
+ {% endfor %} - {% if stats.tournament_history|length > 10 %} -
- ... and {{ stats.tournament_history|length - 10 }} more -
- {% endif %}
{% else %}
@@ -834,32 +882,34 @@ {% if stats.league_history %}
{% for league in stats.league_history %} -
-
- {% else %} @@ -884,6 +934,7 @@ }; let currentChart = null; + let accuracyChartInstance = null; let currentTournamentType = '40 Targets'; // Will be updated based on available data let tournamentsByType = {}; let shotAccuracyData = {}; @@ -896,37 +947,50 @@ updateChart(); } - // Load shot accuracy data from archive - async function loadShotAccuracyData() { + // Load shot accuracy data from template context + function loadShotAccuracyData() { try { - const playerId = '{{ player.id }}'; // Get player ID from template - const response = await fetch(`/api/archive/player/${playerId}/shot-accuracy`); - const result = await response.json(); - - if (result.status === 'success') { - shotAccuracyData = result.data; - console.log('Shot accuracy data loaded:', shotAccuracyData); - + // Get shot accuracy data directly from template context + const statsData = {{ stats|tojson }}; + console.log('Stats data from template:', statsData); + + if (statsData && statsData.shot_accuracy) { + shotAccuracyData = statsData.shot_accuracy; + console.log('Shot accuracy data loaded from template:', shotAccuracyData); + + // Map backend keys to display names + const typeMapping = { + '40_targets': '40 Targets', + '20_targets': '20 Targets', + '4_targets': '4 Targets' + }; + // Auto-select the first tournament type with data const availableTypes = Object.keys(shotAccuracyData); if (availableTypes.length > 0) { - // Update current type to first available type - const firstAvailableType = availableTypes[0]; - currentTournamentType = firstAvailableType; - + // Find the first type with actual data + for (const backendType of availableTypes) { + const typeData = shotAccuracyData[backendType]; + const hasData = Object.values(typeData).some(v => v > 0); + if (hasData) { + // Convert backend key to display name + currentTournamentType = typeMapping[backendType] || backendType; + console.log('Selected tournament type:', currentTournamentType, 'from backend key:', backendType); + break; + } + } + // Update button states updateActiveButton(); } - + updateChart(); // Refresh chart with accuracy data } else { - console.log('No shot accuracy data available:', result.message); - // Still try to show basic tournament data without shot accuracy + console.log('No shot accuracy data available in template'); updateChart(); } } catch (error) { console.error('Error loading shot accuracy data:', error); - console.log('API endpoints may not be set up yet. Showing basic tournament data only.'); updateChart(); } } @@ -1068,9 +1132,23 @@ }); // Also check if we have aggregated shot accuracy data for this tournament type - if (shotAccuracyData && shotAccuracyData[currentTournamentType]) { + console.log('Current tournament type:', currentTournamentType); + console.log('Available keys in shotAccuracyData:', Object.keys(shotAccuracyData)); + + // Map display names to backend keys + const typeMapping = { + '40 Targets': '40_targets', + '20 Targets': '20_targets', + '4 Targets': '4_targets' + }; + + const backendKey = typeMapping[currentTournamentType] || currentTournamentType; + console.log('Looking for data with key:', backendKey); + + if (shotAccuracyData && shotAccuracyData[backendKey]) { hasData = true; - const typeData = shotAccuracyData[currentTournamentType]; + const typeData = shotAccuracyData[backendKey]; + console.log('Found shot accuracy data for type:', typeData); tens = typeData.tens || 0; nines = typeData.nines || 0; eights = typeData.eights || 0; @@ -1082,10 +1160,13 @@ twos = typeData.twos || 0; ones = typeData.ones || 0; zeros = typeData.zeros || 0; + } else { + console.log('No shot accuracy data found for type:', currentTournamentType, 'mapped to:', backendKey); + console.log('Full shotAccuracyData object:', shotAccuracyData); } const accuracyStatsDiv = document.getElementById('accuracyStats'); - + if (!hasData) { // Hide the accuracy stats section if no data is available accuracyStatsDiv.style.display = 'none'; @@ -1108,11 +1189,58 @@ document.getElementById('ones').textContent = ones; document.getElementById('zeros').textContent = zeros; + // Render accuracy bar chart + createAccuracyChart(tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros); + console.log(`Shot accuracy for ${currentTournamentType}:`, { tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros }); } + // Create bar chart for shot accuracy distribution using CSS + function createAccuracyChart(tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros) { + const chartContainer = document.getElementById('accuracyBarChart'); + if (!chartContainer) { + console.warn('Accuracy bar chart container not found'); + return; + } + + const data = [ + { label: '10', count: tens, color: '#4CAF50' }, + { label: '9', count: nines, color: '#8BC34A' }, + { label: '8', count: eights, color: '#CDDC39' }, + { label: '7', count: sevens, color: '#FFC107' }, + { label: '6', count: sixes, color: '#FF9800' }, + { label: '5', count: fives, color: '#FF7043' }, + { label: '4', count: fours, color: '#F44336' }, + { label: '3', count: threes, color: '#E91E63' }, + { label: '2', count: twos, color: '#9C27B0' }, + { label: '1', count: ones, color: '#673AB7' }, + { label: '0', count: zeros, color: '#9E9E9E' } + ]; + + // Find max count for scaling + const maxCount = Math.max(...data.map(d => d.count), 1); + + // Clear previous content + chartContainer.innerHTML = ''; + + // Create bars + data.forEach(item => { + const barHeight = (item.count / maxCount) * 100; + const bar = document.createElement('div'); + bar.className = 'accuracy-bar'; + bar.innerHTML = ` +
+
${item.label}
+
${item.count}
+ `; + chartContainer.appendChild(bar); + }); + + console.log('CSS Bar chart created successfully with data:', {tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros}); + } + // Create chart for current tournament type function createChart(tournaments) { const canvas = document.getElementById('tournamentChart'); diff --git a/tv_app.py b/tv_app.py index 549a56e..3067349 100644 --- a/tv_app.py +++ b/tv_app.py @@ -163,6 +163,56 @@ def get_archived_leagues(): def load_archive_file(filepath): return ArchiveStorage.load_archive_file(filepath) +def calculate_shot_accuracy(participant): + """ + Extract shot accuracy breakdown from participant targets. + Returns dict with counts for each shot value (0-10). + """ + accuracy_data = { + 'tens': 0, + 'nines': 0, + 'eights': 0, + 'sevens': 0, + 'sixes': 0, + 'fives': 0, + 'fours': 0, + 'threes': 0, + 'twos': 0, + 'ones': 0, + 'zeros': 0 + } + + targets = participant.get('targets', {}) + for target_num, shots in targets.items(): + # Extract all shots from this target (shot1, shot2, etc.) + for shot_key, shot_value in shots.items(): + shot_val = int(shot_value) + if shot_val == 10: + accuracy_data['tens'] += 1 + elif shot_val == 9: + accuracy_data['nines'] += 1 + elif shot_val == 8: + accuracy_data['eights'] += 1 + elif shot_val == 7: + accuracy_data['sevens'] += 1 + elif shot_val == 6: + accuracy_data['sixes'] += 1 + elif shot_val == 5: + accuracy_data['fives'] += 1 + elif shot_val == 4: + accuracy_data['fours'] += 1 + elif shot_val == 3: + accuracy_data['threes'] += 1 + elif shot_val == 2: + accuracy_data['twos'] += 1 + elif shot_val == 1: + accuracy_data['ones'] += 1 + elif shot_val == 0: + accuracy_data['zeros'] += 1 + + return accuracy_data + + def analyze_player_performance(player_id, archives_data): """Analyze performance of a specific player across all archives""" player_stats = { @@ -176,46 +226,58 @@ def analyze_player_performance(player_id, archives_data): 'total_shots_fired': 0, 'performance_trend': [], 'tournament_history': [], - 'league_history': [] + 'league_history': [], + 'shot_accuracy': { + '4_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0}, + '20_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0}, + '40_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0} + } } - + # Process tournament archives for archive in archives_data.get('tournaments', []): try: data = load_archive_file(archive['filepath']) if not data: continue - + results = data.get('results', {}) participants = results.get('participants', {}) - + if str(player_id) in participants: participant = participants[str(player_id)] score = participant.get('total_score', 0) completed = participant.get('completed', False) - + if completed: player_stats['total_tournaments'] += 1 player_stats['tournament_scores'].append(score) - + if score > player_stats['best_tournament_score']: player_stats['best_tournament_score'] = score if score < player_stats['worst_tournament_score']: player_stats['worst_tournament_score'] = score - + # Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS tournament_type = archive.get('tournament_type', '20_targets') shots_in_tournament = count_shots_in_tournament(participant, tournament_type) - + player_stats['total_shots_fired'] += shots_in_tournament - + + # Calculate and aggregate shot accuracy + if tournament_type in player_stats['shot_accuracy']: + shot_accuracy = calculate_shot_accuracy(participant) + for key in shot_accuracy: + player_stats['shot_accuracy'][tournament_type][key] += shot_accuracy[key] + # Add to history player_stats['tournament_history'].append({ 'date': archive['archived_at'], 'score': score, 'tournament_type': archive['tournament_type'], 'completed': completed, - 'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED + 'shots_fired': shots_in_tournament, # NOW CORRECTLY CALCULATED + 'filename': archive.get('filename', '') }) except Exception as e: print(f"Error analyzing tournament archive: {e}") @@ -235,18 +297,22 @@ def analyze_player_performance(player_id, archives_data): participant = participants[str(player_id)] final_score = participant.get('final_score', 0) total_score = participant.get('total_score', 0) - tournaments_participated = participant.get('tournaments_participated', 0) - + tournament_results = participant.get('tournament_results', []) + + # Count tournaments participated based on tournament_results array + tournaments_participated = len([t for t in tournament_results if t.get('participated', False)]) + player_stats['total_leagues'] += 1 player_stats['league_scores'].append(final_score) - + player_stats['league_history'].append({ 'date': archive['archived_at'], 'final_score': final_score, 'total_score': total_score, 'tournaments_participated': tournaments_participated, 'joker_used': participant.get('joker_used', False), - 'tournament_results': participant.get('tournament_results', []) + 'tournament_results': tournament_results, + 'filename': archive.get('filename', '') }) except Exception as e: print(f"Error analyzing league archive: {e}")