{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Tables\n",
    "\n",
    "Twelve recipes for team and player marks in tables: great_tables with sdvplot's logo and headshot cells, its\n",
    "twenty themes, the cell helpers (color pills, percentile bars, indicator boxes, legends, tier lists, snaked\n",
    "lists, captions) and image export for social posts, then the same marks in reactable and plottable. The data\n",
    "is one season each from the NHL (its standings endpoint), MLB (ESPN), the NBA, WNBA and men's college basketball\n",
    "(hoopR and wehoop) and the NFL (nflverse), all through sportsdataverse-py. Tables saved as images go to a\n",
    "temporary folder."
   ]
  },
  {
   "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.mlb as mlb\n",
    "import sportsdataverse.nba as nba\n",
    "import sportsdataverse.nfl as nfl\n",
    "import sportsdataverse.nhl as nhl\n",
    "import sportsdataverse.wnba as wnba\n",
    "from great_tables import GT\n",
    "from IPython.display import Image, display\n",
    "\n",
    "import sdvplot\n",
    "from sdvplot.great_tables import (\n",
    "    gt_538_caption,\n",
    "    gt_color_pills,\n",
    "    gt_grid,\n",
    "    gt_indicator_boxes,\n",
    "    gt_legend_continuous,\n",
    "    gt_merge_stack_team_color,\n",
    "    gt_percentile_bar,\n",
    "    gt_save_crop,\n",
    "    gt_sdv_headshots,\n",
    "    gt_sdv_logos,\n",
    "    gt_snake,\n",
    "    gt_social_crop,\n",
    "    gt_theme_almanac,\n",
    "    gt_theme_athletic,\n",
    "    gt_theme_kenpom,\n",
    "    gt_theme_preview,\n",
    "    gt_theme_savant,\n",
    "    gt_theme_sdv,\n",
    "    gt_theme_sdv_team,\n",
    "    gt_tiers,\n",
    ")\n",
    "\n",
    "NFL_SEASON = 2025  # nflverse names a season by the year it starts\n",
    "SEASON = 2026  # the 2026 MLB and WNBA seasons, and the 2025-26 NBA, NHL and college basketball season\n",
    "OUT = Path(tempfile.mkdtemp())  # saved images go here, not into your working folder"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "The shared tables: the NHL's final 2025-26 standings (from the NHL's standings endpoint, as of the last day of\n",
    "the regular season) and MLB's final 2026 standings from ESPN."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "nhl_standings = nhl.nhl_standings(\"2026-04-16\")\n",
    "mlb_standings = mlb.espn_mlb_standings(season=SEASON).with_columns(\n",
    "    pl.col(\"wins\", \"losses\", \"points_for\", \"points_against\", \"point_differential\").cast(pl.Int64)\n",
    ")\n",
    "nhl_standings[\"conference_name\"].unique().sort().to_list(), mlb_standings[\"group_name\"].unique().sort().to_list()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 1. Logos in a team column, with a theme\n",
    "\n",
    "`gt_sdv_logos(gt, column, league=...)` turns each cell's team into its logo; any id the index knows works (here\n",
    "the NHL's own abbreviations). `gt_theme_sdv` is the SportsDataverse house style: Chivo labels, a Lato body and\n",
    "the SDV gradient under the column labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "east = (\n",
    "    nhl_standings.filter(pl.col(\"conference_name\") == \"Eastern\")\n",
    "    .sort(\"division_name\", \"division_sequence\")\n",
    "    .select(\n",
    "        division=\"division_name\",\n",
    "        logo=\"team_abbrev_default\",\n",
    "        team=\"team_name_default\",\n",
    "        gp=\"games_played\",\n",
    "        w=\"wins\",\n",
    "        l=\"losses\",\n",
    "        otl=\"ot_losses\",\n",
    "        pts=\"points\",\n",
    "        pct=\"point_pctg\",\n",
    "        diff=\"goal_differential\",\n",
    "    )\n",
    ")\n",
    "gt = (\n",
    "    GT(east, groupname_col=\"division\")\n",
    "    .tab_header(\"Eastern Conference standings, 2025-26\", \"Final regular season, by division\")\n",
    "    .cols_label(logo=\"\", team=\"Team\", gp=\"GP\", w=\"W\", l=\"L\", otl=\"OTL\", pts=\"PTS\", pct=\"PTS%\", diff=\"DIFF\")\n",
    "    .fmt_number(\"pct\", decimals=3)\n",
    "    .tab_source_note(\"Data: NHL via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_theme_sdv(gt_sdv_logos(gt, \"logo\", league=\"nhl\", height=26))\n",
    "gt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Compare themes, and save them as one image\n",
    "\n",
    "`gt_theme_preview` gives the same rows in each theme you name; `gt_grid` lays tables out side by side and,\n",
    "with `file=`, saves the grid as a PNG (through headless Chrome). MLB's six best records in four looks:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "The same MLB standings table in four sdvplot great_tables themes, saved as one image with gt_grid",
     "title": "Four great_tables themes side by side"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "best = (\n",
    "    mlb_standings.sort([\"win_percent\", \"team_abbreviation\"], descending=[True, False])\n",
    "    .head(6)\n",
    "    .select(logo=\"team_abbreviation\", Team=\"team_display_name\", W=\"wins\", L=\"losses\", Diff=\"point_differential\")\n",
    ")\n",
    "themes = gt_theme_preview(\n",
    "    best, themes=[\"gt_theme_athletic\", \"gt_theme_savant\", \"gt_theme_broadsheet\", \"gt_theme_ncaa\"], n=6\n",
    ")\n",
    "tables = [gt_sdv_logos(t.cols_label(logo=\"\"), \"logo\", league=\"mlb\", height=22) for t in themes.values()]\n",
    "grid = gt_grid(\n",
    "    tables,\n",
    "    ncol=2,\n",
    "    labels=[name.removeprefix(\"gt_theme_\") for name in themes],\n",
    "    title=\"One table, four themes\",\n",
    "    subtitle=f\"MLB's best records, {SEASON}\",\n",
    "    source_note=\"Data: ESPN via sportsdataverse-py\",\n",
    "    file=OUT / \"themes.png\",\n",
    ")\n",
    "display(Image(filename=grid, width=760))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. Headshots and a two-line name cell\n",
    "\n",
    "`gt_sdv_headshots` turns ESPN athlete ids into photos. `gt_merge_stack_team_color` stacks the player's name over\n",
    "the team's, the second line in the team's color. The NBA's top scorers, saved with `gt_save_crop`, the way\n",
    "you would post them:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "A great_tables leaderboard of the NBA's top ten scorers with headshots, team logos and stacked names",
     "title": "NBA scoring leaders table with headshots"
    },
    "tags": [
     "gallery"
    ]
   },
   "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.sort(\"game_date\")\n",
    "    .group_by(\"athlete_id\", \"athlete_display_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        team=pl.col(\"team_abbreviation\").last(),\n",
    "        team_name=pl.col(\"team_display_name\").last(),\n",
    "        gp=pl.len(),\n",
    "        ppg=pl.col(\"points\").mean(),\n",
    "        rpg=pl.col(\"rebounds\").mean(),\n",
    "        apg=pl.col(\"assists\").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(\"gp\") >= 50)\n",
    "    .sort(\"ppg\", descending=True)\n",
    "    .head(10)\n",
    "    .with_columns(rank=pl.int_range(1, 11), photo=pl.col(\"athlete_id\"), logo=pl.col(\"team\"))\n",
    "    .select(\"rank\", \"photo\", \"athlete_display_name\", \"team_name\", \"team\", \"logo\", \"gp\", \"ppg\", \"rpg\", \"apg\", \"ts\")\n",
    ")\n",
    "gt = (\n",
    "    GT(scorers)\n",
    "    .tab_header(\"The NBA's top scorers, 2025-26\", \"Regular season, 50 or more games\")\n",
    "    .cols_label(\n",
    "        rank=\"\", photo=\"\", athlete_display_name=\"Player\", logo=\"\", gp=\"GP\", ppg=\"PTS\", rpg=\"REB\", apg=\"AST\", ts=\"TS%\"\n",
    "    )\n",
    "    .fmt_number([\"ppg\", \"rpg\", \"apg\"], decimals=1)\n",
    "    .fmt_percent(\"ts\", decimals=1)\n",
    "    .tab_source_note(\"Data: hoopR (ESPN) via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_merge_stack_team_color(gt, \"athlete_display_name\", \"team_name\", \"team\", league=\"nba\")\n",
    "gt = gt_sdv_headshots(gt, \"photo\", league=\"nba\", height=44)\n",
    "gt = gt_sdv_logos(gt, \"logo\", league=\"nba\", height=24).cols_hide(\"team\")\n",
    "gt = gt_theme_athletic(gt)\n",
    "display(Image(filename=gt_save_crop(gt, OUT / \"scorers.png\"), width=720))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 4. Color pills with a matching legend\n",
    "\n",
    "`gt_color_pills` shows each value as a rounded pill filled from a palette, with black or white text, whichever\n",
    "reads; give it a `domain` so the colors mean the same thing in every table. It records its scale, so\n",
    "`gt_legend_continuous(gt)` draws a key that cannot disagree with the cells. The American League:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "al = (\n",
    "    mlb_standings.filter(pl.col(\"group_name\") == \"American League\")\n",
    "    .sort([\"win_percent\", \"team_abbreviation\"], descending=[True, False])\n",
    "    .select(\n",
    "        logo=\"team_abbreviation\",\n",
    "        team=\"team_display_name\",\n",
    "        w=\"wins\",\n",
    "        l=\"losses\",\n",
    "        pct=\"win_percent\",\n",
    "        rs=\"points_for\",\n",
    "        ra=\"points_against\",\n",
    "        diff=\"point_differential\",\n",
    "    )\n",
    ")\n",
    "gt = (\n",
    "    GT(al)\n",
    "    .tab_header(f\"American League, {SEASON}\", \"Final regular season\")\n",
    "    .cols_label(logo=\"\", team=\"Team\", w=\"W\", l=\"L\", pct=\"PCT\", rs=\"RS\", ra=\"RA\", diff=\"DIFF\")\n",
    "    .fmt_number(\"pct\", decimals=3)\n",
    "    .tab_source_note(\"Data: ESPN via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_theme_savant(gt_sdv_logos(gt, \"logo\", league=\"mlb\", height=24))  # theme first, then the fills\n",
    "gt = gt_color_pills(gt, \"diff\", palette=[\"#C84630\", \"#F4F4F4\", \"#2A7AB9\"], domain=(-250, 250), digits=0)\n",
    "gt_legend_continuous(gt, title=\"Run differential\", labels=[\"-250\", \"0\", \"+250\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 5. Gotcha: stripes and themes can paint over your fills\n",
    "\n",
    "Two rules keep cell fills visible. Rule 1 is for plain fills: `data_color`, `gt_color_ranks` (built on it) and\n",
    "`tab_style` fills. `gt_color_results`, `gt_highlight_cells` and sdvplot's row helpers mark their fills `!important`,\n",
    "which stripes cannot cover. Rule 2 is for every fill.\n",
    "\n",
    "1. **Turn row striping off when you fill cells.** In a notebook, and on these pages, great_tables shows a table\n",
    "   with every CSS rule marked `!important`, so a striped row's background beats the fill. Use\n",
    "   `opt_row_striping(row_striping=False)` (or a theme's own switch, such as `row_striping_include_table_body`).\n",
    "   Saved images and `as_raw_html()` keep the fills, so a table can look right in an export and wrong here.\n",
    "2. **Apply a theme before the fills.** Some themes band their rows with cell styles: `gt_theme_kenpom` fills\n",
    "   every body row. Applied after the fills, it replaces them, and turning striping off does not help, because\n",
    "   those bands are cell styles, not striping.\n",
    "\n",
    "The first two tables show rule 1; the pair at the end shows rule 2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "top = (\n",
    "    nhl_standings.sort([\"goal_differential\", \"team_abbrev_default\"], descending=[True, False])\n",
    "    .head(8)\n",
    "    .select(logo=\"team_abbrev_default\", team=\"team_name_default\", diff=\"goal_differential\")\n",
    ")\n",
    "\n",
    "\n",
    "def fill(gt):\n",
    "    return gt.data_color(\"diff\", palette=[\"#FFFFFF\", \"#2E7D32\"], domain=[0, 130])\n",
    "\n",
    "\n",
    "base = gt_sdv_logos(GT(top).cols_label(logo=\"\", team=\"Team\", diff=\"Goal diff.\"), \"logo\", league=\"nhl\", height=22)\n",
    "display(fill(base.opt_row_striping()).tab_header(\"Striping on: every other fill is covered\"))\n",
    "display(fill(base.opt_row_striping(row_striping=False)).tab_header(\"Striping off: every fill shows\"))\n",
    "gt_grid(\n",
    "    [gt_theme_kenpom(fill(base)), fill(gt_theme_kenpom(base))],\n",
    "    labels=[\"Fill, then theme: the fill is gone\", \"Theme, then fill: the fill shows\"],\n",
    "    source_note=\"Data: NHL via sportsdataverse-py\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 6. Percentile bars, in a team's colors\n",
    "\n",
    "`gt_percentile_bar` draws each percentile as a track with a marker. Here the WNBA's best regular-season team\n",
    "against the league, each stat ranked so 100 is best (fewest points allowed and turnovers count as high).\n",
    "`gt_theme_sdv_team` themes the table in that team's colors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "games = wnba.load_wnba_team_boxscore(seasons=[SEASON]).filter(pl.col(\"season_type\") == 2)\n",
    "made, tried = pl.col(\"three_point_field_goals_made\"), pl.col(\"three_point_field_goals_attempted\")\n",
    "per_game = (\n",
    "    games.group_by(\"team_abbreviation\", \"team_display_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        gp=pl.len(),\n",
    "        wins=pl.col(\"team_winner\").sum(),\n",
    "        Points=pl.col(\"team_score\").mean(),\n",
    "        Allowed=pl.col(\"opponent_team_score\").mean(),\n",
    "        Rebounds=pl.col(\"total_rebounds\").mean(),\n",
    "        Assists=pl.col(\"assists\").mean(),\n",
    "        Turnovers=pl.col(\"turnovers\").mean(),\n",
    "        three=made.sum() / tried.sum(),\n",
    "    )\n",
    "    .rename({\"three\": \"3P%\"})\n",
    "    .filter(pl.col(\"gp\") > 10)\n",
    ")\n",
    "lower_is_better = {\"Points\": False, \"Allowed\": True, \"Rebounds\": False, \"Assists\": False, \"3P%\": False,\n",
    "                   \"Turnovers\": True}  # fmt: skip\n",
    "ranks = per_game.with_columns(\n",
    "    ((pl.col(s).rank(\"average\", descending=low) - 1) / (pl.len() - 1) * 100).alias(f\"{s} pct\")\n",
    "    for s, low in lower_is_better.items()\n",
    ")\n",
    "team = ranks.sort([\"wins\", \"team_abbreviation\"], descending=[True, False]).row(0, named=True)\n",
    "card = pl.DataFrame(\n",
    "    {\n",
    "        \"stat\": list(lower_is_better),\n",
    "        \"value\": [f\"{team[s]:.1%}\" if s == \"3P%\" else f\"{team[s]:.1f}\" for s in lower_is_better],\n",
    "        \"pct\": [team[f\"{s} pct\"] for s in lower_is_better],\n",
    "    }\n",
    ")\n",
    "won_lost = f\"{team['wins']}-{team['gp'] - team['wins']}\"\n",
    "gt = (\n",
    "    GT(card)\n",
    "    .tab_header(f\"{team['team_display_name']}, {SEASON}\", f\"{won_lost}; per game, as a percentile among WNBA teams\")\n",
    "    .cols_label(stat=\"\", value=\"Per game\", pct=\"Percentile\")\n",
    "    .tab_source_note(\"Data: wehoop (ESPN) via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_theme_sdv_team(gt, team[\"team_abbreviation\"], league=\"wnba\")\n",
    "gt_percentile_bar(gt, \"pct\", width=260)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "## 7. Indicator boxes: a season at a glance\n",
    "\n",
    "`gt_indicator_boxes` swaps values for filled or empty boxes; `key_columns` keeps the columns that are not\n",
    "boxes. One row per team, one box per week: green for a win, grey for a loss or tie, white for the bye. The\n",
    "AFC North:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "schedule = nfl.load_nfl_schedule([NFL_SEASON]).filter(pl.col(\"game_type\") == \"REG\")\n",
    "results = pl.concat(\n",
    "    [\n",
    "        schedule.select(\"week\", team=\"home_team\", pts=\"home_score\", opp=\"away_score\"),\n",
    "        schedule.select(\"week\", team=\"away_team\", pts=\"away_score\", opp=\"home_score\"),\n",
    "    ]\n",
    ").filter(pl.col(\"team\").is_in([\"BAL\", \"CIN\", \"CLE\", \"PIT\"]))\n",
    "record = results.group_by(\"team\", maintain_order=True).agg(\n",
    "    w=(pl.col(\"pts\") > pl.col(\"opp\")).sum(),\n",
    "    l=(pl.col(\"pts\") < pl.col(\"opp\")).sum(),\n",
    "    t=(pl.col(\"pts\") == pl.col(\"opp\")).sum(),\n",
    ")\n",
    "boxes = results.with_columns(win=(pl.col(\"pts\") > pl.col(\"opp\")).cast(pl.Int8)).pivot(\n",
    "    on=\"week\", index=\"team\", values=\"win\"\n",
    ")\n",
    "weeks = [str(w) for w in range(1, schedule[\"week\"].max() + 1)]  # a team's bye week is a missing value\n",
    "grid = (\n",
    "    boxes.join(record, on=\"team\")\n",
    "    .sort([\"w\", \"team\"], descending=[True, False])\n",
    "    .select(\n",
    "        pl.col(\"team\").alias(\"logo\"),\n",
    "        pl.when(pl.col(\"t\") > 0)\n",
    "        .then(pl.format(\"{}-{}-{}\", \"w\", \"l\", \"t\"))\n",
    "        .otherwise(pl.format(\"{}-{}\", \"w\", \"l\"))\n",
    "        .alias(\"record\"),\n",
    "        *weeks,\n",
    "    )\n",
    ")\n",
    "gt = (\n",
    "    GT(grid)\n",
    "    .tab_header(f\"The AFC North, week by week, {NFL_SEASON}\", \"Green: a win. White: the bye week.\")\n",
    "    .cols_label(logo=\"\", record=\"Record\")\n",
    "    .tab_source_note(\"Data: nflverse via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_sdv_logos(gt_theme_sdv(gt), \"logo\", league=\"nfl\", height=28)\n",
    "gt = gt_indicator_boxes(gt, key_columns=[\"logo\", \"record\"], color_yes=\"#2E7D32\", color_na=\"#FFFFFF\", box_width=22)\n",
    "gt.cols_move_to_start([\"logo\", \"record\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## 8. A tier list as a table\n",
    "\n",
    "`gt_tiers` takes one row per tier, a tier column and image columns (URLs or paths); `logo_url` supplies each\n",
    "team's logo. NFL teams tiered by net EPA per play (offense minus defense allowed), saved as an image:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "A great_tables tier list of NFL teams by net EPA per play, five colored tiers of team logos",
     "title": "NFL tier list built with gt_tiers"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "weeks_stats = nfl.load_nfl_team_stats([NFL_SEASON]).filter(pl.col(\"season_type\") == \"REG\")\n",
    "plays = pl.col(\"attempts\") + pl.col(\"sacks_suffered\") + pl.col(\"carries\")\n",
    "epa = pl.col(\"passing_epa\") + pl.col(\"rushing_epa\")\n",
    "net = (\n",
    "    weeks_stats.group_by(\"team\", maintain_order=True)\n",
    "    .agg(off=epa.sum() / plays.sum())\n",
    "    .join(\n",
    "        weeks_stats.group_by(team=pl.col(\"opponent_team\"), maintain_order=True).agg(dfn=epa.sum() / plays.sum()),\n",
    "        on=\"team\",\n",
    "    )\n",
    "    .with_columns(net=pl.col(\"off\") - pl.col(\"dfn\"))\n",
    "    .sort(\"net\", descending=True)\n",
    "    .with_columns(\n",
    "        tier=pl.when(pl.col(\"net\") >= 0.1)\n",
    "        .then(pl.lit(\"Elite\"))\n",
    "        .when(pl.col(\"net\") >= 0.03)\n",
    "        .then(pl.lit(\"Good\"))\n",
    "        .when(pl.col(\"net\") > -0.03)\n",
    "        .then(pl.lit(\"Average\"))\n",
    "        .when(pl.col(\"net\") > -0.1)\n",
    "        .then(pl.lit(\"Below average\"))\n",
    "        .otherwise(pl.lit(\"Rebuilding\"))\n",
    "    )\n",
    ")\n",
    "levels = {\n",
    "    \"Elite\": \"#1B7837\",\n",
    "    \"Good\": \"#7FBC41\",\n",
    "    \"Average\": \"#C9C9C9\",\n",
    "    \"Below average\": \"#F1A340\",\n",
    "    \"Rebuilding\": \"#C84630\",\n",
    "}\n",
    "by_tier = net.group_by(\"tier\", maintain_order=True).agg(pl.col(\"team\"))\n",
    "width = by_tier[\"team\"].list.len().max()\n",
    "rows = {\"tier\": by_tier[\"tier\"].to_list()}\n",
    "for i in range(width):\n",
    "    rows[f\"t{i}\"] = [sdvplot.logo_url(t[i], \"nfl\") if i < len(t) else None for t in by_tier[\"team\"]]\n",
    "gt = (\n",
    "    gt_tiers(GT(pl.DataFrame(rows)), levels, img_height=\"46px\")\n",
    "    .tab_header(f\"NFL tiers, {NFL_SEASON}\", \"Net EPA per play: offense minus defense allowed\")\n",
    "    .tab_source_note(\"Data: nflverse via sportsdataverse-py\")\n",
    ")\n",
    "display(Image(filename=gt_save_crop(gt, OUT / \"tiers.png\", bg=\"#121212\"), width=760))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "## 9. A long list in two columns, with a caption\n",
    "\n",
    "`gt_snake` wraps a long table into side-by-side blocks, suffixing the columns `_1`, `_2`; it rebuilds the table,\n",
    "so apply formats, logos and the theme after it. `gt_538_caption` adds a FiveThirtyEight-style note under a\n",
    "rule. Men's college basketball's top 40 by adjusted efficiency margin:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "top40 = (\n",
    "    mbb.load_mbb_ratings(SEASON)\n",
    "    .join(sdvplot.teams(\"mbb\").select(\"team_id\", \"short_name\"), on=\"team_id\")\n",
    "    .sort(\"adj_em\", descending=True)\n",
    "    .head(40)\n",
    "    .select(rank=pl.int_range(1, 41), logo=\"team_id\", team=\"short_name\", em=\"adj_em\")\n",
    ")\n",
    "gt = gt_snake(GT(top40).cols_label(rank=\"\", logo=\"\", team=\"Team\", em=\"Margin\"), n_cols=2)\n",
    "gt = gt.fmt_number([\"em_1\", \"em_2\"], decimals=1, force_sign=True)\n",
    "gt = gt_sdv_logos(gt, [\"logo_1\", \"logo_2\"], league=\"mbb\", height=20)\n",
    "gt = gt_theme_almanac(gt.tab_header(\"Men's college basketball's top 40, 2025-26\"))\n",
    "gt_538_caption(\n",
    "    gt,\n",
    "    top_caption=\"Margin: adjusted points scored minus allowed per 100 possessions, against an average team.\",\n",
    "    bottom_caption=\"Data: hoopR (ESPN) via sportsdataverse-py\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22",
   "metadata": {},
   "source": [
    "## 10. Save for social: a trimmed image and a square post\n",
    "\n",
    "`gt_save_crop` trims the page around the table and pads an even border; `gt_social_crop` centers the table on\n",
    "a canvas of a fixed ratio (1:1, 4:5, 16:9) without cropping it, and `width=` sets the final pixel width. The\n",
    "percentile card from recipe 6, as a 1080 x 1080 post:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "A great_tables percentile card for the WNBA's best team, centered on a 1080 by 1080 canvas",
     "title": "A WNBA team percentile card as a square social image"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "card_gt = (\n",
    "    GT(card)\n",
    "    .tab_header(f\"{team['team_display_name']}, {SEASON}\", f\"{won_lost}; per game, percentile among WNBA teams\")\n",
    "    .cols_label(stat=\"\", value=\"Per game\", pct=\"Percentile\")\n",
    "    .tab_source_note(\"Data: wehoop (ESPN) via sportsdataverse-py\")\n",
    ")\n",
    "card_gt = gt_theme_sdv_team(card_gt, team[\"team_abbreviation\"], league=\"wnba\", density=\"social\")\n",
    "card_gt = gt_percentile_bar(card_gt, \"pct\", width=300)\n",
    "trimmed = gt_save_crop(card_gt, OUT / \"card.png\")\n",
    "square = gt_social_crop(card_gt, OUT / \"card_square.png\", aspect_ratio=\"1:1\", width=1080, bg=\"#F4F4F4\")\n",
    "for path in (trimmed, square):\n",
    "    w, h = plt.imread(path).shape[1::-1]\n",
    "    print(f\"{Path(path).name}: {w} x {h} px\")\n",
    "display(Image(filename=square, width=540))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {},
   "source": [
    "## 11. reactable: a sortable, searchable table\n",
    "\n",
    "`sdvplot.reactable` gives reactable columns: `reactable_sdv_logos` renders a team column as logos and\n",
    "`reactable_sdv_team_color_bar` draws each value as a bar in the row's team color, as long as its share of\n",
    "`max_value`. A `max_value` a little past the 82-game maximum keeps every bar short of the number, so the number\n",
    "never sits on a dark fill. Click a header to sort; search by team."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25",
   "metadata": {},
   "outputs": [],
   "source": [
    "from reactable import Column, Reactable, embed_css\n",
    "\n",
    "from sdvplot.reactable import reactable_sdv_logos, reactable_sdv_team_color_bar\n",
    "\n",
    "embed_css()\n",
    "nba_teams = (\n",
    "    nba.load_nba_team_boxscore(seasons=[SEASON])\n",
    "    .filter(pl.col(\"season_type\") == 2)\n",
    "    .group_by(\"team_abbreviation\", \"team_display_name\", maintain_order=True)\n",
    "    .agg(\n",
    "        gp=pl.len(),\n",
    "        wins=pl.col(\"team_winner\").sum(),\n",
    "        ppg=pl.col(\"team_score\").mean().round(1),\n",
    "        diff=(pl.col(\"team_score\") - pl.col(\"opponent_team_score\")).mean().round(1),\n",
    "    )\n",
    "    .filter(pl.col(\"gp\") > 10)\n",
    "    .sort([\"wins\", \"team_abbreviation\"], descending=[True, False])\n",
    "    .drop(\"gp\")\n",
    ")\n",
    "Reactable(\n",
    "    nba_teams,\n",
    "    columns=[\n",
    "        reactable_sdv_logos(league=\"nba\", id=\"team_abbreviation\", name=\"\", width=60),\n",
    "        Column(id=\"team_display_name\", name=\"Team\", min_width=200),\n",
    "        reactable_sdv_team_color_bar(\n",
    "            nba_teams, \"team_abbreviation\", league=\"nba\", max_value=82 * 1.15, id=\"wins\", name=\"Wins\", width=220\n",
    "        ),\n",
    "        Column(id=\"ppg\", name=\"Points per game\"),\n",
    "        Column(id=\"diff\", name=\"Point differential\"),\n",
    "    ],\n",
    "    default_page_size=10,\n",
    "    searchable=True,\n",
    "    striped=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26",
   "metadata": {},
   "source": [
    "## 12. plottable: a table drawn by matplotlib\n",
    "\n",
    "`sdvplot.plottable.logo_column` is a plottable `ColumnDefinition` that draws each row's team as its logo, so the\n",
    "table is an ordinary matplotlib figure you can save like any chart. The National League's eight best\n",
    "records:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plottable import ColumnDefinition, Table\n",
    "\n",
    "from sdvplot.plottable import logo_column\n",
    "\n",
    "nl = (\n",
    "    mlb_standings.filter(pl.col(\"group_name\") == \"National League\")\n",
    "    .sort([\"win_percent\", \"team_abbreviation\"], descending=[True, False])\n",
    "    .head(8)\n",
    "    .select(logo=\"team_abbreviation\", team=\"team_display_name\", w=\"wins\", l=\"losses\", diff=\"point_differential\")\n",
    "    .to_pandas()\n",
    "    .set_index(\"logo\", drop=False)\n",
    ")\n",
    "fig, ax = plt.subplots(figsize=(8, 5.5))\n",
    "Table(\n",
    "    nl,\n",
    "    ax=ax,\n",
    "    index_col=\"logo\",\n",
    "    column_definitions=[\n",
    "        logo_column(\"logo\", league=\"mlb\", title=\"\", width=0.5),\n",
    "        ColumnDefinition(\"team\", title=\"Team\", width=2, textprops={\"ha\": \"left\"}),\n",
    "        ColumnDefinition(\"w\", title=\"W\"),\n",
    "        ColumnDefinition(\"l\", title=\"L\"),\n",
    "        ColumnDefinition(\"diff\", title=\"Diff\"),\n",
    "    ],\n",
    ")\n",
    "ax.set_title(f\"The National League's best eight records, {SEASON}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, \"Data: ESPN via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "sdvplot": {
   "description": "Twelve table recipes: great_tables logos, headshots, themes, color pills, percentile bars, indicator boxes, tier lists and snaked lists, export for social, and the same marks in reactable and plottable.",
   "label": "Tables",
   "position": 4
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
