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:
@@ -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;
|
||||||
|
|||||||
@@ -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,7 +856,8 @@
|
|||||||
<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 %}
|
||||||
|
<a href="/archive/tournament/{{ tournament.filename }}" class="history-item-link" style="text-decoration: none;">
|
||||||
<div class="history-item">
|
<div class="history-item">
|
||||||
<div class="history-info">
|
<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-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>
|
||||||
<div class="history-score">{{ tournament.score }}</div>
|
<div class="history-score">{{ tournament.score }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
{% 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,6 +882,7 @@
|
|||||||
{% 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 %}
|
||||||
|
<a href="/archive/league/{{ league.filename }}" class="league-history-item-link" style="text-decoration: none;">
|
||||||
<div class="league-history-item">
|
<div class="league-history-item">
|
||||||
<div class="league-header">
|
<div class="league-header">
|
||||||
<div class="league-info">
|
<div class="league-info">
|
||||||
@@ -860,6 +909,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
{% 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');
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user