{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Logos in matplotlib\n",
    "\n",
    "Twelve short recipes for putting team logos, headshots and other images on matplotlib charts: points, bar ends,\n",
    "axes, line ends, titles and tier lists, plus the sizing, overlap, era and export questions that come up along\n",
    "the way. Each recipe answers one \"how do I ...?\" with real data from one season: NFL team stats from nflverse,\n",
    "NBA and men's college basketball from hoopR, and NHL from fastRhockey, all read from GitHub release files by\n",
    "sportsdataverse-py."
   ]
  },
  {
   "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.mbb as mbb\n",
    "import sportsdataverse.nba as nba\n",
    "import sportsdataverse.nfl as nfl\n",
    "import sportsdataverse.nhl as nhl\n",
    "from IPython.display import Image, display\n",
    "from matplotlib import patheffects\n",
    "\n",
    "import sdvplot\n",
    "from sdvplot.matplotlib import add_images, team_tiers, title_image\n",
    "\n",
    "NFL_SEASON = 2025  # nflverse names a season by the year it starts\n",
    "SEASON = 2026  # the 2025-26 NBA, NHL and college basketball season, named by the year it ends\n",
    "NFLVERSE = \"Data: nflverse via sportsdataverse-py\"\n",
    "HOOPR = \"Data: hoopR (ESPN) via sportsdataverse-py\"\n",
    "FASTRHOCKEY = \"Data: fastRhockey via sportsdataverse-py\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "The recipes share four small tables, each loaded once. NFL offense and defense EPA per play come from\n",
    "nflverse's weekly team stats (passes, sacks and runs). For the NBA, ESPN's team box score gives point\n",
    "differential per game; keeping teams with more than ten games drops the All-Star Game's three teams. The NHL\n",
    "team box score is one row per team per game, and the college ratings carry ESPN team ids, which is all\n",
    "sdvplot needs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def epa_per_play(stats: pl.DataFrame) -> pl.DataFrame:\n",
    "    plays = pl.col(\"attempts\") + pl.col(\"sacks_suffered\") + pl.col(\"carries\")\n",
    "    epa = pl.col(\"passing_epa\") + pl.col(\"rushing_epa\")\n",
    "    offense = stats.group_by(\"team\", maintain_order=True).agg(off_epa=epa.sum() / plays.sum())\n",
    "    defense = stats.group_by(team=pl.col(\"opponent_team\"), maintain_order=True).agg(def_epa=epa.sum() / plays.sum())\n",
    "    return offense.join(defense, on=\"team\").sort(\"team\")\n",
    "\n",
    "\n",
    "nfl_weeks = nfl.load_nfl_team_stats([NFL_SEASON]).filter(pl.col(\"season_type\") == \"REG\")\n",
    "nfl_epa = epa_per_play(nfl_weeks)\n",
    "\n",
    "nba_box = nba.load_nba_team_boxscore(seasons=[SEASON]).filter(pl.col(\"season_type\") == 2)\n",
    "nba_teams = (\n",
    "    nba_box.group_by(\"team_abbreviation\", maintain_order=True)\n",
    "    .agg(games=pl.len(), diff=(pl.col(\"team_score\") - pl.col(\"opponent_team_score\")).mean())\n",
    "    .filter(pl.col(\"games\") > 10)\n",
    "    .sort(\"team_abbreviation\")\n",
    ")\n",
    "\n",
    "nhl_games = nhl.load_nhl_team_box(seasons=[SEASON]).filter(pl.col(\"game_id\") // 10_000 % 100 == 2)\n",
    "\n",
    "mbb_ratings = (\n",
    "    mbb.load_mbb_ratings(SEASON)\n",
    "    .join(sdvplot.teams(\"mbb\").select(\"team_id\", \"conference\"), on=\"team_id\")\n",
    "    .sort(\"team_id\")\n",
    ")\n",
    "nfl_epa.height, nba_teams.height, nhl_games[\"team_abbrev\"].n_unique(), mbb_ratings.height"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 1. Use logos as scatter points\n",
    "\n",
    "`add_logos` draws each team's logo centered on its (x, y). It does not move the axis limits, so set them\n",
    "first (here from the data, with a margin for the logos)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Scatter of NFL offensive and defensive EPA per play with each team's logo as its marker",
     "title": "NFL offense vs defense with logos as points"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.set_xlim(nfl_epa[\"off_epa\"].min() - 0.03, nfl_epa[\"off_epa\"].max() + 0.03)\n",
    "ax.set_ylim(nfl_epa[\"def_epa\"].max() + 0.03, nfl_epa[\"def_epa\"].min() - 0.03)  # inverted: good defense is up\n",
    "ax.axvline(nfl_epa[\"off_epa\"].mean(), color=\"grey\", linewidth=0.8, linestyle=\":\")\n",
    "ax.axhline(nfl_epa[\"def_epa\"].mean(), color=\"grey\", linewidth=0.8, linestyle=\":\")\n",
    "sdvplot.add_logos(ax, nfl_epa[\"off_epa\"], nfl_epa[\"def_epa\"], nfl_epa[\"team\"], league=\"nfl\", height=0.08)\n",
    "ax.set_xlabel(\"Offense: EPA per play\")\n",
    "ax.set_ylabel(\"Defense: EPA per play allowed\")\n",
    "ax.set_title(f\"NFL offense vs defense, {NFL_SEASON} regular season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, NFLVERSE, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Put a logo at the end of each bar\n",
    "\n",
    "Place each logo just past its bar's end: above a positive bar, below a negative one. Bar colors come from\n",
    "`team_colors`, which returns one color per team in the order given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "ranked = nba_teams.sort([\"diff\", \"team_abbreviation\"], descending=[True, False])\n",
    "x = list(range(ranked.height))\n",
    "ends = [d + 1.4 if d >= 0 else d - 1.4 for d in ranked[\"diff\"]]\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5.5))\n",
    "ax.bar(x, ranked[\"diff\"], color=sdvplot.team_colors(ranked[\"team_abbreviation\"], \"nba\"), width=0.75)\n",
    "ax.axhline(0, color=\"black\", linewidth=0.8)\n",
    "ax.set_ylim(ranked[\"diff\"].min() - 3.5, ranked[\"diff\"].max() + 3.5)\n",
    "sdvplot.add_logos(ax, x, ends, ranked[\"team_abbreviation\"], league=\"nba\", height=0.055)\n",
    "ax.set_xticks([])\n",
    "ax.set_ylabel(\"Average point differential per game\")\n",
    "ax.spines[[\"top\", \"right\", \"bottom\"]].set_visible(False)\n",
    "ax.set_title(\"NBA point differential, 2025-26 regular season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, HOOPR, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. Swap axis labels for logos (x and y)\n",
    "\n",
    "`axis_logos` replaces the tick labels of a team axis with logos. It reads the labels when called, so draw\n",
    "the chart first; the labels just need to be team values `resolve` understands (here the data's own NHL and\n",
    "NFL abbreviations)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "nhl_scoring = (\n",
    "    nhl_games.group_by(\"team_abbrev\", maintain_order=True)\n",
    "    .agg(gpg=pl.col(\"goals\").mean())\n",
    "    .sort([\"gpg\", \"team_abbrev\"], descending=[True, False])\n",
    "    .head(10)\n",
    ")\n",
    "nfl_sacks = (\n",
    "    nfl_weeks.group_by(\"team\", maintain_order=True).agg(pl.col(\"def_sacks\").sum()).sort(\"def_sacks\", \"team\").tail(10)\n",
    ")\n",
    "\n",
    "fig, (left, right) = plt.subplots(1, 2, figsize=(10, 5))\n",
    "colors = sdvplot.team_colors(nhl_scoring[\"team_abbrev\"], \"nhl\")\n",
    "left.bar(nhl_scoring[\"team_abbrev\"], nhl_scoring[\"gpg\"], color=colors)\n",
    "left.set_ylim(2.5, nhl_scoring[\"gpg\"].max() + 0.2)\n",
    "left.set_title(\"NHL goals per game, 2025-26 (top 10)\", loc=\"left\", fontsize=10, fontweight=\"bold\")\n",
    "sdvplot.axis_logos(left, \"x\", league=\"nhl\", height=0.08)\n",
    "\n",
    "right.barh(nfl_sacks[\"team\"], nfl_sacks[\"def_sacks\"], color=sdvplot.team_colors(nfl_sacks[\"team\"], \"nfl\"))\n",
    "right.set_title(f\"NFL sacks, {NFL_SEASON} (top 10)\", loc=\"left\", fontsize=10, fontweight=\"bold\")\n",
    "sdvplot.axis_logos(right, \"y\", league=\"nfl\", height=0.07)\n",
    "fig.text(0.99, 0.01, f\"{FASTRHOCKEY} | {NFLVERSE}\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 4. Label each line's last point with a logo\n",
    "\n",
    "A logo at the end of a line replaces a legend. Leave room on the right with `set_xlim`, then put each logo a\n",
    "little past the team's last point. Teams that finish close together would stack their logos, so walk up the\n",
    "finishing order and keep each logo at least one logo-height above the one below, with a thin leader line back\n",
    "to its point. The Pacific Division's season, as cumulative goal differential:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Cumulative goal differential lines for the eight Pacific Division teams, each ending in the team's logo",
     "title": "NHL Pacific Division goal differential with logos at the line ends"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "PACIFIC = [\"ANA\", \"CGY\", \"EDM\", \"LAK\", \"SEA\", \"SJS\", \"VAN\", \"VGK\"]\n",
    "runs = (\n",
    "    nhl_games.filter(pl.col(\"team_abbrev\").is_in(PACIFIC))\n",
    "    .sort(\"game_date\")\n",
    "    .with_columns(\n",
    "        game_no=pl.int_range(1, pl.len() + 1).over(\"team_abbrev\"),\n",
    "        goal_diff=(pl.col(\"goals\") - pl.col(\"goals_against\")).cum_sum().over(\"team_abbrev\"),\n",
    "    )\n",
    ")\n",
    "last = (\n",
    "    runs.group_by(\"team_abbrev\", maintain_order=True)\n",
    "    .agg(pl.all().sort_by(\"game_no\").last())\n",
    "    .sort(\"goal_diff\", \"team_abbrev\")\n",
    ")\n",
    "gap = 9  # goals: about one logo height on this axis\n",
    "spots = []\n",
    "for y in last[\"goal_diff\"]:\n",
    "    spots.append(max(y, spots[-1] + gap) if spots else y)\n",
    "end = last[\"game_no\"].max()\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 6))\n",
    "for team in PACIFIC:\n",
    "    run = runs.filter(pl.col(\"team_abbrev\") == team)\n",
    "    ax.plot(run[\"game_no\"], run[\"goal_diff\"], color=sdvplot.team_colors(team, \"nhl\"), linewidth=2)\n",
    "for y, spot in zip(last[\"goal_diff\"], spots, strict=True):\n",
    "    ax.plot([end, end + 4], [y, spot], color=\"grey\", linewidth=0.6)\n",
    "ax.axhline(0, color=\"grey\", linewidth=0.8)\n",
    "ax.set_xlim(0, end + 10)\n",
    "sdvplot.add_logos(ax, [end + 6] * last.height, spots, last[\"team_abbrev\"], league=\"nhl\", height=0.07)\n",
    "ax.set_xlabel(\"Game\")\n",
    "ax.set_ylabel(\"Cumulative goal differential (no shootout goals)\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_title(\"The Pacific Division's 2025-26 season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, FASTRHOCKEY, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 5. Size and fade logos to highlight a group\n",
    "\n",
    "`height` is a fraction of the Axes height, so a logo keeps its size relative to the plot whatever the figure\n",
    "size; `alpha` fades it. Two calls: the league faded and small, then the AFC West large and opaque."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "AFC_WEST = [\"DEN\", \"KC\", \"LAC\", \"LV\"]\n",
    "focus = nfl_epa.filter(pl.col(\"team\").is_in(AFC_WEST))\n",
    "rest = nfl_epa.filter(~pl.col(\"team\").is_in(AFC_WEST))\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.set_xlim(nfl_epa[\"off_epa\"].min() - 0.03, nfl_epa[\"off_epa\"].max() + 0.03)\n",
    "ax.set_ylim(nfl_epa[\"def_epa\"].max() + 0.03, nfl_epa[\"def_epa\"].min() - 0.03)\n",
    "sdvplot.add_logos(ax, rest[\"off_epa\"], rest[\"def_epa\"], rest[\"team\"], league=\"nfl\", height=0.06, alpha=0.25)\n",
    "sdvplot.add_logos(ax, focus[\"off_epa\"], focus[\"def_epa\"], focus[\"team\"], league=\"nfl\", height=0.12)\n",
    "ax.set_xlabel(\"Offense: EPA per play\")\n",
    "ax.set_ylabel(\"Defense: EPA per play allowed\")\n",
    "ax.set_title(f\"The AFC West against the league, {NFL_SEASON}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, NFLVERSE, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 6. Keep overlapping logos readable\n",
    "\n",
    "Logos are drawn in row order, so the last row ends on top. On the left, the Big Ten in the data's order (by\n",
    "team id) hides some of its best teams; on the right, sorting weakest to strongest puts the contenders on top,\n",
    "and a smaller `height` cuts the overlap."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "big_ten = mbb_ratings.filter(pl.col(\"conference\") == \"Big Ten Conference\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10, 5), sharex=True, sharey=True)\n",
    "for ax, frame, height, label in [\n",
    "    (axes[0], big_ten, 0.13, \"Data order, height 0.13\"),\n",
    "    (axes[1], big_ten.sort(\"adj_em\"), 0.09, \"Best drawn last, height 0.09\"),\n",
    "]:\n",
    "    ax.set_xlim(big_ten[\"adj_o\"].min() - 3, big_ten[\"adj_o\"].max() + 3)\n",
    "    ax.set_ylim(big_ten[\"adj_d\"].max() + 3, big_ten[\"adj_d\"].min() - 3)  # inverted: good defense is up\n",
    "    sdvplot.add_logos(ax, frame[\"adj_o\"], frame[\"adj_d\"], frame[\"team_id\"], league=\"mbb\", height=height)\n",
    "    ax.set_title(label, loc=\"left\", fontsize=10)\n",
    "    ax.set_xlabel(\"Adjusted offense (points per 100)\")\n",
    "axes[0].set_ylabel(\"Adjusted defense (points allowed per 100)\")\n",
    "fig.suptitle(\"Big Ten adjusted efficiency, 2025-26\", x=0.01, ha=\"left\", fontweight=\"bold\")\n",
    "fig.subplots_adjust(bottom=0.15)\n",
    "fig.text(0.99, 0.01, HOOPR, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "For a single team that must stay visible, draw it in its own `add_logos` call with a higher `zorder`.\n",
    "\n",
    "## 7. Show the logo a team wore that season\n",
    "\n",
    "nflverse files past seasons under today's codes (`LV`, `LAC`, `LA`), but `season=` still picks the mark in\n",
    "use that year: 2012 brings back the Oakland, San Diego and St. Louis logos. Teams with no older mark in the\n",
    "archive keep today's."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "epa_2012 = epa_per_play(nfl.load_nfl_team_stats([2012]).filter(pl.col(\"season_type\") == \"REG\"))\n",
    "moved = {\"LV\": (\"Oakland\", -26), \"LAC\": (\"San Diego\", 24), \"LA\": (\"St. Louis\", -26)}  # label, offset (pt)\n",
    "then = epa_2012.filter(pl.col(\"team\").is_in(list(moved)))\n",
    "rest = epa_2012.filter(~pl.col(\"team\").is_in(list(moved)))\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.set_xlim(epa_2012[\"off_epa\"].min() - 0.03, epa_2012[\"off_epa\"].max() + 0.03)\n",
    "ax.set_ylim(epa_2012[\"def_epa\"].max() + 0.03, epa_2012[\"def_epa\"].min() - 0.03)\n",
    "x, y = \"off_epa\", \"def_epa\"\n",
    "sdvplot.add_logos(ax, rest[x], rest[y], rest[\"team\"], league=\"nfl\", season=2012, height=0.06, alpha=0.3)\n",
    "sdvplot.add_logos(ax, then[x], then[y], then[\"team\"], league=\"nfl\", season=2012, height=0.1)\n",
    "for team, xi, yi in then.select(\"team\", x, y).iter_rows():\n",
    "    label, dy = moved[team]\n",
    "    ax.annotate(label, (xi, yi), xytext=(0, dy), textcoords=\"offset points\", ha=\"center\", fontweight=\"bold\")\n",
    "ax.set_xlabel(\"Offense: EPA per play\")\n",
    "ax.set_ylabel(\"Defense: EPA per play allowed\")\n",
    "ax.set_title(\"NFL offense vs defense, 2012 regular season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, NFLVERSE, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## 8. Put a logo beside the title\n",
    "\n",
    "`title_image` sets the title and draws a team's logo (or any image) beside it. `height` is in points, so give\n",
    "a tall image room with `pad=`. One team's season, week by week:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {},
   "outputs": [],
   "source": [
    "plays = pl.col(\"attempts\") + pl.col(\"sacks_suffered\") + pl.col(\"carries\")\n",
    "kc = (\n",
    "    nfl_weeks.filter(pl.col(\"team\") == \"KC\")\n",
    "    .with_columns(epa=(pl.col(\"passing_epa\") + pl.col(\"rushing_epa\")) / plays)\n",
    "    .sort(\"week\")\n",
    ")\n",
    "good, bad = sdvplot.team_colors(\"KC\", \"nfl\"), sdvplot.team_colors(\"KC\", \"nfl\", \"secondary\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 5))\n",
    "ax.bar(kc[\"week\"], kc[\"epa\"], color=[good if e >= 0 else bad for e in kc[\"epa\"]], edgecolor=\"black\")\n",
    "ax.axhline(0, color=\"black\", linewidth=0.8)\n",
    "ax.set_xticks(kc[\"week\"].to_list(), kc[\"opponent_team\"].to_list(), fontsize=8)\n",
    "bye = sorted(set(range(1, kc[\"week\"].max() + 1)) - set(kc[\"week\"]))\n",
    "ax.set_xlabel(f\"Opponent, by week (bye: week {bye[0]})\")\n",
    "ax.set_ylabel(\"Offense EPA per play\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "title = f\"Chiefs offense, week by week, {NFL_SEASON}\"\n",
    "title_image(ax, \"KC\", title, league=\"nfl\", height=28, loc=\"left\", fontweight=\"bold\", pad=12)\n",
    "fig.subplots_adjust(bottom=0.15)\n",
    "fig.text(0.99, 0.01, NFLVERSE, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "## 9. Build a tier list\n",
    "\n",
    "`team_tiers` takes a frame with `team` and `tier_no` (1 on top) and returns a finished figure on sdvplotR's\n",
    "Tiermaker theme; `tier_desc` names the tiers. NBA teams tiered by point differential:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "tiers = nba_teams.sort([\"diff\", \"team_abbreviation\"], descending=[True, False]).with_columns(\n",
    "    team=pl.col(\"team_abbreviation\"),\n",
    "    tier_no=pl.when(pl.col(\"diff\") >= 6)\n",
    "    .then(1)\n",
    "    .when(pl.col(\"diff\") >= 2)\n",
    "    .then(2)\n",
    "    .when(pl.col(\"diff\") >= -2)\n",
    "    .then(3)\n",
    "    .when(pl.col(\"diff\") >= -6)\n",
    "    .then(4)\n",
    "    .otherwise(5),\n",
    ")\n",
    "fig = team_tiers(\n",
    "    tiers,\n",
    "    \"nba\",\n",
    "    title=\"NBA tiers, 2025-26\",\n",
    "    subtitle=\"By average point differential per game\",\n",
    "    caption=HOOPR,\n",
    "    tier_desc={1: \"+6 or better\", 2: \"+2 to +6\", 3: \"-2 to +2\", 4: \"-6 to -2\", 5: \"Below -6\"},\n",
    ")\n",
    "fig.set_size_inches(9, 6)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22",
   "metadata": {},
   "source": [
    "## 10. Place any image: conference logos\n",
    "\n",
    "`add_images` is `add_logos` for any picture, by URL or local path, with the same `height`. Conferences are not\n",
    "teams, so their marks come from ESPN's conference logo URLs, one per bar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {},
   "outputs": [],
   "source": [
    "CONFERENCES = {\n",
    "    \"Southeastern Conference\": \"sec\",\n",
    "    \"Big Ten Conference\": \"big_ten\",\n",
    "    \"Big 12 Conference\": \"big_12\",\n",
    "    \"Big East Conference\": \"big_east\",\n",
    "    \"Atlantic Coast Conference\": \"acc\",\n",
    "    \"Mountain West Conference\": \"mountain_west\",\n",
    "    \"West Coast Conference\": \"west_coast\",\n",
    "    \"Atlantic 10 Conference\": \"atlantic_10\",\n",
    "    \"American Conference\": \"american\",\n",
    "    \"Missouri Valley Conference\": \"missouri_valley\",\n",
    "}\n",
    "conf = (\n",
    "    mbb_ratings.filter(pl.col(\"conference\").is_in(list(CONFERENCES)))\n",
    "    .group_by(\"conference\", maintain_order=True)\n",
    "    .agg(pl.col(\"adj_em\").mean())\n",
    "    .sort(\"adj_em\")\n",
    ")\n",
    "urls = [f\"https://a.espncdn.com/i/teamlogos/ncaa_conf/500/{CONFERENCES[c]}.png\" for c in conf[\"conference\"]]\n",
    "y = list(range(conf.height))\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.barh(y, conf[\"adj_em\"], color=\"#4a6fa5\", height=0.6)\n",
    "ax.set_xlim(0, conf[\"adj_em\"].max() + 5)\n",
    "add_images(ax, conf[\"adj_em\"] + 2.5, y, urls, height=0.08)\n",
    "ax.set_yticks([])\n",
    "ax.set_xlabel(\"Average adjusted efficiency margin (points per 100 possessions)\")\n",
    "ax.spines[[\"top\", \"right\", \"left\"]].set_visible(False)\n",
    "ax.set_title(\"How strong is the average team? Ten conferences, 2025-26\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, f\"{HOOPR}; conference logos: ESPN\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {},
   "source": [
    "## 11. Put headshots on a scatter\n",
    "\n",
    "`add_headshots` works like `add_logos` with player ids instead of teams; ESPN athlete ids, as in hoopR's player\n",
    "box score, work directly. The season's top scorers (50+ games), by volume and efficiency:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25",
   "metadata": {},
   "outputs": [],
   "source": [
    "players = nba.load_nba_player_boxscore(seasons=[SEASON]).filter((pl.col(\"season_type\") == 2) & ~pl.col(\"did_not_play\"))\n",
    "scorers = (\n",
    "    players.group_by(\"athlete_id\", \"athlete_display_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        games=pl.len(),\n",
    "        ppg=pl.col(\"points\").mean(),\n",
    "        ts=pl.col(\"points\").sum()\n",
    "        / (2 * (pl.col(\"field_goals_attempted\").sum() + 0.44 * pl.col(\"free_throws_attempted\").sum())),\n",
    "    )\n",
    "    .filter(pl.col(\"games\") >= 50)\n",
    "    .sort(\"ppg\", descending=True)\n",
    "    .head(10)\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.set_xlim(scorers[\"ppg\"].min() - 1, scorers[\"ppg\"].max() + 2)\n",
    "ax.set_ylim(scorers[\"ts\"].min() - 0.015, scorers[\"ts\"].max() + 0.015)\n",
    "sdvplot.add_headshots(ax, scorers[\"ppg\"], scorers[\"ts\"], scorers[\"athlete_id\"], league=\"nba\", height=0.1)\n",
    "halo = [patheffects.withStroke(linewidth=3, foreground=\"white\")]  # keeps a name readable over a photo\n",
    "for name, ppg, ts in scorers.select(\"athlete_display_name\", \"ppg\", \"ts\").iter_rows():\n",
    "    ax.annotate(name.split()[-1], (ppg, ts), xytext=(0, -24), textcoords=\"offset points\", ha=\"center\",\n",
    "                fontsize=8, path_effects=halo, zorder=4)  # fmt: skip\n",
    "ax.yaxis.set_major_formatter(lambda v, _: f\"{v:.0%}\")\n",
    "ax.set_xlabel(\"Points per game\")\n",
    "ax.set_ylabel(\"True shooting %\")\n",
    "ax.set_title(\"The NBA's top scorers, 2025-26: volume vs efficiency\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, HOOPR, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26",
   "metadata": {},
   "source": [
    "## 12. Save at social media sizes\n",
    "\n",
    "Size the figure in inches times dpi: 10.8 x 10.8 in at 100 dpi is 1080 x 1080 px (square), 12 x 6.75 in is\n",
    "1200 x 675 px (a landscape card). Skip `bbox_inches=\"tight\"`, which trims the canvas to a different size;\n",
    "logos scale with the Axes, so nothing needs resizing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27",
   "metadata": {},
   "outputs": [],
   "source": [
    "out = Path(tempfile.mkdtemp())\n",
    "for name, size in {\"square\": (10.8, 10.8), \"landscape\": (12, 6.75)}.items():\n",
    "    fig, ax = plt.subplots(figsize=size, dpi=100, layout=\"constrained\")\n",
    "    ax.set_xlim(nfl_epa[\"off_epa\"].min() - 0.03, nfl_epa[\"off_epa\"].max() + 0.03)\n",
    "    ax.set_ylim(nfl_epa[\"def_epa\"].max() + 0.03, nfl_epa[\"def_epa\"].min() - 0.03)\n",
    "    sdvplot.add_logos(ax, nfl_epa[\"off_epa\"], nfl_epa[\"def_epa\"], nfl_epa[\"team\"], league=\"nfl\", height=0.08)\n",
    "    ax.set_xlabel(\"Offense: EPA per play\")\n",
    "    ax.set_ylabel(\"Defense: EPA per play allowed\")\n",
    "    ax.set_title(f\"NFL offense vs defense, {NFL_SEASON}\", loc=\"left\", fontweight=\"bold\", fontsize=16)\n",
    "    fig.text(0.99, 0.005, NFLVERSE, ha=\"right\", fontsize=9, color=\"grey\")\n",
    "    fig.savefig(out / f\"{name}.png\", dpi=100)\n",
    "    plt.close(fig)\n",
    "    print(name, plt.imread(out / f\"{name}.png\").shape[1::-1])\n",
    "\n",
    "display(Image(filename=out / \"landscape.png\", width=600))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "sdvplot": {
   "description": "Twelve matplotlib recipes: logos as points, at bar ends, on axes and line ends, beside titles, in tier lists, plus sizing, overlap, eras, any image, headshots and social-size exports.",
   "label": "Logos in matplotlib",
   "position": 1
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
