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;
}
/* 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;
+207 -79
View File
@@ -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 @@
</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 -->
<div class="accuracy-stats" id="accuracyStats">
<div class="accuracy-stat">
@@ -805,7 +856,8 @@
<div class="panel-title">🎯 <span data-i18n="analysis.tournament_history">Tournament History</span></div>
{% if stats.tournament_history %}
<div class="history-list">
{% for tournament in stats.tournament_history[:10] %}
{% for tournament in stats.tournament_history %}
<a href="/archive/tournament/{{ tournament.filename }}" class="history-item-link" style="text-decoration: none;">
<div class="history-item">
<div class="history-info">
<div class="history-date">{{ tournament.date[:10] if tournament.date != 'Unknown' else (translations.analysis.unknown_date if translations else 'Unknown Date') }}</div>
@@ -813,12 +865,8 @@
</div>
<div class="history-score">{{ tournament.score }}</div>
</div>
</a>
{% 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>
{% else %}
<div class="empty-state">
@@ -834,6 +882,7 @@
{% if stats.league_history %}
<div class="history-list">
{% for league in stats.league_history %}
<a href="/archive/league/{{ league.filename }}" class="league-history-item-link" style="text-decoration: none;">
<div class="league-history-item">
<div class="league-header">
<div class="league-info">
@@ -860,6 +909,7 @@
</div>
</div>
</div>
</a>
{% endfor %}
</div>
{% 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,23 +947,38 @@
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();
// Get shot accuracy data directly from template context
const statsData = {{ stats|tojson }};
console.log('Stats data from template:', statsData);
if (result.status === 'success') {
shotAccuracyData = result.data;
console.log('Shot accuracy data loaded:', shotAccuracyData);
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();
@@ -920,13 +986,11 @@
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,6 +1160,9 @@
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');
@@ -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 = `
<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
function createChart(tournaments) {
const canvas = document.getElementById('tournamentChart');
+70 -4
View File
@@ -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,7 +226,12 @@ 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
@@ -209,13 +264,20 @@ def analyze_player_performance(player_id, archives_data):
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,7 +297,10 @@ 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)
@@ -246,7 +311,8 @@ def analyze_player_performance(player_id, archives_data):
'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}")