{"spec_id":"map-tile-background","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nmap-tile-background: Map with Tile Background\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-16\n\"\"\"\n\nimport io\nimport os\nimport urllib.request\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.colors import LinearSegmentedColormap, Normalize\nfrom PIL import Image\n\n\n# Theme-adaptive chrome (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nMIDPOINT = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\n\n# Continuous data → Imprint diverging cmap, oriented cold-blue → neutral → hot-red\n# so the temperature reads with the conventional hot→red / cold→blue mapping.\nimprint_div = LinearSegmentedColormap.from_list(\"imprint_div\", [\"#4467A3\", MIDPOINT, \"#AE3030\"])\n\n# Data: a curated, well-spread set of Bay Area weather stations (avoids the dense\n# southern cluster that caused label overlap in the previous attempt). Temperature\n# encodes the Bay's coast-to-inland microclimate gradient.\nnp.random.seed(42)\n\nstations_data = {\n    \"name\": [\n        \"SF Downtown\",\n        \"Oakland\",\n        \"Berkeley\",\n        \"Richmond\",\n        \"Concord\",\n        \"Walnut Creek\",\n        \"Livermore\",\n        \"Fremont\",\n        \"Hayward\",\n        \"Palo Alto\",\n        \"San Jose\",\n        \"Half Moon Bay\",\n    ],\n    \"lat\": [37.7749, 37.8044, 37.8716, 37.9358, 37.9780, 37.9101, 37.6819, 37.5485, 37.6688, 37.4419, 37.3382, 37.4636],\n    \"lon\": [\n        -122.4194,\n        -122.2712,\n        -122.2727,\n        -122.3477,\n        -122.0311,\n        -122.0652,\n        -121.7680,\n        -121.9886,\n        -122.0808,\n        -122.1430,\n        -121.8863,\n        -122.4286,\n    ],\n    \"temperature\": [17.8, 19.4, 18.1, 16.9, 24.6, 23.5, 26.2, 21.0, 19.8, 20.3, 22.7, 15.4],\n    # Per-station label offset (points) + horizontal alignment, hand-tuned to\n    # keep every label clear of its marker and of its neighbours.\n    \"off\": [\n        (-9, 9, \"right\"),\n        (9, -13, \"left\"),\n        (9, 7, \"left\"),\n        (-9, 7, \"right\"),\n        (9, 7, \"left\"),\n        (9, -13, \"left\"),\n        (9, 7, \"left\"),\n        (9, -13, \"left\"),\n        (-9, -13, \"right\"),\n        (-9, 7, \"right\"),\n        (9, 7, \"left\"),\n        (9, -13, \"left\"),\n    ],\n}\n\ndf = pd.DataFrame(stations_data)\n\n# --- Web Mercator tiling -----------------------------------------------------\nZOOM = 10  # city-level detail for a metro-area extent\nTILE = 256\nN = TILE * 2**ZOOM  # global pixel span at this zoom\n\n# Web Mercator: project lon/lat → global-pixel coords. Inlined (vectorized numpy)\n# to keep a flat, no-helper-functions structure; gx = (lon+180)/360·N,\n# gy = (1 − asinh(tan(lat))/π)/2·N.\ndf[\"gx\"] = (df[\"lon\"] + 180.0) / 360.0 * N\ndf[\"gy\"] = (1.0 - np.arcsinh(np.tan(np.radians(df[\"lat\"]))) / np.pi) / 2.0 * N\n\n# Padded data bounds in lon/lat, projected with the same formula, then expand to\n# the 16:9 canvas aspect so the map fills the whole frame without distorting it.\npad_lon, pad_lat = 0.12, 0.14\nwx0 = (df[\"lon\"].min() - pad_lon + 180.0) / 360.0 * N\nwx1 = (df[\"lon\"].max() + pad_lon + 180.0) / 360.0 * N\nwy_top = (1.0 - np.arcsinh(np.tan(np.radians(df[\"lat\"].max() + pad_lat))) / np.pi) / 2.0 * N\nwy_bot = (1.0 - np.arcsinh(np.tan(np.radians(df[\"lat\"].min() - pad_lat))) / np.pi) / 2.0 * N\nW, H = wx1 - wx0, wy_bot - wy_top\nASPECT = 16 / 9\nif W / H < ASPECT:\n    new_w = H * ASPECT\n    cx = (wx0 + wx1) / 2\n    wx0, wx1 = cx - new_w / 2, cx + new_w / 2\nelse:\n    new_h = W / ASPECT\n    cy = (wy_top + wy_bot) / 2\n    wy_top, wy_bot = cy - new_h / 2, cy + new_h / 2\n\n# Tiles covering the window\ntx0, tx1 = int(wx0 // TILE), int((wx1 - 1) // TILE)\nty0, ty1 = int(wy_top // TILE), int((wy_bot - 1) // TILE)\n\nstitched = Image.new(\"RGB\", ((tx1 - tx0 + 1) * TILE, (ty1 - ty0 + 1) * TILE))\nheaders = {\"User-Agent\": \"anyplot.ai/1.0 (educational visualization)\"}\nfor tx in range(tx0, tx1 + 1):\n    for ty in range(ty0, ty1 + 1):\n        url = f\"https://tile.openstreetmap.org/{ZOOM}/{tx}/{ty}.png\"\n        try:\n            req = urllib.request.Request(url, headers=headers)\n            with urllib.request.urlopen(req, timeout=10) as resp:\n                tile = Image.open(io.BytesIO(resp.read())).convert(\"RGB\")\n        except Exception:\n            tile = Image.new(\"RGB\", (TILE, TILE), (224, 222, 214))\n        stitched.paste(tile, ((tx - tx0) * TILE, (ty - ty0) * TILE))\n\n# Crop the stitched mosaic to the exact 16:9 window (in global-pixel coords)\nox, oy = tx0 * TILE, ty0 * TILE\ncropped = stitched.crop((int(round(wx0 - ox)), int(round(wy_top - oy)), int(round(wx1 - ox)), int(round(wy_bot - oy))))\n\n# --- Plot --------------------------------------------------------------------\nsns.set_theme(style=\"white\", font_scale=1.0)\n\nfig = plt.figure(figsize=(8, 4.5), dpi=400)  # → 3200 × 1800 px\nfig.patch.set_facecolor(PAGE_BG)\nax = fig.add_axes([0, 0, 1, 1])  # full-bleed map\nax.set_axis_off()\n\nax.imshow(cropped, extent=[wx0, wx1, wy_bot, wy_top], aspect=\"auto\", zorder=0)\nax.set_xlim(wx0, wx1)\nax.set_ylim(wy_bot, wy_top)\n\nnorm = Normalize(vmin=df[\"temperature\"].min(), vmax=df[\"temperature\"].max())\n\n# Faint dark outer ring behind every marker. The white edge alone leaves the\n# near-midpoint markers (off-white fill) low-contrast on the pale OSM tiles; this\n# thin INK_SOFT ring guarantees a clean separation from the background for all\n# temperatures. Sizes mirror seaborn's linear (170→560) size mapping, scaled up.\nring_size = 170 + (df[\"temperature\"] - df[\"temperature\"].min()) / (\n    df[\"temperature\"].max() - df[\"temperature\"].min()\n) * (560 - 170)\nax.scatter(\n    df[\"gx\"], df[\"gy\"], s=ring_size * 1.16, facecolors=\"none\", edgecolors=INK_SOFT, linewidths=0.9, alpha=0.6, zorder=2\n)\n\nsns.scatterplot(\n    data=df,\n    x=\"gx\",\n    y=\"gy\",\n    hue=\"temperature\",\n    size=\"temperature\",\n    sizes=(170, 560),\n    palette=imprint_div,\n    hue_norm=norm,\n    edgecolor=\"white\",\n    linewidth=1.8,\n    alpha=0.92,\n    ax=ax,\n    legend=False,\n    zorder=3,\n)\n\n# Station labels with theme-adaptive callout boxes\nfor _, row in df.iterrows():\n    dx, dy, ha = row[\"off\"]\n    ax.annotate(\n        row[\"name\"],\n        (row[\"gx\"], row[\"gy\"]),\n        xytext=(dx, dy),\n        textcoords=\"offset points\",\n        fontsize=8.5,\n        ha=ha,\n        va=\"center\",\n        color=INK,\n        fontweight=\"bold\",\n        bbox={\"boxstyle\": \"round,pad=0.25\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.85},\n        zorder=4,\n    )\n\n# Title banner (overlaid on the full-bleed map, theme-adaptive box)\ntitle = \"Bay Area Weather Stations · map-tile-background · seaborn · anyplot.ai\"\nax.text(\n    0.5,\n    0.955,\n    title,\n    transform=ax.transAxes,\n    ha=\"center\",\n    va=\"center\",\n    fontsize=round(14 * 67 / len(title)),\n    fontweight=\"bold\",\n    color=INK,\n    bbox={\"boxstyle\": \"round,pad=0.5\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.93},\n    zorder=6,\n)\n\n# Colorbar in a translucent panel, bottom-left (over Pacific water — no markers there)\npanel = ax.inset_axes([0.025, 0.05, 0.30, 0.105], zorder=5)\npanel.set_facecolor(ELEVATED_BG)\npanel.patch.set_alpha(0.92)\npanel.set_xticks([])\npanel.set_yticks([])\nfor spine in panel.spines.values():\n    spine.set_edgecolor(INK_SOFT)\n    spine.set_linewidth(0.6)\n\ncax = ax.inset_axes([0.05, 0.075, 0.25, 0.022], zorder=6)\nsm = plt.cm.ScalarMappable(cmap=imprint_div, norm=norm)\nsm.set_array([])\ncbar = fig.colorbar(sm, cax=cax, orientation=\"horizontal\")\ncbar.outline.set_edgecolor(INK_SOFT)\ncbar.outline.set_linewidth(0.6)\ncbar.ax.tick_params(labelsize=7, color=INK_SOFT, labelcolor=INK_SOFT, length=2)\nax.text(\n    0.175,\n    0.13,\n    \"Temperature (°C)\",\n    transform=ax.transAxes,\n    ha=\"center\",\n    va=\"center\",\n    fontsize=8.5,\n    fontweight=\"bold\",\n    color=INK,\n    zorder=7,\n)\n\n# OpenStreetMap attribution (required by license), bottom-right\nax.text(\n    0.99,\n    0.025,\n    \"© OpenStreetMap contributors\",\n    transform=ax.transAxes,\n    ha=\"right\",\n    va=\"bottom\",\n    fontsize=7,\n    color=INK_MUTED,\n    bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.85},\n    zorder=6,\n)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}