1- """ pyplots .ai
1+ """ anyplot .ai
22map-tile-background: Map with Tile Background
3- Library: bokeh 3.8.2 | Python 3.13.11
4- Quality: 91 /100 | Created : 2026-01-20
3+ Library: bokeh 3.9.0 | Python 3.13.13
4+ Quality: 90 /100 | Updated : 2026-05-27
55"""
66
7+ import os
8+ import sys
9+
10+
11+ # bokeh.py shadows the installed bokeh package — remove script dir from sys.path
12+ _here = os .path .abspath (os .path .dirname (__file__ ))
13+ sys .path = [p for p in sys .path if os .path .abspath (p or "." ) != _here ]
14+
15+ import time
16+ from pathlib import Path
17+
718import numpy as np
819import xyzservices .providers as xyz
9- from bokeh .io import export_png , output_file , save
10- from bokeh .models import ColumnDataSource , HoverTool
20+ from bokeh .io import output_file , save
21+ from bokeh .models import ColorBar , ColumnDataSource , HoverTool , LinearColorMapper
1122from bokeh .plotting import figure
23+ from selenium import webdriver
24+ from selenium .webdriver .chrome .options import Options
1225
1326
14- # Data: European capital cities with visitor counts (millions per year)
15- np .random .seed (42 )
27+ THEME = os .getenv ("ANYPLOT_THEME" , "light" )
28+ PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
29+ ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
30+ INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
31+ INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
1632
17- # City coordinates (lon, lat )
33+ # Data: European capital cities with annual visitor counts (millions )
1834cities = {
1935 "Paris" : (2.3522 , 48.8566 ),
2036 "London" : (- 0.1276 , 51.5074 ),
3349 "Warsaw" : (21.0122 , 52.2297 ),
3450}
3551
36- # Extract data
3752names = list (cities .keys ())
3853lons = np .array ([cities [c ][0 ] for c in names ])
3954lats = np .array ([cities [c ][1 ] for c in names ])
40-
41- # Visitor counts (millions per year) - realistic estimates
4255visitors = 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 ])
4356
57+ # Web Mercator projection (required for tile maps)
58+ k = 6378137
59+ x_merc = lons * (k * np .pi / 180.0 )
60+ y_merc = np .log (np .tan ((90 + lats ) * np .pi / 360.0 )) * k
4461
45- # Convert lon/lat to Web Mercator projection (required for tile maps)
46- def lonlat_to_mercator (lon , lat ):
47- """Convert longitude/latitude to Web Mercator coordinates."""
48- k = 6378137 # Earth radius in meters
49- x = lon * (k * np .pi / 180.0 )
50- y = np .log (np .tan ((90 + lat ) * np .pi / 360.0 )) * k
51- return x , y
52-
53-
54- x_merc , y_merc = lonlat_to_mercator (lons , lats )
55-
56- # Create data source
5762source = ColumnDataSource (
5863 data = {
5964 "x" : x_merc ,
@@ -62,52 +67,111 @@ def lonlat_to_mercator(lon, lat):
6267 "lat" : lats ,
6368 "name" : names ,
6469 "visitors" : visitors ,
65- "size" : visitors * 2.0 + 25 , # Scale size by visitors
70+ "size" : visitors * 2.0 + 20 ,
6671 }
6772)
6873
69- # Create figure with Web Mercator projection
74+ # imprint_seq colormap: #009E73 (brand green) → #4467A3 (blue)
75+ ANYPLOT_SEQ256 = [
76+ "#{:02X}{:02X}{:02X}" .format (
77+ int (round (0 + 68 * t / 255 )), int (round (158 - 55 * t / 255 )), int (round (115 + 48 * t / 255 ))
78+ )
79+ for t in range (256 )
80+ ]
81+ color_mapper = LinearColorMapper (palette = ANYPLOT_SEQ256 , low = visitors .min (), high = visitors .max ())
82+
83+ tile_provider = xyz .CartoDB .Positron if THEME == "light" else xyz .CartoDB .DarkMatter
84+
85+ title = "map-tile-background · python · bokeh · anyplot.ai"
86+ n = len (title )
87+ title_pt = max (34 , round (50 * 67 / n )) if n > 67 else 50
88+
7089p = figure (
71- width = 4800 ,
72- height = 2700 ,
90+ width = 3200 ,
91+ height = 1800 ,
7392 x_axis_type = "mercator" ,
7493 y_axis_type = "mercator" ,
75- title = "map-tile-background · bokeh · pyplots.ai" ,
94+ title = title ,
7695 tools = "pan,wheel_zoom,box_zoom,reset" ,
7796 active_scroll = "wheel_zoom" ,
97+ toolbar_location = None ,
98+ min_border_bottom = 160 ,
99+ min_border_left = 180 ,
100+ min_border_top = 110 ,
101+ min_border_right = 250 ,
78102)
79103
80- # Add tile background (CartoDB Positron for clean look)
81- p .add_tile (xyz .CartoDB .Positron )
104+ p .add_tile (tile_provider )
105+
106+ p .scatter (
107+ "x" ,
108+ "y" ,
109+ source = source ,
110+ size = "size" ,
111+ fill_color = {"field" : "visitors" , "transform" : color_mapper },
112+ fill_alpha = 0.85 ,
113+ line_color = "white" ,
114+ line_width = 2 ,
115+ )
82116
83- # Plot city markers with color based on visitor count
84- p .scatter ("x" , "y" , source = source , size = "size" , fill_color = "#306998" , fill_alpha = 0.7 , line_color = "white" , line_width = 3 )
117+ color_bar = ColorBar (
118+ color_mapper = color_mapper ,
119+ title = "Visitors (M/year)" ,
120+ title_text_font_size = "28pt" ,
121+ title_text_color = INK ,
122+ major_label_text_font_size = "24pt" ,
123+ major_label_text_color = INK_SOFT ,
124+ background_fill_color = ELEVATED_BG ,
125+ width = 80 ,
126+ )
127+ p .add_layout (color_bar , "right" )
85128
86- # Add hover tool
87129hover = HoverTool (
88130 tooltips = [("City" , "@name" ), ("Visitors" , "@visitors{0.0}M/year" ), ("Location" , "@lat{0.00}°N, @lon{0.00}°E" )]
89131)
90132p .add_tools (hover )
91133
92- # Styling for large canvas
93- p .title .text_font_size = "32pt"
134+ p .title .text_font_size = f"{ title_pt } pt"
94135p .title .align = "center"
136+ p .title .text_color = INK
137+
138+ p .xaxis .axis_label = "Longitude (°)"
139+ p .yaxis .axis_label = "Latitude (°)"
140+ p .xaxis .axis_label_text_font_size = "42pt"
141+ p .yaxis .axis_label_text_font_size = "42pt"
142+ p .xaxis .axis_label_text_color = INK
143+ p .yaxis .axis_label_text_color = INK
144+ p .xaxis .major_label_text_font_size = "34pt"
145+ p .yaxis .major_label_text_font_size = "34pt"
146+ p .xaxis .major_label_text_color = INK_SOFT
147+ p .yaxis .major_label_text_color = INK_SOFT
95148
96- # Axis labels
97- p .xaxis .axis_label = "Longitude"
98- p .yaxis .axis_label = "Latitude"
99- p .xaxis .axis_label_text_font_size = "22pt"
100- p .yaxis .axis_label_text_font_size = "22pt"
101- p .xaxis .major_label_text_font_size = "16pt"
102- p .yaxis .major_label_text_font_size = "16pt"
103-
104- # Grid styling
105149p .xgrid .grid_line_color = None
106150p .ygrid .grid_line_color = None
107151
108- # Save as PNG
109- export_png (p , filename = "plot.png" )
152+ p .background_fill_color = PAGE_BG
153+ p .border_fill_color = PAGE_BG
154+ p .outline_line_color = INK_SOFT
110155
111- # Also save as HTML for interactivity
112- output_file ("plot.html" )
156+ output_file (f"plot-{ THEME } .html" )
113157save (p )
158+
159+ W , H = 3200 , 1800
160+ opts = Options ()
161+ for arg in (
162+ "--headless=new" ,
163+ "--no-sandbox" ,
164+ "--disable-dev-shm-usage" ,
165+ "--disable-gpu" ,
166+ f"--window-size={ W } ,{ H } " ,
167+ "--hide-scrollbars" ,
168+ ):
169+ opts .add_argument (arg )
170+ driver = webdriver .Chrome (options = opts )
171+ driver .execute_cdp_cmd (
172+ "Emulation.setDeviceMetricsOverride" , {"width" : W , "height" : H , "deviceScaleFactor" : 1 , "mobile" : False }
173+ )
174+ driver .get (f"file://{ Path (f'plot-{ THEME } .html' ).resolve ()} " )
175+ time .sleep (3 )
176+ driver .save_screenshot (f"plot-{ THEME } .png" )
177+ driver .quit ()
0 commit comments