Skip to content

Commit c98f606

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

2 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""
2+
bullet-basic: Basic Bullet Chart
3+
Library: highcharts
4+
"""
5+
6+
import json
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
from PIL import Image
13+
from selenium import webdriver
14+
from selenium.webdriver.chrome.options import Options
15+
16+
17+
# Data - Multiple KPIs with actual values, targets, and qualitative ranges
18+
# Normalized to percentage scale (0-100) for consistent display
19+
metrics = [
20+
{
21+
"name": "Revenue",
22+
"actual": 275,
23+
"target": 250,
24+
"max_value": 300,
25+
"ranges": [50, 75, 100], # Poor/Satisfactory/Good as percentages
26+
"unit": "$K",
27+
},
28+
{
29+
"name": "Profit",
30+
"actual": 22,
31+
"target": 27,
32+
"max_value": 35,
33+
"ranges": [43, 71, 100], # 15/35, 25/35, 35/35 as percentages
34+
"unit": "%",
35+
},
36+
{
37+
"name": "New Customers",
38+
"actual": 1650,
39+
"target": 1500,
40+
"max_value": 2000,
41+
"ranges": [50, 70, 100], # 1000/2000, 1400/2000, 2000/2000
42+
"unit": "",
43+
},
44+
{
45+
"name": "Satisfaction",
46+
"actual": 4.5,
47+
"target": 4.7,
48+
"max_value": 5.0,
49+
"ranges": [70, 84, 100], # 3.5/5, 4.2/5, 5/5 as percentages
50+
"unit": "/5",
51+
},
52+
]
53+
54+
# Grayscale colors for qualitative ranges (light to dark = poor to good)
55+
range_colors = ["#e0e0e0", "#b0b0b0", "#808080"]
56+
57+
# Build series data - normalize all values to 0-100 scale for consistent display
58+
series_data = []
59+
for metric in metrics:
60+
max_val = metric["max_value"]
61+
series_data.append(
62+
{
63+
"y": round((metric["actual"] / max_val) * 100, 1),
64+
"target": round((metric["target"] / max_val) * 100, 1),
65+
# Store original values for display
66+
"actual_value": metric["actual"],
67+
"target_value": metric["target"],
68+
"unit": metric["unit"],
69+
}
70+
)
71+
72+
# Build categories with metric names
73+
categories = []
74+
for metric in metrics:
75+
if metric["unit"]:
76+
categories.append(f"{metric['name']} ({metric['unit']})")
77+
else:
78+
categories.append(metric["name"])
79+
80+
# Chart options for bullet chart
81+
chart_options = {
82+
"chart": {
83+
"type": "bullet",
84+
"width": 4800,
85+
"height": 2700,
86+
"backgroundColor": "#ffffff",
87+
"inverted": True, # Horizontal bullet charts
88+
"marginLeft": 450, # Space for category labels
89+
"spacing": [100, 100, 100, 100],
90+
},
91+
"title": {
92+
"text": "bullet-basic \u00b7 highcharts \u00b7 pyplots.ai",
93+
"style": {"fontSize": "48px", "fontWeight": "bold"},
94+
},
95+
"subtitle": {
96+
"text": "Q4 Performance Dashboard - Actual vs Target",
97+
"style": {"fontSize": "32px", "color": "#666666"},
98+
},
99+
"xAxis": {"categories": categories, "labels": {"style": {"fontSize": "32px", "fontWeight": "bold"}}},
100+
"yAxis": {
101+
"gridLineWidth": 0,
102+
"min": 0,
103+
"max": 100,
104+
"title": {"text": "% of Target Range", "style": {"fontSize": "28px"}},
105+
"labels": {"format": "{value}%", "style": {"fontSize": "24px"}},
106+
"plotBands": [
107+
# Poor range (0-50%)
108+
{"from": 0, "to": 50, "color": range_colors[0]},
109+
# Satisfactory range (50-75%)
110+
{"from": 50, "to": 75, "color": range_colors[1]},
111+
# Good range (75-100%)
112+
{"from": 75, "to": 100, "color": range_colors[2]},
113+
],
114+
},
115+
"legend": {"enabled": False},
116+
"plotOptions": {
117+
"bullet": {
118+
"pointPadding": 0.3,
119+
"borderWidth": 0,
120+
"groupPadding": 0.2,
121+
"color": "#306998", # Python Blue for actual value bar
122+
"targetOptions": {
123+
"width": "180%",
124+
"height": 6,
125+
"borderWidth": 0,
126+
"color": "#1a1a1a", # Dark target line
127+
},
128+
"dataLabels": {
129+
"enabled": True,
130+
"format": "{point.actual_value}{point.unit}",
131+
"style": {"fontSize": "28px", "fontWeight": "bold", "color": "#ffffff"},
132+
"inside": True,
133+
"align": "right",
134+
},
135+
}
136+
},
137+
"series": [{"name": "Performance", "data": series_data}],
138+
"tooltip": {
139+
"headerFormat": '<span style="font-size: 24px; font-weight: bold;">{point.key}</span><br/>',
140+
"pointFormat": '<span style="font-size: 20px;">Actual: <b>{point.actual_value}{point.unit}</b><br/>Target: <b>{point.target_value}{point.unit}</b><br/>Performance: <b>{point.y}%</b></span>',
141+
"style": {"fontSize": "20px"},
142+
},
143+
"credits": {"enabled": False},
144+
}
145+
146+
# Download Highcharts JS files for inline embedding
147+
highcharts_url = "https://code.highcharts.com/highcharts.js"
148+
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
149+
bullet_url = "https://code.highcharts.com/modules/bullet.js"
150+
151+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
152+
highcharts_js = response.read().decode("utf-8")
153+
154+
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
155+
highcharts_more_js = response.read().decode("utf-8")
156+
157+
with urllib.request.urlopen(bullet_url, timeout=30) as response:
158+
bullet_js = response.read().decode("utf-8")
159+
160+
# Generate HTML with inline scripts
161+
chart_options_json = json.dumps(chart_options)
162+
html_content = f"""<!DOCTYPE html>
163+
<html>
164+
<head>
165+
<meta charset="utf-8">
166+
<script>{highcharts_js}</script>
167+
<script>{highcharts_more_js}</script>
168+
<script>{bullet_js}</script>
169+
</head>
170+
<body style="margin:0;">
171+
<div id="container" style="width: 4800px; height: 2700px;"></div>
172+
<script>
173+
document.addEventListener('DOMContentLoaded', function() {{
174+
Highcharts.chart('container', {chart_options_json});
175+
}});
176+
</script>
177+
</body>
178+
</html>"""
179+
180+
# Write temp HTML file
181+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
182+
f.write(html_content)
183+
temp_path = f.name
184+
185+
# Also save the HTML for interactive viewing
186+
with open("plot.html", "w", encoding="utf-8") as f:
187+
f.write(html_content)
188+
189+
# Take screenshot with headless Chrome
190+
chrome_options = Options()
191+
chrome_options.add_argument("--headless")
192+
chrome_options.add_argument("--no-sandbox")
193+
chrome_options.add_argument("--disable-dev-shm-usage")
194+
chrome_options.add_argument("--disable-gpu")
195+
chrome_options.add_argument("--window-size=4800,2900")
196+
197+
driver = webdriver.Chrome(options=chrome_options)
198+
driver.get(f"file://{temp_path}")
199+
time.sleep(5) # Wait for chart to render
200+
driver.save_screenshot("plot_raw.png")
201+
driver.quit()
202+
203+
# Crop to exact 4800x2700 dimensions
204+
img = Image.open("plot_raw.png")
205+
img_cropped = img.crop((0, 0, 4800, 2700))
206+
img_cropped.save("plot.png")
207+
Path("plot_raw.png").unlink()
208+
209+
Path(temp_path).unlink() # Clean up temp file
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Per-library metadata for highcharts implementation of bullet-basic
2+
# Auto-generated by impl-generate.yml
3+
4+
library: highcharts
5+
specification_id: bullet-basic
6+
7+
# Preview URLs (filled by workflow)
8+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/highcharts/plot.png
9+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/highcharts/plot_thumb.png
10+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bullet-basic/highcharts/plot.html
11+
12+
current:
13+
version: 0
14+
generated_at: 2025-12-16T05:53:05Z
15+
generated_by: claude-opus-4-5-20251101
16+
workflow_run: 20257947676
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)