Spaces:
Running
Running
| 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() | |
| 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 functions.df_update import * | |
| from shiny import App, reactive, ui, render | |
| from shiny.ui import h2, tags | |
| from functions.heat_map_functions import * | |
| 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', | |
| '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', | |
| } | |
| 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' } | |
| format_dict = { | |
| 'pitch_percent': '{:.1%}', | |
| 'pitches': '{:.0f}', | |
| 'heart_zone_percent': '{:.1%}', | |
| 'shadow_zone_percent': '{:.1%}', | |
| 'chase_zone_percent': '{:.1%}', | |
| 'waste_zone_percent': '{:.1%}', | |
| 'csw_percent': '{:.1%}', | |
| 'whiff_rate': '{:.1%}', | |
| 'zone_whiff_percent': '{:.1%}', | |
| 'chase_percent': '{:.1%}', | |
| 'bip': '{:.0f}', | |
| 'xwoba_percent_contact': '{:.3f}' | |
| } | |
| format_dict = { | |
| 'pitch_percent': '{:.1%}', | |
| 'pitches': '{:.0f}', | |
| 'heart_zone_percent': '{:.1%}', | |
| 'shadow_zone_percent': '{:.1%}', | |
| 'chase_zone_percent': '{:.1%}', | |
| 'waste_zone_percent': '{:.1%}', | |
| 'csw_percent': '{:.1%}', | |
| 'whiff_rate': '{:.1%}', | |
| 'zone_whiff_percent': '{:.1%}', | |
| 'chase_percent': '{:.1%}', | |
| 'bip': '{:.0f}', | |
| 'xwoba_percent_contact': '{:.3f}' | |
| } | |
| label_translation_dict = { | |
| 'pitch_percent': 'Pitch%', | |
| 'pitches': 'Pitches', | |
| 'heart_zone_percent': 'Heart%', | |
| 'shadow_zone_percent': 'Shado%', | |
| 'chase_zone_percent': 'Chas%', | |
| 'waste_zone_percent': 'Waste%', | |
| 'csw_percent': 'CSW%', | |
| 'whiff_rate': 'Whiff%', | |
| 'zone_whiff_percent': 'Z-Whiff%', | |
| 'chase_percent': 'O-Swing%', | |
| 'bip': 'BBE', | |
| 'xwoba_percent_contact': 'xwOBACON' | |
| } | |
| cmap_sum22 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFB000',]) | |
| cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',]) | |
| cmap_sum2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFFFFF','#FFB000','#FE6100']) | |
| cmap_sum_r = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFB000','#FFFFFF','#648FFF',]) | |
| 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) | |
| # VALID_PASSWORDS.append('') | |
| from shiny import App, reactive, ui, render | |
| from shiny.ui import h2, tags | |
| # Define the login UI | |
| login_ui = ui.page_fluid( | |
| ui.card( | |
| ui.h2([ | |
| "TJStats Pitching Heat Maps 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/117909954", target="_blank"), | |
| "." | |
| ), | |
| ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="25%"), | |
| 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"), | |
| ) | |
| ) | |
| 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'))), | |
| ui.row(ui.input_action_button("get_pitches", "Get Pitch Types", class_="btn-secondary")), | |
| # Rows for selecting plots and split options | |
| ui.row(ui.column(12, ui.output_ui('pitch_type_ui', 'Select Pitch Type'))), | |
| ui.row(ui.column(6, ui.input_select('plot_type', 'Select Plot', ['Pitch%','Whiff%','xwOBACON'])), | |
| ui.column(6, ui.input_switch('scatter_switch', 'Show Pitches', value=False))), | |
| ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))), | |
| # 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 (former panel_main content) | |
| ui.navset_tab( | |
| # Tab for game summary plot | |
| ui.nav("Pitching Summary", | |
| ui.output_text("status"), | |
| ui.output_plot('plot', width='1440px', height=f'{900/1600*1440}px') | |
| ), | |
| ) | |
| ) | |
| # 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): | |
| 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="") | |
| def login_message(): | |
| return "" | |
| # Instead of using @reactive.calc with @reactive.event | |
| cached_data_value = reactive.value(None) # Initialize with None | |
| def cached_data(): | |
| if not hasattr(input, 'pitcher_id') or input.pitcher_id() is None or not hasattr(input, 'date_id') or input.date_id() is None: | |
| return # Exit early if required inputs aren't ready | |
| 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]) | |
| # Simulate an expensive data operation | |
| 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()]) | |
| data_list = scrape.get_data(game_list_input = game_list[:]) | |
| df = (update.update(scrape.get_data_df(data_list = data_list).filter( | |
| (pl.col("pitcher_id") == player_input)& | |
| (pl.col("is_pitch") == True) | |
| ))).with_columns( | |
| pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') | |
| ) | |
| return df | |
| 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'])).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) | |
| is_loading = reactive.value(False) | |
| data_result = reactive.value(None) | |
| def load_data(): | |
| is_loading.set(True) | |
| data_result.set(None) # Clear any previous data | |
| try: | |
| # This will fetch the data | |
| result = cached_data() | |
| data_result.set(result) | |
| except Exception as e: | |
| # Handle any errors | |
| print(f"Error loading data: {e}") | |
| finally: | |
| is_loading.set(False) | |
| def pitch_type_ui(): | |
| # Make sure to add dependencies on both values | |
| input.get_pitches() | |
| loading = is_loading() | |
| data = data_result() | |
| # If loading, show spinner | |
| if loading: | |
| return ui.div( | |
| ui.span("Loading pitch types... ", class_="me-2"), | |
| ui.tags.div(class_="spinner-border spinner-border-sm text-primary"), | |
| style="padding: 10px; background-color: #f8f9fa; border-radius: 5px;" | |
| ) | |
| # If data is loaded, show dropdown | |
| elif data is not None: | |
| df = data | |
| df = df.clone() if hasattr(df, 'clone') else df.copy() | |
| pitch_dict = dict(zip(df['pitch_type'], df['pitch_description'])) | |
| return ui.input_select( | |
| "pitch_type_input", | |
| "Select Pitch Type", | |
| pitch_dict, | |
| selectize=True | |
| ) | |
| # Initial state or after reset | |
| else: | |
| return ui.div( | |
| ui.p("Click 'Get Pitch Types' to load the dropdown.", class_="text-muted"), | |
| style="text-align: center; padding: 10px;" | |
| ) # Empty div with instructions | |
| def date_id(): | |
| # 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") | |
| def status(): | |
| # Only show status when generating | |
| if input.generate == 0: | |
| return "" | |
| return "" | |
| 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]) | |
| scatter_bool = input.scatter_switch() | |
| print(year_input, sport_id, player_input, start_date, end_date) | |
| df = cached_data() | |
| df = df.clone() | |
| pitch_input = input.pitch_type_input() | |
| df_plot = pitch_heat_map(pitch_input, df) | |
| pivot_table_l = pitch_prop(df=df_plot, hand = 'L') | |
| pivot_table_r = pitch_prop(df=df_plot, hand = 'R') | |
| table_left = df_update().update_summary_select(df=df_plot.filter(pl.col('batter_hand') == 'L'), selection=['pitcher_hand']) | |
| table_left = table_left.with_columns( | |
| (pl.col('pitches')/len(df.filter(pl.col('batter_hand') == 'L'))).alias('pitch_percent') | |
| ) | |
| table_right = df_update().update_summary_select(df=df_plot.filter(pl.col('batter_hand') == 'R'), selection=['pitcher_hand']) | |
| table_right = table_right.with_columns( | |
| (pl.col('pitches')/len(df.filter(pl.col('batter_hand') == 'R'))).alias('pitch_percent') | |
| ) | |
| try: | |
| normalize = mcolors.Normalize(vmin=table_left['pitch_percent']*0.5, | |
| vmax=table_left['pitch_percent']*1.5) # Define the range of values | |
| df_colour_left = pd.DataFrame(data=[[get_color(x,normalize,cmap_sum2) for x in pivot_table_l[0]], | |
| [get_color(x,normalize,cmap_sum2) for x in pivot_table_l[1]], | |
| [get_color(x,normalize,cmap_sum2) for x in pivot_table_l[2]]]) | |
| df_colour_left[0] = '#ffffff' | |
| except ValueError: | |
| normalize = mcolors.Normalize(vmin=0, | |
| vmax=1) # Define the range of values | |
| df_colour_left = pd.DataFrame(data=[['#ffffff','#ffffff','#ffffff','#ffffff'], | |
| ['#ffffff','#ffffff','#ffffff','#ffffff'], | |
| ['#ffffff','#ffffff','#ffffff','#ffffff']]) | |
| try: | |
| normalize = mcolors.Normalize(vmin=table_right['pitch_percent']*0.5, | |
| vmax=table_right['pitch_percent']*1.5) # Define the range of values | |
| df_colour_right = pd.DataFrame(data=[[get_color(x,normalize,cmap_sum2) for x in pivot_table_r[0]], | |
| [get_color(x,normalize,cmap_sum2) for x in pivot_table_r[1]], | |
| [get_color(x,normalize,cmap_sum2) for x in pivot_table_r[2]]]) | |
| df_colour_right[0] = '#ffffff' | |
| except ValueError: | |
| normalize = mcolors.Normalize(vmin=0, | |
| vmax=1) # Define the range of values | |
| df_colour_right = pd.DataFrame(data=[['#ffffff','#ffffff','#ffffff','#ffffff'], | |
| ['#ffffff','#ffffff','#ffffff','#ffffff'], | |
| ['#ffffff','#ffffff','#ffffff','#ffffff']]) | |
| table_left = table_left.select( | |
| 'pitch_percent', | |
| 'pitches', | |
| 'heart_zone_percent', | |
| 'shadow_zone_percent', | |
| 'chase_zone_percent', | |
| 'waste_zone_percent', | |
| 'csw_percent', | |
| 'whiff_rate', | |
| 'zone_whiff_percent', | |
| 'chase_percent', | |
| 'bip', | |
| 'xwoba_percent_contact').to_pandas().T | |
| table_right = table_right.select( | |
| 'pitch_percent', | |
| 'pitches', | |
| 'heart_zone_percent', | |
| 'shadow_zone_percent', | |
| 'chase_zone_percent', | |
| 'waste_zone_percent', | |
| 'csw_percent', | |
| 'whiff_rate', | |
| 'zone_whiff_percent', | |
| 'chase_percent', | |
| 'bip', | |
| 'xwoba_percent_contact').to_pandas().T | |
| table_right = table_right.replace({'nan%':'—'}) | |
| table_right = table_right.replace({'nan':'—'}) | |
| p.set(0.6, "Creating plot...") | |
| import matplotlib.pyplot as plt | |
| fig = plt.figure(figsize=(16, 9)) | |
| fig.set_facecolor('white') | |
| sns.set_theme(style="whitegrid", palette=colour_palette) | |
| gs = GridSpec(3, 5, height_ratios=[2,9,1],width_ratios=[1,9,1,9,1]) | |
| gs.update(hspace=0.2, wspace=0.3) | |
| # Add subplots to the grid | |
| ax_header = fig.add_subplot(gs[0, :]) | |
| ax_left = fig.add_subplot(gs[1, 1]) | |
| ax_right = fig.add_subplot(gs[1, 3]) | |
| axfooter = fig.add_subplot(gs[-1, :]) | |
| if input.plot_type() == 'Pitch%': | |
| heat_map_plot(df=df_plot, | |
| ax=ax_left, | |
| cmap=cmap_sum2, | |
| hand='L', | |
| scatter=scatter_bool) | |
| heat_map_plot(df=df_plot, | |
| ax=ax_right, | |
| cmap=cmap_sum2, | |
| hand='R', | |
| scatter=scatter_bool) | |
| if input.plot_type() == 'Whiff%': | |
| heat_map_plot_hex_whiff(df=df_plot, | |
| ax=ax_left, | |
| cmap=cmap_sum, | |
| hand='L', | |
| scatter=scatter_bool) | |
| heat_map_plot_hex_whiff(df=df_plot, | |
| ax=ax_right, | |
| cmap=cmap_sum, | |
| hand='R', | |
| scatter=scatter_bool) | |
| if input.plot_type() == 'xwOBACON': | |
| print(df_plot.filter((pl.col('launch_speed')>0)).select(['batter_name','launch_speed','launch_angle','woba_pred_contact'])) | |
| heat_map_plot_hex_damage(df=df_plot, | |
| ax=ax_left, | |
| cmap=cmap_sum, | |
| hand='L', | |
| scatter=scatter_bool) | |
| heat_map_plot_hex_damage(df=df_plot, | |
| ax=ax_right, | |
| cmap=cmap_sum, | |
| hand='R', | |
| scatter=scatter_bool) | |
| # Load the image | |
| img = mpimg.imread('images/left.png') | |
| imagebox = OffsetImage(img, zoom=0.58) # adjust zoom as needed | |
| ab = AnnotationBbox(imagebox, (1.25, -0.5), box_alignment=(0, 0), frameon=False) | |
| ax_left.add_artist(ab) | |
| # Load the image | |
| img = mpimg.imread('images/right.png') | |
| imagebox = OffsetImage(img, zoom=0.58) # adjust zoom as needed | |
| # Create an AnnotationBbox | |
| ab = AnnotationBbox(imagebox, (-1.25, -0.5), box_alignment=(1, 0), frameon=False) | |
| ax_right.add_artist(ab) | |
| table_plot(ax=ax_left, | |
| table=table_left, | |
| hand='L') | |
| table_plot_pivot(ax=ax_left, | |
| pivot_table=pivot_table_l, | |
| df_colour=df_colour_left) | |
| table_plot(ax=ax_right, | |
| table=table_right, | |
| hand='R') | |
| table_plot_pivot(ax=ax_right, | |
| pivot_table=pivot_table_r, | |
| df_colour=df_colour_right) | |
| from matplotlib.cm import ScalarMappable | |
| from matplotlib.colors import Normalize | |
| # Create a ScalarMappable with the same colormap and normalization | |
| if input.plot_type() == 'Pitch%': | |
| sm = ScalarMappable(cmap=cmap_sum2, norm=Normalize(vmin=0, vmax=1)) | |
| cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100) | |
| cbar.set_ticks([]) | |
| cbar.set_ticks([sm.norm.vmin, sm.norm.vmax]) | |
| cbar.ax.set_xticklabels(['Least', 'Most']) | |
| cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14) | |
| labels = cbar.ax.get_xticklabels() | |
| labels[0].set_horizontalalignment('left') | |
| labels[-1].set_horizontalalignment('right') | |
| labels = cbar.ax.get_xticklabels() | |
| cbar.ax.set_xticklabels(labels) | |
| cbar.ax.tick_params(length=0) | |
| if input.plot_type() == 'Whiff%': | |
| sm = ScalarMappable(cmap=cmap_sum, norm=Normalize(vmin=0.15, vmax=0.35)) | |
| cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100) | |
| cbar.set_ticks([]) | |
| cbar.set_ticks([sm.norm.vmin, sm.norm.vmax]) | |
| cbar.ax.set_xticklabels(['15%', '35%']) | |
| cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14) | |
| labels = cbar.ax.get_xticklabels() | |
| labels[0].set_horizontalalignment('left') | |
| labels[-1].set_horizontalalignment('right') | |
| labels = cbar.ax.get_xticklabels() | |
| cbar.ax.set_xticklabels(labels) | |
| cbar.ax.tick_params(length=0) | |
| if input.plot_type() == 'xwOBACON': | |
| sm = ScalarMappable(cmap=cmap_sum_r, norm=Normalize(vmin=0.25, vmax=0.5)) | |
| cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100) | |
| cbar.set_ticks([]) | |
| cbar.set_ticks([sm.norm.vmin, sm.norm.vmax]) | |
| cbar.ax.set_xticklabels(['.000', '.500']) | |
| cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14) | |
| labels = cbar.ax.get_xticklabels() | |
| labels[0].set_horizontalalignment('left') | |
| labels[-1].set_horizontalalignment('right') | |
| labels = cbar.ax.get_xticklabels() | |
| cbar.ax.set_xticklabels(labels) | |
| cbar.ax.tick_params(length=0) | |
| axfooter.text(x=0.02,y=1,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=14,va='top') | |
| axfooter.text(x=1-0.02,y=1,s='Data: MLB',ha='right',fontname='Calibri',fontsize=14,va='top') | |
| axfooter.axis('off') | |
| # Display the image on the axis | |
| ax_header.set_xlim(-12,12) | |
| ax_header.set_ylim(0, 2) | |
| if input.plot_type() == 'Pitch%': | |
| ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} Pitch Frequency",ha='center',fontsize=24,va='top') | |
| if input.plot_type() == 'Whiff%': | |
| ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} Whiff%",ha='center',fontsize=24,va='top') | |
| if input.plot_type() == 'xwOBACON': | |
| ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} xwOBACON",ha='center',fontsize=24,va='top') | |
| ax_header.text(x=0,y=0.7,s=f"{year_input} {level_dict[str(sport_id)]} Season",ha='center',fontsize=16,va='top') | |
| ax_header.text(x=0,y=0.3,s=f"{df_plot['game_date'][0]} to {df_plot['game_date'][-1]}",ha='center',fontsize=16,va='top',fontstyle='italic') | |
| ax_header.axis('off') | |
| import urllib | |
| import urllib.request | |
| import urllib.error | |
| from urllib.error import HTTPError | |
| plot_header(pitcher_id=player_input, | |
| ax=ax_header, | |
| df_team=scrape.get_teams(), | |
| df_players=scrape.get_players(sport_id,year_input), | |
| sport_id=sport_id,) | |
| fig.subplots_adjust(left=0.03, right=0.97, top=0.97, bottom=0.03) | |
| app = App(app_ui, server) | |