{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# WNBA\n",
    "\n",
    "Nine charts and tables from the 2026 WNBA regular season, the league's first with 15 teams, built on wehoop's ESPN\n",
    "data that sportsdataverse-py loads from release files on GitHub (no stats.wnba.com calls). You'll follow the three\n",
    "newest franchises, rank teams in tiers, chart the scoring leaders with headshots, draw a shot chart, and build a\n",
    "standings table and an interactive Altair chart."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.wnba as wnba\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "SEASON = 2026\n",
    "SOURCE = \"Data: wehoop (ESPN) via sportsdataverse-py\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "Everything below uses the regular season (`season_type` 2), which is final; 2025 is loaded too for Golden State's\n",
    "first season. ESPN files each All-Star Game as a regular-season game (Team Collier vs Team Clark in 2025, Team Coop vs\n",
    "Team Spoon in 2026), so `resolve` warns about those four teams, and dropping the rows it could not resolve removes the\n",
    "games. The Commissioner's Cup final is filed the same way, which is why Las Vegas and New York show 45 games; it\n",
    "stays in the box scores but does not count in the standings."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "box = wnba.load_wnba_team_boxscore(seasons=[SEASON - 1, SEASON]).filter(pl.col(\"season_type\") == 2)\n",
    "\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    team_ids = sdvplot.resolve(box[\"team_abbreviation\"].to_list(), \"wnba\")\n",
    "for w in caught:\n",
    "    print(w.message)\n",
    "\n",
    "box = box.with_columns(team=pl.Series(team_ids, dtype=pl.String)).filter(pl.col(\"team\").is_not_null())\n",
    "current = box.filter(pl.col(\"season\") == SEASON)\n",
    "current.group_by(\"team_abbreviation\", maintain_order=True).agg(games=pl.len()).sort(\n",
    "    [\"games\", \"team_abbreviation\"], descending=[True, False]\n",
    ").head(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 1. The expansion teams, game by game\n",
    "\n",
    "Golden State joined in 2025, Portland and Toronto in 2026. Cumulative wins by game number put all four seasons on\n",
    "one chart; the grey line is a .500 pace. Portland's primary color is a pale ice blue, so every line gets a dark\n",
    "outline to stay visible on white."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Cumulative wins by game number for Golden State 2025 and 2026, Portland 2026 and Toronto 2026, each ending in the team's logo",
     "title": "WNBA expansion teams' win races"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "import matplotlib.patheffects as pe\n",
    "\n",
    "expansion = (pl.col(\"team_abbreviation\") == \"GS\") | (\n",
    "    (pl.col(\"season\") == SEASON) & pl.col(\"team_abbreviation\").is_in([\"POR\", \"TOR\"])\n",
    ")\n",
    "runs = (\n",
    "    box.filter(expansion)\n",
    "    .sort(\"game_date\")\n",
    "    .with_columns(\n",
    "        game_no=pl.int_range(1, pl.len() + 1).over(\"team\", \"season\"),\n",
    "        wins=pl.col(\"team_winner\").cast(pl.Int32).cum_sum().over(\"team\", \"season\"),\n",
    "    )\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.plot([0, 44], [0, 22], color=\"grey\", linewidth=1, linestyle=\"--\")\n",
    "for (team, season), run in runs.group_by(\"team\", \"season\", maintain_order=True):\n",
    "    color = sdvplot.team_colors([team], \"wnba\", season=season)[0]\n",
    "    ax.plot(\n",
    "        run[\"game_no\"],\n",
    "        run[\"wins\"],\n",
    "        color=color,\n",
    "        linewidth=3,\n",
    "        linestyle=\"--\" if season == 2025 else \"-\",\n",
    "        path_effects=[pe.Stroke(linewidth=4.5, foreground=\"#333333\"), pe.Normal()],\n",
    "    )\n",
    "ends = runs.group_by(\"team\", \"season\", maintain_order=True).agg(pl.all().last())\n",
    "sdvplot.add_logos(\n",
    "    ax, ends[\"game_no\"] + 1.8, ends[\"wins\"], ends[\"team\"], league=\"wnba\", season=ends[\"season\"], height=0.08\n",
    ")\n",
    "for row in ends.iter_rows(named=True):\n",
    "    ax.annotate(\n",
    "        f\"{row['season']}: {row['wins']}-{row['game_no'] - row['wins']}\",\n",
    "        (row[\"game_no\"] + 3.4, row[\"wins\"]),\n",
    "        va=\"center\",\n",
    "        fontsize=9,\n",
    "    )\n",
    "ax.set_xlim(0, 52)\n",
    "ax.set_xlabel(\"Game number\")\n",
    "ax.set_ylabel(\"Regular-season wins\")\n",
    "ax.set_title(\n",
    "    \"The WNBA's newest teams: Golden State's first two seasons, Portland and Toronto's first\",\n",
    "    loc=\"left\",\n",
    "    fontsize=11,\n",
    "    fontweight=\"bold\",\n",
    ")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "fig.text(0.99, 0.01, SOURCE, ha=\"right\", va=\"bottom\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Offense vs defense, interactive with Altair\n",
    "\n",
    "Points per 100 possessions (FGA - OREB + TOV + 0.44 x FTA, averaged with the opponent's), as an Altair chart: hover a\n",
    "logo for the numbers. `add_logos` returns a new layered chart, so add the reference lines after it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import altair as alt\n",
    "\n",
    "poss = (\n",
    "    pl.col(\"field_goals_attempted\")\n",
    "    - pl.col(\"offensive_rebounds\")\n",
    "    + pl.col(\"total_turnovers\")\n",
    "    + 0.44 * pl.col(\"free_throws_attempted\")\n",
    ")\n",
    "games = current.with_columns(poss=poss)\n",
    "opponent = games.select(\"game_id\", pl.col(\"team_id\").alias(\"opponent_team_id\"), pl.col(\"poss\").alias(\"opp_poss\"))\n",
    "assert games.schema[\"opponent_team_id\"] == opponent.schema[\"opponent_team_id\"]\n",
    "ratings = (\n",
    "    games.join(opponent, on=[\"game_id\", \"opponent_team_id\"])\n",
    "    .with_columns(game_poss=(pl.col(\"poss\") + pl.col(\"opp_poss\")) / 2)\n",
    "    .group_by(\"team\", \"team_abbreviation\", \"team_display_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        ortg=100 * pl.col(\"team_score\").sum() / pl.col(\"game_poss\").sum(),\n",
    "        drtg=100 * pl.col(\"opponent_team_score\").sum() / pl.col(\"game_poss\").sum(),\n",
    "    )\n",
    "    .with_columns(net=pl.col(\"ortg\") - pl.col(\"drtg\"))\n",
    "    .sort(\"net\", descending=True)\n",
    ")\n",
    "\n",
    "points = (\n",
    "    alt.Chart(ratings.to_pandas())\n",
    "    .mark_circle(size=900, opacity=0)\n",
    "    .encode(\n",
    "        x=alt.X(\"ortg:Q\", scale=alt.Scale(zero=False, padding=30), title=\"Offensive rating (per 100 possessions)\"),\n",
    "        y=alt.Y(\n",
    "            \"drtg:Q\",\n",
    "            scale=alt.Scale(zero=False, reverse=True, padding=30),\n",
    "            title=\"Defensive rating (allowed per 100, better is up)\",\n",
    "        ),\n",
    "        tooltip=[\n",
    "            \"team_display_name\",\n",
    "            alt.Tooltip(\"ortg:Q\", format=\".1f\"),\n",
    "            alt.Tooltip(\"drtg:Q\", format=\".1f\"),\n",
    "            alt.Tooltip(\"net:Q\", format=\"+.1f\"),\n",
    "        ],\n",
    "    )\n",
    "    .properties(\n",
    "        width=600,\n",
    "        height=420,\n",
    "        title=alt.TitleParams(f\"WNBA offense vs defense, {SEASON} regular season\", subtitle=SOURCE),\n",
    "    )\n",
    ")\n",
    "chart = sdvplot.add_logos(points, ratings[\"ortg\"], ratings[\"drtg\"], ratings[\"team\"], league=\"wnba\", height=0.1)\n",
    "means = alt.Chart().mark_rule(strokeDash=[4, 4], color=\"grey\")\n",
    "chart + means.encode(x=alt.datum(ratings[\"ortg\"].mean())) + means.encode(y=alt.datum(ratings[\"drtg\"].mean()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. Team tiers\n",
    "\n",
    "`team_tiers` draws a tier list from a frame with `tier_no` and `team`. Here the tiers are cut from net rating, best\n",
    "first within each tier."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "A dark tier list of WNBA team logos in five net-rating tiers",
     "title": "WNBA team tiers"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import team_tiers\n",
    "\n",
    "tiers = ratings.with_columns(\n",
    "    tier_no=pl.col(\"net\").cut([-6, -2, 2, 6], labels=[\"5\", \"4\", \"3\", \"2\", \"1\"]).cast(pl.String).cast(pl.Int32),\n",
    "    tier_rank=pl.col(\"net\").rank(\"ordinal\", descending=True),\n",
    ")\n",
    "fig = team_tiers(\n",
    "    tiers.select(\"tier_no\", \"team\", \"tier_rank\"),\n",
    "    \"wnba\",\n",
    "    title=f\"WNBA tiers by net rating, {SEASON} regular season\",\n",
    "    subtitle=\"Points per 100 possessions, scored minus allowed\",\n",
    "    caption=SOURCE,\n",
    "    tier_desc={1: \"+6 or better\", 2: \"+2 to +6\", 3: \"-2 to +2\", 4: \"-6 to -2\", 5: \"Worse than -6\"},\n",
    ")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 4. Scoring leaders with headshots, in plotnine\n",
    "\n",
    "`geom_sdv_headshots` takes ESPN athlete ids, which the player box score carries, and `scale_fill_sdv` colors each bar\n",
    "by team. Players need 30 games to qualify."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "plotnine bar chart of the ten WNBA scoring leaders, each bar in team colors with a logo and headshot",
     "title": "WNBA scoring leaders with headshots"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from plotnine import (\n",
    "    aes,\n",
    "    element_blank,\n",
    "    geom_col,\n",
    "    geom_text,\n",
    "    ggplot,\n",
    "    labs,\n",
    "    scale_x_discrete,\n",
    "    scale_y_continuous,\n",
    "    theme,\n",
    "    theme_minimal,\n",
    ")\n",
    "\n",
    "from sdvplot.plotnine import geom_sdv_headshots, geom_sdv_logos, scale_fill_sdv\n",
    "\n",
    "players = wnba.load_wnba_player_boxscore(seasons=[SEASON]).join(\n",
    "    current.select(\"game_id\").unique(), on=\"game_id\", how=\"semi\"\n",
    ")\n",
    "leaders = (\n",
    "    players.filter(~pl.col(\"did_not_play\"))\n",
    "    .group_by(\"athlete_id\", \"athlete_short_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        games=pl.len(),\n",
    "        ppg=pl.col(\"points\").mean(),\n",
    "        team=pl.col(\"team_abbreviation\").sort_by(\"game_date\").last(),\n",
    "    )\n",
    "    .filter(pl.col(\"games\") >= 30)\n",
    "    .sort(\"ppg\", descending=True)\n",
    "    .head(10)\n",
    "    .with_columns(label=pl.col(\"ppg\").round(1).cast(pl.String), logo_y=pl.lit(2.5))\n",
    ")\n",
    "(\n",
    "    ggplot(leaders.to_pandas(), aes(\"athlete_short_name\", \"ppg\"))\n",
    "    + geom_col(aes(fill=\"team\"), width=0.75)\n",
    "    + geom_sdv_logos(aes(y=\"logo_y\", team=\"team\"), league=\"wnba\", height=0.08)\n",
    "    + geom_sdv_headshots(aes(y=\"ppg + 3.3\", player_id=\"athlete_id\"), league=\"wnba\", height=0.13)\n",
    "    + geom_text(aes(y=\"ppg + 7.6\", label=\"label\"), fontweight=\"bold\", size=10)\n",
    "    + scale_fill_sdv(\"wnba\", guide=None)\n",
    "    + scale_x_discrete(limits=leaders[\"athlete_short_name\"].to_list())  # keep the ppg order\n",
    "    + scale_y_continuous(limits=(0, leaders[\"ppg\"].max() + 10), expand=(0, 0))\n",
    "    + labs(\n",
    "        x=\"\", y=\"Points per game\", title=f\"WNBA scoring leaders, {SEASON} regular season (30+ games)\", caption=SOURCE\n",
    "    )\n",
    "    + theme_minimal()\n",
    "    + theme(figure_size=(10, 5.5), panel_grid_major_x=element_blank())\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 5. A shot chart on a team-colored court\n",
    "\n",
    "`load_wnba_shots` holds ESPN's shot locations, already in feet on a center-court frame, the same frame as sportypy's\n",
    "court, so `sdvplot.court_coords` (for the stats.wnba.com legacy frame) is not needed. Fold the right-basket shots onto\n",
    "the left one, then bin them: where Caitlin Clark shot from, on Indiana's court."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib.colors import LinearSegmentedColormap\n",
    "\n",
    "shots = wnba.load_wnba_shots(seasons=[SEASON]).join(current.select(\"game_id\").unique(), on=\"game_id\", how=\"semi\")\n",
    "right = pl.col(\"coordinate_x\") > 0\n",
    "clark = shots.filter(\n",
    "    (pl.col(\"athlete_name_1\") == \"Caitlin Clark\") & ~pl.col(\"type_text\").str.contains(\"Free Throw\")\n",
    ").with_columns(\n",
    "    x=pl.when(right).then(-pl.col(\"coordinate_x\")).otherwise(pl.col(\"coordinate_x\")),\n",
    "    y=pl.when(right).then(-pl.col(\"coordinate_y\")).otherwise(pl.col(\"coordinate_y\")),\n",
    ")\n",
    "made = clark.filter(pl.col(\"scoring_play\")).height\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 6.5))\n",
    "sdvplot.surface(\"wnba\", \"IND\", display_range=\"defense\", ax=ax)\n",
    "red = sdvplot.team_colors([\"IND\"], \"wnba\", which=\"secondary\")[0]\n",
    "cmap = LinearSegmentedColormap.from_list(\"indiana\", [\"#fff4e0\", red])\n",
    "hexes = ax.hexbin(\n",
    "    clark[\"x\"],\n",
    "    clark[\"y\"],\n",
    "    gridsize=(14, 15),\n",
    "    extent=(-47, 0, -25, 25),\n",
    "    mincnt=1,\n",
    "    bins=\"log\",\n",
    "    cmap=cmap,\n",
    "    edgecolors=\"white\",\n",
    "    linewidths=0.4,\n",
    "    zorder=20,\n",
    ")\n",
    "fig.colorbar(hexes, ax=ax, shrink=0.6, label=\"Attempts (log scale)\")\n",
    "ax.set_title(\n",
    "    f\"Caitlin Clark's field goal attempts, {SEASON} regular season\\n\"\n",
    "    f\"{clark.height} attempts, {made / clark.height:.1%} made\",\n",
    "    loc=\"left\",\n",
    "    fontweight=\"bold\",\n",
    ")\n",
    "fig.text(0.99, 0.01, SOURCE, ha=\"right\", va=\"bottom\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 6. A standings table with logos\n",
    "\n",
    "ESPN's standings come long (one row per team and stat); pivot them wide. The top eight records make the playoffs\n",
    "whatever the conference, so one league-wide table with a cut line after eighth tells the story."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "from great_tables import GT\n",
    "\n",
    "from sdvplot.great_tables import gt_cutline, gt_sdv_logos, gt_theme_sdv\n",
    "\n",
    "standings = (\n",
    "    wnba.load_wnba_standings(seasons=[SEASON])\n",
    "    .pivot(on=\"stat_name\", index=[\"group_name\", \"team_abbreviation\", \"team_display_name\"], values=\"display_value\")\n",
    "    .with_columns(\n",
    "        logo=pl.col(\"team_abbreviation\"),\n",
    "        conf=pl.col(\"group_name\").str.replace(\" Conference\", \"\"),\n",
    "        wins_n=pl.col(\"wins\").cast(pl.Int32),\n",
    "    )\n",
    "    .sort(\"wins_n\", descending=True)\n",
    "    .select(\n",
    "        \"logo\",\n",
    "        \"team_display_name\",\n",
    "        \"conf\",\n",
    "        \"wins\",\n",
    "        \"losses\",\n",
    "        \"winPercent\",\n",
    "        \"Home\",\n",
    "        \"Road\",\n",
    "        \"Last Ten Games\",\n",
    "        \"streak\",\n",
    "        \"differential\",\n",
    "    )\n",
    ")\n",
    "table = (\n",
    "    GT(standings)\n",
    "    .tab_header(title=f\"WNBA standings, {SEASON} regular season\", subtitle=\"The top eight records make the playoffs\")\n",
    "    .cols_label(\n",
    "        logo=\"\",\n",
    "        team_display_name=\"Team\",\n",
    "        conf=\"Conf\",\n",
    "        wins=\"W\",\n",
    "        losses=\"L\",\n",
    "        winPercent=\"Pct\",\n",
    "        **{\"Last Ten Games\": \"L10\"},\n",
    "        streak=\"Strk\",\n",
    "        differential=\"Diff\",\n",
    "    )\n",
    "    .cols_align(\"left\", columns=\"team_display_name\")\n",
    "    .tab_source_note(SOURCE)\n",
    ")\n",
    "table = gt_theme_sdv(gt_sdv_logos(table, \"logo\", league=\"wnba\", height=26))\n",
    "gt_cutline(table, after=8, label=\"Playoff line\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "## 7. A team palette for seaborn\n",
    "\n",
    "`palette` maps the data's own abbreviations to colors for seaborn. Every game's final margin, one strip per team,\n",
    "sorted by average margin; a thin black edge keeps the pale colors (Portland, New York) visible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import seaborn as sns\n",
    "\n",
    "margins = current.with_columns(margin=pl.col(\"team_score\") - pl.col(\"opponent_team_score\"))\n",
    "order = (\n",
    "    margins.group_by(\"team_abbreviation\", maintain_order=True)\n",
    "    .agg(pl.col(\"margin\").mean())\n",
    "    .sort(\"margin\", descending=True)\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "ax.axhline(0, color=\"grey\", linewidth=0.8)\n",
    "np.random.seed(2026)  # seaborn jitters from numpy's global random state: a seed keeps the chart the same\n",
    "sns.stripplot(\n",
    "    margins.to_pandas(),\n",
    "    x=\"team_abbreviation\",\n",
    "    y=\"margin\",\n",
    "    order=order[\"team_abbreviation\"].to_list(),\n",
    "    hue=\"team_abbreviation\",\n",
    "    palette=sdvplot.palette(\"wnba\", teams=margins[\"team_abbreviation\"]),\n",
    "    legend=False,\n",
    "    jitter=0.25,\n",
    "    size=5,\n",
    "    edgecolor=\"black\",\n",
    "    linewidth=0.4,\n",
    "    ax=ax,\n",
    ")\n",
    "ax.set_xlabel(\"\")\n",
    "ax.set_ylabel(\"Final margin (points)\")\n",
    "ax.set_title(f\"Every WNBA game's margin, {SEASON} regular season, best average first\", loc=\"left\", fontweight=\"bold\")\n",
    "sdvplot.axis_logos(ax, \"x\", league=\"wnba\", height=0.08)\n",
    "fig.text(0.99, 0.01, SOURCE, ha=\"right\", va=\"bottom\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## 8. plotnine: the season as a running point differential, by conference\n",
    "\n",
    "Running point differential through the season, one line per team in its color (`scale_color_sdv`), faceted by\n",
    "conference, with `geom_sdv_logos` marking where each team finished."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plotnine import facet_wrap, geom_hline, geom_line, theme_bw\n",
    "\n",
    "from sdvplot.plotnine import scale_color_sdv\n",
    "\n",
    "conferences = sdvplot.teams(\"wnba\").select(\"team_id\", \"conference\")\n",
    "running = (\n",
    "    current.join(conferences, left_on=\"team\", right_on=\"team_id\")\n",
    "    .sort(\"game_date\")\n",
    "    .with_columns(\n",
    "        game_no=pl.int_range(1, pl.len() + 1).over(\"team\"),\n",
    "        diff=(pl.col(\"team_score\") - pl.col(\"opponent_team_score\")).cum_sum().over(\"team\"),\n",
    "    )\n",
    ")\n",
    "finish = running.group_by(\"team\", maintain_order=True).agg(pl.all().last())\n",
    "\n",
    "(\n",
    "    ggplot(running.to_pandas(), aes(\"game_no\", \"diff\", color=\"team_abbreviation\"))\n",
    "    + geom_hline(yintercept=0, color=\"grey\")\n",
    "    + geom_line(size=1)\n",
    "    + geom_sdv_logos(aes(team=\"team\"), data=finish.to_pandas(), league=\"wnba\", height=0.075)\n",
    "    + facet_wrap(\"conference\")\n",
    "    + scale_color_sdv(\"wnba\", guide=None)\n",
    "    + labs(\n",
    "        x=\"Game number\",\n",
    "        y=\"Running point differential\",\n",
    "        title=f\"The {SEASON} WNBA regular season, game by game\",\n",
    "        caption=SOURCE,\n",
    "    )\n",
    "    + theme_bw()\n",
    "    + theme(figure_size=(10, 5))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "## 9. Home and road, as a dumbbell with logos on the axis\n",
    "\n",
    "Home and road win percentages from the standings, one row per team, sorted by the home edge. `axis_logos` reads the\n",
    "y tick labels, so set them to the teams' abbreviations first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "record = lambda col: pl.col(col).str.split(\"-\").list.eval(pl.element().cast(pl.Int32))  # noqa: E731\n",
    "split = (\n",
    "    wnba.load_wnba_standings(seasons=[SEASON])\n",
    "    .pivot(on=\"stat_name\", index=\"team_abbreviation\", values=\"display_value\")\n",
    "    .with_columns(\n",
    "        home=record(\"Home\").list.first() / record(\"Home\").list.sum(),\n",
    "        road=record(\"Road\").list.first() / record(\"Road\").list.sum(),\n",
    "    )\n",
    "    .with_columns(edge=pl.col(\"home\") - pl.col(\"road\"))\n",
    "    .sort(\"edge\")\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "y = list(range(split.height))\n",
    "ax.hlines(y, split[\"road\"], split[\"home\"], color=\"lightgrey\", linewidth=3, zorder=1)\n",
    "ax.scatter(split[\"road\"], y, color=\"white\", edgecolors=\"grey\", s=70, zorder=2, label=\"Road\")\n",
    "ax.scatter(\n",
    "    split[\"home\"],\n",
    "    y,\n",
    "    color=sdvplot.team_colors(split[\"team_abbreviation\"], \"wnba\"),\n",
    "    edgecolors=\"black\",\n",
    "    s=70,\n",
    "    zorder=3,\n",
    "    label=\"Home\",\n",
    ")\n",
    "ax.set_yticks(y, split[\"team_abbreviation\"])\n",
    "ax.set_xlim(0, 1)\n",
    "ax.xaxis.set_major_formatter(lambda v, _: f\"{v:.0%}\")\n",
    "ax.legend(loc=\"lower right\")\n",
    "ax.set_xlabel(\"Win percentage\")\n",
    "ax.set_title(f\"WNBA home vs road, {SEASON} regular season: biggest home edge on top\", loc=\"left\", fontweight=\"bold\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "sdvplot.axis_logos(ax, \"y\", league=\"wnba\", height=0.05)\n",
    "fig.text(0.99, 0.01, SOURCE, ha=\"right\", va=\"bottom\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "sdvplot": {
   "description": "Nine examples from the 2026 WNBA regular season: the expansion teams' win races, tiers, headshot leaders, a shot chart, a standings table and an interactive Altair chart.",
   "label": "WNBA",
   "position": 21
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
