{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Cricket\n",
    "\n",
    "Nine charts and tables from the 2026 Indian Premier League and the 2026 ICC Men's T20 World Cup: a points table with\n",
    "logos, run rates and net run rate in team colors, the points race, a scorecard card for the IPL final, a World Cup\n",
    "tier list and an interactive run-rate chart. The data is ESPN's cricket feed (ESPNcricinfo), read through\n",
    "[sportsdataverse-py](https://py.sportsdataverse.org/) (`sportsdataverse.cricket`), whose wrappers take ESPN's league\n",
    "id: `8048` is the IPL and `8604` the men's T20 World Cup."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime as dt\n",
    "import re\n",
    "import warnings\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.cricket as cricket\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "IPL, SEASON = \"8048\", 2026  # the 2026 IPL ran from 28 March to the final on 31 May"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "Load the IPL season: the final league table, and every match from ESPN's scoreboard, one day at a time. Each match\n",
    "gives both teams' ids, scores and the colors ESPN uses for them, and a result line (\"RCB won by 6 wkts (26b rem)\").\n",
    "The winner comes from that line, because a tie settled by a Super Over marks neither side as the winner; a washout\n",
    "reads \"No result\" and is worth a point each. The points rebuilt that way match the official table."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "table = cricket.espn_cricket_standings(IPL, season=SEASON).sort(\"rank\")\n",
    "rows = []\n",
    "for day in pl.date_range(dt.date(2026, 3, 28), dt.date(2026, 5, 31), eager=True):\n",
    "    raw = cricket.espn_cricket_scoreboard(IPL, dates=day.strftime(\"%Y%m%d\"), return_parsed=False)\n",
    "    for event in raw.get(\"events\", []):\n",
    "        comp = event[\"competitions\"][0]\n",
    "        result = comp[\"status\"][\"summary\"]\n",
    "        won = re.search(r\"(\\w+) won\", result)  # \"RCB won by 5 wkts\", \"Match tied (KKR won the Super Over)\"\n",
    "        for side in comp[\"competitors\"]:\n",
    "            rows.append(\n",
    "                {\n",
    "                    \"event_id\": event[\"id\"],\n",
    "                    \"date\": day,\n",
    "                    \"stage\": comp[\"description\"],\n",
    "                    \"result\": result,\n",
    "                    \"team_id\": side[\"id\"],\n",
    "                    \"team\": side[\"team\"][\"abbreviation\"],\n",
    "                    \"color\": side[\"team\"][\"color\"],\n",
    "                    \"score\": side[\"score\"],\n",
    "                    \"winner\": won.group(1) if won else None,\n",
    "                }\n",
    "            )\n",
    "matches = pl.DataFrame(rows).with_columns(\n",
    "    pts=pl.when(pl.col(\"winner\").is_null()).then(1).when(pl.col(\"winner\") == pl.col(\"team\")).then(2).otherwise(0)\n",
    ")\n",
    "team_colors = dict(zip(matches[\"team_id\"], matches[\"color\"], strict=True))  # ESPN's colors, keyed by team id\n",
    "league = matches.filter(pl.col(\"stage\").str.ends_with(\"Match\"))  # \"1st Match\" ... \"70th Match\"; then the playoffs\n",
    "check = (\n",
    "    league.group_by(\"team_id\", maintain_order=True)\n",
    "    .agg(pl.col(\"pts\").sum())\n",
    "    .join(table.select(\"team_id\", \"match_points\"), on=\"team_id\")\n",
    ")\n",
    "assert (check[\"pts\"] == check[\"match_points\"]).all()\n",
    "table.height, matches[\"event_id\"].n_unique()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 1. One key for nations, franchises and women's sides\n",
    "\n",
    "sdvplot's `cricket` key holds national sides and franchises together. Its `team_id` is the ESPNcricinfo id, which is\n",
    "also ESPN's team id, so the ids in this data resolve directly. A nation is one team in every format: India is `6` in\n",
    "a Test, an ODI or the T20 World Cup below. The index has no abbreviations for cricket, so ESPN's `RCB` does not\n",
    "resolve; a full name does."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(sdvplot.resolve(table[\"team_id\"], \"cricket\").to_list())\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    print(sdvplot.resolve([\"RCB\", \"Royal Challengers Bengaluru\", \"Royal Challengers Bengaluru Women\"], \"cricket\"))\n",
    "print(caught[0].message)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Franchise families across leagues\n",
    "\n",
    "IPL owners now run teams in leagues around the world, and the women's sides are teams of their own. Each is a\n",
    "separate team with its own id and logo; the names resolve because each is unique in the index. Names change while\n",
    "ids stay: two Hundred sides were renamed for 2026 (Northern Superchargers became Sunrisers Leeds, Oval Invincibles\n",
    "became MI London), and ESPN still serves their old marks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "families = {\n",
    "    \"Knight Riders\": [\n",
    "        \"Kolkata Knight Riders\",\n",
    "        \"Trinbago Knight Riders\",\n",
    "        \"Abu Dhabi Knight Riders\",\n",
    "        \"Los Angeles Knight Riders\",\n",
    "    ],\n",
    "    \"Super Kings\": [\"Chennai Super Kings\", \"Joburg Super Kings\", \"Texas Super Kings\"],\n",
    "    \"Capitals\": [\"Delhi Capitals\", \"Delhi Capitals Women\", \"Dubai Capitals\", \"Pretoria Capitals\"],\n",
    "    \"Sunrisers\": [\"Sunrisers Hyderabad\", \"Sunrisers Eastern Cape\", \"Sunrisers Leeds Men\", \"Sunrisers Leeds Women\"],\n",
    "    \"Mumbai Indians\": [\"Mumbai Indians\", \"MI New York\", \"MI London Men\", \"MI London Women\"],\n",
    "}\n",
    "fig, axes = plt.subplots(4, len(families), figsize=(10, 6))\n",
    "for column, names in zip(axes.T, families.values(), strict=True):\n",
    "    for ax in column:\n",
    "        ax.axis(\"off\")\n",
    "    for ax, name, team_id in zip(column, names, sdvplot.resolve(names, \"cricket\"), strict=False):\n",
    "        ax.imshow(sdvplot.logo_image(team_id, \"cricket\", size=160))\n",
    "        ax.set_title(f\"{name}\\n{team_id}\", fontsize=7.5)\n",
    "fig.suptitle(\"One owner, many teams: each has its own id and logo\", fontweight=\"bold\")\n",
    "fig.subplots_adjust(hspace=0.45)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. The IPL 2026 points table\n",
    "\n",
    "`gt_sdv_logos` turns the team ids into logos and `gt_cutline` marks the four playoff places. Net run rate (NRR)\n",
    "breaks ties on points: it is the runs a team scored per over minus the runs it conceded per over, the two columns\n",
    "beside it. Three teams finished on 18 points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from great_tables import GT\n",
    "\n",
    "from sdvplot.great_tables import gt_cutline, gt_sdv_logos, gt_theme_athletic\n",
    "\n",
    "points = table.select(\n",
    "    \"rank\",\n",
    "    pl.col(\"team_id\").alias(\"logo\"),\n",
    "    \"team\",\n",
    "    pl.col(\"matches_played\").alias(\"p\"),\n",
    "    pl.col(\"matches_won\").alias(\"w\"),\n",
    "    pl.col(\"matches_lost\").alias(\"l\"),\n",
    "    pl.col(\"noresult\").alias(\"nr\"),\n",
    "    pl.col(\"match_points\").alias(\"pts\"),\n",
    "    \"netrr\",\n",
    "    pl.col(\"for\").alias(\"rr_for\"),\n",
    "    pl.col(\"against\").alias(\"rr_against\"),\n",
    ")\n",
    "gt = (\n",
    "    GT(points)\n",
    "    .tab_header(\"Indian Premier League 2026\", \"League stage; the top four reached the playoffs\")\n",
    "    .cols_label(\n",
    "        rank=\"\",\n",
    "        logo=\"\",\n",
    "        team=\"Team\",\n",
    "        p=\"P\",\n",
    "        w=\"W\",\n",
    "        l=\"L\",\n",
    "        nr=\"NR\",\n",
    "        pts=\"Pts\",\n",
    "        netrr=\"NRR\",\n",
    "        rr_for=\"Scored\",\n",
    "        rr_against=\"Conceded\",\n",
    "    )\n",
    "    .tab_spanner(\"Runs per over\", [\"rr_for\", \"rr_against\"])\n",
    "    .fmt_number(\"netrr\", decimals=3, force_sign=True)\n",
    "    .fmt_number([\"rr_for\", \"rr_against\"], decimals=2)\n",
    "    .cols_align(\"left\", \"team\")\n",
    "    .tab_source_note(\"Royal Challengers Bengaluru won the final. Data: ESPN via sportsdataverse-py\")\n",
    ")\n",
    "gt = gt_theme_athletic(gt_sdv_logos(gt, \"logo\", league=\"cricket\", height=26)).cols_align(\"left\", \"team\")\n",
    "gt_cutline(gt, after=4, label=\"Playoffs\", label_position=\"above\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 4. Run rates: batting against bowling\n",
    "\n",
    "Runs scored per over against runs conceded per over, one logo per team. The y axis is reversed so the stingy bowling\n",
    "sides sit at the top; teams above the diagonal scored faster than they conceded (a positive NRR)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(8.5, 6))\n",
    "ax.scatter(table[\"for\"], table[\"against\"], alpha=0)  # sets the limits; the logos are the points\n",
    "lo, hi = 8.6, 11.0\n",
    "ax.plot([lo, hi], [lo, hi], color=\"grey\", lw=0.8, ls=\"--\")  # scored = conceded: NRR 0\n",
    "ax.set(xlim=(lo, hi), ylim=(hi, lo), xlabel=\"Runs scored per over\", ylabel=\"Runs conceded per over (reversed)\")\n",
    "sdvplot.add_logos(ax, table[\"for\"], table[\"against\"], table[\"team_id\"], league=\"cricket\", height=0.11)\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_title(\"IPL 2026: batting and bowling run rates, league stage\", 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()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "Gujarat Titans conceded 8.76 runs an over, almost half a run better than any other side; Punjab Kings scored fastest\n",
    "(10.84) and conceded fastest (10.54)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13",
   "metadata": {},
   "source": [
    "## 5. Net run rate in team colors\n",
    "\n",
    "The index's cricket colors are fallback colors (`color_source == \"fallback\"`), so the bars use the colors ESPN's\n",
    "scoreboard carried for each team. `axis_logos` swaps the x-axis team ids for logos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14",
   "metadata": {},
   "outputs": [],
   "source": [
    "nrr = table.sort(\"netrr\", descending=True)\n",
    "fig, ax = plt.subplots(figsize=(9, 5.5))\n",
    "ax.bar(nrr[\"team_id\"], nrr[\"netrr\"], color=[team_colors[t] for t in nrr[\"team_id\"]])\n",
    "ax.axhline(0, color=\"black\", lw=0.8)\n",
    "for x, value in enumerate(nrr[\"netrr\"]):\n",
    "    ax.text(\n",
    "        x,\n",
    "        value + (0.03 if value > 0 else -0.03),\n",
    "        f\"{value:+.3f}\",\n",
    "        ha=\"center\",\n",
    "        va=\"bottom\" if value > 0 else \"top\",\n",
    "        fontsize=9,\n",
    "    )\n",
    "sdvplot.axis_logos(ax, \"x\", league=\"cricket\", height=0.1)\n",
    "ax.set_ylabel(\"Net run rate\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_title(\"IPL 2026 net run rate, league stage\", 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()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15",
   "metadata": {},
   "source": [
    "## 6. The points race\n",
    "\n",
    "Points after each of the fourteen league matches, one panel per team in table order: the team's line in its color,\n",
    "every other team in grey behind it. A layer without the facet column repeats in every panel, which is all the grey\n",
    "background needs. `geom_sdv_logos` puts the logo in each panel."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Ten small panels of cumulative IPL 2026 points, each team in its ESPN color over the others in grey",
     "title": "IPL points race"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from plotnine import (\n",
    "    aes,\n",
    "    element_text,\n",
    "    facet_wrap,\n",
    "    geom_step,\n",
    "    ggplot,\n",
    "    labs,\n",
    "    scale_color_manual,\n",
    "    scale_x_continuous,\n",
    "    scale_y_continuous,\n",
    "    theme,\n",
    "    theme_minimal,\n",
    ")\n",
    "\n",
    "from sdvplot.plotnine import geom_sdv_logos\n",
    "\n",
    "order = table[\"team\"].to_list()\n",
    "race = (\n",
    "    league.sort(\"date\", \"event_id\")\n",
    "    .with_columns(game=pl.int_range(1, pl.len() + 1).over(\"team_id\"), total=pl.col(\"pts\").cum_sum().over(\"team_id\"))\n",
    "    .drop(\"team\")\n",
    "    .join(table.select(\"team_id\", \"team\"), on=\"team_id\")\n",
    "    .to_pandas()\n",
    ")\n",
    "race[\"team\"] = race[\"team\"].astype(\"category\").cat.set_categories(order)\n",
    "logos = table.select(\"team_id\", \"team\").with_columns(game=pl.lit(3.5), total=pl.lit(15.5)).to_pandas()\n",
    "logos[\"team\"] = logos[\"team\"].astype(\"category\").cat.set_categories(order)\n",
    "(\n",
    "    ggplot(race, aes(\"game\", \"total\"))\n",
    "    + geom_step(aes(group=\"team_id\"), data=race.drop(columns=\"team\"), color=\"#dddddd\", size=0.5)\n",
    "    + geom_step(aes(color=\"team_id\"), size=1.2, show_legend=False)\n",
    "    + geom_sdv_logos(aes(team=\"team_id\"), data=logos, league=\"cricket\", height=0.25)\n",
    "    + scale_color_manual(values=team_colors)\n",
    "    + scale_x_continuous(breaks=[1, 7, 14])\n",
    "    + scale_y_continuous(limits=(0, 19))\n",
    "    + facet_wrap(\"team\", ncol=5)\n",
    "    + labs(\n",
    "        x=\"League match\",\n",
    "        y=\"Points\",\n",
    "        title=\"The IPL 2026 points race\",\n",
    "        caption=\"Two points a win, one for a no result. Data: ESPN via sportsdataverse-py\",\n",
    "    )\n",
    "    + theme_minimal()\n",
    "    + theme(figure_size=(10, 5.5), plot_title=element_text(weight=\"bold\"), strip_text=element_text(size=8))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17",
   "metadata": {},
   "source": [
    "Punjab Kings had 13 points after seven matches, took two from their last seven and missed the playoffs by a point."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## 7. A scorecard card for the final\n",
    "\n",
    "ESPN's match summary carries every player's innings: runs, balls, boundaries and whether he was out, inside each\n",
    "roster entry. One panel per innings, the batters in batting order, bars in the batting side's color and the team's\n",
    "logo by each panel title."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Both innings of the 2026 IPL final as batting bars in team colors with team logos",
     "title": "IPL final scorecard card"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import title_image\n",
    "\n",
    "final_id = matches.filter(pl.col(\"stage\") == \"Final\")[\"event_id\"][0]\n",
    "summary = cricket.espn_cricket_summary(IPL, event_id=final_id, return_parsed=False)\n",
    "batting = []\n",
    "for side in summary[\"rosters\"]:\n",
    "    for player in side[\"roster\"]:\n",
    "        for innings in player[\"linescores\"]:\n",
    "            for line in innings[\"linescores\"]:\n",
    "                stats = {s[\"name\"]: s[\"displayValue\"] for s in line[\"statistics\"][\"categories\"][0][\"stats\"]}\n",
    "                if stats.get(\"batted\") == \"1\":\n",
    "                    batting.append(\n",
    "                        {\n",
    "                            \"team_id\": side[\"team\"][\"id\"],\n",
    "                            \"innings\": innings[\"period\"],\n",
    "                            \"batter\": player[\"athlete\"][\"displayName\"],\n",
    "                            \"order\": int(stats[\"battingPosition\"]),\n",
    "                            \"runs\": int(stats[\"runs\"]),\n",
    "                            \"balls\": int(stats[\"ballsFaced\"]),\n",
    "                            \"out\": stats[\"outs\"] == \"1\",\n",
    "                        }\n",
    "                    )\n",
    "batting = pl.DataFrame(batting).sort(\"innings\", \"order\")\n",
    "header = summary[\"header\"][\"competitions\"][0]\n",
    "names = {c[\"id\"]: c[\"team\"][\"displayName\"] for c in header[\"competitors\"]}\n",
    "scores = {c[\"id\"]: c[\"score\"] for c in header[\"competitors\"]}\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10, 5.5), sharex=True)\n",
    "for ax, (_, card) in zip(axes, batting.group_by(\"innings\", maintain_order=True), strict=True):\n",
    "    team_id = card[\"team_id\"][0]\n",
    "    y = list(range(card.height))[::-1]\n",
    "    ax.barh(y, card[\"runs\"], color=team_colors[team_id])\n",
    "    ax.set_yticks(y, card[\"batter\"], fontsize=9)\n",
    "    for yy, runs, balls, out in zip(y, card[\"runs\"], card[\"balls\"], card[\"out\"], strict=True):\n",
    "        ax.text(runs + 1, yy, f\"{runs}{'' if out else '*'} ({balls})\", va=\"center\", fontsize=9)\n",
    "    ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "    ax.set_xlabel(\"Runs (balls)\")\n",
    "    title_image(\n",
    "        ax,\n",
    "        team_id,\n",
    "        f\"{names[team_id]}\\n{scores[team_id]}\",\n",
    "        league=\"cricket\",\n",
    "        height=34,\n",
    "        loc=\"left\",\n",
    "        fontsize=10,\n",
    "        fontweight=\"bold\",\n",
    "    )\n",
    "fig.suptitle(f\"IPL 2026 final: {header['status']['summary']}\", fontweight=\"bold\", x=0.02, ha=\"left\")\n",
    "fig.text(0.99, 0.01, \"* not out. Data: ESPN via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "Virat Kohli's 75 not out from 42 balls chased down 156 with two overs to spare; Washington Sundar's 50 not out was\n",
    "the top score for Gujarat."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21",
   "metadata": {},
   "source": [
    "## 8. The T20 World Cup as a tier list\n",
    "\n",
    "The 2026 men's T20 World Cup (ESPN league `8604`): twenty teams, four groups, two Super Eights groups, then semi-finals\n",
    "and a final. The standings give the groups; the knockout results come from three days of scoreboards. `team_tiers`\n",
    "puts each team in the row for how far it went. Italy, at its first World Cup, has no entry in the index yet: it\n",
    "warns once (caught and printed here) and its slot stays empty, rather than taking a guessed logo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Every team of the 2026 men's T20 World Cup as a flag in a tier by how far it went",
     "title": "T20 World Cup tier list"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import team_tiers\n",
    "\n",
    "T20WC = \"8604\"\n",
    "wc = cricket.espn_cricket_standings(T20WC)\n",
    "won, lost = {}, {}\n",
    "for day in (\"20260304\", \"20260305\", \"20260308\"):  # the semi-finals and the final\n",
    "    for event in cricket.espn_cricket_scoreboard(T20WC, dates=day, return_parsed=False)[\"events\"]:\n",
    "        comp = event[\"competitions\"][0]\n",
    "        for side in comp[\"competitors\"]:\n",
    "            (won if side[\"winner\"] == \"true\" else lost)[side[\"id\"]] = comp[\"description\"]\n",
    "super8 = set(wc.filter(pl.col(\"group\").str.starts_with(\"Super\"))[\"team_id\"])\n",
    "tiers = (\n",
    "    wc.filter(pl.col(\"group\").str.starts_with(\"Group\"))\n",
    "    .sort(\"group\", \"rank\")\n",
    "    .select(\n",
    "        pl.col(\"team_id\").alias(\"team\"),\n",
    "        tier_no=pl.col(\"team_id\").map_elements(\n",
    "            lambda t: (\n",
    "                1\n",
    "                if won.get(t) == \"Final\"\n",
    "                else 2\n",
    "                if lost.get(t) == \"Final\"\n",
    "                else 3\n",
    "                if t in lost\n",
    "                else 4\n",
    "                if t in super8\n",
    "                else 5\n",
    "            ),\n",
    "            return_dtype=pl.Int64,\n",
    "        ),\n",
    "    )\n",
    "    .sort(\"tier_no\", maintain_order=True)\n",
    ")\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    fig = team_tiers(\n",
    "        tiers,\n",
    "        \"cricket\",\n",
    "        title=\"ICC Men's T20 World Cup 2026: how far everyone got\",\n",
    "        subtitle=\"India beat New Zealand by 96 runs in the final; group-stage teams in group order\",\n",
    "        caption=\"Data: ESPN via sportsdataverse-py\",\n",
    "        tier_desc={1: \"Champion\", 2: \"Final\", 3: \"Semi-final\", 4: \"Super Eights\", 5: \"Group stage\"},\n",
    "        height=0.075,\n",
    "    )\n",
    "print(*(str(w.message) for w in caught), sep=\"\\n\")\n",
    "fig.set_size_inches(10, 6)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23",
   "metadata": {},
   "source": [
    "## 9. Interactive: World Cup group-stage run rates\n",
    "\n",
    "The group stage in Altair: runs scored against runs conceded per over, with flags as the points and the details on\n",
    "hover. Italy is left out because it has no mark in the index yet (example 8)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24",
   "metadata": {},
   "outputs": [],
   "source": [
    "import altair as alt\n",
    "\n",
    "groups = wc.filter(pl.col(\"group\").str.starts_with(\"Group\") & (pl.col(\"team_id\") != \"31\"))\n",
    "chart = (\n",
    "    alt.Chart(groups.to_pandas())\n",
    "    .mark_circle(size=300, opacity=0)\n",
    "    .encode(\n",
    "        x=alt.X(\"for:Q\", title=\"Runs scored per over\", scale=alt.Scale(zero=False)),\n",
    "        y=alt.Y(\"against:Q\", title=\"Runs conceded per over (reversed)\", scale=alt.Scale(zero=False, reverse=True)),\n",
    "        tooltip=[\"team\", \"group\", \"match_points\", alt.Tooltip(\"netrr:Q\", format=\"+.3f\", title=\"NRR\")],\n",
    "    )\n",
    "    .properties(width=620, height=440, title=\"ICC Men's T20 World Cup 2026, group stage: run rates\")\n",
    ")\n",
    "sdvplot.add_logos(chart, groups[\"for\"], groups[\"against\"], groups[\"team_id\"], league=\"cricket\", height=0.06)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "sdvplot": {
   "description": "An IPL points table with logos, run rates and net run rate in team colors, the points race, a scorecard card for the final and a T20 World Cup tier list, from ESPN's cricket feed.",
   "label": "Cricket",
   "position": 51
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
