Spaces:
Running
Running
Download plotting.py from Ramos-Ramos/npb_data_app: direct link, hf CLI and curl.
- Browser
- Download file 25.7 kB
-
https://huggingface.co/spaces/Ramos-Ramos/npb_data_app/resolve/main/plotting.py
- Command line
-
hf download hf://spaces/Ramos-Ramos/npb_data_app/plotting.py
-
curl -L -o plotting.py https://huggingface.co/spaces/Ramos-Ramos/npb_data_app/resolve/main/plotting.py
25.7 kB
| import matplotlib as mpl | |
| import matplotlib.pyplot as plt | |
| from matplotlib import transforms | |
| from matplotlib.colors import LinearSegmentedColormap | |
| import polars as pl | |
| from pyfonts import load_google_font | |
| from scipy.stats import gaussian_kde | |
| import numpy as np | |
| from PIL import Image | |
| from types import SimpleNamespace | |
| import datetime | |
| from datetime import date | |
| import os | |
| from convert import ( | |
| ball_kind_code_to_color, | |
| get_text_color_from_color, | |
| team_names_short_to_color, | |
| get_text_color_from_team, | |
| ) | |
| from stats import get_player_stats, filter_data_by_date_and_game_kind, get_stat_val | |
| def get_card_data(id, **kwargs): | |
| shared_kwargs = { | |
| k: v | |
| for k, v in kwargs.items() | |
| if not (k.startswith("right_") or k.startswith("left_")) | |
| } | |
| right_kwargs = { | |
| k.removeprefix("right_"): v for k, v in kwargs.items() if k.startswith("right_") | |
| } | |
| left_kwargs = { | |
| k.removeprefix("left_"): v for k, v in kwargs.items() if k.startswith("left_") | |
| } | |
| both, left, right = ( | |
| get_player_stats(id, player_type="pitcher", **shared_kwargs), | |
| get_player_stats( | |
| id, player_type="pitcher", lr="l", **(shared_kwargs | right_kwargs) | |
| ), | |
| get_player_stats( | |
| id, player_type="pitcher", lr="r", **(shared_kwargs | left_kwargs) | |
| ), | |
| ) | |
| player_stats = both.player_stats.join( | |
| left.player_stats, on="pitId", suffix="_left", how="left" | |
| ).join(right.player_stats, on="pitId", suffix="_right", how="left") | |
| pitch_stats = ( | |
| both.pitch_stats.join( | |
| left.pitch_stats, on="ballKind_code", how="full", suffix="_left" | |
| ) | |
| .join(right.pitch_stats, on="ballKind_code", how="full", suffix="_right") | |
| .fill_null(0) | |
| .sort("Count", descending=True) | |
| ) | |
| return SimpleNamespace( | |
| player_stats=player_stats, | |
| pitch_stats=pitch_stats, | |
| both_pitch_shapes=both.pitch_shapes, | |
| left_pitch_shapes=left.pitch_shapes, | |
| right_pitch_shapes=right.pitch_shapes, | |
| ) | |
| def plot_arsenal(ax, pitches): | |
| ax.set_xlim(0, 11) | |
| x = np.arange(len(pitches)) + 0.5 | |
| y = np.zeros(len(pitches)) | |
| ax.scatter( | |
| x, y, c=[ball_kind_code_to_color.get(pitch, "C0") for pitch in pitches], s=170 | |
| ) | |
| for i, pitch in enumerate(pitches): | |
| color = ball_kind_code_to_color.get(pitch, "C0") | |
| ax.text( | |
| x=i + 0.5, | |
| y=0, | |
| s=pitch, | |
| horizontalalignment="center", | |
| verticalalignment="center", | |
| font=font, | |
| color=get_text_color_from_color(color), | |
| ) | |
| def plot_usage(ax, usages): | |
| left = 0 | |
| height = 0.8 | |
| for pitch, usage in usages.iter_rows(): | |
| color = ball_kind_code_to_color[pitch] | |
| ax.barh(0, usage, height=height, left=left, color=color) | |
| if usage > 0.1: | |
| ax.text( | |
| left + usage / 2, | |
| 0, | |
| f"{usage:.0%}", | |
| horizontalalignment="center", | |
| verticalalignment="center", | |
| size=8, | |
| font=font, | |
| color=get_text_color_from_color(color), | |
| ) | |
| left += usage | |
| ax.set_xlim(0, 1) | |
| ax.set_ylim(-height / 2, height / 2 * 2.75) | |
| x_range = np.arange(-100, 100 + 1) | |
| y_range = np.arange(0, 250 + 1) | |
| X, Y = np.meshgrid(x_range, y_range) | |
| def fit_pred_kde(data, **kwargs): | |
| kde = gaussian_kde(data, **kwargs) | |
| Z = kde(np.concat((X, Y)).reshape(2, -1)).reshape(*X.shape) | |
| return Z | |
| def plot_loc(ax, locs): | |
| ax.set_aspect("equal", adjustable="datalim") | |
| ax.set_ylim(-52, 252) | |
| ax.add_patch( | |
| plt.Rectangle( | |
| (-100, 0), width=200, height=250, facecolor="darkgray", edgecolor="dimgray" | |
| ) | |
| ) | |
| ax.add_patch( | |
| plt.Rectangle( | |
| (-80, 25), width=160, height=200, facecolor="gainsboro", edgecolor="dimgray" | |
| ) | |
| ) | |
| ax.add_patch( | |
| plt.Rectangle( | |
| (-60, 50), | |
| width=120, | |
| height=150, | |
| fill=False, | |
| edgecolor="yellowgreen", | |
| linestyle=":", | |
| ) | |
| ) | |
| ax.add_patch( | |
| plt.Rectangle( | |
| (-40, 75), width=80, height=100, facecolor="ivory", edgecolor="darkgray" | |
| ) | |
| ) | |
| ax.add_patch( | |
| plt.Polygon( | |
| [(0, -10), (45, -30), (51, -50), (-51, -50), (-45, -30), (0, -10)], | |
| facecolor="snow", | |
| edgecolor="darkgray", | |
| ) | |
| ) | |
| for (pitch,), _locs in locs.sort( | |
| pl.len().over("general_ballKind_code"), descending=True | |
| ).group_by("general_ballKind_code", maintain_order=True): | |
| if len(_locs) <= 2: | |
| continue | |
| Z = fit_pred_kde(_locs[["x", "y"]].to_numpy().T) | |
| Z = Z / Z.sum() | |
| Z_flat = Z.ravel() | |
| sorted_Z = np.sort(Z_flat) | |
| sorted_Z_idxs = np.argsort(Z_flat) | |
| Z_cumsum = (sorted_Z).cumsum() | |
| t = Z_flat[sorted_Z_idxs[np.argmin(np.abs(Z_cumsum - (1 - 0.4)))]] | |
| ax.contourf( | |
| X, Y, Z, levels=[t, 1], colors=ball_kind_code_to_color[pitch], alpha=0.5 | |
| ) | |
| ax.contour( | |
| X, | |
| Y, | |
| Z, | |
| levels=t.reshape(1), | |
| colors=ball_kind_code_to_color[pitch], | |
| alpha=0.75, | |
| ) | |
| def plot_velo(ax, velos): | |
| trans = transforms.blended_transform_factory(ax.transData, ax.transAxes) | |
| for (pitch,), _velos in velos.sort( | |
| pl.len().over("general_ballKind_code"), descending=True | |
| ).group_by("general_ballKind_code", maintain_order=True): | |
| _velos = _velos.filter( | |
| ( | |
| (pl.col("ballSpeed_mph") - pl.col("ballSpeed_mph").mean()) | |
| / pl.col("ballSpeed_mph").std() | |
| ).abs() | |
| < 3 | |
| ) | |
| if len(_velos) <= 1: | |
| continue | |
| violin = ax.violinplot( | |
| _velos["ballSpeed_mph"], | |
| orientation="horizontal", | |
| side="high", | |
| showextrema=False, | |
| ) | |
| for _violin in violin["bodies"]: | |
| _violin.set_facecolor(ball_kind_code_to_color[pitch]) | |
| mean = _velos["ballSpeed_mph"].mean() | |
| ax.text( | |
| mean, | |
| 0.5, | |
| round(mean), | |
| horizontalalignment="center", | |
| verticalalignment="center", | |
| color="gray", | |
| alpha=0.75, | |
| font=font, | |
| transform=trans, | |
| ) | |
| stat_cmap = LinearSegmentedColormap.from_list( | |
| "stat", colors=["dodgerblue", "snow", "crimson"] | |
| ) | |
| def plot_splits(ax, stats, stat_names): | |
| stats = stats.rename( | |
| { | |
| col: col.replace("PA_", "TBF_") | |
| for col in stats.columns | |
| if col.startswith("PA_") | |
| } | |
| ) | |
| table = mpl.table.Table(ax) | |
| rows = len(stat_names) + 1 | |
| cols = 3 # left, both, right | |
| cell_height = 1 / rows | |
| cell_width = 1 / cols | |
| def _add_cell(**kwargs): | |
| table.add_cell( | |
| width=cell_width, | |
| height=cell_height, | |
| fontproperties=font, | |
| edgecolor="white", | |
| **kwargs, | |
| ) | |
| _add_cell(row=0, col=0, text="LHH", loc="center") | |
| table[(0, 0)].get_text().set_color("gray") | |
| _add_cell(row=0, col=2, text="RHH", loc="center") | |
| table[(0, 2)].get_text().set_color("gray") | |
| for i, stat_name in enumerate(stat_names, start=1): | |
| for handedness in ("left", "right"): | |
| stat = stats[f"{stat_name}_{handedness}"].item() | |
| fmt = get_stat_val(stat_name, "str_format", "{}").replace(":.1f", ":.0f") | |
| if fmt[-1] == "%": | |
| stat *= 100 | |
| stat = fmt.format(stat) | |
| pctl = ( | |
| stats[f"{stat_name}_pctl_{handedness}"].item() | |
| if stat_name != "TBF" | |
| else None | |
| ) | |
| _add_cell( | |
| row=i, | |
| col=0 if handedness == "left" else 2, | |
| text=stat, | |
| loc="center", | |
| facecolor=stat_cmap(pctl) if pctl is not None else "white", | |
| ) | |
| _add_cell(row=i, col=1, text=stat_name, loc="center") | |
| # for _, cell in table.get_celld().items(): | |
| # cell.PAD = 0.2 | |
| table.set_fontsize(7) | |
| ax.add_artist(table) | |
| def plot_pitch_stats(ax, stats, stat_names): | |
| ax.set_aspect("equal", adjustable="datalim") | |
| # axis_to_data = lambda coords: ax.transData.inverted().transform(ax.transAxes.transform(coords)) | |
| table = mpl.table.Table(ax) | |
| rows = len(stat_names) + 1 | |
| cols = len(stats) + 1 | |
| cell_height = 1 / rows | |
| cell_width = 1 / cols | |
| for row, stat in enumerate(stat_names, start=1): | |
| if stat == "YpERA Grade": | |
| stat = "Grade" | |
| cell = table.add_cell( | |
| row=row, | |
| col=0, | |
| width=cell_width, | |
| height=cell_height, | |
| text=stat, | |
| loc="left", | |
| fontproperties=font, | |
| edgecolor="white", | |
| ) | |
| for col, pitch in enumerate(stats["ballKind_code"], start=1): | |
| color = ball_kind_code_to_color.get(pitch, "C0") | |
| cell = table.add_cell( | |
| row=0, | |
| col=col, | |
| width=cell_width, | |
| height=cell_height, | |
| text=pitch, | |
| loc="center", | |
| fontproperties=font, | |
| facecolor=color, | |
| edgecolor="white", | |
| ) | |
| cell.get_text().set_color(get_text_color_from_color(color)) | |
| _stats = stats.filter(pl.col("ballKind_code") == pitch) | |
| qualified = _stats["qualified"].item() | |
| for row, stat_name in enumerate(stat_names, start=1): | |
| stat = _stats[stat_name].item() | |
| stat_pctl = _stats[f"{stat_name}_pctl"].item() | |
| if not ( | |
| (stat_name == "SLG" and _stats["AB"].item() == 0) | |
| or (stat_name == "GB%" and _stats["BIP"].item() == 0) | |
| ): # TMP fix | |
| fmt = get_stat_val(stat_name, "str_format") | |
| postprocess = lambda x: x | |
| if fmt.endswith("%"): | |
| fmt = "{:.0%}" | |
| if fmt == "{:.3f}": | |
| postprocess = lambda x: x.lstrip("0") | |
| text = postprocess(fmt.format(stat)) | |
| color = stat_cmap([0, stat_pctl, 1])[1] if qualified else "gainsboro" | |
| else: | |
| text = "-" | |
| color = "gainsboro" | |
| cell = table.add_cell( | |
| row=row, | |
| col=col, | |
| width=cell_width, | |
| height=cell_height, | |
| text=text, | |
| loc="center", | |
| fontproperties=font, | |
| facecolor=color, | |
| edgecolor="white", | |
| ) | |
| # cell = table.add_cell(row=row, col=col, width=cell_width, height=cell_height, text=fmt.format(stat), loc='center', fontproperties=font, facecolor=(stat_cmap([0, stat_pctl, 1])[1] if qualified else 'gainsboro'), edgecolor='white') | |
| if not qualified: | |
| cell.get_text().set_color("gray") | |
| ax.add_artist(table) | |
| def plot_pitcher_stats(ax, stats, stat_names): | |
| stats = stats.rename({"PA": "TBF"}) | |
| ax.set_aspect("equal", adjustable="datalim") | |
| table = mpl.table.Table(ax) | |
| cell_height = 1 | |
| cell_width = 1 / (len(stat_names) * 2) | |
| qualified = stats["qualified"].item() | |
| for i, stat_name in enumerate(stat_names): | |
| stat = stats[stat_name].item() | |
| fmt = get_stat_val(stat_name, "str_format", "{}").replace(":.1f", ":.0f") | |
| if fmt[-1] == "%": | |
| stat *= 100 | |
| stat = fmt.format(stat) | |
| pctl = stats[f"{stat_name}_pctl"].item() if stat_name != "TBF" else None | |
| table.add_cell( | |
| row=0, | |
| col=i * 2, | |
| width=cell_width, | |
| height=cell_height, | |
| text=stat_name, | |
| loc="center", | |
| fontproperties=font, | |
| edgecolor="white", | |
| ) | |
| cell = table.add_cell( | |
| row=0, | |
| col=i * 2 + 1, | |
| width=cell_width, | |
| height=cell_height, | |
| text=stat, | |
| loc="center", | |
| fontproperties=font, | |
| facecolor=(stat_cmap(pctl) if qualified else "gainsboro") | |
| if pctl is not None | |
| else "white", | |
| edgecolor="white", | |
| ) | |
| if not qualified: | |
| cell.get_text().set_color("gray") | |
| ax.add_artist(table) | |
| font = load_google_font("Saira Extra Condensed", weight="medium") | |
| def create_pitcher_overview_card(id, start_date, end_date, game_kinds, dpi=300): | |
| data = get_card_data( | |
| id, | |
| start_date=start_date, | |
| end_date=end_date, | |
| game_kinds=game_kinds, | |
| pitch_class_type="general", | |
| ) # temporary let min_pitches=1 | |
| fig = plt.figure(figsize=(1080 / 300, 1350 / 300), dpi=dpi) | |
| gs = fig.add_gridspec(8, 6, height_ratios=[1, 1, 1.5, 4, 1, 5, 1, 0.5]) | |
| title_ax = fig.add_subplot(gs[0, :]) | |
| title_ax.text( | |
| x=0, | |
| y=0, | |
| s=data.player_stats["pitcher_name"].item().upper(), | |
| verticalalignment="baseline", | |
| font=font, | |
| size=20, | |
| ) | |
| scope = "" | |
| if start_date == end_date: | |
| scope = start_date.strftime("%y.%m.%d") | |
| else: | |
| scope = start_date.strftime("%y.%m.%d") + " - " + end_date.strftime("%y.%m.%d") | |
| title_game_kinds = [] | |
| for game_kind, abbr in { | |
| "Spring Training": "ST", | |
| "Regular Season": "REG", | |
| "Postseason": "POST", | |
| }.items(): | |
| if game_kind in game_kinds: | |
| title_game_kinds.append(abbr) | |
| scope += "\n" + ",".join(title_game_kinds) | |
| title_ax.text( | |
| 1, | |
| 0.5, | |
| scope, | |
| verticalalignment="center_baseline", | |
| horizontalalignment="right", | |
| font=font, | |
| size=9, | |
| ) | |
| arsenal_ax = fig.add_subplot(gs[1, :]) | |
| plot_arsenal(arsenal_ax, data.pitch_stats["ballKind_code"]) | |
| usage_l_ax = fig.add_subplot(gs[2, :3]) | |
| plot_usage(usage_l_ax, data.pitch_stats[["ballKind_code", "Usage_left"]]) | |
| usage_l_ax.text( | |
| 0, | |
| 1, | |
| "LHH usage", | |
| horizontalalignment="left", | |
| verticalalignment="top", | |
| linespacing=0.5, | |
| color="gray", | |
| font=font, | |
| size=10, | |
| transform=usage_l_ax.transAxes, | |
| ) | |
| usage_r_ax = fig.add_subplot(gs[2, 3:]) | |
| plot_usage(usage_r_ax, data.pitch_stats[["ballKind_code", "Usage_right"]]) | |
| usage_r_ax.text( | |
| 0, | |
| 1, | |
| "RHH usage", | |
| horizontalalignment="left", | |
| verticalalignment="top", | |
| linespacing=0.5, | |
| color="gray", | |
| font=font, | |
| size=10, | |
| transform=usage_r_ax.transAxes, | |
| ) | |
| loc_l_ax = fig.add_subplot(gs[3, :2]) | |
| loc_l_ax.text( | |
| 0, | |
| 1, | |
| "LHH\nloc", | |
| verticalalignment="top", | |
| horizontalalignment="left", | |
| color="gray", | |
| font=font, | |
| size=9, | |
| transform=loc_l_ax.transAxes, | |
| ) | |
| plot_loc(loc_l_ax, data.left_pitch_shapes) | |
| loc_r_ax = fig.add_subplot(gs[3, 4:]) | |
| loc_r_ax.text( | |
| 0, | |
| 1, | |
| "RHH\nloc", | |
| verticalalignment="top", | |
| horizontalalignment="left", | |
| color="gray", | |
| font=font, | |
| size=9, | |
| transform=loc_r_ax.transAxes, | |
| ) | |
| plot_loc(loc_r_ax, data.right_pitch_shapes) | |
| splits_ax = fig.add_subplot(gs[3, 2:4]) | |
| plot_splits( | |
| splits_ax, data.player_stats, ["TBF", "CSW%", "K%", "BB%", "GB%", "SLG"] | |
| ) | |
| velo_ax = fig.add_subplot(gs[4, :]) | |
| plot_velo(velo_ax, data.both_pitch_shapes) | |
| velo_ax.text( | |
| 0, | |
| 1, | |
| "Velo", | |
| verticalalignment="top", | |
| horizontalalignment="left", | |
| color="gray", | |
| font=font, | |
| size=10, | |
| transform=velo_ax.transAxes, | |
| ) | |
| pitch_stats_ax = fig.add_subplot(gs[5, :]) | |
| plot_pitch_stats( | |
| pitch_stats_ax, | |
| data.pitch_stats, | |
| ["SwStr%", "CSW%", "Zone%", "GB%", "SLG", "YpERA Grade"], | |
| ) | |
| pitcher_stats_ax = fig.add_subplot(gs[6, :]) | |
| plot_pitcher_stats( | |
| pitcher_stats_ax, data.player_stats, ["TBF", "CSW%", "K%", "BB%", "GB%", "SLG"] | |
| ) | |
| credits_ax = fig.add_subplot(gs[7, :]) | |
| credits_ax.text( | |
| x=0, | |
| y=0.5, | |
| s="Data: SPAIA, Sanspo", | |
| verticalalignment="center", | |
| font=font, | |
| size=7, | |
| ) | |
| credits_ax.text( | |
| x=1, | |
| y=0.5, | |
| s="@bacc_prop @ws20g4 @yakyucosmo", | |
| horizontalalignment="right", | |
| verticalalignment="center", | |
| font=font, | |
| size=7, | |
| ) | |
| for ax in [ | |
| title_ax, | |
| arsenal_ax, | |
| usage_l_ax, | |
| usage_r_ax, | |
| loc_l_ax, | |
| loc_r_ax, | |
| splits_ax, | |
| velo_ax, | |
| pitch_stats_ax, | |
| pitcher_stats_ax, | |
| credits_ax, | |
| ]: | |
| ax.axis("off") | |
| ax.tick_params( | |
| axis="both", which="both", length=0, labelbottom=False, labelleft=False | |
| ) | |
| return fig | |
| # DAILY/WEEKLY LEADERBOARDS | |
| def get_whiff_leaderboard_data(data, leaders, include_date): | |
| data = ( | |
| data.group_by("pitId", "pitcher_team_name_short", "date") | |
| .agg(pl.col("pitcher_name").first(), pl.col("whiff").sum().alias("Whiffs")) | |
| .sort(["Whiffs", "pitcher_name"], descending=[True, False]) | |
| ) | |
| # if len(data) > 0: | |
| # data = data.filter(pl.col('Whiffs') >= data['Whiffs'][min(leaders, len(data)-1)]) | |
| data = ( | |
| data.rename( | |
| {"pitcher_name": "Player", "pitcher_team_name_short": "Team"} | |
| ).with_columns( | |
| pl.col("date").dt.to_string("%m.%d").alias("Date"), | |
| pl.col("Whiffs").rank(descending=True, method="min").alias("Rank"), | |
| )[["Rank", "Team", "Player"] + (["Date"] if include_date else []) + ["Whiffs"]] | |
| # .with_row_index('Rank', 1) | |
| ) | |
| # data = data.filter(pl.col('Rank') <= leaders) | |
| data = data.filter( | |
| pl.col("Rank") | |
| <= data.group_by("Rank") | |
| .agg(pl.len()) | |
| .sort("Rank") | |
| .filter(pl.col("len").cum_sum() >= leaders)["Rank"] | |
| .min() | |
| ) | |
| return data | |
| def get_velo_leaderboard_data(data, leaders): | |
| data = data.sort(["ballSpeed", "pitcher_name"], descending=[True, False]) | |
| # if len(data) > 0: | |
| # data = data.filter(pl.col('ballSpeed') >= data['ballSpeed'][min(leaders, len(data)-1)]) | |
| data = ( | |
| data.rename( | |
| { | |
| "ballSpeed": "KPH", | |
| "pitcher_name": "Player", | |
| "pitcher_team_name_short": "Team", | |
| } | |
| ) | |
| # .with_row_index('Rank', 1) | |
| .with_columns( | |
| (pl.col("KPH") / 1.609).round(1).alias("MPH"), | |
| pl.col("KPH").rank(descending=True, method="min").alias("Rank"), | |
| )[["Rank", "Team", "Player", "KPH", "MPH"]] | |
| ) | |
| # data = data.filter(pl.col('Rank') <= leaders) | |
| data = data.filter( | |
| pl.col("Rank") | |
| <= data.group_by("Rank") | |
| .agg(pl.len()) | |
| .sort("Rank") | |
| .filter(pl.col("len").cum_sum() >= leaders)["Rank"] | |
| .min() | |
| ) | |
| return data | |
| def create_daily_weekly_leaderboard(stat, leaderboard_date, time_type, leaders, data): | |
| font = load_google_font("Saira Extra Condensed", weight="medium") | |
| bold_font = load_google_font("Saira Extra Condensed", weight="bold") | |
| date_font = load_google_font("Lekton", weight="bold") | |
| assert stat in ("velo", "whiff") | |
| assert time_type in ("daily", "weekly") | |
| if time_type == "daily": | |
| data = filter_data_by_date_and_game_kind( | |
| data, start_date=leaderboard_date, end_date=leaderboard_date | |
| ) | |
| else: | |
| monday = leaderboard_date - datetime.timedelta(days=leaderboard_date.weekday()) | |
| sunday = leaderboard_date + datetime.timedelta( | |
| days=6 - leaderboard_date.weekday() | |
| ) | |
| data = filter_data_by_date_and_game_kind( | |
| data, start_date=monday, end_date=sunday | |
| ) | |
| leaderboard = ( | |
| get_velo_leaderboard_data(data, leaders) | |
| if stat == "velo" | |
| else get_whiff_leaderboard_data( | |
| data, leaders, include_date=time_type == "weekly" | |
| ) | |
| ) | |
| stats = [ | |
| col for col in leaderboard.columns if col not in ["Rank", "Team", "Player"] | |
| ] | |
| stat_col_lens = [ | |
| 1 | |
| if max(leaderboard[stat].cast(pl.String).str.len_chars().max() or 0, len(stat)) | |
| < 5 | |
| else 1.5 | |
| for stat in stats | |
| ] | |
| dpi = 300 | |
| fig = plt.figure(figsize=(1080 / 300, 1350 / 300), dpi=dpi) | |
| gs = fig.add_gridspec( | |
| max(len(leaderboard), 1) + 2, | |
| 3 + len(stats), | |
| height_ratios=[1] | |
| + ( | |
| [9 / (len(leaderboard) + 1)] * (len(leaderboard) + 1) | |
| if len(leaderboard) | |
| else [1, 8] | |
| ), | |
| width_ratios=[1, 1, 8 - sum(stat_col_lens)] + stat_col_lens, | |
| ) | |
| data_offset = 2 | |
| axs = [] | |
| def create_and_add_subplot(indexed_gs): | |
| ax = fig.add_subplot(indexed_gs) | |
| axs.append(ax) | |
| return ax | |
| title_ax = create_and_add_subplot(gs[0, :]) | |
| title_ax.text( | |
| 0, | |
| 0.1, | |
| f"{time_type.upper()} {stat.upper()} LEADERBOARD", | |
| verticalalignment="baseline", | |
| font=bold_font, | |
| size=15, | |
| ) | |
| if time_type == "daily": | |
| title_ax.text( | |
| 1, | |
| 0.1, | |
| leaderboard_date.strftime("%Y.%m.%d (%a)"), | |
| verticalalignment="baseline", | |
| horizontalalignment="right", | |
| font=date_font, | |
| size=7, | |
| ) | |
| else: | |
| title_ax.text( | |
| 1, | |
| 0.1, | |
| monday.strftime("%Y.%m.%d (%a)") + "\n-" + sunday.strftime("%Y.%m.%d (%a)"), | |
| verticalalignment="baseline", | |
| horizontalalignment="right", | |
| font=date_font, | |
| size=7, | |
| ) | |
| rank_ax = create_and_add_subplot(gs[data_offset - 1, 0]) | |
| rank_ax.text( | |
| 0.5, | |
| 0, | |
| "RANK", | |
| verticalalignment="bottom", | |
| horizontalalignment="center", | |
| font=bold_font, | |
| ) | |
| team_ax = create_and_add_subplot(gs[data_offset - 1, 1]) | |
| team_ax.text( | |
| 0.5, | |
| 0, | |
| "TEAM", | |
| verticalalignment="bottom", | |
| horizontalalignment="center", | |
| font=bold_font, | |
| ) | |
| player_ax = create_and_add_subplot(gs[data_offset - 1, 2]) | |
| player_ax.text(0, 0, "PLAYER", verticalalignment="bottom", font=bold_font) | |
| for col, stat in enumerate(stats, start=3): | |
| stat_ax = create_and_add_subplot(gs[data_offset - 1, col]) | |
| stat_ax.text( | |
| 0.5, | |
| 0, | |
| stat.upper(), | |
| verticalalignment="bottom", | |
| horizontalalignment="center", | |
| font=bold_font, | |
| ) | |
| midline_ax = create_and_add_subplot(gs[data_offset - 1, :]) | |
| midline_ax.add_patch(plt.Rectangle((0, 0), 1, 0.01, color="black")) | |
| if len(leaderboard) == 0: | |
| blank_ax = create_and_add_subplot(gs[data_offset:]) | |
| for i, row in enumerate(leaderboard.iter_rows()): | |
| rank, team, player, *stats = row | |
| rank_ax = create_and_add_subplot(gs[i + data_offset, 0]) | |
| rank_ax.text( | |
| 0.5, | |
| 0.5, | |
| rank, | |
| verticalalignment="center_baseline", | |
| horizontalalignment="center", | |
| font=font, | |
| ) | |
| team_ax = create_and_add_subplot(gs[i + data_offset, 1]) | |
| image = Image.open( | |
| os.path.join("assets", "white_insignias", f"{team.lower()}.png") | |
| ) | |
| w, h = image.size | |
| new_longer_side = 512 | |
| if w > h: | |
| w, h = (new_longer_side, round(h * new_longer_side / w)) | |
| else: | |
| w, h = (round(w * new_longer_side / h), new_longer_side) | |
| image = image.resize((w, h)) | |
| ax_s = 512 * 1.5 | |
| team_ax.set_xlim(0, ax_s) | |
| team_ax.set_ylim(0, ax_s) | |
| image = np.array(image) | |
| circle = plt.Circle( | |
| (ax_s / 2, ax_s / 2), | |
| radius=ax_s / 2, | |
| color=team_names_short_to_color[team], | |
| clip_on=False, | |
| zorder=1, | |
| ) | |
| team_ax.add_patch(circle) | |
| team_ax.imshow( | |
| image[..., -1], | |
| cmap=LinearSegmentedColormap.from_list( | |
| "tmp", | |
| [ | |
| team_names_short_to_color[team], | |
| "black" if team in ("Lotte", "Hanshin") else "white", | |
| ], | |
| ), | |
| extent=( | |
| (ax_s - w) / 2, | |
| ax_s - (ax_s - w) / 2, | |
| (ax_s - h) / 2, | |
| ax_s - (ax_s - h) / 2, | |
| ), | |
| zorder=2, | |
| ) | |
| player_ax = create_and_add_subplot(gs[i + data_offset, 2]) | |
| player_ax.text( | |
| 0.02, | |
| 0.5, | |
| player.upper(), | |
| verticalalignment="center_baseline", | |
| font=font, | |
| color=get_text_color_from_team(team), | |
| ) | |
| player_ax.add_patch( | |
| plt.Polygon( | |
| [(0, 0), (0.98, 0), (1, 0.5), (1, 1), (0, 1)], | |
| color=team_names_short_to_color[team], | |
| clip_on=False, | |
| ) | |
| ) | |
| for col, stat in enumerate(stats, start=3): | |
| stat_ax = create_and_add_subplot(gs[i + data_offset, col]) | |
| stat_ax.text( | |
| 0.5, | |
| 0.5, | |
| stat, | |
| verticalalignment="center_baseline", | |
| horizontalalignment="center", | |
| font=font, | |
| ) | |
| for ax in axs: | |
| ax.axis("off") | |
| ax.tick_params( | |
| axis="both", which="both", length=0, labelbottom=False, labelleft=False | |
| ) | |
| return fig | |