I am working on a basketball graphic that uses a KDE plot to graph made shots onto an image of a court that I made. The KDE plot itself looks pretty good, but as you can see by the attached image, the plot does not "fill" all the way to the bottom of the image, which ideally I would like to make that light gray color fill the entire court to make it look better. Here's the following code I am using: import json import matplotlib.pyplot as plt import seaborn as sns from PIL import Image import numpy as np # Set width and height shot_width_x = 800 # Determined by StatsAppClient shot_height_y = 671 # Determined by StatsAppClient court_width_x = 1920 # Determined by Court.png court_height_y = 1610 # Determined by Court.png # Load JSON data with open('data1.json') as f: data = json.load(f) # Function to extract shot positions and made status for a given team def extract_shots(team_players): global shot_width_x, shot_height_y shot_positions = [] shot_made = [] for player in team_players: for shot in player['Shots']: x, y = map(float, shot['ShotPosition'].split(',')) if (x, y) != (0, 0) and 0 <= x <= shot_width_x and 0 <= y <= shot_height_y: # Filter out invalid shots, and free throws # Flip the Y coordinate y = shot_height_y - y shot_positions.append((x, y)) shot_made.append(shot['Made']) else: print(f"Invalid shot detected for player {player['PlayerName']} at position ({x}, {y})") return shot_positions, shot_made def extract_team_names(data): team1 = data['Team1Name'] team2 = data['Team2Name'] return team1, team2 # Function to calculate shot percentages def calculate_percentages(team_players): total_shots = 0 made_threes = 0 made_twos = 0 made_frees = 0 attempted_threes = 0 attempted_twos = 0 attempted_frees = 0 for player in team_players: for shot in player['Shots']: total_shots += 1 if shot['PointValue'] == 3: attempted_threes += 1 if shot['Made']: made_threes += 1 elif shot['PointValue'] == 2: attempted_twos += 1 if shot['Made']: made_twos += 1 elif shot['PointValue'] == 1: attempted_frees += 1 if shot['Made']: made_frees += 1 three_percentage = (made_threes / attempted_threes * 100) if attempted_threes else 0 two_percentage = (made_twos / attempted_twos * 100) if attempted_twos else 0 free_percentage = (made_frees / attempted_frees * 100) if attempted_frees else 0 return three_percentage, two_percentage, free_percentage # Extract team names team1_name, team2_name = extract_team_names(data) # Extract shots for both teams team1_shot_positions, team1_shot_made = extract_shots(data['Team1Players']) team2_shot_positions, team2_shot_made = extract_shots(data['Team2Players']) # Calculate percentages for both teams team1_percentages = calculate_percentages(data['Team1Players']) team2_percentages = calculate_percentages(data['Team2Players']) # Function to create heatmap for a given team def create_heatmap(shot_positions, shot_made, team_name, team_num, percentages, player_names=None): if not shot_positions or not shot_made: print(f"No shot data available for {team_name}. Skipping heatmap creation.") return # Load basketball court image and resize it to court_width_x, court_height_y court_img = Image.open('court-one-side-eagle.png') court_img = court_img.resize((court_width_x, court_height_y)) court_width, court_height = court_img.size # Scale shot positions from shot_width_x, shot_height_y to court_width_x, court_height_y scaled_shot_positions = [(x * (court_width_x / shot_width_x), y * (court_height_y / shot_height_y)) for x, y in shot_positions] # Separate made and missed shots made_shot_positions = [pos for pos, made in zip(scaled_shot_positions, shot_made) if made] missed_shot_positions = [pos for pos, made in zip(scaled_shot_positions, shot_made) if not made] # Create plot with white background fig, ax = plt.subplots(figsize=(12, 8), dpi=200) ax.set_facecolor('white') fig.set_facecolor('white') # Display court image ax.imshow(court_img, extent=[0, court_width, 0, court_height]) # Convert made shot positions to arrays for plotting if made_shot_positions: x_coords, y_coords = zip(*made_shot_positions) else: x_coords, y_coords = [], [] # Create heatmap using KDE plot sns.kdeplot( x=x_coords, y=y_coords, cmap='inferno', # Using inferno colormap fill=True, # Fills the area under the KDE curve alpha=0.7, # Set transparency level levels=50, # Increase number of contour levels for smoother gradient, 50 is a good value thresh=0, # Set threshold to 0 to include all data points ax=ax, # Plot on the same axis bw_adjust=0.8, # Adjust bandwidth clip=((0, court_width_x), (0, court_height_y)), # Clip KDE plot to court dimensions gridsize=100 # Increase grid size for smoother plot ) # Add small dots for shot locations, green if made, red if missed for (x, y), made in zip(scaled_shot_positions, shot_made): if made: ax.scatter(x, y, c='green', marker='o', s=50, edgecolors='black', linewidths=1, alpha=0.6) else: ax.scatter(x, y, c='red', marker='x', s=50, linewidths=1, alpha=0.6) # Set plot limits to match the court image ax.set_xlim(0, court_width_x) ax.set_ylim(0, court_height_y) # Remove axes ax.set_axis_off() # Add title to the heatmap ax.set_title(f'{team_name} Shot Heatmap', fontsize=16, color='black') # Add text for percentages below the graph three_percentage, two_percentage, free_percentage = percentages fig.text(0.5, 0.07, f'Three-Point %: {three_percentage:.2f}% | Two-Point %: {two_percentage:.2f}% | Free Throw %: {free_percentage:.2f}%', color='black', fontsize=16, ha='center') # Add player names below the percentages if provided if player_names: fig.text(0.5, 0.93, f'Players: {", ".join(player_names)}', color='black', fontsize=12, ha='center') # Save plot to a file plt.savefig(f'heatmap_{team_num}.png', bbox_inches='tight', dpi=200) #plt.show() plt.close() # Function to create heatmap for individual players def create_individual_heatmap(team_num, player_numbers): if team_num == 1: team_players = data['Team1Players'] team_name = team1_name else: team_players = data['Team2Players'] team_name = team2_name selected_players = [player for player in team_players if player['PlayerNumber'] in player_numbers] shot_positions, shot_made = extract_shots(selected_players) if not shot_positions or not shot_made: print(f"No shot data available for selected players in {team_name}. Skipping heatmap creation.") return percentages = calculate_percentages(selected_players) player_names = [player['PlayerName'] for player in selected_players] create_heatmap(shot_positions, shot_made, f'{team_name} Selected Players', f'Team{team_num}_Selected', percentages, player_names) # Create heatmaps for both teams #create_heatmap(team1_shot_positions, team1_shot_made, f'{team1_name}', "team1", team1_percentages) #create_heatmap(team2_shot_positions, team2_shot_made, f'{team2_name}', "team2", team2_percentages) # Create combined heatmap combined_shot_positions = team1_shot_positions + team2_shot_positions combined_shot_made = team1_shot_made + team2_shot_made combined_percentages = calculate_percentages(data['Team1Players'] + data['Team2Players']) create_heatmap(combined_shot_positions, combined_shot_made, f'{team1_name} and {team2_name} Combined', "Combined", combined_percentages) # Example usage for individual players #create_individual_heatmap(1, ["2", "4"]) # Team 1, players 2 and 4 #create_individual_heatmap(2, ["1", "3"]) # Team 2, players 1 and 3 And this is a small example of shot data from the data.json file: "Shots": [ { "PointValue": 2, "GUID": "3d30115f-ac05-4486-b9a7-3045c74ffb2c", "PointCredit": 2, "ShotPosition": "358,101", "RealTime": "2025-02-09T13:14:26.8570195-06:00", "FriendlyTime": "1:14 PM (0 min 0 sec ago)", "FriendlyShotValue": "Two Points", "GameTime": null, "Made": true, "ModifiedDate": "0001-01-01T00:00:00" }, { "PointValue": 2, "GUID": "afbf8a09-13ac-4149-bb85-3e89a6a34203", "PointCredit": 2, "ShotPosition": "454,107", "RealTime": "2025-02-09T13:14:31.4624654-06:00", "FriendlyTime": "1:14 PM (0 min 0 sec ago)", "FriendlyShotValue": "Two Points", "GameTime": null, "Made": true, "ModifiedDate": "0001-01-01T00:00:00" }, { "PointValue": 2, "GUID": "020b5191-c191-4d31-abe6-a09c1897e128", "PointCredit": 2, "ShotPosition": "604,368", "RealTime": "2025-02-09T13:14:34.7286614-06:00", "FriendlyTime": "1:14 PM (0 min 0 sec ago)", "FriendlyShotValue": "Two Points", "GameTime": null, "Made": true, "ModifiedDate": "0001-01-01T00:00:00" }, The plot works correctly if I have a shot that is near the half court line, but obviously that wouldn't happen often during most games. Continue reading...