Skip to content

Commit 77c4dc0

Browse files
feat(plotnine): implement bullet-basic (#1037)
## Implementation: `bullet-basic` - plotnine Implements the **plotnine** version of `bullet-basic`. **File:** `plots/bullet-basic/implementations/plotnine.py` **Parent Issue:** #999 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20257947684)* --------- 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 9cad6c7 commit 77c4dc0

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
bullet-basic: Basic Bullet Chart
3+
Library: plotnine
4+
"""
5+
6+
import pandas as pd
7+
from plotnine import (
8+
aes,
9+
coord_flip,
10+
element_blank,
11+
element_text,
12+
geom_rect,
13+
geom_segment,
14+
ggplot,
15+
labs,
16+
scale_fill_manual,
17+
scale_y_continuous,
18+
theme,
19+
theme_minimal,
20+
)
21+
22+
23+
# Data - Multiple KPIs with different performance levels
24+
metrics = [
25+
{"label": "Revenue", "actual": 275, "target": 250, "ranges": [150, 225, 300]},
26+
{"label": "Profit", "actual": 22, "target": 26, "ranges": [15, 22.5, 30]},
27+
{"label": "Orders", "actual": 1050, "target": 1100, "ranges": [600, 900, 1200]},
28+
{"label": "Satisfaction", "actual": 4.5, "target": 4.2, "ranges": [2.5, 3.5, 5.0]},
29+
]
30+
31+
# Build data for the plot - normalize all values to 0-100 scale for consistent display
32+
range_data = []
33+
actual_data = []
34+
target_data = []
35+
label_data = []
36+
37+
for i, m in enumerate(metrics):
38+
y_pos = i
39+
max_val = m["ranges"][-1]
40+
41+
# Qualitative ranges (poor, satisfactory, good)
42+
range_data.append({"y": y_pos, "xmin": 0, "xmax": (m["ranges"][0] / max_val) * 100, "band": "Poor"})
43+
range_data.append(
44+
{
45+
"y": y_pos,
46+
"xmin": (m["ranges"][0] / max_val) * 100,
47+
"xmax": (m["ranges"][1] / max_val) * 100,
48+
"band": "Satisfactory",
49+
}
50+
)
51+
range_data.append({"y": y_pos, "xmin": (m["ranges"][1] / max_val) * 100, "xmax": 100, "band": "Good"})
52+
53+
# Actual value bar
54+
actual_pct = (m["actual"] / max_val) * 100
55+
actual_data.append({"y": y_pos, "xmin": 0, "xmax": actual_pct})
56+
57+
# Target marker
58+
target_pct = (m["target"] / max_val) * 100
59+
target_data.append({"y": y_pos, "target": target_pct})
60+
61+
# Labels with actual values
62+
label_data.append({"y": y_pos, "label": m["label"], "actual": m["actual"]})
63+
64+
df_ranges = pd.DataFrame(range_data)
65+
df_actual = pd.DataFrame(actual_data)
66+
df_target = pd.DataFrame(target_data)
67+
df_labels = pd.DataFrame(label_data)
68+
69+
# Grayscale colors for qualitative bands (poor=dark, satisfactory=medium, good=light)
70+
band_colors = {"Poor": "#D0D0D0", "Satisfactory": "#A0A0A0", "Good": "#707070"}
71+
72+
# Bar height parameters
73+
range_height = 0.7
74+
actual_height = 0.3
75+
76+
# Plot
77+
plot = (
78+
ggplot()
79+
# Background qualitative ranges
80+
+ geom_rect(
81+
df_ranges, aes(xmin="xmin", xmax="xmax", ymin="y - range_height/2", ymax="y + range_height/2", fill="band")
82+
)
83+
+ scale_fill_manual(values=band_colors, limits=["Good", "Satisfactory", "Poor"], guide=None)
84+
# Actual value bar (Python Blue)
85+
+ geom_rect(
86+
df_actual, aes(xmin="xmin", xmax="xmax", ymin="y - actual_height/2", ymax="y + actual_height/2"), fill="#306998"
87+
)
88+
# Target marker (thin black line)
89+
+ geom_segment(
90+
df_target,
91+
aes(x="target", xend="target", y="y - range_height/2.5", yend="y + range_height/2.5"),
92+
color="black",
93+
size=2.5,
94+
)
95+
# Flip coordinates for horizontal bullet charts
96+
+ coord_flip()
97+
# Scale and labels
98+
+ scale_y_continuous(breaks=list(range(len(metrics))), labels=[m["label"] for m in metrics], expand=(0.1, 0.1))
99+
+ labs(title="bullet-basic · plotnine · pyplots.ai", x="", y="Performance (%)")
100+
# Theme
101+
+ theme_minimal()
102+
+ theme(
103+
figure_size=(16, 9),
104+
plot_title=element_text(size=24, ha="center"),
105+
axis_title_x=element_text(size=20),
106+
axis_title_y=element_blank(),
107+
axis_text_x=element_text(size=16),
108+
axis_text_y=element_text(size=18),
109+
panel_grid_major_y=element_blank(),
110+
panel_grid_minor=element_blank(),
111+
legend_position="none",
112+
)
113+
)
114+
115+
# Save
116+
plot.save("plot.png", dpi=300, verbose=False)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Per-library metadata for plotnine implementation of bullet-basic
2+
# Auto-generated by impl-generate.yml
3+
4+
library: plotnine
5+
specification_id: bullet-basic
6+
7+
# Preview URLs (filled by workflow)
8+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/plotnine/plot.png
9+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/plotnine/plot_thumb.png
10+
preview_html: null
11+
12+
current:
13+
version: 0
14+
generated_at: 2025-12-16T05:51:53Z
15+
generated_by: claude-opus-4-5-20251101
16+
workflow_run: 20257947684
17+
issue: 999
18+
quality_score: 93
19+
# Version info (filled by workflow)
20+
python_version: "3.13.11"
21+
library_version: "unknown"
22+
23+
history: []

0 commit comments

Comments
 (0)