|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | ridgeline-basic: Basic Ridgeline Plot |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.9.0 | Python 3.13.13 |
| 4 | +Quality: 89/100 | Updated: 2026-04-30 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import numpy as np |
8 | 10 | from bokeh.io import export_png, output_file, save |
| 11 | +from bokeh.models import ColumnDataSource, FactorRange, HoverTool |
| 12 | +from bokeh.palettes import Viridis256 |
9 | 13 | from bokeh.plotting import figure |
10 | 14 |
|
11 | 15 |
|
| 16 | +# Theme tokens |
| 17 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 18 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 19 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 20 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 21 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 22 | + |
12 | 23 | # Data - Monthly temperature distributions |
13 | 24 | np.random.seed(42) |
14 | 25 |
|
|
18 | 29 | base_temps = [5, 7, 12, 16, 20, 24, 27, 26, 22, 16, 10, 6] |
19 | 30 | temp_data = {} |
20 | 31 | for i, month in enumerate(months): |
21 | | - temps = np.random.normal(base_temps[i], 3, 200) |
22 | | - temp_data[month] = temps |
| 32 | + temp_data[month] = np.random.normal(base_temps[i], 3, 200) |
23 | 33 |
|
24 | | -# Create plot (4800 × 2700 px) |
25 | | -p = figure( |
26 | | - width=4800, |
27 | | - height=2700, |
28 | | - title="ridgeline-basic · bokeh · pyplots.ai", |
29 | | - x_axis_label="Temperature (°C)", |
30 | | - y_axis_label="Month", |
31 | | - y_range=months[::-1], # Reverse to have January at top |
32 | | - toolbar_location=None, |
33 | | -) |
| 34 | +# CVD-safe sequential gradient via Viridis256 — mapped to mean temperature |
| 35 | +# (cold months get dark purple, warm months get yellow) |
| 36 | +min_t, max_t = min(base_temps), max(base_temps) |
| 37 | +colors_by_month = { |
| 38 | + month: Viridis256[int((base_temps[i] - min_t) / (max_t - min_t) * 255)] for i, month in enumerate(months) |
| 39 | +} |
34 | 40 |
|
35 | | -# Color gradient from blue (cold) to yellow/orange (warm) to blue again |
36 | | -colors = [ |
37 | | - "#306998", # Jan - cold blue |
38 | | - "#3A7CA5", # Feb |
39 | | - "#50A3C1", # Mar |
40 | | - "#6BBFCC", # Apr |
41 | | - "#8FD4B4", # May |
42 | | - "#C5E99B", # Jun |
43 | | - "#FFD43B", # Jul - warm yellow |
44 | | - "#FFAA33", # Aug - warm orange |
45 | | - "#E7A467", # Sep |
46 | | - "#DB8C7D", # Oct |
47 | | - "#5BB6CF", # Nov |
48 | | - "#306998", # Dec - cold blue |
49 | | -] |
50 | | - |
51 | | -# Spacing and overlap parameters |
52 | | -ridge_height = 0.65 # Height multiplier for each ridge |
| 41 | +# Ridge parameters — height 1.5 → ~50% overlap between adjacent bands (spec: 50-70%) |
| 42 | +ridge_height = 1.5 |
53 | 43 | x_grid = np.linspace(-5, 40, 300) |
54 | 44 |
|
55 | | -# Plot ridgelines (from bottom to top for proper overlapping) |
56 | | -for i, month in enumerate(reversed(months)): |
| 45 | +# Pre-compute all patch coordinates for ColumnDataSource |
| 46 | +all_xs = [] |
| 47 | +all_ys = [] |
| 48 | +all_colors = [] |
| 49 | +all_months_labels = [] |
| 50 | + |
| 51 | +for _i, month in enumerate(reversed(months)): |
57 | 52 | temps = temp_data[month] |
58 | 53 |
|
59 | | - # Compute KDE using Gaussian kernel (Silverman's rule for bandwidth) |
| 54 | + # Gaussian KDE — Silverman's bandwidth rule |
60 | 55 | n = len(temps) |
61 | 56 | std = np.std(temps) |
62 | 57 | iqr = np.percentile(temps, 75) - np.percentile(temps, 25) |
|
68 | 63 | density += np.exp(-0.5 * ((x_grid - xi) / bandwidth) ** 2) |
69 | 64 | density /= n * bandwidth * np.sqrt(2 * np.pi) |
70 | 65 |
|
71 | | - # Normalize density to fit within ridge height |
72 | 66 | density_normalized = density / density.max() * ridge_height |
73 | 67 |
|
74 | | - # Y position (reversed order so Jan is at top) |
75 | | - color_idx = len(months) - 1 - i # Original month index for color |
76 | | - |
77 | | - # Create patch coordinates |
78 | 68 | x_patch = np.concatenate([[x_grid[0]], x_grid, [x_grid[-1]]]) |
79 | 69 | y_patch_numeric = np.concatenate([[0], density_normalized, [0]]) |
| 70 | + # Categorical offset tuples: (month_label, float_offset) |
| 71 | + y_patches = [(month, float(v)) for v in y_patch_numeric] |
| 72 | + |
| 73 | + all_xs.append(list(x_patch)) |
| 74 | + all_ys.append(y_patches) |
| 75 | + all_colors.append(colors_by_month[month]) |
| 76 | + all_months_labels.append(month) |
| 77 | + |
| 78 | +source = ColumnDataSource(data={"xs": all_xs, "ys": all_ys, "color": all_colors, "month": all_months_labels}) |
| 79 | + |
| 80 | +# Plot (4800 × 2700 px) — FactorRange with top padding prevents Jan peak clipping |
| 81 | +p = figure( |
| 82 | + width=4800, |
| 83 | + height=2700, |
| 84 | + title="ridgeline-basic · bokeh · anyplot.ai", |
| 85 | + x_axis_label="Temperature (°C)", |
| 86 | + y_axis_label="Month", |
| 87 | + y_range=FactorRange(factors=months[::-1], range_padding=0.4), |
| 88 | + toolbar_location=None, |
| 89 | +) |
80 | 90 |
|
81 | | - # For categorical y-axis, use factor offsets |
82 | | - y_patches = [(month, float(y)) for y in y_patch_numeric] |
| 91 | +p.patches("xs", "ys", fill_color="color", fill_alpha=0.85, line_color=INK_SOFT, line_width=2, source=source) |
83 | 92 |
|
84 | | - # Fill with color and add outline |
85 | | - p.patch( |
86 | | - x=list(x_patch), y=y_patches, fill_color=colors[color_idx], fill_alpha=0.85, line_color="#333333", line_width=2 |
87 | | - ) |
| 93 | +# HoverTool — distinctive Bokeh interactivity |
| 94 | +hover = HoverTool(tooltips=[("Month", "@month")]) |
| 95 | +p.add_tools(hover) |
88 | 96 |
|
89 | | -# Style the plot |
| 97 | +# Style |
90 | 98 | p.title.text_font_size = "32pt" |
| 99 | +p.title.text_color = INK |
91 | 100 | p.xaxis.axis_label_text_font_size = "24pt" |
92 | 101 | p.yaxis.axis_label_text_font_size = "24pt" |
| 102 | +p.xaxis.axis_label_text_color = INK |
| 103 | +p.yaxis.axis_label_text_color = INK |
93 | 104 | p.xaxis.major_label_text_font_size = "18pt" |
94 | 105 | p.yaxis.major_label_text_font_size = "18pt" |
95 | | - |
96 | | -# Grid styling |
97 | | -p.xgrid.grid_line_alpha = 0.3 |
98 | | -p.xgrid.grid_line_dash = "dashed" |
99 | | -p.ygrid.grid_line_alpha = 0 |
100 | | - |
101 | | -# Axis styling |
| 106 | +p.xaxis.major_label_text_color = INK_SOFT |
| 107 | +p.yaxis.major_label_text_color = INK_SOFT |
| 108 | +p.xaxis.axis_line_color = INK_SOFT |
| 109 | +p.yaxis.axis_line_color = INK_SOFT |
| 110 | +p.xaxis.major_tick_line_color = INK_SOFT |
102 | 111 | p.xaxis.axis_line_width = 2 |
103 | 112 | p.yaxis.axis_line_width = 2 |
104 | 113 | p.xaxis.major_tick_line_width = 2 |
105 | 114 | p.yaxis.major_tick_line_width = 2 |
106 | 115 |
|
107 | | -# Set x-axis range to show all data |
108 | | -p.x_range.start = -5 |
109 | | -p.x_range.end = 40 |
| 116 | +# Grid |
| 117 | +p.xgrid.grid_line_color = INK |
| 118 | +p.xgrid.grid_line_alpha = 0.10 |
| 119 | +p.xgrid.grid_line_dash = "solid" |
| 120 | +p.ygrid.grid_line_color = INK |
| 121 | +p.ygrid.grid_line_alpha = 0.05 |
110 | 122 |
|
111 | | -# Remove y-axis ticks (we have categorical labels) |
| 123 | +# Remove y-axis tick marks |
112 | 124 | p.yaxis.major_tick_line_color = None |
113 | 125 | p.yaxis.minor_tick_line_color = None |
114 | 126 |
|
115 | | -# Background |
116 | | -p.background_fill_color = "#FAFAFA" |
| 127 | +# Set x-axis range |
| 128 | +p.x_range.start = -5 |
| 129 | +p.x_range.end = 40 |
| 130 | + |
| 131 | +# Background — remove four-sided outline box for cleaner L-frame look |
| 132 | +p.background_fill_color = PAGE_BG |
| 133 | +p.border_fill_color = PAGE_BG |
| 134 | +p.outline_line_color = None |
117 | 135 |
|
118 | | -# Save outputs |
119 | | -output_file("plot.html") |
| 136 | +# Save |
| 137 | +export_png(p, filename=f"plot-{THEME}.png") |
| 138 | +output_file(f"plot-{THEME}.html") |
120 | 139 | save(p) |
121 | | -export_png(p, filename="plot.png") |
|
0 commit comments