import polars as pl import numpy as np import pandas as pd import api_scraper scrape = api_scraper.MLB_Scrape() from functions import df_update from functions import pitch_summary_functions update = df_update.df_update() from stuff_model import feature_engineering as fe from stuff_model import stuff_apply import requests import joblib from matplotlib.gridspec import GridSpec from shiny import App, reactive, ui, render from shiny.ui import h2, tags import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import seaborn as sns from functions.pitch_summary_functions import * from shiny import App, reactive, ui, render from shiny.ui import h2, tags colour_palette = ['#FFB000','#648FFF','#785EF0', '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] year_list = [2017,2018,2019,2020,2021,2022,2023,2024,2025] level_dict = {'1':'MLB', '11':'AAA', # '12':'AA', #'13':'A+', '14':'A', '16':'ROK', '17':'AFL', '22':'College', '21':'Prospects', '51':'International' } function_dict={ 'velocity_kdes':'Velocity Distributions', 'break_plot':'Pitch Movement', 'tj_stuff_roling':'Rolling tjStuff+ by Pitch', 'tj_stuff_roling_game':'Rolling tjStuff+ by Game', 'location_plot_lhb':'Locations vs LHB', 'location_plot_rhb':'Locations vs RHB', 'pitch_usage':'Pitch Usage', } split_dict = {'all':'All', 'left':'LHH', 'right':'RHH'} split_dict_hand = {'all':['L','R'], 'left':['L'], 'right':['R']} type_dict = {'R':'Regular Season', 'S':'Spring', 'P':'Playoffs' } # List of MLB teams and their corresponding ESPN logo URLs mlb_teams = [ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"}, {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"}, {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"}, {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"}, {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"}, {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"}, {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"}, {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"}, {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"}, {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"}, {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"}, {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"}, {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"}, {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"}, {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"}, {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"}, {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"}, {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"}, {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"}, {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"}, {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"}, {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"}, {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"}, {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"}, {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"}, {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"}, {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"}, {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"}, {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"}, {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"}, {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"} ] df_image = pd.DataFrame(mlb_teams) image_dict = df_image.set_index('team')['logo_url'].to_dict() image_dict_flip = df_image.set_index('logo_url')['team'].to_dict() import requests import os CAMPAIGN_ID = os.getenv("CAMPAIGN_ID") ACCESS_TOKEN = os.getenv("ACCESS_TOKEN") BACKUP_PW = os.getenv("BACKUP_PW") ADMIN_PW = os.getenv("ADMIN_PW") url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members" headers = { "Authorization": f"Bearer {ACCESS_TOKEN}" } # Simple parameters, requesting the member's email and currently entitled tiers params = { "fields[member]": "full_name,email", # Request the member's email "include": "currently_entitled_tiers", # Include the currently entitled tiers "page[size]": 10000 # Fetch up to 1000 patrons per request } response = requests.get(url, headers=headers, params=params) VALID_PASSWORDS = [] if response.status_code == 200: data = response.json() for patron in data['data']: try: tiers = patron['relationships']['currently_entitled_tiers']['data'] if any(tier['id'] == '9078921' for tier in tiers): full_name = patron['attributes']['email'] VALID_PASSWORDS.append(full_name) except KeyError: continue VALID_PASSWORDS.append(BACKUP_PW) VALID_PASSWORDS.append(ADMIN_PW) from shiny import App, reactive, ui, render from shiny.ui import h2, tags from datetime import datetime def is_valid_date(date_str): try: datetime.strptime(date_str, "%Y-%m-%d") # Attempt to parse the date return True except ValueError: return False # If parsing fails, it's not in the correct format # Define the login UI login_ui = ui.page_fluid( ui.card( ui.h2([ "TJStats Pitching Summary App ", ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank") ]), ui.p( "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ", ui.tags.a("Patreon post", href="https://www.patreon.com/posts/116064432", target="_blank"), "." ), ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="50%"), ui.tags.input( type="checkbox", id="authenticated", value=False, disabled=True ), ui.input_action_button("login", "Login", class_="btn-primary"), ui.output_text("login_message"), ) ) # Define the UI layout for the app main_ui = ui.page_sidebar( ui.sidebar( # Row for selecting season and level ui.row( ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2025)), ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)), ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R')) ), # Row for the action button to get player list ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")), # Row for selecting the player ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))), # Row for selecting the date range ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))), # Rows for selecting plots and split options ui.row( ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')), ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='break_plot')), ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='pitch_usage')) ), ui.row( ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)), ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50)) ), ui.row( ui.column(6, ui.input_switch("switch", "Custom Team?", False)), ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False)) ), # Row for the action button to generate plot ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")), width="400px" # Added this parameter to control sidebar width ), # Main content area with tabs (placed directly in page_sidebar) ui.navset_tab( ui.nav_panel("Pitching Summary", ui.output_text("status"), ui.output_plot('plot', width='2100px', height='2100px') ), ui.nav_panel("Game Summary", ui.output_text("status2"), ui.output_plot('game_plot', width='2100px', height='2100px') ), ui.nav_panel("Table Range", ui.output_data_frame("grid")), ui.nav_panel("Table Game", ui.output_data_frame("grid_game")), id="tabset" ) ) # Combined UI with conditional panel app_ui = ui.page_fluid( ui.tags.head( ui.tags.script(src="script.js") ), ui.panel_conditional( "!input.authenticated", login_ui ), ui.panel_conditional( "input.authenticated", main_ui ) ) def server(input, output, session): @reactive.Effect @reactive.event(input.login) def check_password(): if input.password() in VALID_PASSWORDS: ui.update_checkbox("authenticated", value=True) ui.update_text("login_message", value="") else: ui.update_text("login_message", value="Invalid password!") ui.update_text("password", value="") @output @render.text def login_message(): return "" @reactive.calc @reactive.event(input.pitcher_id, input.date_id,input.split_id) def cached_data(): year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) start_date = str(input.date_id()[0]) end_date = str(input.date_id()[1]) game_list = scrape.get_player_games_list(sport_id = sport_id, season = year_input, player_id = player_input, start_date = start_date, end_date = end_date, game_type = [input.type_input()]) # if input.tabset() == 'Game Summary': # print(year_input, sport_id, player_input, 'yup') # print(input.date_id()) # game_list = [input.date_id()] data_list = scrape.get_data(game_list_input = game_list[:]) try: df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter( (pl.col("pitcher_id") == player_input)& (pl.col("is_pitch") == True)& (pl.col("start_speed") >= 50)& (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) )))).with_columns( pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') )) return df except TypeError: print("NONE") return None @reactive.calc @reactive.event(input.pitcher_id, input.date_id,input.split_id,input.tabset) def cached_data_daily(): year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) # start_date = str(input.date_id()[0]) # end_date = str(input.date_id()[1]) game_list = [int(input.date_id())] print(game_list) # if input.tabset() == 'Game Summary': # print(year_input, sport_id, player_input, 'yup') # print(input.date_id()) # game_list = data_list = scrape.get_data(game_list_input = game_list[:]) try: df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter( (pl.col("pitcher_id") == player_input)& (pl.col("is_pitch") == True)& (pl.col("start_speed") >= 50)& (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) )))).with_columns( pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') )) return df except TypeError: print("NONE") return None @render.ui @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False) def player_select_ui(): # Get the list of pitchers for the selected level and season df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter( (pl.col("position").is_in(['P','TWP']))| (pl.col("player_id").is_in([686846,806823])) ).sort("name") # Create a dictionary of pitcher IDs and names pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name'])) # Return a select input for choosing a pitcher return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True) @render.ui @reactive.event(input.player_button, input.pitcher_id,input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False) def date_id(): if input.tabset() == 'Pitching Summary' or input.tabset() == 'Table Range': # Create a date range input for selecting the date range within the selected year return ui.input_date_range("date_id", "Select Date Range", start=f"{int(input.year_input())}-01-01", end=f"{int(input.year_input())}-12-31", min=f"{int(input.year_input())}-01-01", max=f"{int(input.year_input())}-12-31") if input.tabset() == 'Game Summary' or input.tabset() == 'Table Game': year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) print('game summary') # start_date = str(input.date_id()[0]) # end_date = str(input.date_id()[1]) game_list = scrape.get_player_games_list(player_id = player_input, season = year_input, sport_id=sport_id, game_type=[input.type_input()], pitching = True) schedule_df = scrape.get_schedule(year_input=[year_input], sport_id= [sport_id], game_type = [input.type_input()]) player_schedule_df = schedule_df.filter(pl.col('game_id').is_in(game_list)).to_pandas().sort_values('date') player_schedule_df['def'] = player_schedule_df['date'].astype(str) + ' - ' + player_schedule_df['away'] + ' @ ' + player_schedule_df['home'] + ' ' game_dict = dict(zip(player_schedule_df['game_id'], player_schedule_df['def'])) # print(game_dict) return ui.input_select("date_id", "Select Game", game_dict) @output @render.text def status(): # Only show status when generating if input.generate == 0: return "" return "" @output @render.plot @reactive.event(input.generate_plot, ignore_none=False) def plot(): # Show progress/loading notification with ui.Progress(min=0, max=1) as p: p.set(message="Generating plot", detail="This may take a while...") p.set(0.3, "Gathering data...") year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) start_date = str(input.date_id()[0]) end_date = str(input.date_id()[1]) if not is_valid_date(start_date): fig = plt.figure(figsize=(26,26)) fig.text(x=0.1,y=0.9,s='Select Date Range and Generate Plot',fontsize=36,ha='left') return fig print(year_input, sport_id, player_input, start_date, end_date) df = cached_data() if df is None: fig = plt.figure(figsize=(26,26)) fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left') return fig df = df.clone() p.set(0.6, "Creating plot...") #plt.rcParams["figure.figsize"] = [10,10] fig = plt.figure(figsize=(26,26)) plt.rcParams.update({'figure.autolayout': True}) fig.set_facecolor('white') sns.set_theme(style="whitegrid", palette=colour_palette) print('this is the one plot') gs = gridspec.GridSpec(6, 8, height_ratios=[6,20,12,36,36,6], width_ratios=[4,18,18,18,18,18,18,4]) gs.update(hspace=0.2, wspace=0.5) # Define the positions of each subplot in the grid ax_headshot = fig.add_subplot(gs[1,1:3]) ax_bio = fig.add_subplot(gs[1,3:5]) ax_logo = fig.add_subplot(gs[1,5:7]) ax_season_table = fig.add_subplot(gs[2,1:7]) ax_plot_1 = fig.add_subplot(gs[3,1:3]) ax_plot_2 = fig.add_subplot(gs[3,3:5]) ax_plot_3 = fig.add_subplot(gs[3,5:7]) ax_table = fig.add_subplot(gs[4,1:7]) ax_footer = fig.add_subplot(gs[-1,1:7]) ax_header = fig.add_subplot(gs[0,1:7]) ax_left = fig.add_subplot(gs[:,0]) ax_right = fig.add_subplot(gs[:,-1]) # Hide axes for footer, header, left, and right ax_footer.axis('off') ax_header.axis('off') ax_left.axis('off') ax_right.axis('off') sns.set_theme(style="whitegrid", palette=colour_palette) fig.set_facecolor('white') df_teams = scrape.get_teams() player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input) player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input) if input.switch(): # Get the logo URL from the image dictionary using the team abbreviation logo_url = input.logo_select() # Send a GET request to the logo URL response = requests.get(logo_url) # Open the image from the response content img = Image.open(BytesIO(response.content)) # Display the image on the axis ax_logo.set_xlim(0, 1.3) ax_logo.set_ylim(0, 1) ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper') # Turn off the axis ax_logo.axis('off') else: plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input)) stat_summary_table(df=df, ax=ax_season_table, player_input=player_input, split=input.split_id(), sport_id=sport_id, game_type=[input.type_input()], start_date_input= str(input.date_id()[0]), end_date_input=str(input.date_id()[1])) # break_plot(df=df_plot,ax=ax2) for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]): if x == 'velocity_kdes': velocity_kdes(df, ax=y, gs=gs, gs_x=[3,4], gs_y=[z,z+2], fig=fig) if x == 'tj_stuff_roling': tj_stuff_roling(df=df, window=int(input.rolling_window()), ax=y) if x == 'tj_stuff_roling_game': tj_stuff_roling_game(df=df, window=int(input.rolling_window()), ax=y) if x == 'break_plot': break_plot(df = df,ax=y) if x == 'location_plot_lhb': location_plot(df = df,ax=y,hand='L') if x == 'location_plot_rhb': location_plot(df = df,ax=y,hand='R') if x == 'pitch_usage': pitch_usage(df = df,ax=y) summary_table(df=df, ax=ax_table) plot_footer(ax_footer) # ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1) # # Hide axes ticks and labels # ax_watermark.set_xticks([]) # ax_watermark.set_yticks([]) # ax_watermark.set_frame_on(False) # Optional: Hide border # img = Image.open('tj stats circle-01_new.jpg') # # Display the image # ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1) ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1) ax_watermark2.set_xlim(0,1) ax_watermark2.set_ylim(0,1) # Hide axes ticks and labels ax_watermark2.set_xticks([]) ax_watermark2.set_yticks([]) ax_watermark2.set_frame_on(False) # Optional: Hide border # Open the image img = Image.open('tj stats circle-01_new.jpg') # Get the original size width, height = img.size # Calculate the new size (50% larger) new_width = int(width * 0.5) new_height = int(height * 0.5) # Resize the image img_resized = img.resize((new_width, new_height)) # Display the image ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1) fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) @output @render.plot @reactive.event(input.generate_plot, ignore_none=False) def game_plot(): # Show progress/loading notification with ui.Progress(min=0, max=1) as p: print(input.date_id(),'TEST') if isinstance(input.date_id(), tuple): fig = plt.figure(figsize=(26,26)) fig.text(x=0.1,y=0.9,s='Select Game and Generate Plot',fontsize=36,ha='left') return fig p.set(message="Generating plot", detail="This may take a while...") p.set(0.3, "Gathering data...") year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) # print(input.game_id()) # print(year_input, sport_id, player_input) # print(year_input, sport_id, player_input, start_date, end_date) df = cached_data_daily() # start_date = str(df['game_date'][0]) # end_date = str(df['game_date'][0]) if df is None: fig = plt.figure(figsize=(26,26)) fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left') return fig df = df.clone() p.set(0.6, "Creating plot...") #plt.rcParams["figure.figsize"] = [10,10] fig = plt.figure(figsize=(26,26)) plt.rcParams.update({'figure.autolayout': True}) fig.set_facecolor('white') sns.set_theme(style="whitegrid", palette=colour_palette) print('this is the one plot') gs = gridspec.GridSpec(6, 8, height_ratios=[6,20,12,36,36,6], width_ratios=[4,18,18,18,18,18,18,4]) gs.update(hspace=0.2, wspace=0.5) # Define the positions of each subplot in the grid ax_headshot = fig.add_subplot(gs[1,1:3]) ax_bio = fig.add_subplot(gs[1,3:5]) ax_logo = fig.add_subplot(gs[1,5:7]) ax_season_table = fig.add_subplot(gs[2,1:7]) ax_plot_1 = fig.add_subplot(gs[3,1:3]) ax_plot_2 = fig.add_subplot(gs[3,3:5]) ax_plot_3 = fig.add_subplot(gs[3,5:7]) ax_table = fig.add_subplot(gs[4,1:7]) ax_footer = fig.add_subplot(gs[-1,1:7]) ax_header = fig.add_subplot(gs[0,1:7]) ax_left = fig.add_subplot(gs[:,0]) ax_right = fig.add_subplot(gs[:,-1]) # Hide axes for footer, header, left, and right ax_footer.axis('off') ax_header.axis('off') ax_left.axis('off') ax_right.axis('off') sns.set_theme(style="whitegrid", palette=colour_palette) fig.set_facecolor('white') df_teams = scrape.get_teams() player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input) player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input) if input.switch(): # Get the logo URL from the image dictionary using the team abbreviation logo_url = input.logo_select() # Send a GET request to the logo URL response = requests.get(logo_url) # Open the image from the response content img = Image.open(BytesIO(response.content)) # Display the image on the axis ax_logo.set_xlim(0, 1.3) ax_logo.set_ylim(0, 1) ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper') # Turn off the axis ax_logo.axis('off') else: plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input)) stat_summary_table(df=df, ax=ax_season_table, player_input=player_input, split=input.split_id(), sport_id=sport_id, game_type=[input.type_input()], start_date_input=None, end_date_input=None) # break_plot(df=df_plot,ax=ax2) for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]): if x == 'velocity_kdes': velocity_kdes(df, ax=y, gs=gs, gs_x=[3,4], gs_y=[z,z+2], fig=fig) if x == 'tj_stuff_roling': tj_stuff_roling(df=df, window=int(input.rolling_window()), ax=y) if x == 'tj_stuff_roling_game': tj_stuff_roling_game(df=df, window=int(input.rolling_window()), ax=y) if x == 'break_plot': break_plot(df = df,ax=y) if x == 'location_plot_lhb': location_plot(df = df,ax=y,hand='L') if x == 'location_plot_rhb': location_plot(df = df,ax=y,hand='R') if x == 'pitch_usage': pitch_usage(df = df,ax=y) summary_table(df=df, ax=ax_table) plot_footer(ax_footer) ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1) # Hide axes ticks and labels ax_watermark.set_xticks([]) ax_watermark.set_yticks([]) ax_watermark.set_frame_on(False) # Optional: Hide border img = Image.open('tj stats circle-01_new.jpg') # Display the image ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1) ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1) ax_watermark2.set_xlim(0,1) ax_watermark2.set_ylim(0,1) # Hide axes ticks and labels ax_watermark2.set_xticks([]) ax_watermark2.set_yticks([]) ax_watermark2.set_frame_on(False) # Optional: Hide border # Open the image img = Image.open('tj stats circle-01_new.jpg') # Get the original size width, height = img.size # Calculate the new size (50% larger) new_width = int(width * 0.5) new_height = int(height * 0.5) # Resize the image img_resized = img.resize((new_width, new_height)) # Display the image ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1) fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) @output @render.data_frame @reactive.event(input.generate_plot, ignore_none=False) def grid(): start_date = str(input.date_id()[0]) if not is_valid_date(start_date): return pd.DataFrame({"Message": ["Select range to generate table"]}) df = cached_data() df = df.clone() features_table = ['start_speed', 'spin_rate', 'extension', 'ivb', 'hb', 'x0', 'z0'] selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand', 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus','pitch_grade'] return render.DataGrid( df.select(selection).to_pandas().round(1), row_selection_mode='multiple', height='700px', width='fit-content', filters=True, ) @output @render.data_frame @reactive.event(input.generate_plot, ignore_none=False) def grid_game(): if isinstance(input.date_id(), tuple): return pd.DataFrame({"Message": ["Select game to generate table"]}) df = cached_data_daily() df = df.clone() features_table = ['start_speed', 'spin_rate', 'extension', 'ivb', 'hb', 'x0', 'z0'] selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand', 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus','pitch_grade'] return render.DataGrid( df.select(selection).to_pandas().round(1), row_selection_mode='multiple', height='700px', width='fit-content', filters=True, ) app = App(app_ui, server)