Hello,
Here is Nephele:
An interactive dark-themed map that plots real-time cloudiness, temperature, humidity and pressure for several cities of the world. Built with Bokeh Server, it pulls data every minute from OpenWeatherMap (using my API key) and renders styled circle markers over a CartoDB βDark Matterβ basemap.
# WM_app.py
import os
import numpy as np
import requests
from datetime import datetime
from bokeh.io import curdoc
from bokeh.models import (
ColumnDataSource,
LinearColorMapper,
ColorBar,
BasicTicker,
WheelZoomTool,
HoverTool,
)
from bokeh.plotting import figure
from bokeh.models import WMTSTileSource
# βββ Helper: convert lat/lon to Web Mercator βββββββββββββββββββββββββββββββββββ
def latlon_to_mercator(lat, lon):
"""Convert (lat, lon) in degrees to Web Mercator (x, y)."""
k = 6378137.0
x = lon * (k * np.pi / 180.0)
y = np.log(np.tan((90 + lat) * np.pi / 360.0)) * k
return x, y
# βββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
API_KEY = os.getenv("OPENWEATHERMAP_API_KEY", "<YOUR_OPENWEATHERMAP_API_KEY>")
UPDATE_INTERVAL_MS = 60 * 1000 # 1 minute
# βββ Expanded List of >50 Cities βββββββββββββββββββββββββββββββββββββββββββββββ
cities = [ {"name": "London, UK", "lat": 51.5074, "lon": -0.1278}, {"name": "Paris, FR", "lat": 48.8566, "lon": 2.3522}, {"name": "Berlin, DE", "lat": 52.5200, "lon": 13.4050}, {"name": "Madrid, ES", "lat": 40.4168, "lon": -3.7038}, {"name": "Rome, IT", "lat": 41.9028, "lon": 12.4964}, {"name": "Lisbon, PT", "lat": 38.7223, "lon": -9.1393}, {"name": "Dublin, IE", "lat": 53.3498, "lon": -6.2603}, {"name": "Brussels, BE", "lat": 50.8503, "lon": 4.3517}, {"name": "Amsterdam, NL", "lat": 52.3676, "lon": 4.9041}, {"name": "Vienna, AT", "lat": 48.2082, "lon": 16.3738}, {"name": "Barcelona, ES", "lat": 41.3851, "lon": 2.1734}, {"name": "Munich, DE", "lat": 48.1351, "lon": 11.5820}, {"name": "Milan, IT", "lat": 45.4642, "lon": 9.1900}, {"name": "Hamburg, DE", "lat": 53.5511, "lon": 9.9937}, {"name": "Frankfurt, DE", "lat": 50.1109, "lon": 8.6821}, {"name": "Zurich, CH", "lat": 47.3769, "lon": 8.5417}, {"name": "Stockholm, SE", "lat": 59.3293, "lon": 18.0686}, {"name": "Copenhagen, DK", "lat": 55.6761, "lon": 12.5683}, {"name": "Oslo, NO", "lat": 59.9139, "lon": 10.7522}, {"name": "Helsinki, FI", "lat": 60.1699, "lon": 24.9384}, {"name": "Reykjavik, IS", "lat": 64.1466, "lon": -21.9426}, {"name": "Warsaw, PL", "lat": 52.2297, "lon": 21.0122}, {"name": "Prague, CZ", "lat": 50.0755, "lon": 14.4378}, {"name": "Budapest, HU", "lat": 47.4979, "lon": 19.0402}, {"name": "Bratislava, SK", "lat": 48.1486, "lon": 17.1077}, {"name": "Ljubljana, SI", "lat": 46.0569, "lon": 14.5058}, {"name": "Zagreb, HR", "lat": 45.8150, "lon": 15.9819}, {"name": "Sarajevo, BA", "lat": 43.8563, "lon": 18.4131}, {"name": "Belgrade, RS", "lat": 44.7866, "lon": 20.4489}, {"name": "Bucharest, RO", "lat": 44.4268, "lon": 26.1025}, {"name": "Sofia, BG", "lat": 42.6977, "lon": 23.3219}, {"name": "Athens, GR", "lat": 37.9838, "lon": 23.7275}, {"name": "Valencia, ES", "lat": 39.4699, "lon": -0.3763}, {"name": "Seville, ES", "lat": 37.3891, "lon": -5.9845}, {"name": "Bilbao, ES", "lat": 43.2630, "lon": -2.9350}, {"name": "Nice, FR", "lat": 43.7102, "lon": 7.2620}, {"name": "Marseille, FR", "lat": 43.2965, "lon": 5.3698}, {"name": "Lyon, FR", "lat": 45.7640, "lon": 4.8357}, {"name": "Manchester, UK", "lat": 53.4808, "lon": -2.2426}, {"name": "Edinburgh, UK", "lat": 55.9533, "lon": -3.1883}, {"name": "Birmingham, UK", "lat": 52.4862, "lon": -1.8904}, {"name": "Valletta, MT", "lat": 35.8989, "lon": 14.5146}, {"name": "Luxembourg, LU", "lat": 49.6116, "lon": 6.1319}, {"name": "Riga, LV", "lat": 56.9496, "lon": 24.1052}, {"name": "Vilnius, LT", "lat": 54.6872, "lon": 25.2797}, {"name": "Tallinn, EE", "lat": 59.43696, "lon": 24.7536}, {"name": "Tokyo, JP", "lat": 35.6895, "lon": 139.6917}, {"name": "Delhi, IN", "lat": 28.7041, "lon": 77.1025}, {"name": "Shanghai, CN", "lat": 31.2304, "lon": 121.4737}, {"name": "SΓ£o Paulo, BR", "lat": -23.5505, "lon": -46.6333}, {"name": "Mexico City, MX", "lat": 19.4326, "lon": -99.1332}, {"name": "Cairo, EG", "lat": 30.0444, "lon": 31.2357}, {"name": "Mumbai, IN", "lat": 19.0760, "lon": 72.8777}, {"name": "Beijing, CN", "lat": 39.9042, "lon": 116.4074}, {"name": "Dhaka, BD", "lat": 23.8103, "lon": 90.4125}, {"name": "Osaka, JP", "lat": 34.6937, "lon": 135.5023}, {"name": "New York, US", "lat": 40.7128, "lon": -74.0060}, {"name": "Karachi, PK", "lat": 24.8607, "lon": 67.0011}, {"name": "Buenos Aires, AR", "lat": -34.6037, "lon": -58.3816}, {"name": "Chongqing, CN", "lat": 29.4316, "lon": 106.9123}, {"name": "Istanbul, TR", "lat": 41.0082, "lon": 28.9784}, {"name": "Kolkata, IN", "lat": 22.5726, "lon": 88.3639}, {"name": "Manila, PH", "lat": 14.5995, "lon": 120.9842}, {"name": "Lagos, NG", "lat": 6.5244, "lon": 3.3792}, {"name": "Rio de Janeiro, BR", "lat": -22.9068, "lon": -43.1729}, {"name": "Tianjin, CN", "lat": 39.3434, "lon": 117.3616}, {"name": "Kinshasa, CD", "lat": -4.4419, "lon": 15.2663}, {"name": "Guangzhou, CN", "lat": 23.1291, "lon": 113.2644}, {"name": "Los Angeles, US", "lat": 34.0522, "lon": -118.2437}, {"name": "Moscow, RU", "lat": 55.7558, "lon": 37.6173}, {"name": "Shenzhen, CN", "lat": 22.5431, "lon": 114.0579}, {"name": "Lahore, PK", "lat": 31.5204, "lon": 74.3587}, {"name": "Bangalore, IN", "lat": 12.9716, "lon": 77.5946}, {"name": "Paris, FR", "lat": 48.8566, "lon": 2.3522}, {"name": "BogotΓ‘, CO", "lat": 4.7110, "lon": -74.0721}, {"name": "Chennai, IN", "lat": 13.0827, "lon": 80.2707}, {"name": "Jakarta, ID", "lat": -6.2088, "lon": 106.8456}, {"name": "Lima, PE", "lat": -12.0464, "lon": -77.0428}, {"name": "Bangkok, TH", "lat": 13.7563, "lon": 100.5018}, {"name": "Seoul, KR", "lat": 37.5665, "lon": 126.9780}, {"name": "Nagoya, JP", "lat": 35.1815, "lon": 136.9066}, {"name": "Hyderabad, IN", "lat": 17.3850, "lon": 78.4867}, {"name": "London, UK", "lat": 51.5074, "lon": -0.1278}, {"name": "Tehran, IR", "lat": 35.6892, "lon": 51.3890}, {"name": "Chicago, US", "lat": 41.8781, "lon": -87.6298}, {"name": "Chengdu, CN", "lat": 30.5728, "lon": 104.0668}, {"name": "Nanjing, CN", "lat": 32.0603, "lon": 118.7969}, {"name": "Wuhan, CN", "lat": 30.5928, "lon": 114.3055}, {"name": "Ho Chi Minh City, VN", "lat": 10.8231, "lon": 106.6297}, {"name": "Luanda, AO", "lat": -8.8390, "lon": 13.2894}, {"name": "Ahmedabad, IN", "lat": 23.0225, "lon": 72.5714}, {"name": "Kuala Lumpur, MY", "lat": 3.1390, "lon": 101.6869}, {"name": "Xiβan, CN", "lat": 34.3416, "lon": 108.9398}, {"name": "Hong Kong, HK", "lat": 22.3193, "lon": 114.1694}, {"name": "Hangzhou, CN", "lat": 30.2741, "lon": 120.1551}, {"name": "Foshan, CN", "lat": 23.0215, "lon": 113.1214}, {"name": "Shenyang, CN", "lat": 41.8057, "lon": 123.4315}, ]
# Precompute mercator coordinates
merc_x, merc_y = zip(*(latlon_to_mercator(c["lat"], c["lon"]) for c in cities))
# ColumnDataSource for our plot
source = ColumnDataSource(
data=dict(
x=list(merc_x),
y=list(merc_y),
name=[c["name"] for c in cities],
cloud=[0] * len(cities), # placeholder, will be updated
temp=[0] * len(cities), # temperature Β°C
humidity=[0] * len(cities), # humidity %
pressure=[0] * len(cities), # pressure hPa
)
)
# βββ Build dark-background map figure ββββββββββββββββββββββββββββββββββββββββββ
# Generate dynamic title with current datetime
title_str = f"Live Weather Map β {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
p = figure(
x_axis_type="mercator",
y_axis_type="mercator",
sizing_mode="stretch_both",
title=title_str,
background_fill_color="#2F2F2F",
border_fill_color="#2F2F2F",
outline_line_color="#444444",
)
dark_url = "https://basemaps.cartocdn.com/dark_all/{Z}/{X}/{Y}.png"
tile_provider = WMTSTileSource(url=dark_url)
p.add_tile(tile_provider)
# Enable wheel zoom by default
wheel_zoom = WheelZoomTool()
p.add_tools(wheel_zoom)
p.toolbar.active_scroll = wheel_zoom
# style title & axes for dark theme
p.title.text_color = "deepskyblue"
p.title.text_font = "Helvetica"
p.title.text_font_style = "bold"
p.title.text_font_size = "25pt"
for axis in (p.xaxis, p.yaxis):
axis.axis_line_color = "white"
axis.major_tick_line_color = "white"
axis.major_label_text_color = "white"
axis.minor_tick_line_color = "white"
# remove grid lines for a cleaner map
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
# Color mapper and circles
color_mapper = LinearColorMapper(palette="Turbo256", low=-10, high=40)
circles = p.scatter(
"x",
"y",
source=source,
size=20,
fill_color={"field": "temp", "transform": color_mapper},
fill_alpha=0.9,
line_color=None,
)
# βββ Enhanced HoverTool with HTML styling βββββββββββββββββββββββββββββββββββββ
hover = HoverTool(
renderers=[circles],
point_policy="follow_mouse",
tooltips="""
<div style='background-color: rgba(0,0,0,0.8); padding:10px; border-radius:5px; width:180px;'>
<div style='font-size:27px; color:#FFD700; font-weight:bold;'>@name</div>
<div style='font-size:23px; color:#FFFFFF;'>βοΈ @cloud{0.0}%</div>
<div style='font-size:23px; color:#FFFFFF;'>π‘οΈ @temp{0.0}Β°C</div>
<div style='font-size:23px; color:#FFFFFF;'>π§ @humidity{0.0}%</div>
<div style='font-size:23px; color:#FFFFFF;'>π @pressure{0.0}hPa</div>
</div>
""",
)
p.add_tools(hover)
# color bar for reference
color_bar = ColorBar(
title_text_font_size="16pt",
major_label_text_font_size="14pt",
background_fill_color="#2F2F2F",
color_mapper=color_mapper,
title_text_color="white",
major_label_text_color="white",
label_standoff=10,
ticker=BasicTicker(desired_num_ticks=5),
title="Temperature (Β°C)",
location=(0, 0),
)
p.add_layout(color_bar, "right")
# βββ Data fetch + update βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def fetch_and_update():
new_cloud, new_temp, new_hum, new_pressure = [], [], [], []
for city in cities:
params = {
"lat": city["lat"],
"lon": city["lon"],
"appid": API_KEY,
"units": "metric",
}
try:
data = requests.get(
"https://api.openweathermap.org/data/2.5/weather", params=params
).json()
clouds = data.get("clouds", {}).get("all", 0)
temp = data.get("main", {}).get("temp", 0)
hum = data.get("main", {}).get("humidity", 0)
pressure = data.get("main", {}).get("pressure", 0)
except Exception:
clouds, temp, hum, pressure = 0, 0, 0, 0
new_cloud.append(clouds)
new_temp.append(temp)
new_hum.append(hum)
new_pressure.append(pressure)
# Update all three columns at once
source.data.update(
cloud=new_cloud, temp=new_temp, humidity=new_hum, pressure=new_pressure
)
# Initial load + periodic refresh
fetch_and_update()
curdoc().add_periodic_callback(fetch_and_update, UPDATE_INTERVAL_MS)
# Add to document
curdoc().add_root(p)
bokeh serve --show WM_app.py
(Nephele or NΞ΅ΟΞλη means βcloudβ in Greek)