{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# WNBA scoring leaders card\n",
    "\n",
    "**The brief:** the regular season just ended, and the social team wants a scoring-leaders card: the top ten in\n",
    "points per game with each player's face and team, as a 1080 x 1080 image for Instagram, plus a top-five cut at\n",
    "1200 x 675 for X and Bluesky. The box scores are hoopR/wehoop's ESPN data through `sportsdataverse.wnba`; the\n",
    "headshots come from ESPN's CDN through `sdvplot.add_headshots`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tempfile\n",
    "from pathlib import Path\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.wnba as wnba\n",
    "from IPython.display import Image\n",
    "from matplotlib.colors import to_rgb\n",
    "from PIL import Image as PILImage\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "SEASON = 2026\n",
    "OUT = Path(tempfile.mkdtemp(prefix=\"sdvplot-recipe-\"))  # where the exports go; use your own folder"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "## 1. Get the data\n",
    "\n",
    "One row per player per game. ESPN files the All-Star Game and the Commissioner's Cup final as regular-season\n",
    "games, but neither counts in the official stats; the schedule marks the standard games with `type_abbreviation`\n",
    "\"STD\", so a semi join keeps only those. Players who did not play are dropped, the qualifier is 30 games (about 70%\n",
    "of the 44-game schedule), and a player traded mid-season is listed with her last team."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "regular = wnba.load_wnba_schedule(seasons=[SEASON]).filter(pl.col(\"season_type\") == 2)\n",
    "print(\n",
    "    regular.group_by(\"type_abbreviation\", maintain_order=True).len().sort(\"type_abbreviation\")\n",
    ")  # STD, plus one ALLSTAR and one CC (the Cup final)\n",
    "standard = regular.filter(pl.col(\"type_abbreviation\") == \"STD\").select(\"game_id\")\n",
    "\n",
    "box = wnba.load_wnba_player_boxscore(seasons=[SEASON])\n",
    "assert box.schema[\"game_id\"] == standard.schema[\"game_id\"]  # one dtype on both sides of the join key\n",
    "box = box.join(standard, on=\"game_id\", how=\"semi\").filter(~pl.col(\"did_not_play\"))\n",
    "leaders = (\n",
    "    box.group_by(\"athlete_id\", \"athlete_display_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\", \"athlete_display_name\"], descending=[True, False])\n",
    "    .head(10)\n",
    "    .with_columns(rank=pl.int_range(1, pl.len() + 1))\n",
    ")\n",
    "leaders"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 2. The first draft\n",
    "\n",
    "A horizontal bar chart is the right shape for a ranked list of names."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "ax.barh(leaders[\"athlete_display_name\"], leaders[\"ppg\"])\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "The leader is at the bottom (`barh` draws the first row lowest), every bar is the same blue, the exact values are\n",
    "missing, and nothing says what or when.\n",
    "\n",
    "## 3. Order, team colors and values\n",
    "\n",
    "Inverting the y axis puts No. 1 on top. Each bar takes its team's color, with one catch: a few primaries (the Aces'\n",
    "silver, the Liberty's seafoam) are too light to read on a light card, so a small WCAG luminance check swaps those to\n",
    "the team's secondary color. The values go at the end of each bar, which makes the x axis unnecessary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "def luminance(color):\n",
    "    \"\"\"WCAG relative luminance, 0 (black) to 1 (white).\"\"\"\n",
    "    r, g, b = (c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 for c in to_rgb(color))\n",
    "    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n",
    "\n",
    "\n",
    "primary = sdvplot.team_colors(leaders[\"team\"], \"wnba\")\n",
    "secondary = sdvplot.team_colors(leaders[\"team\"], \"wnba\", which=\"secondary\")\n",
    "leaders = leaders.with_columns(\n",
    "    color=pl.Series([p if luminance(p) < 0.3 else s for p, s in zip(primary, secondary, strict=True)])\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "ax.barh(leaders[\"athlete_display_name\"], leaders[\"ppg\"], color=leaders[\"color\"])\n",
    "ax.invert_yaxis()\n",
    "for y, v in enumerate(leaders[\"ppg\"]):\n",
    "    ax.text(v + 0.3, y, f\"{v:.1f}\", va=\"center\", fontsize=9, fontweight=\"bold\")\n",
    "ax.xaxis.set_visible(False)\n",
    "ax.spines[[\"top\", \"right\", \"bottom\"]].set_visible(False)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 4. Faces and logos\n",
    "\n",
    "A card like this sells on faces. Moving to a blank canvas (an axes with fixed 0-100 x coordinates and one unit per\n",
    "row) makes room for a column of headshots and a small team logo under each name. `add_headshots` and `add_logos`\n",
    "size their images as a fraction of the axes height, so with ten rows a headshot of 0.085 fills most of a row."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rows(ax, data, bar_from=47, bar_to=94):\n",
    "    \"\"\"Ranked rows on a 0-100 canvas: rank, headshot, name, team logo and a bar with its value.\"\"\"\n",
    "    n = data.height\n",
    "    ax.set(xlim=(0, 100), ylim=(n + 0.5, 0.5))\n",
    "    ax.axis(\"off\")\n",
    "    face, logo = 0.85 / n, 0.32 / n  # image heights as a fraction of the axes: most of a row, a third of one\n",
    "    sdvplot.add_headshots(ax, [12] * n, data[\"rank\"], data[\"athlete_id\"], league=\"wnba\", height=face)\n",
    "    sdvplot.add_logos(ax, [22.5] * n, data[\"rank\"] + 0.2, data[\"team\"], league=\"wnba\", season=SEASON, height=logo)\n",
    "    scale = (bar_to - bar_from) / data[\"ppg\"].max()\n",
    "    for row in data.iter_rows(named=True):\n",
    "        y = row[\"rank\"]\n",
    "        ax.text(3, y, str(row[\"rank\"]), ha=\"center\", va=\"center\", fontsize=15, fontweight=\"bold\", color=\"#9a9a9a\")\n",
    "        ax.text(21, y - 0.17, row[\"athlete_display_name\"], va=\"center\", fontsize=11, fontweight=\"bold\")\n",
    "        ax.text(25, y + 0.2, row[\"team\"], va=\"center\", fontsize=8.5, color=\"#6b6b6b\")\n",
    "        ax.barh(y, row[\"ppg\"] * scale, left=bar_from, height=0.56, color=row[\"color\"])\n",
    "        ax.text(\n",
    "            bar_from + row[\"ppg\"] * scale - 1,\n",
    "            y,\n",
    "            f\"{row['ppg']:.1f}\",\n",
    "            ha=\"right\",\n",
    "            va=\"center\",\n",
    "            fontsize=11,\n",
    "            fontweight=\"bold\",\n",
    "            color=\"white\",\n",
    "        )\n",
    "\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 6))\n",
    "rows(ax, leaders)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 5. Make it a card\n",
    "\n",
    "The finishing pass is the frame: a warm off-white background, the headline as the title (the stat goes in the\n",
    "subtitle), the qualifier stated, and a footer with the source. Everything is placed in inches from the edges, so the\n",
    "same function draws the square post and a wider one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "BG, INK, GREY = \"#f6f4ef\", \"#1d1d1d\", \"#6b6b6b\"\n",
    "\n",
    "\n",
    "def card(data, figsize, dpi=100):\n",
    "    w, h = figsize\n",
    "    fig = plt.figure(figsize=figsize, dpi=dpi, facecolor=BG)\n",
    "    top, bottom = 1.15, 0.45  # inches for the header and the footer\n",
    "    ax = fig.add_axes((0.25 / w, bottom / h, 1 - 0.5 / w, 1 - (top + bottom) / h), facecolor=BG)\n",
    "    rows(ax, data)\n",
    "    leader = data.row(0, named=True)\n",
    "    fig.text(\n",
    "        0.3 / w,\n",
    "        1 - 0.3 / h,\n",
    "        f\"{leader['athlete_display_name']} won the {SEASON} scoring title\",\n",
    "        fontsize=19,\n",
    "        fontweight=\"bold\",\n",
    "        color=INK,\n",
    "        va=\"top\",\n",
    "    )\n",
    "    fig.text(\n",
    "        0.3 / w,\n",
    "        1 - 0.75 / h,\n",
    "        f\"Points per game, {SEASON} WNBA regular season (minimum 30 games)\",\n",
    "        fontsize=10.5,\n",
    "        color=GREY,\n",
    "        va=\"top\",\n",
    "    )\n",
    "    fig.text(0.3 / w, 0.18 / h, \"Data: wehoop (ESPN) via sportsdataverse-py\", fontsize=8, color=GREY)\n",
    "    fig.text(1 - 0.3 / w, 0.18 / h, \"#WNBA  |  made with sdvplot\", fontsize=8, color=GREY, ha=\"right\")\n",
    "    return fig\n",
    "\n",
    "\n",
    "fig = card(leaders, (7.2, 7.2))\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 6. Export for Instagram and X\n",
    "\n",
    "The square holds all ten. The 16:9 post for X and Bluesky is too short for ten readable rows, so it gets the top\n",
    "five from the same function: changing the content to fit the format beats shrinking the type. Inches times dpi\n",
    "gives the exact pixels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Social card of the 2026 WNBA points-per-game leaders: rank, headshot, name, team logo and a bar in team colors for each of the top ten",
     "title": "WNBA scoring leaders card"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "exports = {\n",
    "    \"wnba_scoring_1080x1080.png\": card(leaders, (7.2, 7.2), dpi=150),\n",
    "    \"wnba_scoring_top5_1200x675.png\": card(leaders.head(5), (8, 4.5), dpi=150),\n",
    "}\n",
    "for name, fig in exports.items():\n",
    "    fig.savefig(OUT / name, dpi=150, facecolor=BG)\n",
    "    plt.close(fig)\n",
    "    print(name, PILImage.open(OUT / name).size)\n",
    "Image(OUT / \"wnba_scoring_1080x1080.png\", width=600)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "The top-five cut for X and Bluesky:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "Image(OUT / \"wnba_scoring_top5_1200x675.png\", width=700)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "sdvplot": {
   "description": "Make a WNBA scoring-leaders card with headshots, team logos and team-color bars, exported at 1080x1080 for Instagram and as a top-five 1200x675 cut for X.",
   "label": "WNBA scoring leaders card",
   "position": 4
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
