Skip to content

Commit 1b50dd3

Browse files
github-actions[bot]claude[bot]MarkusNeusinger
authored
feat(bokeh): implement ridgeline-basic (#5577)
## Implementation: `ridgeline-basic` - python/bokeh Implements the **python/bokeh** version of `ridgeline-basic`. **File:** `plots/ridgeline-basic/implementations/python/bokeh.py` **Parent Issue:** #923 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25145277876)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 80b61c2 commit 1b50dd3

2 files changed

Lines changed: 250 additions & 197 deletions

File tree

plots/ridgeline-basic/implementations/python/bokeh.py

Lines changed: 82 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import os
8+
79
import numpy as np
810
from bokeh.io import export_png, output_file, save
11+
from bokeh.models import ColumnDataSource, FactorRange, HoverTool
12+
from bokeh.palettes import Viridis256
913
from bokeh.plotting import figure
1014

1115

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+
1223
# Data - Monthly temperature distributions
1324
np.random.seed(42)
1425

@@ -18,45 +29,29 @@
1829
base_temps = [5, 7, 12, 16, 20, 24, 27, 26, 22, 16, 10, 6]
1930
temp_data = {}
2031
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)
2333

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+
}
3440

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
5343
x_grid = np.linspace(-5, 40, 300)
5444

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)):
5752
temps = temp_data[month]
5853

59-
# Compute KDE using Gaussian kernel (Silverman's rule for bandwidth)
54+
# Gaussian KDE Silverman's bandwidth rule
6055
n = len(temps)
6156
std = np.std(temps)
6257
iqr = np.percentile(temps, 75) - np.percentile(temps, 25)
@@ -68,54 +63,77 @@
6863
density += np.exp(-0.5 * ((x_grid - xi) / bandwidth) ** 2)
6964
density /= n * bandwidth * np.sqrt(2 * np.pi)
7065

71-
# Normalize density to fit within ridge height
7266
density_normalized = density / density.max() * ridge_height
7367

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
7868
x_patch = np.concatenate([[x_grid[0]], x_grid, [x_grid[-1]]])
7969
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+
)
8090

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)
8392

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)
8896

89-
# Style the plot
97+
# Style
9098
p.title.text_font_size = "32pt"
99+
p.title.text_color = INK
91100
p.xaxis.axis_label_text_font_size = "24pt"
92101
p.yaxis.axis_label_text_font_size = "24pt"
102+
p.xaxis.axis_label_text_color = INK
103+
p.yaxis.axis_label_text_color = INK
93104
p.xaxis.major_label_text_font_size = "18pt"
94105
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
102111
p.xaxis.axis_line_width = 2
103112
p.yaxis.axis_line_width = 2
104113
p.xaxis.major_tick_line_width = 2
105114
p.yaxis.major_tick_line_width = 2
106115

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
110122

111-
# Remove y-axis ticks (we have categorical labels)
123+
# Remove y-axis tick marks
112124
p.yaxis.major_tick_line_color = None
113125
p.yaxis.minor_tick_line_color = None
114126

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
117135

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")
120139
save(p)
121-
export_png(p, filename="plot.png")

0 commit comments

Comments
 (0)