Skip to content

Commit

Permalink
Complete Overhaul FE (#5)
Browse files Browse the repository at this point in the history
* add lots to team overview page

* checkin before restructuring to multipage

* total FE overhaul

* minor updates

* make sure state is there even if they start on sub page
  • Loading branch information
NathanEmb authored Dec 10, 2024
1 parent 5abcc77 commit 60645a2
Show file tree
Hide file tree
Showing 12 changed files with 503 additions and 366 deletions.
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ requires-python = ">=3.13"
dependencies = [
"espn-api>=0.43.0",
"pandas>=2.2.3",
"panel>=1.5.4",
"streamlit-autorefresh>=1.0.1",
"streamlit>=1.40.2",
"watchfiles>=1.0.0",
"matplotlib>=3.9.3",
"groq>=0.13.0",
]

[tool.ruff]
Expand Down
117 changes: 110 additions & 7 deletions src/backend.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import os
import random
from copy import deepcopy

import pandas as pd
from espn_api.basketball import League, Team
from groq import Groq

import src.constants as const
import src.prompts as prompts


def get_league(league_id: int = const.SPACEJAM_LEAGUE_ID, year: int = const.YEAR):
def get_league(league_id: int = const.SPACEJAM_LEAGUE_ID, year: int = const.YEAR) -> League:
"""Get the league object for the specified league_id and year."""
league = League(league_id, year)
league.teams = {team.team_name: team for team in league.teams}
return league


def get_league_all_raw_stats_df(league: League):
def get_league_all_raw_stats_df(league: League) -> pd.DataFrame:
"""Get every team's stats for all categories."""
league_stats = []
for team in league.teams.values():
temp_dict = deepcopy(team.stats)
Expand All @@ -26,7 +32,8 @@ def get_league_all_raw_stats_df(league: League):
return df[list(const.ALL_RAW_DATA_TABLE_DEF.keys())].sort_values(by="Standing")


def get_league_all_raw_data_rankings(league: League):
def get_league_all_raw_data_rankings(league: League) -> pd.DataFrame:
"""Get every team's ranking for all stats."""
raw_stats_df = get_league_all_raw_stats_df(league)
# Rank only numeric columns
want_big_num_df = raw_stats_df[const.WANT_BIG_NUM]
Expand All @@ -42,7 +49,8 @@ def get_league_all_raw_data_rankings(league: League):
return ranked_df[list(const.ALL_DATA_RANKED_TABLE_DEF.keys())].sort_values(by="Standing")


def get_league_cat_raw_stats_df(league: League):
def get_league_cat_raw_stats_df(league: League) -> pd.DataFrame:
"""Get every team's stats for only roto categories."""
league_stats = []
for team in league.teams.values():
temp_dict = deepcopy(team.stats)
Expand All @@ -56,7 +64,8 @@ def get_league_cat_raw_stats_df(league: League):
return df[list(const.CAT_ONLY_RAW_DATA_TABLE_DEF.keys())].sort_values(by="Standing")


def get_league_cat_data_rankings(league: League):
def get_league_cat_data_rankings(league: League) -> pd.DataFrame:
"""Get every team's ranking for only roto categories."""
raw_stats_df = get_league_all_raw_stats_df(league)
# Rank only numeric columns
want_big_num_df = raw_stats_df[const.WANT_BIG_NUM]
Expand All @@ -71,11 +80,105 @@ def get_league_cat_data_rankings(league: League):
return ranked_df[list(const.CAT_ONLY_DATA_RANKED_TABLE_DEF.keys())].sort_values(by="Standing")


def get_average_team_stats(team: Team, num_days: int):
def get_average_team_stats(team: Team, num_days: int) -> pd.DataFrame:
"""Get Stats for team averaged over specified number of days From todays date."""
SUPPORTED_TIMES = [30, 15, 7]

if num_days not in SUPPORTED_TIMES:
raise ValueError(f"num_days must be one of {SUPPORTED_TIMES}")

stat_key = f"{const.YEAR}_last_{num_days}"
player_avgs = {player.name: player.stats[stat_key].get("avg", {}) for player in team.roster}
return player_avgs
player_avgs = pd.DataFrame(player_avgs).T.fillna(0)
pd.set_option("future.no_silent_downcasting", True) # otherwise FutureWarning
return player_avgs.replace("Infinity", 0).round(2) # Replace Infinity with 0 kinda hacky but eh


def agg_player_avgs(
seven_day_stats: pd.DataFrame, fifteen_day_stats: pd.DataFrame, thirty_day_stats: pd.DataFrame
) -> pd.DataFrame:
"""Aggregate player averages over different timeframes."""

avg_seven_day_stats = seven_day_stats.aggregate("mean")
avg_fifteen_day_stats = fifteen_day_stats.aggregate("mean")
avg_thirty_day_stats = thirty_day_stats.aggregate("mean")
agg_stats = pd.DataFrame(
{
"Past 7 Days": avg_seven_day_stats[const.NINE_CATS],
"Past 15 Days": avg_fifteen_day_stats[const.NINE_CATS],
"Past 30 Days": avg_thirty_day_stats[const.NINE_CATS],
}
)
return agg_stats


def get_team_breakdown(team_cat_ranks: dict) -> tuple[dict, dict, dict]:
"""Given a row from the league rankings dataframe, parse the team's strengths, weaknesses, and punts.
Args:
team_cat_ranks (dict): A row from the league rankings dataframe.
Returns:
strengths (list): The categories in which the team excels.
weaknesses (list): The categories in which the team is average.
punts (list): The categories in which the team is weak."""
strengths = {}
weaknesses = {}
punts = {}

for cat in const.NINE_CATS:
if team_cat_ranks[cat] <= 4:
strengths[cat] = team_cat_ranks[cat]
elif team_cat_ranks[cat] >= 8:
punts[cat] = team_cat_ranks[cat]
else:
weaknesses[cat] = team_cat_ranks[cat]
return strengths, weaknesses, punts


def get_prompt(prompt_map: dict):
"""
Returns a value from the dictionary based on the weighted probability.
:param prompt_map: A dictionary where keys are percentages (adding up to 1.0) and values are strings.
:return: A randomly selected value based on the key percentages.
"""
rand_val = random.random() # Random float between 0 and 1.
cumulative = 0

for percent, prompt in sorted(prompt_map.items()):
cumulative += percent
if rand_val < cumulative:
return prompt


def get_mainpage_joke():
client = Groq(
# This is the default and can be omitted
api_key=os.environ.get("GROQ_API_KEY")
)
prompt = get_prompt(prompts.mainpage_prompt_map)
chat_completion = client.chat.completions.create(messages=prompt, model="llama3-8b-8192")
return chat_completion.choices[0].message.content


def get_teamviewer_joke(team_name):
client = Groq(
# This is the default and can be omitted
api_key=os.environ.get("GROQ_API_KEY")
)
prompt = [
{
"role": "system",
"content": "Your job is to roast fantasy basketball team names. Be witty, and a little mean.",
},
{
"role": "user",
"content": f"Roast the team name choice of: '{team_name}'. Limit response to 100 characters",
},
]
chat_completion = client.chat.completions.create(messages=prompt, model="llama3-8b-8192")
return chat_completion.choices[0].message.content


if __name__ == "__main__":
Expand Down
6 changes: 6 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
SPACEJAM_LEAGUE_ID = 233677
YEAR = 2025

# STREAMLIT CONSTANTS
STREAMLIT_PAGE_TITLE = "SpaceJam Fantasy Basketball League"
LEAGUE_OVERVIEW_TITLE = "League Overview"
TEAM_PAGE_TITLE = "Team Viewer"
MATCHUP_PAGE_TITLE = "The Thunderdome"

NINE_CATS = ["PTS", "BLK", "STL", "AST", "REB", "TO", "3PM", "FG%", "FT%"]

ALL_RAW_DATA_TABLE_DEF = {
Expand Down
49 changes: 49 additions & 0 deletions src/frontend/Spacejam_Dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import matplotlib.pyplot as plt
import streamlit as st
from streamlit_autorefresh import st_autorefresh

import src.backend as be

# App configuration
icon_url = "https://spacejam-dashboard.s3.us-east-2.amazonaws.com/assets/the-last-spacejam.jpg"
st.set_page_config(layout="wide", page_title="Spacejam Dashboard", page_icon=icon_url)
st.logo(icon_url, size="large")
refresh_in_sec = 600
count = st_autorefresh(interval=refresh_in_sec * 1000, limit=100, key="statscounter")


# Cached data behind it all
@st.cache_data
def update_league_data():
return be.get_league()


league_data = update_league_data()
if "league_data" not in st.session_state:
st.session_state.league_data = league_data
league_df = be.get_league_cat_data_rankings(league_data)
if "league_df" not in st.session_state:
st.session_state.league_df = league_df
teams = [team.team_name for team in league_data.teams.values()]
if "teams" not in st.session_state:
st.session_state.teams = teams

# Sidebar for page selection
st.sidebar.success("Welcome to the Spacejam Dashboard, written by the Tatums.")
st.sidebar.subheader("And now, a joke powered by AI 🤖")
st.sidebar.write(be.get_mainpage_joke())

# Main Page content
st.title("Spacejam Dashboard")
st.subheader("About")
st.markdown(
"This dashboard serves two purposes: \n 1. Be a fun project for me to work on. \n 2. Provide some more data to everyone who isn't already paying for a fancy schmancy site already."
)
st.subheader("Category Rankings (Green is good)")
st.markdown(
"This was the first bit of data that I wanted to understand when I was left with my head in my hands after a loss to Will saying 'What are the Tatum's good for?'"
)

gyr = plt.colormaps["RdYlGn"].reversed()
league_df_styled = league_df.style.background_gradient(cmap=gyr)
st.dataframe(league_df_styled, use_container_width=True, hide_index=True, height=460)
29 changes: 29 additions & 0 deletions src/frontend/figures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import math

import matplotlib.cm as cm
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import src.constants as const


def create_cat_bar_charts(agg: pd.DataFrame) -> list[plt.Figure]:
"""Create a bar chart for each column in the DataFrame."""
# Create a figure with subplots
num_columns = len(agg.columns)
root = np.ceil(math.sqrt(num_columns)).astype(int)
fig, axes = plt.subplots(root, root, figsize=(15, 10), constrained_layout=True)

# Plot each column as a bar chart in its own subplot
colors = cm.viridis(np.linspace(0, 1, 3)) # You can use any colormap
for i in range(3):
for j in range(3):
index = i * 3 + j
column_data = const.NINE_CATS[index]
axes[i, j].bar(agg.index, agg[column_data], color=colors) # Bar chart
axes[i, j].set_title(column_data) # Set title to column name
axes[i, j].set_xticks(range(len(agg.index))) # Position the ticks
axes[i, j].set_xticklabels(agg.index, rotation=45) # Rotate for readability
axes[i, j].set_ylabel("Avg Per Game") # Label for y-axis
return fig
92 changes: 92 additions & 0 deletions src/frontend/pages/1_Team_Viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import streamlit as st
from espn_api.basketball import Team

import src.backend as be
import src.constants as const
import src.frontend.figures as fig
import src.frontend.streamlit_utils as su

# App configuration
icon_url = "https://spacejam-dashboard.s3.us-east-2.amazonaws.com/assets/the-last-spacejam.jpg"
st.logo(icon_url, size="large")


# Cached data behind it all
@st.cache_data
def update_league_data():
return be.get_league()


league_data = update_league_data()
if "league_data" not in st.session_state:
st.session_state.league_data = league_data
league_df = be.get_league_cat_data_rankings(league_data)
if "league_df" not in st.session_state:
st.session_state.league_df = league_df
teams = [team.team_name for team in league_data.teams.values()]
if "teams" not in st.session_state:
st.session_state.teams = teams


league_data = st.session_state.league_data
teams = st.session_state.teams
league_df = st.session_state.league_df

st.sidebar.subheader("And now, a joke powered by AI 🤖")

st.title(const.TEAM_PAGE_TITLE)
chosen_team = st.selectbox("Team", teams)
st.sidebar.write(be.get_teamviewer_joke(chosen_team))
team_data = league_data.teams[chosen_team]
seven_day_stats = be.get_average_team_stats(team_data, 7)
fifteen_day_stats = be.get_average_team_stats(team_data, 15)
thirty_day_stats = be.get_average_team_stats(team_data, 30)
agg_stats = be.agg_player_avgs(seven_day_stats, fifteen_day_stats, thirty_day_stats)
team_obj: Team = league_data.teams[chosen_team]
standing_col, record_col, acquisitions_col, div_col = st.columns(4, vertical_alignment="center")
with standing_col:
st.metric("League Wide Standing", f"{team_obj.standing}")
with div_col:
st.metric("Division:", f"{team_obj.division_name}")
with record_col:
st.metric("Current Record:", f"{team_obj.wins}W - {team_obj.losses}L - {team_obj.ties}T")
with acquisitions_col:
st.metric("Total Acquisitions Used:", f"{team_obj.acquisitions} / 70")

st.header("Category Rankings")
left_col, mid_col, right_col = st.columns(3)
team_data = league_df.loc[league_df["Team"] == chosen_team].to_dict("records")[0]
strengths, weaknesses, punts = be.get_team_breakdown(team_data)

num_cats_per_row = 3
with left_col:
st.subheader("Team Strengths")
st.markdown("Team ranks in top 4 of these categories.")
su.create_metric_grid(strengths, num_cats_per_row)

with mid_col:
st.subheader("Could go either way")
st.markdown("Team ranks in middle 4 of these categories.")
su.create_metric_grid(weaknesses, num_cats_per_row)

with right_col:
st.subheader("Team Punts")
st.markdown("Team ranks in bottom 4 of league in these categories. (hopefully on purpose)")
su.create_metric_grid(punts, num_cats_per_row)

with st.expander("🏀 Individual Player Stats"):
timeframe = st.radio("Past:", ["7 Days", "15 Days", "30 Days"], horizontal=True)
show_cols = st.multiselect(
"Column Filter", options=seven_day_stats.columns, default=const.NINE_CATS
)
if timeframe == "7 Days":
st.dataframe(seven_day_stats[show_cols], use_container_width=True)
elif timeframe == "15 Days":
st.dataframe(fifteen_day_stats[show_cols], use_container_width=True)
elif timeframe == "30 Days":
st.dataframe(thirty_day_stats[show_cols], use_container_width=True)

with st.expander("📊 Team Category Trends"):
st.write("Team Trends - ", chosen_team)
fig = fig.create_cat_bar_charts(agg_stats.T)
st.pyplot(fig)
31 changes: 31 additions & 0 deletions src/frontend/pages/2_Matchup_Viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import streamlit as st

import src.backend as be

# App configuration
icon_url = "https://spacejam-dashboard.s3.us-east-2.amazonaws.com/assets/the-last-spacejam.jpg"
st.set_page_config(layout="wide", page_title="Spacejam Dashboard", page_icon=icon_url)
st.logo(icon_url, size="large")


# Cached data behind it all
@st.cache_data
def update_league_data():
return be.get_league()


league_data = update_league_data()
if "league_data" not in st.session_state:
st.session_state.league_data = league_data
league_df = be.get_league_cat_data_rankings(league_data)
if "league_df" not in st.session_state:
st.session_state.league_df = league_df
teams = [team.team_name for team in league_data.teams.values()]
if "teams" not in st.session_state:
st.session_state.teams = teams


st.title("Coming soon....🚧")

st.header("And now, a joke powered by AI 🤖")
st.write(be.get_mainpage_joke())
Loading

0 comments on commit 60645a2

Please sign in to comment.