{"spec_id":"bubble-map-geographic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbubble-map-geographic: Bubble Map with Sized Geographic Markers\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-18\n\"\"\"\n\nimport sys\n\n\n# Remove script directory from sys.path to avoid shadowing the installed bokeh package\n# (script is named bokeh.py, which would otherwise shadow the bokeh package on import)\nif sys.path and sys.path[0] not in (\"\", \"-c\"):\n    sys.path.pop(0)\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, LabelSet, Legend, LegendItem, WMTSTileSource\nfrom bokeh.plotting import figure\nfrom bokeh.resources import CDN\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\n\n# Okabe-Ito palette (canonical order)\nOKABE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data - World cities with population (in millions, 2024 estimates)\ncities = {\n    \"name\": [\n        \"Tokyo\",\n        \"Delhi\",\n        \"Shanghai\",\n        \"São Paulo\",\n        \"Mexico City\",\n        \"Cairo\",\n        \"Mumbai\",\n        \"Beijing\",\n        \"Dhaka\",\n        \"Osaka\",\n        \"New York\",\n        \"Karachi\",\n        \"Buenos Aires\",\n        \"Istanbul\",\n        \"Lagos\",\n        \"London\",\n        \"Paris\",\n        \"Bangkok\",\n        \"Lima\",\n        \"Sydney\",\n        \"Toronto\",\n        \"Singapore\",\n        \"Berlin\",\n        \"Madrid\",\n        \"Johannesburg\",\n    ],\n    \"lat\": [\n        35.68,\n        28.61,\n        31.23,\n        -23.55,\n        19.43,\n        30.04,\n        19.08,\n        39.90,\n        23.81,\n        34.69,\n        40.71,\n        24.86,\n        -34.60,\n        41.01,\n        6.52,\n        51.51,\n        48.86,\n        13.76,\n        -12.05,\n        -33.87,\n        43.65,\n        1.35,\n        52.52,\n        40.42,\n        -26.20,\n    ],\n    \"lon\": [\n        139.69,\n        77.21,\n        121.47,\n        -46.63,\n        -99.13,\n        31.24,\n        72.88,\n        116.41,\n        90.41,\n        135.50,\n        -74.01,\n        67.01,\n        -58.38,\n        28.98,\n        3.38,\n        -0.13,\n        2.35,\n        100.50,\n        -77.03,\n        151.21,\n        -79.38,\n        103.82,\n        13.40,\n        -3.70,\n        28.04,\n    ],\n    \"population\": [\n        37.4,\n        32.9,\n        28.5,\n        22.4,\n        21.8,\n        21.3,\n        20.7,\n        20.5,\n        22.5,\n        19.1,\n        18.9,\n        16.5,\n        15.5,\n        15.6,\n        15.4,\n        9.5,\n        11.1,\n        10.7,\n        10.9,\n        5.3,\n        6.2,\n        5.9,\n        3.6,\n        6.7,\n        5.8,\n    ],\n    \"region\": [\n        \"Asia\",\n        \"Asia\",\n        \"Asia\",\n        \"Americas\",\n        \"Americas\",\n        \"Africa\",\n        \"Asia\",\n        \"Asia\",\n        \"Asia\",\n        \"Asia\",\n        \"Americas\",\n        \"Asia\",\n        \"Americas\",\n        \"Europe\",\n        \"Africa\",\n        \"Europe\",\n        \"Europe\",\n        \"Asia\",\n        \"Americas\",\n        \"Oceania\",\n        \"Americas\",\n        \"Asia\",\n        \"Europe\",\n        \"Europe\",\n        \"Africa\",\n    ],\n}\n\n# Convert lat/lon to Web Mercator projection (EPSG:3857), inlined\nk = 6378137\nlats = np.array(cities[\"lat\"])\nlons = np.array(cities[\"lon\"])\nmercator_x = lons * (k * np.pi / 180.0)\nmercator_y = np.log(np.tan((90 + lats) * np.pi / 360.0)) * k\n\npopulations = np.array(cities[\"population\"])\n\n# Scale bubble area proportionally to population (sqrt for perceptually accurate encoding)\nmin_size, max_size = 20, 90\npop_normalized = (populations - populations.min()) / (populations.max() - populations.min())\nsizes = min_size + np.sqrt(pop_normalized) * (max_size - min_size)\n\n# Okabe-Ito color by region — Asia first gets brand green #009E73\nregion_order = [\"Asia\", \"Americas\", \"Europe\", \"Africa\", \"Oceania\"]\nregion_colors = {region: OKABE[i] for i, region in enumerate(region_order)}\n\n# Plot\ntile_url = (\n    \"https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png\"\n    if THEME == \"light\"\n    else \"https://tiles.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png\"\n)\n\np = figure(\n    width=4800,\n    height=2700,\n    x_axis_type=\"mercator\",\n    y_axis_type=\"mercator\",\n    title=\"World Cities by Population · bubble-map-geographic · python · bokeh · anyplot.ai\",\n    tools=\"pan,wheel_zoom,box_zoom,reset,hover,save\",\n    tooltips=[\n        (\"City\", \"@name\"),\n        (\"Population\", \"@population{0.0} million\"),\n        (\"Region\", \"@region\"),\n        (\"Coordinates\", \"(@lat{0.00}°, @lon{0.00}°)\"),\n    ],\n)\n\np.add_tile(WMTSTileSource(url=tile_url))\n\n# Bubbles per region, one renderer each for legend\nlegend_items = []\nfor region in region_order:\n    color = region_colors[region]\n    mask = np.array([r == region for r in cities[\"region\"]])\n    if mask.any():\n        rsrc = ColumnDataSource(\n            data={\n                \"x\": mercator_x[mask],\n                \"y\": mercator_y[mask],\n                \"size\": sizes[mask],\n                \"name\": [cities[\"name\"][i] for i in range(len(cities[\"name\"])) if mask[i]],\n                \"population\": populations[mask],\n                \"region\": [region] * mask.sum(),\n            }\n        )\n        renderer = p.scatter(\n            x=\"x\",\n            y=\"y\",\n            source=rsrc,\n            size=\"size\",\n            fill_color=color,\n            fill_alpha=0.65,\n            line_color=PAGE_BG,\n            line_width=1.5,\n        )\n        legend_items.append(LegendItem(label=region, renderers=[renderer]))\n\n# Region legend\nlegend = Legend(\n    items=legend_items,\n    title=\"Region\",\n    title_text_font_size=\"24pt\",\n    title_text_color=INK,\n    label_text_font_size=\"20pt\",\n    label_text_color=INK_SOFT,\n    glyph_height=40,\n    glyph_width=40,\n    spacing=15,\n    padding=20,\n    background_fill_alpha=0.90,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    border_line_width=1,\n)\np.add_layout(legend, \"right\")\n\n# Size reference legend\nref_pops = [5, 15, 25, 35]\nref_x = 19500000\nref_y0 = 6000000\nref_dy = 1800000\n\nref_sizes = [\n    min_size + np.sqrt((pop - populations.min()) / (populations.max() - populations.min())) * (max_size - min_size)\n    for pop in ref_pops\n]\nref_source = ColumnDataSource(\n    data={\n        \"x\": [ref_x] * len(ref_pops),\n        \"y\": [ref_y0 - i * ref_dy for i in range(len(ref_pops))],\n        \"size\": ref_sizes,\n        \"label\": [f\"{pop}M\" for pop in ref_pops],\n    }\n)\np.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=ref_source,\n    size=\"size\",\n    fill_color=OKABE[0],\n    fill_alpha=0.65,\n    line_color=PAGE_BG,\n    line_width=1.5,\n)\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"label\",\n        source=ref_source,\n        x_offset=60,\n        y_offset=-10,\n        text_font_size=\"18pt\",\n        text_color=INK_SOFT,\n        text_align=\"left\",\n    )\n)\np.text(\n    x=[ref_x - 100000],\n    y=[ref_y0 + 1200000],\n    text=[\"Population\"],\n    text_font_size=\"22pt\",\n    text_font_style=\"bold\",\n    text_color=INK,\n)\n\n# Style\np.title.text_font_size = \"32pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.xaxis.axis_label = \"Longitude (°)\"\np.yaxis.axis_label = \"Latitude (°)\"\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\np.x_range.start = -15000000\np.x_range.end = 22000000\np.y_range.start = -6000000\np.y_range.end = 8500000\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Save\noutput_file(f\"plot-{THEME}.html\")\nsave(p, title=\"World Cities by Population · bubble-map-geographic · python · bokeh · anyplot.ai\", resources=CDN)\n\nW, H = 4800, 2700\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.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(5)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}