{"spec_id":"scatter-map-geographic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-map-geographic: Scatter Map with Geographic Points\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-18\n\"\"\"\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 ColorBar, ColumnDataSource, LinearColorMapper, WMTSTileSource\nfrom bokeh.plotting import figure\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\"\nBRAND = \"#009E73\"\n\n# Data - Major cities with population and elevation\ncities = {\n    \"name\": [\n        \"New York\",\n        \"Los Angeles\",\n        \"Chicago\",\n        \"Houston\",\n        \"Phoenix\",\n        \"Philadelphia\",\n        \"San Antonio\",\n        \"San Diego\",\n        \"Dallas\",\n        \"San Jose\",\n        \"London\",\n        \"Paris\",\n        \"Berlin\",\n        \"Madrid\",\n        \"Rome\",\n        \"Tokyo\",\n        \"Beijing\",\n        \"Shanghai\",\n        \"Mumbai\",\n        \"Sydney\",\n        \"São Paulo\",\n        \"Mexico City\",\n        \"Cairo\",\n        \"Lagos\",\n        \"Johannesburg\",\n        \"Toronto\",\n        \"Vancouver\",\n        \"Montreal\",\n        \"Buenos Aires\",\n        \"Lima\",\n    ],\n    \"lat\": [\n        40.71,\n        34.05,\n        41.88,\n        29.76,\n        33.45,\n        39.95,\n        29.42,\n        32.72,\n        32.78,\n        37.34,\n        51.51,\n        48.86,\n        52.52,\n        40.42,\n        41.90,\n        35.68,\n        39.90,\n        31.23,\n        19.08,\n        -33.87,\n        -23.55,\n        19.43,\n        30.04,\n        6.52,\n        -26.20,\n        43.65,\n        49.28,\n        45.50,\n        -34.60,\n        -12.05,\n    ],\n    \"lon\": [\n        -74.01,\n        -118.24,\n        -87.63,\n        -95.37,\n        -112.07,\n        -75.17,\n        -98.49,\n        -117.16,\n        -96.80,\n        -121.89,\n        -0.13,\n        2.35,\n        13.40,\n        -3.70,\n        12.50,\n        139.69,\n        116.41,\n        121.47,\n        72.88,\n        151.21,\n        -46.63,\n        -99.13,\n        31.24,\n        3.38,\n        28.04,\n        -79.38,\n        -123.12,\n        -73.57,\n        -58.38,\n        -77.03,\n    ],\n    \"population\": [\n        8.3,\n        3.9,\n        2.7,\n        2.3,\n        1.6,\n        1.6,\n        1.5,\n        1.4,\n        1.3,\n        1.0,\n        8.9,\n        2.2,\n        3.6,\n        3.2,\n        2.9,\n        13.9,\n        21.5,\n        24.9,\n        20.7,\n        5.3,\n        12.3,\n        8.9,\n        10.2,\n        15.4,\n        5.8,\n        2.9,\n        0.7,\n        1.8,\n        3.1,\n        10.5,\n    ],\n    \"elevation\": [\n        10,\n        71,\n        182,\n        15,\n        340,\n        12,\n        198,\n        19,\n        131,\n        25,\n        11,\n        35,\n        34,\n        657,\n        21,\n        40,\n        55,\n        4,\n        14,\n        58,\n        760,\n        2240,\n        75,\n        41,\n        1753,\n        76,\n        0,\n        47,\n        25,\n        154,\n    ],\n}\n\n\n# Convert lat/lon to Web Mercator projection (required for tile maps)\ndef lat_lon_to_mercator(lat, lon):\n    k = 6378137\n    x = lon * (k * np.pi / 180.0)\n    y = np.log(np.tan((90 + lat) * np.pi / 360.0)) * k\n    return x, y\n\n\nlats = np.array(cities[\"lat\"])\nlons = np.array(cities[\"lon\"])\nmercator_x, mercator_y = lat_lon_to_mercator(lats, lons)\n\n# Normalize size based on population (scaled for visibility on large canvas)\npopulations = np.array(cities[\"population\"])\nsizes = 25 + (populations / populations.max()) * 55\n\n# Create color mapper for elevation\nelevations = np.array(cities[\"elevation\"])\ncolor_mapper = LinearColorMapper(palette=\"Viridis256\", low=elevations.min(), high=elevations.max())\n\n# Create data source\nsource = ColumnDataSource(\n    data={\n        \"x\": mercator_x,\n        \"y\": mercator_y,\n        \"name\": cities[\"name\"],\n        \"lat\": lats,\n        \"lon\": lons,\n        \"population\": populations,\n        \"elevation\": elevations,\n        \"size\": sizes,\n    }\n)\n\n# Create figure with tile map\np = figure(\n    width=4800,\n    height=2700,\n    x_axis_type=\"mercator\",\n    y_axis_type=\"mercator\",\n    title=\"scatter-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        (\"Elevation\", \"@elevation m\"),\n        (\"Coordinates\", \"(@lat, @lon)\"),\n    ],\n)\n\n# Add map tiles\ntile_url = \"https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png\"\ntile_source = WMTSTileSource(url=tile_url)\np.add_tile(tile_source)\n\n# Add scatter points\np.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=source,\n    size=\"size\",\n    fill_color={\"field\": \"elevation\", \"transform\": color_mapper},\n    fill_alpha=0.75,\n    line_color=BRAND,\n    line_width=2.5,\n)\n\n# Add color bar for elevation\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    title=\"Elevation (m)\",\n    title_text_font_size=\"22pt\",\n    major_label_text_font_size=\"18pt\",\n    label_standoff=15,\n    border_line_color=INK_SOFT,\n    location=(0, 0),\n    width=40,\n    height=600,\n    margin=20,\n)\np.add_layout(color_bar, \"right\")\n\n# Add size legend for population by creating reference points\n\nlegend_items = [(\"1M\", 1.0), (\"10M\", 10.0), (\"25M\", 24.9)]\nlegend_x_pos = -15000000\nlegend_y_start = 7500000\ny_offset = 800000\n\nfor i, (label, pop) in enumerate(legend_items):\n    size = 25 + (pop / populations.max()) * 55\n    y = legend_y_start - (i * y_offset)\n    p.scatter(x=[legend_x_pos], y=[y], size=size, fill_color=BRAND, fill_alpha=0.75, line_color=BRAND, line_width=2.5)\n    p.text(x=[legend_x_pos + 2000000], y=[y], text=[label], text_font_size=\"18pt\", text_color=INK_SOFT)\n\n# Styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\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.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\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\n# Background and border\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\np.outline_line_width = 2\n\n# Set view to show entire world\np.x_range.start = -15000000\np.x_range.end = 18000000\np.y_range.start = -6000000\np.y_range.end = 8500000\n\n# Save as interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome using Selenium\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(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}