Skip to content

Commit 595e04d

Browse files
feat(bokeh): implement bullet-basic (#1040)
## Implementation: `bullet-basic` - bokeh Implements the **bokeh** version of `bullet-basic`. **File:** `plots/bullet-basic/implementations/bokeh.py` **Parent Issue:** #999 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20257947614)* --------- 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 5425a66 commit 595e04d

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
bullet-basic: Basic Bullet Chart
3+
Library: bokeh
4+
"""
5+
6+
from bokeh.io import export_png, output_file, save
7+
from bokeh.models import Label
8+
from bokeh.plotting import figure
9+
10+
11+
# Data - Sales performance metrics with targets
12+
metrics = [
13+
{"label": "Revenue", "actual": 275, "target": 250, "ranges": [150, 225, 300]},
14+
{"label": "Profit", "actual": 85, "target": 100, "ranges": [50, 75, 100]},
15+
{"label": "Orders", "actual": 320, "target": 350, "ranges": [200, 300, 400]},
16+
{"label": "Customers", "actual": 1450, "target": 1200, "ranges": [800, 1100, 1500]},
17+
{"label": "Satisfaction", "actual": 4.2, "target": 4.5, "ranges": [3.0, 4.0, 5.0]},
18+
]
19+
20+
# Configuration
21+
num_metrics = len(metrics)
22+
bar_spacing = 1.5 # Space between each bullet row
23+
bar_height = 0.8 # Maximum height of range bars
24+
25+
# Qualitative range colors (grayscale: poor, satisfactory, good - light to dark)
26+
range_colors = ["#dddddd", "#aaaaaa", "#777777"]
27+
28+
# Create figure
29+
p = figure(
30+
width=4800,
31+
height=2700,
32+
x_range=(0, 110),
33+
y_range=(-0.5, num_metrics * bar_spacing),
34+
title="bullet-basic · bokeh · pyplots.ai",
35+
x_axis_label="% of Target Range",
36+
toolbar_location=None,
37+
)
38+
39+
# Remove y-axis ticks and gridlines
40+
p.yaxis.visible = False
41+
p.ygrid.grid_line_color = None
42+
p.xgrid.grid_line_alpha = 0.3
43+
p.xgrid.grid_line_dash = "dashed"
44+
45+
# Draw bullets for each metric
46+
for i, metric in enumerate(metrics):
47+
y_pos = (num_metrics - 1 - i) * bar_spacing # Reverse so first is at top
48+
actual = metric["actual"]
49+
target = metric["target"]
50+
ranges = metric["ranges"]
51+
max_range = ranges[-1]
52+
53+
# Normalize values to percentage of max range
54+
norm_actual = (actual / max_range) * 100
55+
norm_target = (target / max_range) * 100
56+
norm_ranges = [(r / max_range) * 100 for r in ranges]
57+
58+
# Draw qualitative ranges (background bands) - from outer to inner
59+
for j in range(len(norm_ranges) - 1, -1, -1):
60+
range_width = norm_ranges[j]
61+
height_factor = 1 - j * 0.25 # Decrease height for inner ranges
62+
h = bar_height * height_factor
63+
p.rect(x=range_width / 2, y=y_pos, width=range_width, height=h, color=range_colors[j], line_color=None)
64+
65+
# Draw actual value bar (primary measure)
66+
actual_bar_height = bar_height * 0.35
67+
p.rect(x=norm_actual / 2, y=y_pos, width=norm_actual, height=actual_bar_height, color="#306998", line_color=None)
68+
69+
# Draw target marker (thin black vertical line)
70+
target_marker_height = bar_height * 0.6
71+
p.rect(x=norm_target, y=y_pos, width=0.8, height=target_marker_height, color="#1a1a1a", line_color=None)
72+
73+
# Add metric label on the left
74+
label = Label(
75+
x=-2,
76+
y=y_pos,
77+
text=metric["label"],
78+
text_font_size="22pt",
79+
text_color="#333333",
80+
text_align="right",
81+
text_baseline="middle",
82+
text_font_style="bold",
83+
)
84+
p.add_layout(label)
85+
86+
# Add actual value text label on the right
87+
value_label = Label(
88+
x=norm_actual + 3,
89+
y=y_pos,
90+
text=str(metric["actual"]),
91+
text_font_size="18pt",
92+
text_color="#306998",
93+
text_align="left",
94+
text_baseline="middle",
95+
text_font_style="bold",
96+
)
97+
p.add_layout(value_label)
98+
99+
# Extend x_range to accommodate labels (but clip axis display)
100+
p.x_range.start = -35
101+
p.x_range.end = 115
102+
103+
# Styling
104+
p.title.text_font_size = "36pt"
105+
p.title.text_color = "#333333"
106+
p.title.align = "center"
107+
p.xaxis.axis_label_text_font_size = "24pt"
108+
p.xaxis.major_label_text_font_size = "18pt"
109+
110+
# Only show positive tick marks on x-axis
111+
p.xaxis.ticker = [0, 20, 40, 60, 80, 100]
112+
113+
# Axis styling
114+
p.xaxis.axis_line_color = "#666666"
115+
p.outline_line_color = None
116+
117+
# Save as PNG
118+
export_png(p, filename="plot.png")
119+
120+
# Save as HTML for interactivity
121+
output_file("plot.html")
122+
save(p)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Per-library metadata for bokeh implementation of bullet-basic
2+
# Auto-generated by impl-generate.yml
3+
4+
library: bokeh
5+
specification_id: bullet-basic
6+
7+
# Preview URLs (filled by workflow)
8+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/bokeh/plot.png
9+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/bokeh/plot_thumb.png
10+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/bokeh/plot.html
11+
12+
current:
13+
version: 0
14+
generated_at: 2025-12-16T05:52:35Z
15+
generated_by: claude-opus-4-5-20251101
16+
workflow_run: 20257947614
17+
issue: 999
18+
quality_score: 95
19+
# Version info (filled by workflow)
20+
python_version: "3.13.11"
21+
library_version: "unknown"
22+
23+
history: []

0 commit comments

Comments
 (0)