Skip to content

Commit 42ad90a

Browse files
feat(bokeh): implement dumbbell-basic (#5418)
## Implementation: `dumbbell-basic` - python/bokeh Implements the **python/bokeh** version of `dumbbell-basic`. **File:** `plots/dumbbell-basic/implementations/python/bokeh.py` **Parent Issue:** #945 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24945052285)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent f47c454 commit 42ad90a

2 files changed

Lines changed: 276 additions & 176 deletions

File tree

Lines changed: 112 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
dumbbell-basic: Basic Dumbbell Chart
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.14.4
4+
Quality: 88/100 | Updated: 2026-04-26
55
"""
66

7+
import os
8+
79
from bokeh.io import export_png, output_file, save
8-
from bokeh.models import ColumnDataSource
10+
from bokeh.models import ColumnDataSource, HoverTool
911
from bokeh.plotting import figure
1012

1113

12-
# Data - Employee satisfaction scores before and after policy changes
14+
# Theme tokens
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
BRAND = "#009E73" # Okabe-Ito 1 — "Before"
21+
ACCENT = "#D55E00" # Okabe-Ito 2 — "After"
22+
23+
# Data — Employee satisfaction scores before and after policy changes
24+
# (one department regressed, the rest improved by varying amounts)
1325
categories = [
1426
"Engineering",
1527
"Marketing",
@@ -20,63 +32,116 @@
2032
"Operations",
2133
"Research & Development",
2234
]
23-
start_values = [62, 58, 71, 55, 68, 72, 60, 65] # Before policy changes
24-
end_values = [78, 74, 82, 70, 85, 80, 73, 88] # After policy changes
35+
start_values = [62, 58, 71, 55, 68, 78, 60, 65]
36+
end_values = [78, 74, 82, 70, 85, 72, 73, 88]
2537

26-
# Sort by difference for better visualization
27-
differences = [e - s for s, e in zip(start_values, end_values, strict=True)]
28-
sorted_data = sorted(zip(categories, start_values, end_values, differences, strict=True), key=lambda x: x[3])
29-
categories = [d[0] for d in sorted_data]
30-
start_values = [d[1] for d in sorted_data]
31-
end_values = [d[2] for d in sorted_data]
38+
# Sort ascending by change so the largest improvements sit at the top
39+
deltas = [e - s for s, e in zip(start_values, end_values, strict=True)]
40+
ordered = sorted(zip(categories, start_values, end_values, deltas, strict=True), key=lambda d: d[3])
41+
categories = [d[0] for d in ordered]
42+
start_values = [d[1] for d in ordered]
43+
end_values = [d[2] for d in ordered]
44+
deltas = [d[3] for d in ordered]
3245

33-
# Create figure with categorical y-axis (horizontal orientation)
46+
# Plot
3447
p = figure(
3548
width=4800,
3649
height=2700,
3750
y_range=categories,
38-
title="dumbbell-basic · bokeh · pyplots.ai",
51+
x_range=(45, 95),
52+
title="Employee Satisfaction · dumbbell-basic · bokeh · anyplot.ai",
3953
x_axis_label="Satisfaction Score",
4054
y_axis_label="Department",
55+
background_fill_color=PAGE_BG,
56+
border_fill_color=PAGE_BG,
57+
toolbar_location=None,
4158
)
4259

43-
# Create connecting lines (thin and subtle)
44-
for i, cat in enumerate(categories):
45-
p.line(x=[start_values[i], end_values[i]], y=[cat, cat], line_width=4, line_color="#888888", line_alpha=0.6)
60+
# Connecting segments — thin, subtle, behind the dots
61+
seg_source = ColumnDataSource(
62+
data={"y": categories, "x_start": start_values, "x_end": end_values, "delta": [f"{d:+d}" for d in deltas]}
63+
)
64+
p.segment(
65+
x0="x_start", x1="x_end", y0="y", y1="y", source=seg_source, line_color=INK_SOFT, line_alpha=0.45, line_width=4
66+
)
4667

47-
# Data sources for dots
48-
source_start = ColumnDataSource(data={"x": start_values, "y": categories, "label": ["Before"] * len(categories)})
49-
source_end = ColumnDataSource(data={"x": end_values, "y": categories, "label": ["After"] * len(categories)})
68+
# "Before" dots — Okabe-Ito brand green
69+
before_source = ColumnDataSource(data={"x": start_values, "y": categories, "phase": ["Before"] * len(categories)})
70+
before_glyph = p.scatter(
71+
x="x",
72+
y="y",
73+
source=before_source,
74+
size=34,
75+
fill_color=BRAND,
76+
line_color=PAGE_BG,
77+
line_width=2,
78+
legend_label="Before policy changes",
79+
)
5080

51-
# Plot start dots (Before - Python Blue)
52-
p.scatter(x="x", y="y", source=source_start, size=25, color="#306998", alpha=0.9, legend_label="Before Policy Changes")
81+
# "After" dots — Okabe-Ito vermillion
82+
after_source = ColumnDataSource(data={"x": end_values, "y": categories, "phase": ["After"] * len(categories)})
83+
after_glyph = p.scatter(
84+
x="x",
85+
y="y",
86+
source=after_source,
87+
size=34,
88+
fill_color=ACCENT,
89+
line_color=PAGE_BG,
90+
line_width=2,
91+
legend_label="After policy changes",
92+
)
5393

54-
# Plot end dots (After - Python Yellow)
55-
p.scatter(x="x", y="y", source=source_end, size=25, color="#FFD43B", alpha=0.9, legend_label="After Policy Changes")
94+
# Hover tooltip (HTML interactivity)
95+
p.add_tools(
96+
HoverTool(
97+
renderers=[before_glyph, after_glyph], tooltips=[("Department", "@y"), ("Phase", "@phase"), ("Score", "@x")]
98+
)
99+
)
56100

57-
# Styling for 4800x2700 px
101+
# Typography
58102
p.title.text_font_size = "36pt"
103+
p.title.text_color = INK
104+
p.title.text_font_style = "normal"
105+
p.title.align = "center"
106+
59107
p.xaxis.axis_label_text_font_size = "24pt"
60108
p.yaxis.axis_label_text_font_size = "24pt"
61-
p.xaxis.major_label_text_font_size = "18pt"
62-
p.yaxis.major_label_text_font_size = "18pt"
63-
64-
# Grid styling (subtle)
65-
p.xgrid.grid_line_alpha = 0.3
66-
p.xgrid.grid_line_dash = "dashed"
67-
p.ygrid.grid_line_alpha = 0.3
68-
p.ygrid.grid_line_dash = "dashed"
69-
70-
# Legend styling
71-
p.legend.label_text_font_size = "18pt"
72-
p.legend.location = "bottom_right"
73-
p.legend.background_fill_alpha = 0.7
74-
75-
# Set x-axis range with padding
76-
p.x_range.start = 45
77-
p.x_range.end = 95
78-
79-
# Save outputs
80-
export_png(p, filename="plot.png")
81-
output_file("plot.html")
109+
p.xaxis.major_label_text_font_size = "20pt"
110+
p.yaxis.major_label_text_font_size = "20pt"
111+
p.xaxis.axis_label_text_color = INK
112+
p.yaxis.axis_label_text_color = INK
113+
p.xaxis.major_label_text_color = INK_SOFT
114+
p.yaxis.major_label_text_color = INK_SOFT
115+
p.xaxis.axis_label_standoff = 18
116+
p.yaxis.axis_label_standoff = 18
117+
118+
# Spines and ticks — keep an L-shape, suppress chart outline
119+
p.outline_line_color = None
120+
p.xaxis.axis_line_color = INK_SOFT
121+
p.yaxis.axis_line_color = INK_SOFT
122+
p.xaxis.major_tick_line_color = INK_SOFT
123+
p.yaxis.major_tick_line_color = INK_SOFT
124+
p.xaxis.minor_tick_line_color = None
125+
p.yaxis.minor_tick_line_color = None
126+
127+
# Grid — subtle vertical guides only
128+
p.xgrid.grid_line_color = INK
129+
p.xgrid.grid_line_alpha = 0.10
130+
p.ygrid.grid_line_color = None
131+
132+
# Legend — placed inside top-left so it never collides with the data range
133+
p.legend.location = "top_left"
134+
p.legend.background_fill_color = ELEVATED_BG
135+
p.legend.background_fill_alpha = 0.95
136+
p.legend.border_line_color = INK_SOFT
137+
p.legend.border_line_alpha = 0.4
138+
p.legend.label_text_color = INK_SOFT
139+
p.legend.label_text_font_size = "20pt"
140+
p.legend.spacing = 10
141+
p.legend.padding = 18
142+
p.legend.margin = 24
143+
144+
# Save
145+
export_png(p, filename=f"plot-{THEME}.png")
146+
output_file(f"plot-{THEME}.html", title="Employee Satisfaction · dumbbell-basic · bokeh · anyplot.ai")
82147
save(p)

0 commit comments

Comments
 (0)