{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# MLB\n",
    "\n",
    "This page is regenerated every week by sdvplot's docs workflow. It builds the division standings, charts run differential with logos and lists the league leaders with\n",
    "their headshots, for the latest MLB season with games: the season to date from opening day to the end of September,\n",
    "the final regular season after that. Data: the MLB Stats API and ESPN, read through\n",
    "[sportsdataverse-py](https://py.sportsdataverse.org/); no key needed."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1",
   "metadata": {},
   "source": [
    "The season runs inside one calendar year, but before opening day the Stats API standings for the new season come back\n",
    "empty or with no games played. The helper turns that into a `NoDataError` and the page steps back one season."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2",
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime as dt\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.mlb as mlb\n",
    "from IPython.display import Markdown, display\n",
    "from sportsdataverse.errors import NoDataError\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "today = dt.date.today()\n",
    "current = today.year\n",
    "STATS_API = \"Data: MLB Stats API via sportsdataverse-py\"\n",
    "\n",
    "\n",
    "def standings_for(season):\n",
    "    table = mlb.parse_mlb_api_standings(mlb.mlb_standings(season=season, hydrate=\"division\"))\n",
    "    if table.is_empty() or table[\"games_played\"].max() == 0:\n",
    "        raise NoDataError(f\"no {season} regular-season games yet\")\n",
    "    return table\n",
    "\n",
    "\n",
    "try:\n",
    "    season, standings = current, standings_for(current)\n",
    "except NoDataError as err:\n",
    "    print(f\"{err}; showing {current - 1} instead\")\n",
    "    season, standings = current - 1, standings_for(current - 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3",
   "metadata": {},
   "source": [
    "The Stats API's season calendar (`mlb_season`) says whether the regular season is still being played, so the status\n",
    "line can say what the numbers cover."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4",
   "metadata": {},
   "outputs": [],
   "source": [
    "calendar = mlb.mlb_season(season_id=season).row(0, named=True)\n",
    "regular_end = dt.date.fromisoformat(calendar[\"regular_season_end_date\"])\n",
    "post_end = dt.date.fromisoformat(calendar[\"post_season_end_date\"])\n",
    "if season < current:\n",
    "    status = f\"**Offseason:** the final {season} regular season; the {current} season has no games yet.\"\n",
    "    through = \"final regular season\"\n",
    "elif today <= regular_end:\n",
    "    games = int(standings[\"games_played\"].median())\n",
    "    status = f\"**Season to date:** the {season} season to date, about {games} games per team.\"\n",
    "    through = f\"through {games} games\"\n",
    "elif today <= post_end:\n",
    "    status = f\"**Postseason:** the final {season} regular season; the postseason is under way.\"\n",
    "    through = \"final regular season\"\n",
    "else:\n",
    "    status = f\"**Offseason:** the final {season} regular season.\"\n",
    "    through = \"final regular season\"\n",
    "display(Markdown(status))\n",
    "\n",
    "clubs = mlb.parse_mlb_api_teams(mlb.mlb_teams(season=season)).select(pl.col(\"id\").alias(\"team_id\"), \"abbreviation\")\n",
    "assert standings.schema[\"team_id\"] == clubs.schema[\"team_id\"]\n",
    "standings = standings.join(clubs, on=\"team_id\").select(\n",
    "    \"abbreviation\",\n",
    "    \"team_name\",\n",
    "    division=\"standings_division_name\",\n",
    "    rank=pl.col(\"division_rank\").cast(pl.Int64),\n",
    "    w=\"wins\",\n",
    "    l=\"losses\",\n",
    "    pct=\"winning_percentage\",\n",
    "    gb=\"games_back\",\n",
    "    rs=\"runs_scored\",\n",
    "    ra=\"runs_allowed\",\n",
    "    diff=\"run_differential\",\n",
    "    strk=\"streak_streak_code\",\n",
    ")\n",
    "standings.sort(\"division\", \"rank\").head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5",
   "metadata": {},
   "source": [
    "## 1. Division standings\n",
    "\n",
    "Six tables in one, grouped by division. `gt_sdv_logos` turns the Stats API abbreviations into logos and\n",
    "`gt_color_pills` draws the run differential on a scale centred at zero."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from great_tables import GT\n",
    "\n",
    "from sdvplot.great_tables import gt_color_pills, gt_save_crop, gt_sdv_logos, gt_theme_broadsheet\n",
    "\n",
    "table = standings.sort(\"division\", \"rank\").select(\n",
    "    \"division\", \"abbreviation\", \"team_name\", \"w\", \"l\", \"pct\", \"gb\", \"rs\", \"ra\", \"diff\", \"strk\"\n",
    ")\n",
    "reach = max(abs(table[\"diff\"].min()), table[\"diff\"].max())\n",
    "gt = (\n",
    "    GT(table, groupname_col=\"division\", id=\"mlb-standings\")  # fixed id: no random one each run\n",
    "    .tab_header(f\"MLB standings, {season}\", f\"By division, {through}\")\n",
    "    .cols_label(\n",
    "        abbreviation=\"\",\n",
    "        team_name=\"Team\",\n",
    "        w=\"W\",\n",
    "        l=\"L\",\n",
    "        pct=\"Pct\",\n",
    "        gb=\"GB\",\n",
    "        rs=\"RS\",\n",
    "        ra=\"RA\",\n",
    "        diff=\"Diff\",\n",
    "        strk=\"Streak\",\n",
    "    )\n",
    "    .tab_source_note(STATS_API)\n",
    ")\n",
    "gt = gt_color_pills(gt, \"diff\", palette=[\"#c84630\", \"#f7f7f7\", \"#2a7ab9\"], domain=[-reach, reach], digits=0)\n",
    "gt = gt_theme_broadsheet(gt_sdv_logos(gt, \"abbreviation\", league=\"mlb\", height=24))\n",
    "gt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7",
   "metadata": {},
   "source": [
    "`gt_save_crop` renders the same table to a trimmed PNG, ready to post."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "MLB standings table grouped by division with team logos, records, games back and run differential pills.",
     "title": "MLB division standings"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "gt_save_crop(gt, width=900)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9",
   "metadata": {},
   "source": [
    "## 2. Run differential\n",
    "\n",
    "Every club's run differential as a bar in its colors, best at the top, the logo at the end of each bar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Horizontal bars of every MLB club's run differential in team colors, with logos at the bar ends.",
     "title": "MLB run differential"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "rd = standings.sort(\"diff\", \"abbreviation\")  # ties broken by name, so each re-render matches\n",
    "fig, ax = plt.subplots(figsize=(9, 8))\n",
    "y = list(range(rd.height))\n",
    "ax.barh(y, rd[\"diff\"], color=sdvplot.team_colors(rd[\"abbreviation\"].to_list(), \"mlb\"), height=0.72)\n",
    "ax.axvline(0, color=\"#222222\", lw=0.8)\n",
    "reach = max(abs(rd[\"diff\"].min()), rd[\"diff\"].max())\n",
    "ends = [d + (0.06 if d >= 0 else -0.06) * reach for d in rd[\"diff\"]]\n",
    "ax.set_xlim(-1.15 * reach, 1.15 * reach)\n",
    "ax.set_ylim(-0.8, rd.height - 0.2)\n",
    "ax.set_yticks(y, [f\"{name} ({d:+d})\" for name, d in zip(rd[\"team_name\"], rd[\"diff\"], strict=True)], fontsize=8)\n",
    "ax.spines[[\"top\", \"right\", \"left\"]].set_visible(False)\n",
    "ax.tick_params(axis=\"y\", length=0)\n",
    "ax.set_xlabel(\"Run differential (runs scored minus runs allowed)\")\n",
    "ax.set_title(f\"MLB run differential, {season} {through}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, STATS_API, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "sdvplot.add_logos(ax, ends, y, rd[\"abbreviation\"], league=\"mlb\", season=season, height=0.03)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11",
   "metadata": {},
   "source": [
    "## 3. League leaders with headshots\n",
    "\n",
    "ESPN's leaders endpoint sorted by one statistic at a time (`season_type=2` is the regular season; the rate stats list\n",
    "qualified players). Its athlete ids feed `gt_sdv_headshots` and its team abbreviations feed `gt_sdv_logos`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sdvplot.great_tables import gt_sdv_headshots, gt_theme_savant\n",
    "\n",
    "CATEGORIES = [  # (label, ESPN group, statistic, sort order, format)\n",
    "    (\"Home runs\", \"batting\", \"homeRuns\", \"desc\", \"{:.0f}\"),\n",
    "    (\"Batting average\", \"batting\", \"avg\", \"desc\", \"{:.3f}\"),\n",
    "    (\"OPS\", \"batting\", \"OPS\", \"desc\", \"{:.3f}\"),\n",
    "    (\"Stolen bases\", \"batting\", \"stolenBases\", \"desc\", \"{:.0f}\"),\n",
    "    (\"ERA\", \"pitching\", \"ERA\", \"asc\", \"{:.2f}\"),\n",
    "    (\"Strikeouts\", \"pitching\", \"strikeouts\", \"desc\", \"{:.0f}\"),\n",
    "]\n",
    "rows = []\n",
    "for name, group, stat, order, fmt in CATEGORIES:\n",
    "    raw = mlb.espn_mlb_leaders(\n",
    "        season=season, season_type=2, sort=f\"{group}.{stat}:{order}\", limit=3, return_parsed=False\n",
    "    )\n",
    "    labels = next(c[\"names\"] for c in raw[\"categories\"] if c[\"name\"] == group)\n",
    "    for rank, a in enumerate(raw[\"athletes\"], start=1):\n",
    "        values = dict(zip(labels, next(c[\"values\"] for c in a[\"categories\"] if c[\"name\"] == group), strict=True))\n",
    "        rows.append(\n",
    "            {\n",
    "                \"category\": name,\n",
    "                \"rank\": rank,\n",
    "                \"espn_id\": a[\"athlete\"][\"id\"],\n",
    "                \"player\": a[\"athlete\"][\"displayName\"],\n",
    "                \"team\": a[\"athlete\"][\"teamShortName\"],\n",
    "                \"value\": fmt.format(values[stat]),\n",
    "            }\n",
    "        )\n",
    "leaders = pl.DataFrame(rows)\n",
    "\n",
    "leaders_gt = (\n",
    "    GT(leaders, groupname_col=\"category\", id=\"mlb-leaders\")\n",
    "    .tab_header(f\"MLB leaders, {season}\", f\"Top three, {through}\")\n",
    "    .cols_label(rank=\"\", espn_id=\"\", player=\"Player\", team=\"\", value=\"\")\n",
    "    .cols_align(\"right\", \"value\")\n",
    "    .tab_source_note(\"Data: ESPN via sportsdataverse-py\")\n",
    ")\n",
    "leaders_gt = gt_sdv_headshots(leaders_gt, \"espn_id\", league=\"mlb\", height=36)\n",
    "leaders_gt = gt_theme_savant(gt_sdv_logos(leaders_gt, \"team\", league=\"mlb\", height=22))\n",
    "leaders_gt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Table of the top three MLB players in six batting and pitching categories, with headshots and team logos.",
     "title": "MLB leaders with headshots"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "gt_save_crop(leaders_gt, width=700)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "sdvplot": {
   "description": "The latest MLB season, rebuilt every week: division standings, run differential with logos and the league leaders with headshots.",
   "label": "MLB",
   "position": 5
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
