Warming Stripes now with Bokeh and random data!
from bokeh.plotting import figure, show, output_file
from bokeh.models import LinearColorMapper, ColorBar, BasicTicker, ColumnDataSource
from bokeh.layouts import column
from bokeh.models import Title
import pandas as pd
import numpy as np
from bokeh.palettes import RdBu11
def create_stripes_plot(data, year_col, value_col,
title="Warming Stripes", subtitle=None,
width=1200, height=400,
palette=None, reference_period=None,
output_path=None):
"""
Create a 'warming stripes' visualization using Bokeh.
Parameters:
-----------
data : pandas.DataFrame
Data containing years and temperature/value data
year_col : str
Column name for years
value_col : str
Column name for values (e.g., temperature anomaly)
title : str
Main plot title
subtitle : str
Optional subtitle
width : int
Plot width in pixels
height : int
Plot height in pixels
palette : list
Optional color palette (defaults to blue-white-red)
reference_period : tuple
Optional (start_year, end_year) to calculate baseline average
output_path : str
Optional file path to save HTML output
Returns:
--------
bokeh.plotting.figure
The configured Bokeh figure
"""
# Prepare data
df = data.copy().sort_values(year_col).reset_index(drop=True)
# Calculate anomaly relative to reference period if specified
if reference_period:
start_year, end_year = reference_period
baseline = df[(df[year_col] >= start_year) &
(df[year_col] <= end_year)][value_col].mean()
df['anomaly'] = df[value_col] - baseline
plot_value = 'anomaly'
else:
plot_value = value_col
# Calculate stripe boundaries
df['left'] = df[year_col] - 0.5
df['right'] = df[year_col] + 0.5
df['bottom'] = 0
df['top'] = 1
# Default palette (blue to white to red)
if palette is None:
palette = list(reversed(RdBu11)) # Blue (cold) to Red (warm)
# Create color mapper
color_mapper = LinearColorMapper(
palette=palette,
low=df[plot_value].min(),
high=df[plot_value].max()
)
# Create ColumnDataSource
source = ColumnDataSource(df)
# Create figure without axes
p = figure(tools = "hover,save",tooltips=[(year_col, f"@{year_col}"), (value_col, f"@{value_col}")],
width=width,
height=height,
x_range=(df[year_col].min() - 0.5, df[year_col].max() + 0.5),
y_range=(0, 1)
)
# Plot vertical stripes (one per year)
p.quad(
left='left',
right='right',
bottom='bottom',
top='top',
color={'field': plot_value, 'transform': color_mapper},
line_color=None,
source=source,
hover_line_color='lime',
hover_line_width=2
)
# Add color bar
color_bar = ColorBar(
color_mapper=color_mapper,
ticker=BasicTicker(desired_num_ticks=10),
label_standoff=12,
location=(0, 0),
title="°C" if "temp" in value_col.lower() else "Value"
)
p.add_layout(color_bar, 'right')
# Styling
p.xaxis.axis_label = None
p.yaxis.visible = False
p.xgrid.visible = False
p.ygrid.visible = False
p.outline_line_color = None
# Add year labels at key points
key_years = []
min_year = df[year_col].min()
max_year = df[year_col].max()
key_years = list(range(int(min_year), int(max_year), 20))
p.xaxis.ticker = key_years
p.xaxis.major_label_text_font_size = "12pt"
# Add reference period markers if specified
if reference_period:
start_year, end_year = reference_period
# Add text annotations
p.text(x=[start_year], y=[1.05], text=[str(start_year)],
text_align="center", text_font_size="10pt")
p.text(x=[end_year], y=[1.05], text=[str(end_year)],
text_align="center", text_font_size="10pt")
# Create title layout
title_obj = Title(text=title, text_font_size="16pt")
p.add_layout(title_obj, 'above')
if subtitle:
subtitle_obj = Title(text=subtitle, text_font_size="10pt", text_font_style="italic")
p.add_layout(subtitle_obj, 'above')
# Save to file if specified
if output_path:
output_file(output_path)
return p
# ============================================================================
# EXAMPLE 1: Global Temperature Stripes (Like Ed Hawkins' warming stripes)
# ============================================================================
# Generate realistic temperature data from 1880-2023
years = list(range(1880, 2024))
np.random.seed(42)
# Create temperature anomaly with warming trend
temp_data = []
for i, year in enumerate(years):
# Base trend: cooling until 1910, then warming acceleration
if year < 1910:
trend = -0.3 + (year - 1880) * 0.002
elif year < 1980:
trend = -0.2 + (year - 1910) * 0.005
else:
trend = 0.15 + (year - 1980) * 0.02
# Add natural variability
noise = np.random.normal(0, 0.1)
temp_anomaly = trend + noise
temp_data.append({
'year': year,
'temperature_anomaly': -temp_anomaly
})
temp_df = pd.DataFrame(temp_data)
plot1 = create_stripes_plot(
data=temp_df,
year_col='year',
value_col='temperature_anomaly',
title='Global Temperature in 1880-2023 (compared to 1951-1980 average)',
subtitle='Reproduction of "warming stripes" chart originally designed by Ed Hawkins',
reference_period=(1951, 1980),
height=300,
output_path='example1_warming_stripes.html'
)
show(plot1)
# ============================================================================
# EXAMPLE 2: Arctic Sea Ice Extent Stripes
# ============================================================================
# Generate Arctic sea ice extent data (declining trend)
ice_years = list(range(1979, 2024))
ice_data = []
for i, year in enumerate(ice_years):
# Declining trend with variability
base_extent = 7.5 - (year - 1979) * 0.08 # Million km²
noise = np.random.normal(0, 0.3)
extent = base_extent + noise
ice_data.append({
'year': year,
'ice_extent': extent
})
ice_df = pd.DataFrame(ice_data)
plot2 = create_stripes_plot(
data=ice_df,
year_col='year',
value_col='ice_extent',
title='Arctic Sea Ice Extent 1979-2023',
subtitle='Minimum extent (million km²) - showing declining trend',
height=300,
output_path='example2_arctic_ice.html'
)
show(plot2)
# ============================================================================
# EXAMPLE 3: Ocean Heat Content Stripes
# ============================================================================
# Generate ocean heat content data (increasing trend)
ocean_years = list(range(1960, 2024))
ocean_data = []
for i, year in enumerate(ocean_years):
# Increasing trend with acceleration
if year < 1990:
trend = (year - 1960) * 2
else:
trend = 60 + (year - 1990) * 8
noise = np.random.normal(0, 15)
heat_content = trend + noise
ocean_data.append({
'year': year,
'ocean_heat': heat_content
})
ocean_df = pd.DataFrame(ocean_data)
# Custom palette (cool to hot)
ocean_palette = ['#08519c', '#3182bd', '#6baed6', '#9ecae1', '#c6dbef',
'#fee5d9', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15']
plot3 = create_stripes_plot(
data=ocean_df,
year_col='year',
value_col='ocean_heat',
title='Ocean Heat Content 1960-2023',
subtitle='Upper 2000m heat content anomaly (10²² Joules)',
palette=ocean_palette,
height=300,
output_path='example3_ocean_heat.html'
)
show(plot3)
