Add shot accuracy data calculation and visualization to player stats page

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-11-12 13:00:52 +01:00
parent 8b503be144
commit a876c121ef
3 changed files with 322 additions and 185 deletions
+1 -58
View File
@@ -41,64 +41,7 @@
color: #333; color: #333;
} }
/* Standardized Navigation Bar */ /* Standardized Container */
.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 { .container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
+236 -108
View File
@@ -25,63 +25,6 @@
color: #333; 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 */ /* Standardized Container */
.container { .container {
max-width: 1400px; max-width: 1400px;
@@ -321,6 +264,67 @@
font-weight: 500; 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 { .chart-container {
position: relative; position: relative;
flex: 1; flex: 1;
@@ -447,6 +451,44 @@
border-color: rgba(33, 150, 243, 0.2); 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 { .history-info {
flex: 1; flex: 1;
} }
@@ -462,6 +504,9 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: bold; font-weight: bold;
color: #0d47a1; color: #0d47a1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.history-score { .history-score {
@@ -742,6 +787,12 @@
</div> </div>
</div> </div>
<!-- Shot Accuracy Bar Chart -->
<div class="accuracy-chart-container">
<h4 data-i18n="analysis.shot_accuracy">Shot Accuracy Distribution</h4>
<div class="accuracy-bar-chart" id="accuracyBarChart"></div>
</div>
<!-- Shot Accuracy Stats --> <!-- Shot Accuracy Stats -->
<div class="accuracy-stats" id="accuracyStats"> <div class="accuracy-stats" id="accuracyStats">
<div class="accuracy-stat"> <div class="accuracy-stat">
@@ -805,20 +856,17 @@
<div class="panel-title">🎯 <span data-i18n="analysis.tournament_history">Tournament History</span></div> <div class="panel-title">🎯 <span data-i18n="analysis.tournament_history">Tournament History</span></div>
{% if stats.tournament_history %} {% if stats.tournament_history %}
<div class="history-list"> <div class="history-list">
{% for tournament in stats.tournament_history[:10] %} {% for tournament in stats.tournament_history %}
<div class="history-item"> <a href="/archive/tournament/{{ tournament.filename }}" class="history-item-link" style="text-decoration: none;">
<div class="history-info"> <div class="history-item">
<div class="history-date">{{ tournament.date[:10] if tournament.date != 'Unknown' else (translations.analysis.unknown_date if translations else 'Unknown Date') }}</div> <div class="history-info">
<div class="history-type">{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} {{ translations.results.shots if translations else 'shots' }}</div> <div class="history-date">{{ tournament.date[:10] if tournament.date != 'Unknown' else (translations.analysis.unknown_date if translations else 'Unknown Date') }}</div>
<div class="history-type">{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} {{ translations.results.shots if translations else 'shots' }}</div>
</div>
<div class="history-score">{{ tournament.score }}</div>
</div> </div>
<div class="history-score">{{ tournament.score }}</div> </a>
</div>
{% endfor %} {% endfor %}
{% if stats.tournament_history|length > 10 %}
<div style="text-align: center; padding: 8px; color: #666; font-size: 0.8rem;">
... and {{ stats.tournament_history|length - 10 }} more
</div>
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
@@ -834,32 +882,34 @@
{% if stats.league_history %} {% if stats.league_history %}
<div class="history-list"> <div class="history-list">
{% for league in stats.league_history %} {% for league in stats.league_history %}
<div class="league-history-item"> <a href="/archive/league/{{ league.filename }}" class="league-history-item-link" style="text-decoration: none;">
<div class="league-header"> <div class="league-history-item">
<div class="league-info"> <div class="league-header">
<div class="history-date">{{ league.date[:10] if league.date != 'Unknown' else 'Unknown Date' }}</div> <div class="league-info">
<div class="history-type"> <div class="history-date">{{ league.date[:10] if league.date != 'Unknown' else 'Unknown Date' }}</div>
League Championship • {{ league.tournaments_participated }}/6 tournaments <div class="history-type">
{% if league.joker_used %} 🃏 {{ translations.league.joker_used if translations else 'Joker Used' }}{% endif %} League Championship • {{ league.tournaments_participated }}/6 tournaments
{% if league.joker_used %} • 🃏 {{ translations.league.joker_used if translations else 'Joker Used' }}{% endif %}
</div>
</div>
<div class="league-score-display">{{ league.final_score }}</div>
</div>
<div class="league-details">
<div class="league-summary">
<span>Final Score: <span class="league-final-score">{{ league.final_score }}</span></span>
<span class="league-total-score">Total: {{ league.total_score }}</span>
</div>
<div class="tournament-results">
{% for result in league.tournament_results %}
<span class="tournament-result {{ 'participated' if result.participated else 'joker' }}">
T{{ result.tournament }}: {{ result.score if result.participated else (translations.league.joker_used if translations else 'Joker') }}
</span>
{% endfor %}
</div> </div>
</div> </div>
<div class="league-score-display">{{ league.final_score }}</div>
</div> </div>
</a>
<div class="league-details">
<div class="league-summary">
<span>Final Score: <span class="league-final-score">{{ league.final_score }}</span></span>
<span class="league-total-score">Total: {{ league.total_score }}</span>
</div>
<div class="tournament-results">
{% for result in league.tournament_results %}
<span class="tournament-result {{ 'participated' if result.participated else 'joker' }}">
T{{ result.tournament }}: {{ result.score if result.participated else (translations.league.joker_used if translations else 'Joker') }}
</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
@@ -884,6 +934,7 @@
}; };
let currentChart = null; let currentChart = null;
let accuracyChartInstance = null;
let currentTournamentType = '40 Targets'; // Will be updated based on available data let currentTournamentType = '40 Targets'; // Will be updated based on available data
let tournamentsByType = {}; let tournamentsByType = {};
let shotAccuracyData = {}; let shotAccuracyData = {};
@@ -896,23 +947,38 @@
updateChart(); updateChart();
} }
// Load shot accuracy data from archive // Load shot accuracy data from template context
async function loadShotAccuracyData() { function loadShotAccuracyData() {
try { try {
const playerId = '{{ player.id }}'; // Get player ID from template // Get shot accuracy data directly from template context
const response = await fetch(`/api/archive/player/${playerId}/shot-accuracy`); const statsData = {{ stats|tojson }};
const result = await response.json(); console.log('Stats data from template:', statsData);
if (result.status === 'success') { if (statsData && statsData.shot_accuracy) {
shotAccuracyData = result.data; shotAccuracyData = statsData.shot_accuracy;
console.log('Shot accuracy data loaded:', shotAccuracyData); 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 // Auto-select the first tournament type with data
const availableTypes = Object.keys(shotAccuracyData); const availableTypes = Object.keys(shotAccuracyData);
if (availableTypes.length > 0) { if (availableTypes.length > 0) {
// Update current type to first available type // Find the first type with actual data
const firstAvailableType = availableTypes[0]; for (const backendType of availableTypes) {
currentTournamentType = firstAvailableType; 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 // Update button states
updateActiveButton(); updateActiveButton();
@@ -920,13 +986,11 @@
updateChart(); // Refresh chart with accuracy data updateChart(); // Refresh chart with accuracy data
} else { } else {
console.log('No shot accuracy data available:', result.message); console.log('No shot accuracy data available in template');
// Still try to show basic tournament data without shot accuracy
updateChart(); updateChart();
} }
} catch (error) { } catch (error) {
console.error('Error loading shot accuracy data:', 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(); updateChart();
} }
} }
@@ -1068,9 +1132,23 @@
}); });
// Also check if we have aggregated shot accuracy data for this tournament type // 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; hasData = true;
const typeData = shotAccuracyData[currentTournamentType]; const typeData = shotAccuracyData[backendKey];
console.log('Found shot accuracy data for type:', typeData);
tens = typeData.tens || 0; tens = typeData.tens || 0;
nines = typeData.nines || 0; nines = typeData.nines || 0;
eights = typeData.eights || 0; eights = typeData.eights || 0;
@@ -1082,6 +1160,9 @@
twos = typeData.twos || 0; twos = typeData.twos || 0;
ones = typeData.ones || 0; ones = typeData.ones || 0;
zeros = typeData.zeros || 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'); const accuracyStatsDiv = document.getElementById('accuracyStats');
@@ -1108,11 +1189,58 @@
document.getElementById('ones').textContent = ones; document.getElementById('ones').textContent = ones;
document.getElementById('zeros').textContent = zeros; 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}:`, { console.log(`Shot accuracy for ${currentTournamentType}:`, {
tens, nines, eights, sevens, sixes, fives, fours, threes, twos, ones, zeros 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 = `
<div class="accuracy-bar-value" style="height: ${barHeight}%; background-color: ${item.color};" title="${item.label}: ${item.count}"></div>
<div class="accuracy-bar-label">${item.label}</div>
<div class="accuracy-bar-count">${item.count}</div>
`;
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 // Create chart for current tournament type
function createChart(tournaments) { function createChart(tournaments) {
const canvas = document.getElementById('tournamentChart'); const canvas = document.getElementById('tournamentChart');
+70 -4
View File
@@ -163,6 +163,56 @@ def get_archived_leagues():
def load_archive_file(filepath): def load_archive_file(filepath):
return ArchiveStorage.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): def analyze_player_performance(player_id, archives_data):
"""Analyze performance of a specific player across all archives""" """Analyze performance of a specific player across all archives"""
player_stats = { player_stats = {
@@ -176,7 +226,12 @@ def analyze_player_performance(player_id, archives_data):
'total_shots_fired': 0, 'total_shots_fired': 0,
'performance_trend': [], 'performance_trend': [],
'tournament_history': [], '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 # Process tournament archives
@@ -209,13 +264,20 @@ def analyze_player_performance(player_id, archives_data):
player_stats['total_shots_fired'] += shots_in_tournament 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 # Add to history
player_stats['tournament_history'].append({ player_stats['tournament_history'].append({
'date': archive['archived_at'], 'date': archive['archived_at'],
'score': score, 'score': score,
'tournament_type': archive['tournament_type'], 'tournament_type': archive['tournament_type'],
'completed': completed, '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: except Exception as e:
print(f"Error analyzing tournament archive: {e}") print(f"Error analyzing tournament archive: {e}")
@@ -235,7 +297,10 @@ def analyze_player_performance(player_id, archives_data):
participant = participants[str(player_id)] participant = participants[str(player_id)]
final_score = participant.get('final_score', 0) final_score = participant.get('final_score', 0)
total_score = participant.get('total_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['total_leagues'] += 1
player_stats['league_scores'].append(final_score) player_stats['league_scores'].append(final_score)
@@ -246,7 +311,8 @@ def analyze_player_performance(player_id, archives_data):
'total_score': total_score, 'total_score': total_score,
'tournaments_participated': tournaments_participated, 'tournaments_participated': tournaments_participated,
'joker_used': participant.get('joker_used', False), '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: except Exception as e:
print(f"Error analyzing league archive: {e}") print(f"Error analyzing league archive: {e}")