Skip to content

Commit 3248d37

Browse files
feat(letsplot): implement bullet-basic (#1043)
## Implementation: `bullet-basic` - letsplot Implements the **letsplot** version of `bullet-basic`. **File:** `plots/bullet-basic/implementations/letsplot.py` **Parent Issue:** #999 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20257947779)* --------- 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 c7b50ff commit 3248d37

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
bullet-basic: Basic Bullet Chart
3+
Library: letsplot
4+
"""
5+
# ruff: noqa: F405
6+
7+
import os
8+
import shutil
9+
10+
import pandas as pd
11+
from lets_plot import * # noqa: F403, F405
12+
13+
14+
LetsPlot.setup_html()
15+
16+
# Data - Multiple KPIs for a dashboard view
17+
metrics = ["Revenue", "Profit", "Customer Satisfaction", "Market Share"]
18+
actual = [275, 82, 4.2, 35]
19+
target = [300, 90, 4.5, 40]
20+
poor = [100, 40, 2.5, 15]
21+
satisfactory = [200, 70, 3.5, 30]
22+
good = [350, 100, 5.0, 50]
23+
24+
n_metrics = len(metrics)
25+
26+
# Normalize all values to percentage of maximum
27+
actual_pct = [actual[i] / good[i] * 100 for i in range(n_metrics)]
28+
target_pct = [target[i] / good[i] * 100 for i in range(n_metrics)]
29+
poor_pct = [poor[i] / good[i] * 100 for i in range(n_metrics)]
30+
satisfactory_pct = [satisfactory[i] / good[i] * 100 for i in range(n_metrics)]
31+
32+
# Create long-form dataframe for stacked ranges
33+
# Each row represents the width of one range segment
34+
range_data = []
35+
for i in range(n_metrics):
36+
range_data.append({"metric": metrics[i], "range_type": "1_Poor", "width": poor_pct[i]})
37+
range_data.append(
38+
{"metric": metrics[i], "range_type": "2_Satisfactory", "width": satisfactory_pct[i] - poor_pct[i]}
39+
)
40+
range_data.append({"metric": metrics[i], "range_type": "3_Good", "width": 100 - satisfactory_pct[i]})
41+
42+
range_df = pd.DataFrame(range_data)
43+
44+
# Actual values dataframe
45+
actual_df = pd.DataFrame({"metric": metrics, "actual": actual_pct})
46+
47+
# Target dataframe with numeric y positions for vertical segments
48+
# Use index-based positioning to create visible target markers
49+
target_df = pd.DataFrame(
50+
{
51+
"metric": metrics,
52+
"target": target_pct,
53+
"y_idx": list(range(n_metrics)), # Numeric index for each metric
54+
}
55+
)
56+
57+
# Create the plot using horizontal stacked bar for ranges
58+
plot = (
59+
ggplot()
60+
# Stacked bar for qualitative ranges (full width background)
61+
+ geom_bar(
62+
data=range_df,
63+
mapping=aes(x="width", y="metric", fill="range_type"),
64+
stat="identity",
65+
orientation="y",
66+
position="stack",
67+
width=0.7,
68+
)
69+
# Actual value bar (narrower overlay)
70+
+ geom_bar(
71+
data=actual_df,
72+
mapping=aes(x="actual", y="metric"),
73+
stat="identity",
74+
orientation="y",
75+
fill="#306998",
76+
color="#1e4461",
77+
width=0.4,
78+
size=0.5,
79+
)
80+
# Target marker as thin vertical line using geom_tile (narrow width acts as line)
81+
+ geom_tile(
82+
data=target_df,
83+
mapping=aes(x="target", y="metric"),
84+
fill="black",
85+
width=0.6, # Very narrow width creates thin vertical line
86+
height=0.55, # Slightly taller than actual bar for visibility
87+
)
88+
# Grayscale fills for ranges
89+
+ scale_fill_manual(
90+
values=["#555555", "#999999", "#CCCCCC"], name="Performance Range", labels=["Poor", "Satisfactory", "Good"]
91+
)
92+
# Axis labels
93+
+ scale_x_continuous(name="Performance (%)", limits=[0, 110])
94+
+ scale_y_discrete(limits=list(reversed(metrics)))
95+
+ labs(title="bullet-basic · letsplot · pyplots.ai", y="")
96+
+ theme_minimal()
97+
+ theme(
98+
plot_title=element_text(size=24),
99+
axis_title_x=element_text(size=20),
100+
axis_text_x=element_text(size=16),
101+
axis_text_y=element_text(size=18),
102+
legend_title=element_text(size=18),
103+
legend_text=element_text(size=16),
104+
legend_position="bottom",
105+
panel_grid_major_y=element_blank(),
106+
panel_grid_minor=element_blank(),
107+
panel_grid_major_x=element_line(size=0.5, color="#E0E0E0"), # Subtle x grid
108+
)
109+
+ ggsize(1600, 900)
110+
)
111+
112+
# Save as PNG (scaled 3x for 4800x2700)
113+
ggsave(plot, "plot.png", scale=3)
114+
115+
# Save as HTML for interactive viewing
116+
ggsave(plot, "plot.html")
117+
118+
# Move files from lets-plot-images subfolder to current directory
119+
if os.path.exists("lets-plot-images/plot.png"):
120+
shutil.move("lets-plot-images/plot.png", "plot.png")
121+
if os.path.exists("lets-plot-images/plot.html"):
122+
shutil.move("lets-plot-images/plot.html", "plot.html")
123+
if os.path.exists("lets-plot-images"):
124+
shutil.rmtree("lets-plot-images")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Per-library metadata for letsplot implementation of bullet-basic
2+
# Auto-generated by impl-generate.yml
3+
4+
library: letsplot
5+
specification_id: bullet-basic
6+
7+
# Preview URLs (filled by workflow)
8+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/letsplot/plot.png
9+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/letsplot/plot_thumb.png
10+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/letsplot/plot.html
11+
12+
current:
13+
version: 0
14+
generated_at: 2025-12-16T05:59:31Z
15+
generated_by: claude-opus-4-5-20251101
16+
workflow_run: 20257947779
17+
issue: 999
18+
quality_score: 91
19+
# Version info (filled by workflow)
20+
python_version: "3.13.11"
21+
library_version: "unknown"
22+
23+
history: []

0 commit comments

Comments
 (0)