{"spec_id":"map-tile-background","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nmap-tile-background: Map with Tile Background\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-27\n\"\"\"\n\nimport io\nimport math\nimport os\nimport urllib.request\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom PIL import Image\n\n\n# Theme tokens\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\"\n\n# Continuous colormap (imprint_seq: brand green → blue) for visitor density\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Data: Rome tourist attractions with daily visitor counts (realistic scale)\nlocations = {\n    \"Colosseum\": (41.8902, 12.4922, 21000),\n    \"Vatican Museums\": (41.9065, 12.4536, 20500),\n    \"St. Peter's Basilica\": (41.9022, 12.4539, 19500),\n    \"Trevi Fountain\": (41.9009, 12.4833, 18000),\n    \"Pantheon\": (41.8986, 12.4769, 15000),\n    \"Roman Forum\": (41.8925, 12.4853, 13000),\n    \"Spanish Steps\": (41.9060, 12.4828, 12000),\n    \"Piazza Navona\": (41.8992, 12.4730, 10000),\n    \"Castel Sant'Angelo\": (41.9031, 12.4663, 8500),\n    \"Villa Borghese\": (41.9137, 12.4855, 7000),\n    \"Trastevere\": (41.8867, 12.4692, 6500),\n    \"Campo de' Fiori\": (41.8956, 12.4722, 5500),\n}\n\n# Per-attraction label offsets (directional spread to reduce crowding)\nlabel_offsets = {\n    \"Colosseum\": (8, -8),\n    \"Vatican Museums\": (-5, 8),\n    \"St. Peter's Basilica\": (-5, -12),\n    \"Trevi Fountain\": (8, 5),\n    \"Pantheon\": (8, -10),\n    \"Roman Forum\": (8, -8),\n    \"Spanish Steps\": (8, 5),\n    \"Piazza Navona\": (-5, 8),\n    \"Castel Sant'Angelo\": (8, 8),\n    \"Villa Borghese\": (8, 4),\n    \"Trastevere\": (-5, -10),\n    \"Campo de' Fiori\": (-5, 8),\n}\n\nnames = list(locations.keys())\nlats = np.array([v[0] for v in locations.values()])\nlons = np.array([v[1] for v in locations.values()])\nvisitors = np.array([v[2] for v in locations.values()])\n\n# Bounding box with padding\nlat_min = lats.min() - 0.015\nlat_max = lats.max() + 0.015\nlon_min = lons.min() - 0.025\nlon_max = lons.max() + 0.025\n\n# Theme-adaptive tile provider\nzoom = 14\nn_tiles = 2**zoom\nif THEME == \"light\":\n    tile_url = \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\"\n    attribution = \"© OpenStreetMap contributors\"\nelse:\n    tile_url = \"https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png\"\n    attribution = \"© OpenStreetMap contributors, © CARTO\"\n\n# Tile index range for the bounding box\ntx_min = int((lon_min + 180) / 360 * n_tiles)\ntx_max = int((lon_max + 180) / 360 * n_tiles)\nty_min = int((1 - math.asinh(math.tan(math.radians(lat_max))) / math.pi) / 2 * n_tiles)\nty_max = int((1 - math.asinh(math.tan(math.radians(lat_min))) / math.pi) / 2 * n_tiles)\n\n# Fetch and stitch tiles into a single image\ntile_size = 256\nstitched = Image.new(\"RGB\", ((tx_max - tx_min + 1) * tile_size, (ty_max - ty_min + 1) * tile_size))\nua = {\"User-Agent\": \"anyplot.ai/1.0 (https://anyplot.ai; visualization demo)\"}\nfor tx in range(tx_min, tx_max + 1):\n    for ty in range(ty_min, ty_max + 1):\n        req = urllib.request.Request(tile_url.format(z=zoom, x=tx, y=ty), headers=ua)\n        with urllib.request.urlopen(req, timeout=10) as resp:\n            tile = Image.open(io.BytesIO(resp.read())).convert(\"RGB\")\n        stitched.paste(tile, ((tx - tx_min) * tile_size, (ty - ty_min) * tile_size))\n\n# Geographic extent of the stitched image [left, right, bottom, top]\nnw_lon = tx_min / n_tiles * 360 - 180\nnw_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * ty_min / n_tiles))))\nse_lon = (tx_max + 1) / n_tiles * 360 - 180\nse_lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (ty_max + 1) / n_tiles))))\n\n# Plot\ntitle = \"Rome Tourist Attractions · map-tile-background · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title)))\n\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nax.imshow(np.array(stitched), extent=[nw_lon, se_lon, se_lat, nw_lat], aspect=\"auto\", zorder=0)\n\n# Scatter: size and color both encode daily visitor count\nsizes = 60 + (visitors - visitors.min()) / (visitors.max() - visitors.min()) * 290\n\nscatter = ax.scatter(\n    lons,\n    lats,\n    c=visitors,\n    s=sizes,\n    cmap=imprint_seq,\n    vmin=visitors.min(),\n    vmax=visitors.max(),\n    alpha=0.9,\n    edgecolors=PAGE_BG,\n    linewidth=1.5,\n    zorder=5,\n)\n\n# Attraction name labels with per-attraction directional offsets\nfor name, lon, lat in zip(names, lons, lats, strict=False):\n    dx, dy = label_offsets.get(name, (6, 4))\n    ax.annotate(\n        name,\n        (lon, lat),\n        xytext=(dx, dy),\n        textcoords=\"offset points\",\n        fontsize=6,\n        color=INK,\n        bbox={\"boxstyle\": \"round,pad=0.15\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.75},\n        zorder=8,\n    )\n\n# Colorbar\ncbar = plt.colorbar(scatter, ax=ax, shrink=0.75, pad=0.02)\ncbar.set_label(\"Daily Visitors\", fontsize=8, color=INK_SOFT)\ncbar.ax.tick_params(labelsize=7, colors=INK_SOFT)\ncbar.outline.set_edgecolor(INK_SOFT)\n\n# Axis limits and labels\nax.set_xlim(lon_min, lon_max)\nax.set_ylim(lat_min, lat_max)\nax.set_xlabel(\"Longitude\", fontsize=10, color=INK)\nax.set_ylabel(\"Latitude\", fontsize=10, color=INK)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f\"{x:.3f}°E\"))\nax.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, p: f\"{y:.3f}°N\"))\n\nfor spine in (\"left\", \"bottom\"):\n    ax.spines[spine].set_color(INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\n\n# Tile attribution (required by provider license)\nax.text(\n    0.99,\n    0.01,\n    attribution,\n    transform=ax.transAxes,\n    fontsize=6,\n    ha=\"right\",\n    va=\"bottom\",\n    color=INK_MUTED,\n    bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.85},\n    zorder=10,\n)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}