{"spec_id":"map-tile-background","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nmap-tile-background: Map with Tile Background\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-27\n\"\"\"\n\nimport os\nimport sys\n\n\n# bokeh.py shadows the installed bokeh package — remove script dir from sys.path\n_here = os.path.abspath(os.path.dirname(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _here]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport xyzservices.providers as xyz\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, HoverTool, LinearColorMapper\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\"\n\n# Data: European capital cities with annual visitor counts (millions)\ncities = {\n    \"Paris\": (2.3522, 48.8566),\n    \"London\": (-0.1276, 51.5074),\n    \"Rome\": (12.4964, 41.9028),\n    \"Barcelona\": (2.1734, 41.3851),\n    \"Amsterdam\": (4.9041, 52.3676),\n    \"Berlin\": (13.4050, 52.5200),\n    \"Prague\": (14.4378, 50.0755),\n    \"Vienna\": (16.3738, 48.2082),\n    \"Madrid\": (-3.7038, 40.4168),\n    \"Lisbon\": (-9.1393, 38.7223),\n    \"Brussels\": (4.3517, 50.8503),\n    \"Dublin\": (-6.2603, 53.3498),\n    \"Copenhagen\": (12.5683, 55.6761),\n    \"Stockholm\": (18.0686, 59.3293),\n    \"Warsaw\": (21.0122, 52.2297),\n}\n\nnames = list(cities.keys())\nlons = np.array([cities[c][0] for c in names])\nlats = np.array([cities[c][1] for c in names])\nvisitors = np.array([19.1, 21.0, 10.1, 12.0, 8.0, 6.1, 8.0, 7.7, 7.5, 4.5, 4.0, 5.5, 3.5, 4.0, 3.0])\n\n# Web Mercator projection (required for tile maps)\nk = 6378137\nx_merc = lons * (k * np.pi / 180.0)\ny_merc = np.log(np.tan((90 + lats) * np.pi / 360.0)) * k\n\nsource = ColumnDataSource(\n    data={\n        \"x\": x_merc,\n        \"y\": y_merc,\n        \"lon\": lons,\n        \"lat\": lats,\n        \"name\": names,\n        \"visitors\": visitors,\n        \"size\": visitors * 2.0 + 20,\n    }\n)\n\n# imprint_seq colormap: #009E73 (brand green) → #4467A3 (blue)\nANYPLOT_SEQ256 = [\n    \"#{:02X}{:02X}{:02X}\".format(\n        int(round(0 + 68 * t / 255)), int(round(158 - 55 * t / 255)), int(round(115 + 48 * t / 255))\n    )\n    for t in range(256)\n]\ncolor_mapper = LinearColorMapper(palette=ANYPLOT_SEQ256, low=visitors.min(), high=visitors.max())\n\ntile_provider = xyz.CartoDB.Positron if THEME == \"light\" else xyz.CartoDB.DarkMatter\n\ntitle = \"map-tile-background · python · bokeh · anyplot.ai\"\nn = len(title)\ntitle_pt = max(34, round(50 * 67 / n)) if n > 67 else 50\n\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"mercator\",\n    y_axis_type=\"mercator\",\n    title=title,\n    tools=\"pan,wheel_zoom,box_zoom,reset\",\n    active_scroll=\"wheel_zoom\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=250,\n)\n\np.add_tile(tile_provider)\n\np.scatter(\n    \"x\",\n    \"y\",\n    source=source,\n    size=\"size\",\n    fill_color={\"field\": \"visitors\", \"transform\": color_mapper},\n    fill_alpha=0.85,\n    line_color=\"white\",\n    line_width=2,\n)\n\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    title=\"Visitors (M/year)\",\n    title_text_font_size=\"28pt\",\n    title_text_color=INK,\n    major_label_text_font_size=\"24pt\",\n    major_label_text_color=INK_SOFT,\n    background_fill_color=ELEVATED_BG,\n    width=80,\n)\np.add_layout(color_bar, \"right\")\n\nhover = HoverTool(\n    tooltips=[(\"City\", \"@name\"), (\"Visitors\", \"@visitors{0.0}M/year\"), (\"Location\", \"@lat{0.00}°N, @lon{0.00}°E\")]\n)\np.add_tools(hover)\n\np.title.text_font_size = f\"{title_pt}pt\"\np.title.align = \"center\"\np.title.text_color = INK\n\np.xaxis.axis_label = \"Longitude (°)\"\np.yaxis.axis_label = \"Latitude (°)\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}