Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 78 additions & 31 deletions plots/scatter-map-geographic/implementations/python/bokeh.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
""" pyplots.ai
""" anyplot.ai
scatter-map-geographic: Scatter Map with Geographic Points
Library: bokeh 3.8.2 | Python 3.13.11
Quality: 91/100 | Created: 2026-01-10
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 84/100 | Updated: 2026-05-18
"""

import os
import time
from pathlib import Path

import numpy as np
from bokeh.io import export_png, save
from bokeh.io import output_file, save
from bokeh.models import ColorBar, ColumnDataSource, LinearColorMapper, WMTSTileSource
from bokeh.plotting import figure
from bokeh.resources import CDN
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# 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"
BRAND = "#009E73"

# Data - Major cities with population and elevation
np.random.seed(42)
cities = {
"name": [
"New York",
Expand Down Expand Up @@ -179,8 +191,7 @@

# Convert lat/lon to Web Mercator projection (required for tile maps)
def lat_lon_to_mercator(lat, lon):
"""Convert latitude/longitude to Web Mercator (EPSG:3857) coordinates."""
k = 6378137 # Earth radius in meters
k = 6378137
x = lon * (k * np.pi / 180.0)
y = np.log(np.tan((90 + lat) * np.pi / 360.0)) * k
return x, y
Expand All @@ -192,7 +203,7 @@ def lat_lon_to_mercator(lat, lon):

# Normalize size based on population (scaled for visibility on large canvas)
populations = np.array(cities["population"])
sizes = 25 + (populations / populations.max()) * 55 # Range: 25-80 for 4800x2700
sizes = 25 + (populations / populations.max()) * 55

# Create color mapper for elevation
elevations = np.array(cities["elevation"])
Expand All @@ -218,7 +229,7 @@ def lat_lon_to_mercator(lat, lon):
height=2700,
x_axis_type="mercator",
y_axis_type="mercator",
title="World Cities · scatter-map-geographic · bokeh · pyplots.ai",
title="scatter-map-geographic · python · bokeh · anyplot.ai",
tools="pan,wheel_zoom,box_zoom,reset,hover,save",
tooltips=[
("City", "@name"),
Expand All @@ -228,7 +239,7 @@ def lat_lon_to_mercator(lat, lon):
],
)

# Add map tiles (CartoDB Positron for clean basemap)
# Add map tiles
tile_url = "https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
tile_source = WMTSTileSource(url=tile_url)
p.add_tile(tile_source)
Expand All @@ -241,7 +252,7 @@ def lat_lon_to_mercator(lat, lon):
size="size",
fill_color={"field": "elevation", "transform": color_mapper},
fill_alpha=0.75,
line_color="#306998",
line_color=BRAND,
line_width=2.5,
)

Expand All @@ -252,40 +263,76 @@ def lat_lon_to_mercator(lat, lon):
title_text_font_size="22pt",
major_label_text_font_size="18pt",
label_standoff=15,
border_line_color=None,
border_line_color=INK_SOFT,
location=(0, 0),
width=40,
height=600,
margin=20,
)
p.add_layout(color_bar, "right")

# Add size legend for population by creating reference points

legend_items = [("1M", 1.0), ("10M", 10.0), ("25M", 24.9)]
legend_x_pos = -15000000
legend_y_start = 7500000
y_offset = 800000

for i, (label, pop) in enumerate(legend_items):
size = 25 + (pop / populations.max()) * 55
y = legend_y_start - (i * y_offset)
p.scatter(x=[legend_x_pos], y=[y], size=size, fill_color=BRAND, fill_alpha=0.75, line_color=BRAND, line_width=2.5)
p.text(x=[legend_x_pos + 2000000], y=[y], text=[label], text_font_size="18pt", text_color=INK_SOFT)

# Styling
p.title.text_font_size = "32pt"
p.title.text_color = "#306998"
p.xaxis.axis_label = "Longitude"
p.yaxis.axis_label = "Latitude"
p.xaxis.axis_label_text_font_size = "24pt"
p.yaxis.axis_label_text_font_size = "24pt"
p.title.text_font_size = "28pt"
p.title.text_color = INK
p.xaxis.axis_label = "Longitude (°)"
p.yaxis.axis_label = "Latitude (°)"
p.xaxis.axis_label_text_font_size = "22pt"
p.yaxis.axis_label_text_font_size = "22pt"
p.xaxis.major_label_text_font_size = "18pt"
p.yaxis.major_label_text_font_size = "18pt"
p.xaxis.axis_line_width = 2
p.yaxis.axis_line_width = 2
p.xaxis.major_tick_line_width = 2
p.yaxis.major_tick_line_width = 2
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT

# Background and border
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = INK_SOFT
p.outline_line_width = 2

# Set view to show entire world
p.x_range.start = -15000000
p.x_range.end = 18000000
p.y_range.start = -6000000
p.y_range.end = 8500000

# Background and border
p.background_fill_color = None
p.border_fill_color = "#ffffff"
p.outline_line_color = "#306998"
p.outline_line_width = 2
# Save as interactive HTML
output_file(f"plot-{THEME}.html")
save(p)

# Save as PNG and HTML
export_png(p, filename="plot.png")
save(p, filename="plot.html", title="World Cities · scatter-map-geographic · bokeh", resources=CDN)
# Screenshot with headless Chrome using Selenium
W, H = 4800, 2700
opts = Options()
for arg in (
"--headless=new",
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
f"--window-size={W},{H}",
"--hide-scrollbars",
):
opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()
Loading
Loading