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
146 changes: 78 additions & 68 deletions plots/heatmap-basic/implementations/bokeh.py
Original file line number Diff line number Diff line change
@@ -1,122 +1,132 @@
""" pyplots.ai
heatmap-basic: Basic Heatmap
Library: bokeh 3.8.1 | Python 3.13.11
Quality: 92/100 | Created: 2025-12-23
Library: bokeh 3.8.2 | Python 3.14.3
Quality: 91/100 | Updated: 2026-02-15
"""

import numpy as np
import pandas as pd
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary pandas import. The ColumnDataSource can be created directly from a dictionary without using pandas. The old implementation used ColumnDataSource(data={...}) which is simpler and doesn't require pandas as a dependency. This violates the KISS principle for plot implementations which should be simple, readable scripts.

Copilot uses AI. Check for mistakes.
from bokeh.io import export_png, save
from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LabelSet, LinearColorMapper
from bokeh.palettes import Viridis256
from bokeh.models import BasicTicker, ColumnDataSource, HoverTool, LabelSet
from bokeh.plotting import figure
from bokeh.resources import CDN
from bokeh.transform import linear_cmap


# Data - Monthly sales performance by product category
# Data - Monthly temperature anomalies (°C) by city
np.random.seed(42)
x_labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
y_labels = ["Product A", "Product B", "Product C", "Product D", "Product E", "Product F"]

# Generate heatmap values (sales performance 0-100)
values = np.random.rand(len(y_labels), len(x_labels)) * 100

# Flatten data for ColumnDataSource
x_data = []
y_data = []
value_data = []
text_data = []
text_color_data = []

for i, y in enumerate(y_labels):
for j, x in enumerate(x_labels):
x_data.append(x)
y_data.append(y)
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"]
cities = ["Oslo", "Berlin", "Madrid", "Cairo", "Mumbai", "Tokyo", "Sydney"]

# Generate realistic temperature anomalies with geographic patterns
base_anomalies = np.random.randn(len(cities), len(months)) * 0.6
# Northern cities: colder winters, warmer summers
for i, city in enumerate(cities):
seasonal = np.sin(np.linspace(-np.pi / 2, 3 * np.pi / 4, len(months)))
if city in ("Oslo", "Berlin"):
base_anomalies[i] += seasonal * 1.5 - 0.3
elif city in ("Madrid", "Cairo"):
base_anomalies[i] += seasonal * 1.2 + 0.4
elif city == "Mumbai":
base_anomalies[i] += 0.8
elif city == "Sydney":
base_anomalies[i] -= seasonal * 0.9
elif city == "Tokyo":
base_anomalies[i] += seasonal * 0.7

values = np.round(base_anomalies, 1)

# Flatten to DataFrame for ColumnDataSource
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Flatten to DataFrame for ColumnDataSource" is misleading since ColumnDataSource doesn't require a DataFrame. It should be "Flatten to list of dicts for ColumnDataSource" or simply "Flatten data for ColumnDataSource".

Suggested change
# Flatten to DataFrame for ColumnDataSource
# Flatten data for ColumnDataSource

Copilot uses AI. Check for mistakes.
records = []
for i, city in enumerate(cities):
for j, month in enumerate(months):
val = values[i, j]
value_data.append(val)
text_data.append(f"{val:.0f}")
# Use white text on dark cells, black on light cells
text_color_data.append("white" if val > 50 else "black")

source = ColumnDataSource(
data={"x": x_data, "y": y_data, "value": value_data, "text": text_data, "text_color": text_color_data}
)

# Color mapper
color_mapper = LinearColorMapper(palette=Viridis256, low=0, high=100)

# Create figure with categorical axes
records.append(
{
"month": month,
"city": city,
"anomaly": val,
"label": f"{val:+.1f}",
"text_color": "white" if abs(val) > 1.2 else "#333333",
}
)

source = ColumnDataSource(pd.DataFrame(records))
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should be simplified to source = ColumnDataSource(data=records) without wrapping records in pd.DataFrame. This makes the code simpler and removes the unnecessary pandas dependency.

Copilot uses AI. Check for mistakes.

# Color mapping — diverging palette for positive/negative anomalies
blues = ["#2166ac", "#4393c3", "#92c5de", "#d1e5f0"]
reds = ["#fddbc7", "#f4a582", "#d6604d", "#b2182b"]
diverging_palette = blues[::-1] + ["#f7f7f7"] + reds

# Create figure
p = figure(
width=4800,
height=2700,
x_range=x_labels,
y_range=y_labels,
x_range=months,
y_range=list(reversed(cities)),
title="heatmap-basic · bokeh · pyplots.ai",
x_axis_label="Month",
y_axis_label="Product",
x_axis_label="Month (2024)",
y_axis_label="City",
toolbar_location=None,
tools="",
)

# Plot heatmap rectangles
p.rect(
x="x",
y="y",
width=1,
height=1,
source=source,
fill_color={"field": "value", "transform": color_mapper},
line_color=None,
)
# Plot heatmap rectangles with linear_cmap
cmap = linear_cmap("anomaly", diverging_palette, low=-2.5, high=2.5)
r = p.rect(x="month", y="city", width=1, height=1, source=source, fill_color=cmap, line_color="white", line_width=2)

# Add value annotations in cells
# Add value annotations
labels = LabelSet(
x="x",
y="y",
text="text",
x="month",
y="city",
text="label",
text_color="text_color",
source=source,
text_align="center",
text_baseline="middle",
text_font_size="24pt",
text_font_size="22pt",
)
p.add_layout(labels)

# Add color bar
color_bar = ColorBar(
color_mapper=color_mapper,
# Color bar from renderer (idiomatic Bokeh pattern)
color_bar = r.construct_color_bar(
width=40,
ticker=BasicTicker(desired_num_ticks=10),
label_standoff=16,
major_label_text_font_size="18pt",
border_line_color=None,
location=(0, 0),
width=40,
title="Sales Score",
padding=10,
title="Anomaly (°C)",
title_text_font_size="20pt",
title_standoff=20,
)
p.add_layout(color_bar, "right")

# HoverTool for interactive HTML version
hover = HoverTool(tooltips=[("City", "@city"), ("Month", "@month"), ("Anomaly", "@anomaly{+0.0} °C")], renderers=[r])
p.add_tools(hover)

# Styling for 4800x2700 px
p.title.text_font_size = "28pt"
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"

# Grid styling - disabled for heatmap
# Grid and axes
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None

# Axis styling
p.axis.axis_line_color = "#cccccc"
p.axis.major_tick_line_color = "#cccccc"
p.axis.axis_line_color = None
p.axis.major_tick_line_color = None
p.outline_line_color = None

# Background
p.background_fill_color = "#f8f8f8"
p.min_border_right = 120
p.background_fill_color = "#fafafa"
p.border_fill_color = "white"
p.outline_line_color = None

# Save PNG
export_png(p, filename="plot.png")

# Save HTML for interactive version
# Save HTML with interactive hover
save(p, filename="plot.html", resources=CDN, title="heatmap-basic · bokeh · pyplots.ai")
Loading