|
1 | 1 | """ pyplots.ai |
2 | 2 | heatmap-basic: Basic Heatmap |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.8.2 | Python 3.14.3 |
| 4 | +Quality: 91/100 | Updated: 2026-02-15 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
| 8 | +import pandas as pd |
8 | 9 | from bokeh.io import export_png, save |
9 | | -from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LabelSet, LinearColorMapper |
10 | | -from bokeh.palettes import Viridis256 |
| 10 | +from bokeh.models import BasicTicker, ColumnDataSource, HoverTool, LabelSet |
11 | 11 | from bokeh.plotting import figure |
12 | 12 | from bokeh.resources import CDN |
| 13 | +from bokeh.transform import linear_cmap |
13 | 14 |
|
14 | 15 |
|
15 | | -# Data - Monthly sales performance by product category |
| 16 | +# Data - Monthly temperature anomalies (°C) by city |
16 | 17 | np.random.seed(42) |
17 | | -x_labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"] |
18 | | -y_labels = ["Product A", "Product B", "Product C", "Product D", "Product E", "Product F"] |
19 | | - |
20 | | -# Generate heatmap values (sales performance 0-100) |
21 | | -values = np.random.rand(len(y_labels), len(x_labels)) * 100 |
22 | | - |
23 | | -# Flatten data for ColumnDataSource |
24 | | -x_data = [] |
25 | | -y_data = [] |
26 | | -value_data = [] |
27 | | -text_data = [] |
28 | | -text_color_data = [] |
29 | | - |
30 | | -for i, y in enumerate(y_labels): |
31 | | - for j, x in enumerate(x_labels): |
32 | | - x_data.append(x) |
33 | | - y_data.append(y) |
| 18 | +months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"] |
| 19 | +cities = ["Oslo", "Berlin", "Madrid", "Cairo", "Mumbai", "Tokyo", "Sydney"] |
| 20 | + |
| 21 | +# Generate realistic temperature anomalies with geographic patterns |
| 22 | +base_anomalies = np.random.randn(len(cities), len(months)) * 0.6 |
| 23 | +# Northern cities: colder winters, warmer summers |
| 24 | +for i, city in enumerate(cities): |
| 25 | + seasonal = np.sin(np.linspace(-np.pi / 2, 3 * np.pi / 4, len(months))) |
| 26 | + if city in ("Oslo", "Berlin"): |
| 27 | + base_anomalies[i] += seasonal * 1.5 - 0.3 |
| 28 | + elif city in ("Madrid", "Cairo"): |
| 29 | + base_anomalies[i] += seasonal * 1.2 + 0.4 |
| 30 | + elif city == "Mumbai": |
| 31 | + base_anomalies[i] += 0.8 |
| 32 | + elif city == "Sydney": |
| 33 | + base_anomalies[i] -= seasonal * 0.9 |
| 34 | + elif city == "Tokyo": |
| 35 | + base_anomalies[i] += seasonal * 0.7 |
| 36 | + |
| 37 | +values = np.round(base_anomalies, 1) |
| 38 | + |
| 39 | +# Flatten to DataFrame for ColumnDataSource |
| 40 | +records = [] |
| 41 | +for i, city in enumerate(cities): |
| 42 | + for j, month in enumerate(months): |
34 | 43 | val = values[i, j] |
35 | | - value_data.append(val) |
36 | | - text_data.append(f"{val:.0f}") |
37 | | - # Use white text on dark cells, black on light cells |
38 | | - text_color_data.append("white" if val > 50 else "black") |
39 | | - |
40 | | -source = ColumnDataSource( |
41 | | - data={"x": x_data, "y": y_data, "value": value_data, "text": text_data, "text_color": text_color_data} |
42 | | -) |
43 | | - |
44 | | -# Color mapper |
45 | | -color_mapper = LinearColorMapper(palette=Viridis256, low=0, high=100) |
46 | | - |
47 | | -# Create figure with categorical axes |
| 44 | + records.append( |
| 45 | + { |
| 46 | + "month": month, |
| 47 | + "city": city, |
| 48 | + "anomaly": val, |
| 49 | + "label": f"{val:+.1f}", |
| 50 | + "text_color": "white" if abs(val) > 1.2 else "#333333", |
| 51 | + } |
| 52 | + ) |
| 53 | + |
| 54 | +source = ColumnDataSource(pd.DataFrame(records)) |
| 55 | + |
| 56 | +# Color mapping — diverging palette for positive/negative anomalies |
| 57 | +blues = ["#2166ac", "#4393c3", "#92c5de", "#d1e5f0"] |
| 58 | +reds = ["#fddbc7", "#f4a582", "#d6604d", "#b2182b"] |
| 59 | +diverging_palette = blues[::-1] + ["#f7f7f7"] + reds |
| 60 | + |
| 61 | +# Create figure |
48 | 62 | p = figure( |
49 | 63 | width=4800, |
50 | 64 | height=2700, |
51 | | - x_range=x_labels, |
52 | | - y_range=y_labels, |
| 65 | + x_range=months, |
| 66 | + y_range=list(reversed(cities)), |
53 | 67 | title="heatmap-basic · bokeh · pyplots.ai", |
54 | | - x_axis_label="Month", |
55 | | - y_axis_label="Product", |
| 68 | + x_axis_label="Month (2024)", |
| 69 | + y_axis_label="City", |
56 | 70 | toolbar_location=None, |
57 | 71 | tools="", |
58 | 72 | ) |
59 | 73 |
|
60 | | -# Plot heatmap rectangles |
61 | | -p.rect( |
62 | | - x="x", |
63 | | - y="y", |
64 | | - width=1, |
65 | | - height=1, |
66 | | - source=source, |
67 | | - fill_color={"field": "value", "transform": color_mapper}, |
68 | | - line_color=None, |
69 | | -) |
| 74 | +# Plot heatmap rectangles with linear_cmap |
| 75 | +cmap = linear_cmap("anomaly", diverging_palette, low=-2.5, high=2.5) |
| 76 | +r = p.rect(x="month", y="city", width=1, height=1, source=source, fill_color=cmap, line_color="white", line_width=2) |
70 | 77 |
|
71 | | -# Add value annotations in cells |
| 78 | +# Add value annotations |
72 | 79 | labels = LabelSet( |
73 | | - x="x", |
74 | | - y="y", |
75 | | - text="text", |
| 80 | + x="month", |
| 81 | + y="city", |
| 82 | + text="label", |
76 | 83 | text_color="text_color", |
77 | 84 | source=source, |
78 | 85 | text_align="center", |
79 | 86 | text_baseline="middle", |
80 | | - text_font_size="24pt", |
| 87 | + text_font_size="22pt", |
81 | 88 | ) |
82 | 89 | p.add_layout(labels) |
83 | 90 |
|
84 | | -# Add color bar |
85 | | -color_bar = ColorBar( |
86 | | - color_mapper=color_mapper, |
| 91 | +# Color bar from renderer (idiomatic Bokeh pattern) |
| 92 | +color_bar = r.construct_color_bar( |
| 93 | + width=40, |
87 | 94 | ticker=BasicTicker(desired_num_ticks=10), |
88 | 95 | label_standoff=16, |
89 | 96 | major_label_text_font_size="18pt", |
90 | 97 | border_line_color=None, |
91 | | - location=(0, 0), |
92 | | - width=40, |
93 | | - title="Sales Score", |
| 98 | + padding=10, |
| 99 | + title="Anomaly (°C)", |
94 | 100 | title_text_font_size="20pt", |
| 101 | + title_standoff=20, |
95 | 102 | ) |
96 | 103 | p.add_layout(color_bar, "right") |
97 | 104 |
|
| 105 | +# HoverTool for interactive HTML version |
| 106 | +hover = HoverTool(tooltips=[("City", "@city"), ("Month", "@month"), ("Anomaly", "@anomaly{+0.0} °C")], renderers=[r]) |
| 107 | +p.add_tools(hover) |
| 108 | + |
98 | 109 | # Styling for 4800x2700 px |
99 | 110 | p.title.text_font_size = "28pt" |
100 | 111 | p.xaxis.axis_label_text_font_size = "22pt" |
101 | 112 | p.yaxis.axis_label_text_font_size = "22pt" |
102 | 113 | p.xaxis.major_label_text_font_size = "18pt" |
103 | 114 | p.yaxis.major_label_text_font_size = "18pt" |
104 | 115 |
|
105 | | -# Grid styling - disabled for heatmap |
| 116 | +# Grid and axes |
106 | 117 | p.xgrid.grid_line_color = None |
107 | 118 | p.ygrid.grid_line_color = None |
108 | | - |
109 | | -# Axis styling |
110 | | -p.axis.axis_line_color = "#cccccc" |
111 | | -p.axis.major_tick_line_color = "#cccccc" |
| 119 | +p.axis.axis_line_color = None |
| 120 | +p.axis.major_tick_line_color = None |
| 121 | +p.outline_line_color = None |
112 | 122 |
|
113 | 123 | # Background |
114 | | -p.background_fill_color = "#f8f8f8" |
| 124 | +p.min_border_right = 120 |
| 125 | +p.background_fill_color = "#fafafa" |
115 | 126 | p.border_fill_color = "white" |
116 | | -p.outline_line_color = None |
117 | 127 |
|
118 | 128 | # Save PNG |
119 | 129 | export_png(p, filename="plot.png") |
120 | 130 |
|
121 | | -# Save HTML for interactive version |
| 131 | +# Save HTML with interactive hover |
122 | 132 | save(p, filename="plot.html", resources=CDN, title="heatmap-basic · bokeh · pyplots.ai") |
0 commit comments