### Get All Players (ATP and WTA)
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Retrieves a list of all ATP and WTA players with their rankings and biographical data. Supports filtering by tour using the 'tour' query parameter.
```bash
# Get all players (both tours)
curl -X GET "http://localhost:5002/api/players"
```
```json
{
"ATP": [
{"rank": 1, "name": "Jannik Sinner", "tour": "ATP", "country": "ITA", "birthdate": "2001-08-16", "age": 23.5},
{"rank": 2, "name": "Carlos Alcaraz", "tour": "ATP", "country": "ESP", "birthdate": "2003-05-05", "age": 21.8}
],
"WTA": [
{"rank": 1, "name": "Iga Swiatek", "tour": "WTA", "country": "POL", "birthdate": "2001-05-31", "age": 23.7},
{"rank": 2, "name": "Aryna Sabalenka", "tour": "WTA", "country": "BLR", "birthdate": "1998-05-05", "age": 26.8}
]
}
```
```bash
# Filter by tour
curl -X GET "http://localhost:5002/api/players?tour=ATP"
```
```json
{
"players": [
{"rank": 1, "name": "Jannik Sinner", "tour": "ATP", "country": "ITA", "birthdate": "2001-08-16", "age": 23.5}
]
}
```
--------------------------------
### Get Surface-Specific Career Stats
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Filters match data by surface type before calculating career statistics.
```python
hard_matches = matches[matches['surf'] == 'Hard']
hard_career = calculate_career_stats(hard_matches)
print(hard_career.loc['career', ['W-L', 'win%']])
```
--------------------------------
### Get All Players
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Retrieves ATP and WTA player rankings with biographical data. Supports filtering by tour.
```APIDOC
## GET /api/players
### Description
Retrieves ATP and WTA player rankings with biographical data including rank, country, birthdate, and age.
### Method
GET
### Endpoint
/api/players
#### Query Parameters
- **tour** (string) - Optional - Filters players by tour (e.g., 'ATP', 'WTA').
### Response
#### Success Response (200)
- **ATP** (array) - List of ATP players with their stats.
- **WTA** (array) - List of WTA players with their stats.
- **players** (array) - List of players when filtered by tour.
### Request Example
```bash
# Get all players (both tours)
curl -X GET "http://localhost:5002/api/players"
# Filter by tour
curl -X GET "http://localhost:5002/api/players?tour=ATP"
```
### Response Example
```json
{
"ATP": [
{"rank": 1, "name": "Jannik Sinner", "tour": "ATP", "country": "ITA", "birthdate": "2001-08-16", "age": 23.5},
{"rank": 2, "name": "Carlos Alcaraz", "tour": "ATP", "country": "ESP", "birthdate": "2003-05-05", "age": 21.8}
],
"WTA": [
{"rank": 1, "name": "Iga Swiatek", "tour": "WTA", "country": "POL", "birthdate": "2001-05-31", "age": 23.7},
{"rank": 2, "name": "Aryna Sabalenka", "tour": "WTA", "country": "BLR", "birthdate": "1998-05-05", "age": 26.8}
]
}
```
```
--------------------------------
### Initialize Head-to-Head Charts
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/h2h.html
Creates line and bar charts using Chart.js to display player performance metrics. Requires a valid DOM element context and pre-processed data objects.
```javascript
y(); }); // Cumulative wins chart h2hProgressChart = new Chart(document.getElementById('h2hProgress').getContext('2d'), { type: 'line', data: { labels, datasets: [{ label: p1, data: s1, borderColor: 'rgb(30,58,138)', backgroundColor: 'rgba(30,58,138,.1)', tension: .25 }, { label: p2, data: s2, borderColor: 'rgb(239,68,68)', backgroundColor: 'rgba(239,68,68,.1)', tension: .25 }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 10 } } }, scales: { x: { display: false }, y: { beginAtZero: true, ticks: { precision: 0 } } } } }); // Wins by surface chart const surfaces = ['Hard','Clay','Grass','Carpet']; const b = response.summary.surface_breakdown || {}; h2hSurfacesChart = new Chart(document.getElementById('h2hSurfaces').getContext('2d'), { type: 'bar', data: { labels: surfaces, datasets: [{ label: p1, data: surfaces.map(s => (b[s] && b[s][p1]) ? b[s][p1] : 0), backgroundColor: 'rgba(30,58,138,.8)' }, { label: p2, data: surfaces.map(s => (b[s] && b[s][p2]) ? b[s][p2] : 0), backgroundColor: 'rgba(16,185,129,.8)' }]}, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { stepSize: 1, precision: 0 } } }, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 10 } } } } }); // Wins by round chart h2hRoundsChart = new Chart(document.getElementById('h2hRounds').getContext('2d'), { type: 'bar', data: { labels: roundLabelsArray, datasets: [{ label: p1, data: p1RoundWins, backgroundColor: 'rgba(30,58,138,.8)' }, { label: p2, data: p2RoundWins, backgroundColor: 'rgba(245,158,11,.8)' }]}, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', scales: { x: { beginAtZero: true, ticks: { stepSize: 1, precision: 0 } } }, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 10 } } } } }); }, error: function(xhr) { $('.loading').hide(); let errorMsg = xhr.responseJSON ? xhr.responseJSON.error : 'An error occurred'; $('.error-message').text(errorMsg).show(); } }); }); {% endblock %}
```
--------------------------------
### Render Performance Chart
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/compare.html
Initializes a Chart.js bar chart to visualize win, hold, and break percentages for two players.
```javascript
// --- Performance Chart (MODIFIED) --- const winPct = getStat(statsData, 'win%'); const hold = getStat(statsData, 'hold%'); const brk = getStat(statsData, 'break%'); let chartHtml = `
`; $('#results').html(surfaceIndicator + overviewHtml + detailsHtml + chartHtml); if (performanceChart) performanceChart.destroy(); performanceChart = new Chart(document.getElementById('performanceChart').getContext('2d'), { type: 'bar', data: { labels: ['Win %', 'Hold %', 'Break %'], datasets: [{ label: player1, data: [winPct.p1, hold.p1, brk.p1], backgroundColor: 'rgba(30,58,138,.7)', borderColor: 'rgb(30,58,138)', borderWidth: 1 }, { label: player2, data: [winPct.p2, hold.p2, brk.p2], backgroundColor: 'rgba(16,185,129,.7)', borderColor: 'rgb(16,185,129)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: 'Percentage (%)' } } }, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)}%` } } } } }); }
```
--------------------------------
### Initialize DataTables for Player Tables
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/players.html
Initializes DataTables for both ATP and WTA player tables. Configures pagination, sorting, column definitions, and language settings for an interactive experience.
```javascript
function initPlayersTable(sel){
$(sel).DataTable({
pageLength: 18,
order: [[0, 'asc']], // Sort by rank ascending
lengthChange: false,
autoWidth: false,
columnDefs: [
{ targets: [0, 3], type: 'num', className: 'dt-body-right' }, // Rank & Age numeric/right-aligned
{ targets: [2], className: 'dt-body-center' } // Nat center
],
language: {
search: 'Filter:'
}, // Each table maintains its own state independently
stateSave: false, // Ensure tables don't interfere with each other
drawCallback: function() {
// No cross-table synchronization
}
});
}
// Initialize tables independently
initPlayersTable('#atpTable');
initPlayersTable('#wtaTable');
```
--------------------------------
### Render WTA Player List in Jinja2
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/players.html
Iterates through the WTA player list to display rank, name, country, and age. Uses Jinja2 templating for dynamic content generation.
```html
{% for p in wta_players %} {% endfor %}
Rank
Player
Nat
Age
{{ p.rank }}
[{{ p.name }}]({{ url_for('career_stats', player=p.name) }})
{{ p.country or '-' }}
{% if p.age is not none %} {{ "%.1f"|format(p.age) }} {% else %} - {% endif %}
```
--------------------------------
### Player Autocomplete Initialization
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/base.html
Initializes player name autocomplete for input fields with IDs 'player', 'player1', and 'player2'. Requires jQuery and a global PLAYER_SUGGEST_URL.
```javascript
window.PLAYER_SUGGEST_URL = "{{ url_for('player_suggest') }}";
function attachAutocomplete(selector, options) {
var $el = $(selector);
if (!$el.length || typeof $el.autocomplete !== 'function') return;
var minLen = (options && options.minLength) || 2;
var limit = (options && options.limit) || 10;
var tour = (options && options.tour) || null;
$el.autocomplete({
minLength: minLen,
delay: 150,
source: function(request, response) {
var query = { q: request.term, limit: limit };
if (tour) query.tour = tour;
$.getJSON(window.PLAYER_SUGGEST_URL, query, function(data) {
var items = (data && data.suggestions) ? data.suggestions : [];
response(items.map(function(it) {
if (typeof it === 'string') return { label: it, value: it };
return { label: it.label || it.name, value: it.value || it.name };
}));
});
}
});
}
$(function(){
if ($('#player').length) attachAutocomplete('#player');
if ($('#player1').length) attachAutocomplete('#player1');
if ($('#player2').length) attachAutocomplete('#player2');
});
```
--------------------------------
### Fetch Player Matches with Python
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Retrieves match data for a player, utilizing local SQLite caching. The force_refresh parameter can be used to bypass the cache.
```python
from mod import get_player_matches
# Fetch player matches (uses cache if available)
matches_df = get_player_matches("Roger Federer")
# Force refresh from web source
matches_df = get_player_matches("Roger Federer", force_refresh=True)
# DataFrame columns: date, tourn, surf, level, wl, round, score, opp, orank,
# aces, dfs, pts, firsts, fwon, swon, games, saved, chances,
# oaces, odfs, opts, ofirsts, ofwon, oswon, ogames, osaved, ochances,
# tb_won, tb_lost
print(matches_df[['date', 'tourn', 'opp', 'wl', 'score']].head())
# date tourn opp wl score
# 0 2024-06-24 Wimbledon Matteo Berrettini W 6-3 6-4 6-2
# 1 2024-06-22 Wimbledon Denis Shapovalov W 7-5 7-5 6-4
```
--------------------------------
### Cache and Database Configuration
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Sets expiration times for cache data and defines the database file path. These parameters control data freshness and storage location.
```python
# Cache Configuration
CACHE_EXPIRE_HOURS = 6
PLAYER_LIST_CACHE_DAYS = 7
# Database Configuration
DATABASE_PATH = 'tennis_stats.db'
```
--------------------------------
### Generate Match Statistics HTML
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/compare.html
Constructs the HTML structure for match statistics, including win percentages and average opponent rank, while applying conditional styling based on performance.
```javascript
1_value ? `${tbWL.player1_value} (${row.player1_value || 'N/A'}%)` : (row.player1_value ? `${row.player1_value}%` : 'N/A'); p2Display = tbWL && tbWL.player2_value ? `${tbWL.player2_value} (${row.player2_value || 'N/A'}%)` : (row.player2_value ? `${row.player2_value}%` : 'N/A'); detailsHtml += `Tiebreak
${p1Display} vs ${p2Display}
`; } else { detailsHtml += `${formatStatName(stat)}
${row.player1_value || 'N/A'}% vs ${row.player2_value || 'N/A'}%
`; } } }); detailsHtml += ``; // --- Match Stats --- detailsHtml += ``; const matchStats = ['top5_win%', 'top10_win%', 'top20_win%', 'top50_win%', 'top100_win%', 'avg_opp_rank']; matchStats.forEach(stat => { const row = statsData.find(s => s.stat === stat); if (row) { const p1Better = isBetterValue(stat, row.player1_value, row.player2_value); let p1Display, p2Display; if (stat.startsWith('top') && stat.endsWith('_win%')) { const wlRow = statsData.find(s => s.stat === stat.replace('_win%', '_W-L')); p1Display = wlRow && wlRow.player1_value ? `${wlRow.player1_value} (${row.player1_value || 'N/A'}%)` : (row.player1_value ? `${row.player1_value}%` : 'N/A'); p2Display = wlRow && wlRow.player2_value ? `${wlRow.player2_value} (${row.player2_value || 'N/A'}%)` : (row.player2_value ? `${row.player2_value}%` : 'N/A'); } else { // For avg_opp_rank p1Display = row.player1_value || 'N/A'; p2Display = row.player2_value || 'N/A'; } detailsHtml += `
${formatStatName(stat)}
${p1Display} vs ${p2Display}
`; } }); detailsHtml += `
`;
```
--------------------------------
### Calculate Career Statistics with Python
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Computes lifetime aggregated statistics for a player based on their match history.
```python
from mod import get_player_matches, calculate_career_stats
matches = get_player_matches("Serena Williams")
career = calculate_career_stats(matches)
print(career.loc['career', ['W-L', 'win%', 'tb_W-L', 'tb_win%', 'top5_W-L', 'top5_win%']])
# W-L 857-153
# win% 84.8
# tb_W-L 142-98
# tb_win% 59.2
# top5_W-L 98-45
# top5_win% 68.5
```
--------------------------------
### Access Player Directory
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Retrieves ranked player lists and provides autocomplete suggestions for player names.
```python
from mod import get_players, suggest_players
# Get all ranked players
all_players = get_players()
print(f"ATP players: {len(all_players['ATP'])}, WTA players: {len(all_players['WTA'])}")
# Get specific tour
atp_players = get_players(tour='ATP')
print(f"Top 5 ATP: {[p['name'] for p in atp_players[:5]]}")
# Force refresh from web
fresh_players = get_players(tour='WTA', force_refresh=True)
# Autocomplete suggestions
suggestions = suggest_players("nad", limit=5)
print(suggestions)
# [{'label': 'Rafael Nadal', 'value': 'Rafael Nadal', 'tour': 'ATP'}]
# Filter by tour
wta_suggestions = suggest_players("sab", limit=3, tour='WTA')
print(wta_suggestions)
# [{'label': 'Aryna Sabalenka', 'value': 'Aryna Sabalenka', 'tour': 'WTA'}]
```
--------------------------------
### Render Match Statistics HTML
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/career.html
Generates the HTML structure for displaying match statistics like hold percentage and opponent rank comparisons.
```javascript
class="stat-name">Hold %${displayData['hold%'] || 'N/A'}%
Break Points Saved %
${displayData['bp_saved%'] || 'N/A'}%
Break %
${displayData['break%'] || 'N/A'}%
Tiebreak
${displayData['tb_W-L'] ? `${displayData['tb_W-L']} (${displayData['tb_win%'] || 'N/A'}%)` : displayData['tb_win%'] ? `${displayData['tb_win%']}%` : 'N/A'}
vs Top 5
${displayData['top5_W-L'] ? `${displayData['top5_W-L']} (${displayData['top5_win%'] || 'N/A'}%)` : (displayData['top5_win%'] ? `${displayData['top5_win%']}%` : 'N/A')}
vs Top 10
${displayData['top10_W-L'] ? `${displayData['top10_W-L']} (${displayData['top10_win%'] || 'N/A'}%)` : (displayData['top10_win%'] ? `${displayData['top10_win%']}%` : 'N/A')}
vs Top 20
${displayData['top20_W-L'] ? `${displayData['top20_W-L']} (${displayData['top20_win%'] || 'N/A'}%)` : (displayData['top20_win%'] ? `${displayData['top20_win%']}%` : 'N/A')}
vs Top 50
${displayData['top50_W-L'] ? `${displayData['top50_W-L']} (${displayData['top50_win%'] || 'N/A'}%)` : (displayData['top50_win%'] ? `${displayData['top50_win%']}%` : 'N/A')}
vs Top 100
${displayData['top100_W-L'] ? `${displayData['top100_W-L']} (${displayData['top100_win%'] || 'N/A'}%)` : (displayData['top100_win%'] ? `${displayData['top100_win%']}%` : 'N/A')}
Avg Opponent Rank
${displayData['avg_opp_rank'] || 'N/A'}
`; $('#yearStats').html(statsHtml); updateTrendChart(surface);
```
--------------------------------
### Retrieve Career Statistics via REST API
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Fetches comprehensive career statistics for a specified player using a POST request.
```bash
# Get career statistics for a player
curl -X POST "http://localhost:5002/career" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "player=Iga Swiatek"
```
--------------------------------
### Player Autocomplete Search
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Provides autocomplete suggestions for player names. Accepts a query parameter 'q' for the search term and an optional 'tour' parameter to filter results. The 'limit' parameter controls the number of suggestions returned.
```bash
# Search for players matching query
curl -X GET "http://localhost:5002/api/player-suggest?q=djok&limit=5"
```
```json
{
"suggestions": [
{"label": "Novak Djokovic", "value": "Novak Djokovic", "tour": "ATP"}
]
}
```
```bash
# Filter by tour
curl -X GET "http://localhost:5002/api/player-suggest?q=iga&tour=WTA&limit=3"
```
```json
{
"suggestions": [
{"label": "Iga Swiatek", "value": "Iga Swiatek", "tour": "WTA"}
]
}
```
--------------------------------
### Render ATP Player List in Jinja2
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/players.html
Iterates through the ATP player list to display rank, name, country, and age. Uses Jinja2 templating for dynamic content generation.
```html
{% for p in atp_players %} {% endfor %}
Rank
Player
Nat
Age
{{ p.rank }}
[{{ p.name }}]({{ url_for('career_stats', player=p.name) }})
{{ p.country or '-' }}
{% if p.age is not none %} {{ "%.1f"|format(p.age) }} {% else %} - {% endif %}
```
--------------------------------
### API and Request Configuration
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Configures API base URLs, request delays, and retry attempts. Adjust these settings to manage API interaction and prevent rate limiting.
```python
# API Configuration
BASE_URL = 'https://www.tennisabstract.com'
TA_REPORTS_BASE = f"{BASE_URL}/reports"
REQUEST_DELAY = 0.5 # Delay between requests in seconds
REQUEST_RETRIES = 3 # Number of retries for failed requests
```
--------------------------------
### Format Player Form String
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/career.html
Formats a string representing a player's recent match results (W for win, L for loss) into HTML badges. Displays up to the first 10 results.
```javascript
function displayFormString(formString) {
let html = '';
for (let i = 0; i < formString.length && i < 10; i++) {
const result = formString[i];
html += `${result} `;
}
return html;
}
```
--------------------------------
### Generate Year and Surface Navigation - JavaScript
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/career.html
Dynamically generates HTML for year and surface navigation buttons based on response data. Includes event listeners for displaying statistics.
```javascript
contentHtml += 'Career '; const sortedYears = response.yearly_stats.map(y => y.year).sort((a, b) => b - a); sortedYears.forEach(year => { contentHtml +=
`${year}
`; }); contentHtml += ''; contentHtml +=
`All Surfaces
`; const availableSurfaces = Object.keys(response.surface_breakdown || {}); availableSurfaces.forEach(surface => { contentHtml +=
`${surface}
`; }); contentHtml += '
'; contentHtml += '
'; // *** MODIFICATION: Changed header to bg-dark text-white *** contentHtml +=
`
`; $('#results').html(heroHtml + contentHtml); setTimeout(() => { displayYearStats('career', 'All'); }, 100);
```
--------------------------------
### Career Form Submission Handler
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/career.html
Handles AJAX submission for career data and dynamically renders the hero section with player statistics and recent form.
```javascript
$('#careerForm').on('submit', function(e) { e.preventDefault(); $('.loading').show(); $('.error-message').hide(); $('#results').empty(); $.ajax({ url: '/career', method: 'POST', data: $(this).serialize(), success: function(response) { $('.loading').hide(); careerData = response; currentYear = 'career'; currentSurface = 'All'; let heroHtml = ` ${response.player} Career Record
${response.career_summary['W-L']}
${response.career_summary['win%']}% Win Rate
Avg Opponent Rank
${response.career_summary['avg_opp_rank']}
Years on Tour
${response.yearly_stats.length}
Recent Form ${displayFormString(response.recent_form.form_string)}
Last ${response.recent_form.last_matches}: ${response.recent_form.wins}-${response.recent_form.losses} (${response.recent_form.win_pct}%)
${response.recent_form.win_streak > 1 ? `
${response.recent_form.win_streak} match win streak
` : response.recent_form.loss_streak > 1 ? `
${response.recent_form.loss_streak} match losing streak
` : ''}
`; let contentHtml = ''; contentHtml += '
{
const isWin = match.wl === 'W';
const s = (match.surf || '').toString().toLowerCase();
html += `
${match.date} ${match.tourn} ${match.round}
${match.surf || '-'} ${isWin ? 'def.' : 'lost to'} ${match.opp} ${match.score}
`;
});
html += '
';
return html;
}
```
--------------------------------
### Jinja Template Structure for Player Comparison
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/compare.html
This Jinja template defines the overall structure for the player comparison page, including placeholders for player names, year, and comparison results. It also includes logic for iterating through years.
```html
{% extends "base.html" %} {% block content %}
Player Comparison
-----------------
Player 1
Player 2
Year Career {% for year in range(current_year, 1999, -1) %} {{ year }} {% endfor %}
Compare Players
Loading...
Analyzing player statistics...
{% endblock %} {% block extra_js %}
```
--------------------------------
### Head to Head Analysis Frontend Logic
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/h2h.html
Handles form submission, AJAX data fetching, and dynamic DOM manipulation for displaying match history and charts.
```javascript
function getSurfaceClass(surface) { const surfaceMap = { 'Hard': 'surface-hard', 'Clay': 'surface-clay', 'Grass': 'surface-grass', 'Carpet': 'surface-hard' }; return surfaceMap[surface] || 'surface-hard'; } let h2hProgressChart = null, h2hSurfacesChart = null, h2hRoundsChart = null; $('#h2hForm').on('submit', function(e) { e.preventDefault(); $('.loading').show(); $('.error-message').hide(); $('#results').empty(); $.ajax({ url: '/h2h', method: 'POST', data: $(this).serialize(), success: function(response) { $('.loading').hide(); const p1 = response.summary.player1; const p2 = response.summary.player2; // Hero summary let summaryHtml = `
${p1} vs ${p2} ${response.summary.p1_wins}
${p1} Wins
${response.summary.total_matches}
Total Matches
${response.summary.p2_wins}
${p2} Wins
`; // Charts section let chartsHtml = `
`; // Matches list let matchesHtml = `
`; response.matches.forEach((match) => { const isP1Winner = match.winner_name === p1; matchesHtml += `
${match.match_date} ${match.surface}
${match.tournament} ${match.round}
${p1} ${isP1Winner ? ' ' : ''}
${match.score}
${p2} ${!isP1Winner ? ' ' : ''}
${match.h2h}
`; }); matchesHtml += '
'; $('#results').html(summaryHtml + chartsHtml + matchesHtml); // Build cumulative wins data const byDate = [...response.matches].sort((a,b)=> new Date(a.match_date) - new Date(b.match_date)); let w1 = 0, w2 = 0; const labels = [], s1 = [], s2 = []; byDate.forEach(m => { labels.push(m.match_date); if (m.winner_name === p1) w1++; else if (m.winner_name === p2) w2++; s1.push(w1); s2.push(w2); }); // Calculate wins by round const roundOrder = ['F', 'SF', 'QF', 'R16', 'R32', 'R64', 'R128', 'RR']; const roundLabels = {'F': 'Final', 'SF': 'Semi-Final', 'QF': 'Quarter-Final', 'R16': 'Round of 16', 'R32': 'Round of 32', 'R64': 'Round of 64', 'R128': 'Round of 128', 'RR': 'Round Robin'}; const roundStats = {}; response.matches.forEach(match => { const round = match.round || 'Other'; if (!roundStats[round]) roundStats[round] = { [p1]: 0, [p2]: 0 }; if (match.winner_name === p1) roundStats[round][p1]++; else if (match.winner_name === p2) roundStats[round][p2]++; }); const sortedRounds = Object.keys(roundStats).sort((a, b) => { const aIndex = roundOrder.indexOf(a), bIndex = roundOrder.indexOf(b); if (aIndex === -1 && bIndex === -1) return 0; if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); const roundLabelsArray = sortedRounds.map(r => roundLabels[r] || r); const p1RoundWins = sortedRounds.map(r => roundStats[r][p1] || 0); const p2RoundWins = sortedRounds.map(r => roundStats[r][p2] || 0); // Destroy old charts [h2hProgressChart, h2hSurfacesChart, h2hRoundsChart].forEach(ch => { if (ch && ch.destroy) ch.destro
```
--------------------------------
### Handle Comparison Form Submission
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/compare.html
Processes the AJAX request for player comparison data and dynamically updates the UI with surface selection buttons.
```javascript
$('#compareForm').on('submit', function(e) { e.preventDefault(); $('.loading').show(); $('.error-message').hide(); $('#results').empty(); $('#surfaceFilter').hide(); $.ajax({ url: '/compare', method: 'POST', data: $(this).serialize(), success: function(response) { $('.loading').hide(); allSurfacesData = response.all_surfaces_stats; const available = Object.keys(allSurfacesData).filter(s => allSurfacesData[s] && allSurfacesData[s].length); if (available.length) { const btns = available.map((s, i) => `
${s} `).join(''); $('#surfaceFilter .btn-group').html(btns); $('#surfaceFilter').show(); currentSurface = available[0]; displayComparison(currentSurface); } else { $('#results').html('
No comparison data found for the selected players and year.
'); } }, error: function(xhr) { $('.loading').hide(); let errorMsg = xhr.responseJSON ? xhr.responseJSON.error : 'An error occurred'; $('.error-message').text(errorMsg).show(); }
```
--------------------------------
### Calculate Yearly Statistics with Python
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Computes aggregated yearly statistics from match data, with support for surface-specific filtering.
```python
from mod import get_player_matches, calculate_yearly_stats
matches = get_player_matches("Novak Djokovic")
# Calculate overall yearly stats
yearly_stats = calculate_yearly_stats(matches)
print(yearly_stats.loc[2023, ['W-L', 'win%', 'ace%', '1st_win%', 'tb_win%', 'top10_win%']])
# W-L 55-8
# win% 87.3
# ace% 7.82
# 1st_win% 76.4
# tb_win% 62.5
# top10_win% 73.3
# Filter by surface
clay_stats = calculate_yearly_stats(matches, surface='Clay')
print(clay_stats.loc[2023, ['W-L', 'win%', 'break%']])
# W-L 12-2
# win% 85.7
# break% 42.1
```
--------------------------------
### Strip Percentage Helper Function - JavaScript
Source: https://github.com/thecommishdeuce/tennisabstract/blob/main/templates/career.html
A helper function to parse percentage strings into floating-point numbers, returning null for invalid inputs. Used for chart data preparation.
```javascript
function _stripPct(val) { if (val === null || val === undefined) return null; const num = parseFloat(val); return isFinite(num) ? num : null; }
```
--------------------------------
### Compare Players by Year
Source: https://context7.com/thecommishdeuce/tennisabstract/llms.txt
Compares statistics for two specified players for a given year across all surfaces. Requires player names and the year as form-urlencoded data in a POST request.
```bash
# Compare two players for a specific year
curl -X POST "http://localhost:5002/compare" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "player1=Novak Djokovic&player2=Carlos Alcaraz&year=2024"
```
```json
{
"all_surfaces_stats": {
"All": [
{"stat": "W-L", "player1_value": "38-8", "player2_value": "54-12", "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "win%", "player1_value": 82.6, "player2_value": 81.8, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "ace%", "player1_value": 8.45, "player2_value": 9.12, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "1st_win%", "player1_value": 75.2, "player2_value": 77.8, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "bp_saved%", "player1_value": 64.3, "player2_value": 61.5, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "tb_win%", "player1_value": 58.3, "player2_value": 65.0, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"},
{"stat": "top10_win%", "player1_value": 66.7, "player2_value": 71.4, "player1_name": "Novak Djokovic", "player2_name": "Carlos Alcaraz"}
],
"Hard": [...],
"Clay": [...],
"Grass": [...]
},
"player1": "Novak Djokovic",
"player2": "Carlos Alcaraz",
"year": 2024
}
```