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:
+241
-113
@@ -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,20 +856,17 @@
|
||||
<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] %}
|
||||
<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>
|
||||
<div class="history-type">{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} {{ translations.results.shots if translations else 'shots' }}</div>
|
||||
{% 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>
|
||||
<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 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,32 +882,34 @@
|
||||
{% if stats.league_history %}
|
||||
<div class="history-list">
|
||||
{% for league in stats.league_history %}
|
||||
<div class="league-history-item">
|
||||
<div class="league-header">
|
||||
<div class="league-info">
|
||||
<div class="history-date">{{ league.date[:10] if league.date != 'Unknown' else 'Unknown Date' }}</div>
|
||||
<div class="history-type">
|
||||
League Championship • {{ league.tournaments_participated }}/6 tournaments
|
||||
{% if league.joker_used %} • 🃏 {{ translations.league.joker_used if translations else 'Joker Used' }}{% endif %}
|
||||
<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">
|
||||
<div class="history-date">{{ league.date[:10] if league.date != 'Unknown' else 'Unknown Date' }}</div>
|
||||
<div class="history-type">
|
||||
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 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>
|
||||
</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,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 = `
|
||||
<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');
|
||||
|
||||
Reference in New Issue
Block a user