A map visualization that displays data points on top of tile-based backgrounds such as OpenStreetMap, Stamen, or satellite imagery. Unlike vector-based basemaps, tile backgrounds provide rich geographic context including street-level detail, terrain, or satellite imagery that loads dynamically as the user navigates. This visualization is essential for location-based analysis where real-world geographic context enhances data interpretation.

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