Super nice to make custom projections in Bokeh !! Here is the Mollweide.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColorBar, LinearColorMapper, BasicTicker, HoverTool, ColumnDataSource
from bokeh.palettes import Viridis256
import cartopy.feature as cfeature
from shapely.geometry import LineString, MultiLineString
def mollweide_transform(lon, lat):
"""Transform longitude and latitude to Mollweide projection coordinates."""
# Convert to radians
lon = np.radians(lon)
lat = np.radians(lat)
# Auxiliary angle theta
theta = lat
for i in range(100): # Max iterations
theta_new = theta - (2 * theta + np.sin(2 * theta) - np.pi * np.sin(lat)) / (2 + 2 * np.cos(2 * theta))
if np.all(np.abs(theta - theta_new) < 1e-10):
break
theta = theta_new
# Calculate x and y coordinates
x = 2 * np.sqrt(2) / np.pi * lon * np.cos(theta)
y = np.sqrt(2) * np.sin(theta)
return x, y
# Create sample data with higher resolution
n_lat, n_lon = 180, 360 # Higher resolution for WebGL
lats = np.linspace(-89.5, 89.5, n_lat)
lons = np.linspace(-179.5, 179.5, n_lon)
lon_grid, lat_grid = np.meshgrid(lons, lats)
# Create sample temperature data (example: temperature variation with latitude)
temperature = 20 * np.cos(np.radians(lat_grid)) + \
5 * np.sin(np.radians(2 * lon_grid)) + \
np.random.normal(0, 1, (n_lat, n_lon))
# Create the figure with WebGL
p = figure(width=800, height=400,
title="Global Temperature Distribution (Mollweide Projection)",
x_range=(-2.5, 2.5), y_range=(-1.3, 1.3),
)
# Create color mapper
color_mapper = LinearColorMapper(palette=Viridis256,
low=np.min(temperature),
high=np.max(temperature))
# Create patches for each grid cell
xs = []
ys = []
temps = []
for i in range(n_lat - 1):
for j in range(n_lon - 1):
# Get the four corners of the cell
lons_cell = [lon_grid[i,j], lon_grid[i,j+1], lon_grid[i+1,j+1], lon_grid[i+1,j]]
lats_cell = [lat_grid[i,j], lat_grid[i,j+1], lat_grid[i+1,j+1], lat_grid[i+1,j]]
# Transform to Mollweide projection
x, y = mollweide_transform(lons_cell, lats_cell)
# Check if the cell crosses the date line
if np.any(np.abs(np.diff(lons_cell)) > 180):
continue
# Add the cell if it's valid
if not np.any(np.isnan(x)) and not np.any(np.isnan(y)):
xs.append(x.tolist())
ys.append(y.tolist())
temps.append(temperature[i,j])
# Create ColumnDataSource
source = ColumnDataSource(data=dict(
xs=xs,
ys=ys,
temp=temps
))
# Add the patches with WebGL
patches = p.patches('xs', 'ys',
fill_color={'field': 'temp', 'transform': color_mapper},
line_color=None,
source=source)
# Add coastlines
coastlines = cfeature.NaturalEarthFeature('physical', 'coastline', '110m')
def process_line_string(line_string):
if isinstance(line_string, (LineString, MultiLineString)):
if isinstance(line_string, LineString):
lines = [line_string]
else:
lines = list(line_string.geoms)
for line in lines:
coords = np.array(line.coords)
if len(coords) > 1:
# Split at the dateline
splits = np.where(np.abs(np.diff(coords[:, 0])) > 180)[0] + 1
segments = np.split(coords, splits)
for segment in segments:
if len(segment) > 1:
x, y = mollweide_transform(segment[:, 0], segment[:, 1])
p.line(x, y, line_color='black', line_width=1, line_alpha=0.5)
for geom in coastlines.geometries():
process_line_string(geom)
# Add hover tool
hover = HoverTool(tooltips=[
('Temperature', '@temp{0.1f}°C'),
], renderers=[patches])
p.add_tools(hover)
# Add color bar
color_bar = ColorBar(color_mapper=color_mapper,
ticker=BasicTicker(),
label_standoff=12,
border_line_color=None,
location=(0, 0))
p.add_layout(color_bar, 'right')
# Customize the plot
p.grid.visible = False
p.axis.visible = False
p.title.text_font_size = '14pt'
# Add graticules (longitude and latitude lines)
for lat in np.arange(-75, 76, 15):
lons = np.linspace(-180, 180, 100)
x, y = mollweide_transform(lons, np.full_like(lons, lat))
p.line(x, y, line_color='gray', line_alpha=0.3)
for lon in np.arange(-180, 181, 30):
lats = np.linspace(-89.5, 89.5, 100)
x, y = mollweide_transform(np.full_like(lats, lon), lats)
p.line(x, y, line_color='gray', line_alpha=0.3)
# Add equator with different style
eq_x, eq_y = mollweide_transform(lons, np.zeros_like(lons))
p.line(eq_x, eq_y, line_color='gray', line_width=2, line_alpha=0.5)
# Output to file
# output_file("mollweide.html")
show(p)