import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import pandas as pd
import plotly.express as px
import dash.dash_table as dt
# Data
df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-38/one-hit-wonders.csv"
)
sports = sorted(df["sport_name"].dropna().unique())
leagues = sorted(df["league"].dropna().unique())
years = sorted(df["year"].dropna().unique())
# SPORT EMOJIS
SPORT_EMOJIS = {
"basketball": "π",
"baseball": "βΎ",
"football": "π",
"hockey": "π",
"tennis": "πΎ",
"golf": "ππΌββοΈ",
}
sport_options = [
{
"label": f'{SPORT_EMOJIS.get(s, "π
")} {s.title()}',
"value": s
} for s in sports
]
league_check_options = [{"label": l.upper(), "value": l} for l in leagues]
year_options = [{"label": "All years", "value": "all"}] + [
{"label": str(y), "value": y} for y in years
]
# Nice colors
sport_colors = {
"basketball": "#28559f",
"baseball": "#ffbe0b",
"football": "#ff006e",
"hockey": "#32936f",
"tennis": "#72b1d1",
"default": "#272f30",
}
def get_color(sport):
return sport_colors.get(str(sport).lower(), sport_colors["default"])
color_map = {s: get_color(s) for s in sports}
def kpi_card(label, value, icon=None):
return dbc.Card(
dbc.CardBody([
html.Div([
html.Div(className=f"bi {icon or ''}",
style={"color": "#2667ff", "fontSize": "2.5rem", "marginBottom": "0.3rem"}),
html.H6(label, className="mb-1 text-secondary", style={"textAlign": "center"}),
html.H3(value, className="mb-0 fw-bold", style={"textAlign": "center"}),
], className="d-flex flex-column align-items-center justify-content-center", style={"height": "100%"}),
]),
className="shadow-sm m-1 d-flex align-items-center justify-content-center text-center",
style={
"background": "white",
"minHeight": "130px",
"border": "2.5px solid #fff",
"borderRadius": "18px",
},
)
app = dash.Dash(
__name__,
external_stylesheets=[
dbc.themes.BOOTSTRAP,
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css",
],
)
app.layout = dbc.Container([
html.H1(
"π
One-Hit Wonders β Interactive Sports Timeline Dashboard",
className="mb-2 text-center fw-bold",
style={
"letterSpacing": "2.5px",
"color": "#F0EEEA",
"background": "linear-gradient(82deg, #132345 0%, #173a65 47%, #235199 100%)",
"borderRadius": "20px",
"marginTop": "18px",
}
),
html.Div(
"π‘ Tip: Hover points for details; filter by player, sport, league or year below.",
className="mb-3 text-center",
style={
"color": "#1f3c71",
"fontSize": "1.14rem",
"fontWeight": "500"
}
),
dbc.Row([
dbc.Col(kpi_card("Total Games Played", "-", "bi-bar-chart-fill"), md=3),
dbc.Col(kpi_card("Unique Players", "-", "bi-person-fill"), md=3),
dbc.Col(kpi_card("Average Games / Player", "-", "bi-graph-up"), md=3),
dbc.Col(kpi_card("Top Year", "-", "bi-trophy-fill"), md=3),
], className="mb-3 gx-4", id="kpi-row"),
dbc.Row([
dbc.Col([
dbc.Label("Sport(s):", className="fw-semibold"),
dcc.Dropdown(
id="sport-filter",
options=sport_options,
value=sports,
multi=True,
style={"background": "white"}
),
], md=3),
dbc.Col([
dbc.Label("League(s):", className="fw-semibold mb-2"),
dbc.Card([
dbc.CardBody(
dcc.Checklist(
id="league-checklist",
options=league_check_options,
value=leagues,
inputStyle={
"marginRight": "8px",
"accentColor": "#205295",
"transform": "scale(1.25)"
},
labelStyle={
"display": "inline-block",
"marginRight": "18px",
"padding": "5px 12px",
"borderRadius": "12px",
"background": "#F3F7FA",
"fontWeight": "500"
},
style={"marginBottom": "0.5rem"}
)
)
], style={"background": "white", "border": "2.5px solid #fff", "borderRadius": "14px", "boxShadow": "none"}),
], md=4),
dbc.Col([
dbc.Label("Year:", className="fw-semibold"),
dcc.Dropdown(
id="year-filter",
options=year_options,
value="all",
clearable=False,
style={"background": "white"}
),
], md=2),
dbc.Col([
dbc.Label("Player (name):", className="fw-semibold mb-1"),
dcc.Dropdown(
id="player-filter",
options=[],
value=None,
placeholder="All players",
multi=True,
style={"background": "white"},
),
], md=3),
], className="mb-4 g-2"),
dbc.Row([
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("Stacked Timeline: Games by Year & Sport", className="card-title text-primary"),
dcc.Graph(id="timeline-chart", config={"displayModeBar": False}),
]),
], className="shadow-sm", style={"background": "white", "border": "2.5px solid #fff", "borderRadius": "20px"}), md=6),
dbc.Col(dbc.Card([
dbc.CardBody([
html.H5("Games Played vs. Rank by Sport", className="card-title text-primary"),
dcc.Graph(id="scatter-chart", config={"displayModeBar": False}),
]),
], className="shadow-sm", style={"background": "white", "border": "2.5px solid #fff", "borderRadius": "20px"}), md=6),
], className="mb-4 g-4"),
html.H4("π Filtered Results Table", className="mb-2 text-primary text-center fw-bold"),
dt.DataTable(
id="data-table",
columns=[
{"name": "Name", "id": "name"},
{"name": "Sport", "id": "sport_emoji"}, # id matches df col in callback
{"name": "League", "id": "league"},
{"name": "Year", "id": "year"},
{"name": "Games Played", "id": "played_val"},
{"name": "Rank", "id": "rank"},
],
data=[],
page_size=20,
style_table={"overflowX": "auto", "backgroundColor": "#fff"},
filter_action="native",
sort_action="native",
style_header={"fontWeight": "bold", "background": "#eaf3fb"},
style_cell={"padding": "7px", "background": "#fff", "fontFamily": "sans-serif"},
style_data_conditional=[
{
"if": {
"column_id": "sport_emoji"
},
"fontSize": "1.2em",
}
],
),
], fluid=True, style={
"background": "linear-gradient(120deg, #f8fafc 80%, #e2edfa 100%)",
"minHeight": "100vh"
})
# --- Dynamic PLAYER DROPDOWN based on filters ---
@app.callback(
Output("player-filter", "options"),
Output("player-filter", "value"),
Input("sport-filter", "value"),
Input("league-checklist", "value"),
Input("year-filter", "value"),
prevent_initial_call=False
)
def update_player_dropdown(selected_sports, selected_leagues, selected_year):
if not isinstance(selected_sports, list):
selected_sports = [selected_sports]
if not isinstance(selected_leagues, list):
selected_leagues = [selected_leagues]
dff = df[df["sport_name"].isin(selected_sports) & df["league"].isin(selected_leagues)]
if selected_year and selected_year != "all":
dff = dff[dff["year"] == int(selected_year)]
names = sorted(dff["name"].unique())
options = [{"label": n, "value": n} for n in names]
return options, []
# --- CHARTS, KPIS, DATATABLE MAIN CALLBACK ---
@app.callback(
Output("timeline-chart", "figure"),
Output("scatter-chart", "figure"),
Output("kpi-row", "children"),
Output("data-table", "data"),
Input("sport-filter", "value"),
Input("league-checklist", "value"),
Input("year-filter", "value"),
Input("player-filter", "value"),
)
def update_all(selected_sports, selected_leagues, selected_year, selected_players):
if not selected_players:
selected_players = []
if not isinstance(selected_sports, list):
selected_sports = [selected_sports]
if not isinstance(selected_leagues, list):
selected_leagues = [selected_leagues]
df_filt = df[df["sport_name"].isin(selected_sports) & df["league"].isin(selected_leagues)]
if selected_year and selected_year != "all":
df_filt = df_filt[df_filt["year"] == int(selected_year)]
if selected_players:
df_filt = df_filt[df_filt["name"].isin(selected_players)]
# KPIs
total_games = int(df_filt["played_val"].sum() if not df_filt.empty else 0)
unique_players = df_filt["name"].nunique()
avg_games = "-" if unique_players == 0 else f"{(total_games/unique_players):.1f}"
topyear = "-"
if not df_filt.empty:
top_y = df_filt.groupby("year")["played_val"].sum().idxmax()
topyear = int(top_y) if not pd.isna(top_y) else "-"
kpis = [
dbc.Col(kpi_card("Total Games Played", total_games, "bi-bar-chart-fill"), md=3),
dbc.Col(kpi_card("Unique Players", unique_players, "bi-person-fill"), md=3),
dbc.Col(kpi_card("Average Games / Player", avg_games, "bi-graph-up"), md=3),
dbc.Col(kpi_card("Top Year", topyear, "bi-trophy-fill"), md=3),
]
# Timeline chart
timeline_df = (
df_filt.groupby(["year", "sport_name"])["played_val"]
.sum().reset_index().sort_values(["year", "sport_name"])
)
if timeline_df.empty:
timeline_df["year"] = timeline_df["year"].astype(int)
timeline_fig = px.bar()
else:
timeline_fig = px.bar(
timeline_df,
x="year",
y="played_val",
color="sport_name",
color_discrete_map=color_map,
labels={"year": "Year", "played_val": "Total Games Played", "sport_name": "Sport"},
title=None,
)
timeline_fig.update_traces(marker=dict(line=dict(width=0)), opacity=0.92, width=0.8)
timeline_fig.update_xaxes(type="category")
timeline_fig.update_layout(
barmode="stack",
paper_bgcolor="white",
plot_bgcolor="white",
margin={"t": 8, "b": 32, "l": 18, "r": 18},
font={"color": "#234078", "size": 15},
legend={"title": "Sport"},
yaxis={"gridcolor": "#EEE"},
)
# Scatter chart with sport emoji in tooltip
scatter_df = df_filt.copy()
if not scatter_df.empty:
scatter_df["emoji"] = scatter_df["sport_name"].map(
lambda s: SPORT_EMOJIS.get(s, "π
")
)
customdata_cols = ["emoji", "name", "year", "league"]
scatter_fig = px.scatter(
scatter_df,
x="rank",
y="played_val",
color="sport_name",
color_discrete_map=color_map,
labels={"rank": "Rank", "played_val": "Games Played", "sport_name": "Sport"},
title=None,
)
scatter_fig.update_traces(
marker=dict(size=13, opacity=0.84, line=dict(width=1, color="white")),
hovertemplate=
"<span style='font-size:1.13em;'>%{customdata[0]} <b>%{customdata[1]}</b></span>"
"<br>ποΈ <b>Year:</b> <span style='color:#28559f;'>%{customdata[2]}</span>"
"<br>π <b>League:</b> <span style='color:#ffbe0b;'>%{customdata[3]}</span>"
"<br>π² <b>Games Played:</b> <span style='color:#32936f;'><b>%{y}</b></span>"
"<br>π₯ <b>Rank:</b> <span style='color:#ff006e;'><b>%{x}</b></span>"
"<extra></extra>",
customdata=scatter_df[customdata_cols].values,
)
else:
scatter_fig = px.scatter()
scatter_fig.update_layout(
paper_bgcolor="white",
plot_bgcolor="#FBFBFB",
margin={"t": 8, "b": 30, "l": 18, "r": 18},
font={"color": "#234078", "size": 15},
legend={"title": "Sport"},
xaxis={"gridcolor": "#F1F3F4"}, yaxis={"gridcolor": "#F1F3F4"}
)
scatter_fig.update_xaxes(zeroline=False)
scatter_fig.update_yaxes(zeroline=False)
# --- DATATABLE (table emoji + all filtered data) ---
df_table = df_filt.copy()
df_table["sport_emoji"] = df_table["sport_name"].map(
lambda s: f"{SPORT_EMOJIS.get(s, 'π
')} {s.title()}"
)
# Only show and order needed columns
datalist = df_table[[
"name", "sport_emoji", "league", "year", "played_val", "rank"
]].to_dict("records")
return timeline_fig, scatter_fig, kpis, datalist
if __name__ == "__main__":
app.run(debug=True)