Skip to content

Commit 33f55ae

Browse files
feat(highcharts): implement gauge-basic (#5398)
## Implementation: `gauge-basic` - python/highcharts Implements the **python/highcharts** version of `gauge-basic`. **File:** `plots/gauge-basic/implementations/python/highcharts.py` **Parent Issue:** #857 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24931488920)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent d7a1bb0 commit 33f55ae

2 files changed

Lines changed: 320 additions & 111 deletions

File tree

Lines changed: 94 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,111 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: highcharts unknown | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: highcharts unknown | Python 3.14.4
4+
Quality: 87/100 | Updated: 2026-04-25
55
"""
66

77
import json
8+
import os
89
import tempfile
910
import time
1011
import urllib.request
1112
from pathlib import Path
1213

13-
from PIL import Image
1414
from selenium import webdriver
1515
from selenium.webdriver.chrome.options import Options
1616

1717

18-
# Data - Current sales performance against target
19-
value = 72 # Current value
18+
# Theme tokens
19+
THEME = os.getenv("ANYPLOT_THEME", "light")
20+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
21+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
22+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
23+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
24+
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
25+
26+
# Okabe-Ito zones: low / mid / high (intuitive + colorblind-safe)
27+
ZONE_LOW = "#D55E00" # vermillion (bad)
28+
ZONE_MID = "#E69F00" # orange (warning)
29+
ZONE_HIGH = "#009E73" # brand bluish green (good)
30+
31+
# Data
32+
value = 72
2033
min_value = 0
2134
max_value = 100
22-
thresholds = [30, 70] # Red/Yellow/Green zones
35+
thresholds = [30, 70]
2336

24-
# Chart options for solid gauge
37+
# Chart options
2538
chart_options = {
2639
"chart": {
2740
"type": "gauge",
2841
"width": 4800,
2942
"height": 2700,
30-
"backgroundColor": "#ffffff",
43+
"backgroundColor": PAGE_BG,
3144
"plotBackgroundColor": None,
3245
"plotBorderWidth": 0,
3346
"plotShadow": False,
47+
"spacingTop": 40,
48+
"spacingBottom": 40,
49+
"style": {"color": INK},
3450
},
3551
"title": {
36-
"text": "gauge-basic · highcharts · pyplots.ai",
37-
"style": {"fontSize": "48px", "fontWeight": "bold"},
38-
"y": 80,
52+
"text": "gauge-basic · highcharts · anyplot.ai",
53+
"style": {"fontSize": "72px", "fontWeight": "bold", "color": INK},
54+
"y": 90,
3955
},
4056
"pane": {
4157
"startAngle": -90,
4258
"endAngle": 90,
43-
"center": ["50%", "75%"],
59+
"center": ["50%", "78%"],
4460
"size": "140%",
45-
"background": [
46-
{
47-
"backgroundColor": {
48-
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
49-
"stops": [[0, "#FFF"], [1, "#333"]],
50-
},
51-
"borderWidth": 0,
52-
"outerRadius": "109%",
53-
"innerRadius": "0%",
54-
},
55-
{
56-
"backgroundColor": {
57-
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
58-
"stops": [[0, "#333"], [1, "#FFF"]],
59-
},
60-
"borderWidth": 1,
61-
"outerRadius": "107%",
62-
"innerRadius": "0%",
63-
},
64-
{"backgroundColor": "#DDD", "borderWidth": 0, "outerRadius": "105%", "innerRadius": "103%"},
65-
],
61+
"background": [{"backgroundColor": PAGE_BG, "borderWidth": 0, "outerRadius": "109%", "innerRadius": "0%"}],
6662
},
6763
"yAxis": {
6864
"min": min_value,
6965
"max": max_value,
70-
"minorTickInterval": "auto",
66+
"tickInterval": 10,
67+
"minorTickInterval": 5,
7168
"minorTickWidth": 2,
72-
"minorTickLength": 15,
69+
"minorTickLength": 16,
7370
"minorTickPosition": "inside",
74-
"minorTickColor": "#666",
75-
"tickPixelInterval": 50,
71+
"minorTickColor": INK_SOFT,
7672
"tickWidth": 3,
7773
"tickPosition": "inside",
78-
"tickLength": 20,
79-
"tickColor": "#666",
80-
"labels": {"step": 2, "rotation": "auto", "style": {"fontSize": "28px"}, "distance": 25},
81-
"title": {"text": "Performance (%)", "style": {"fontSize": "36px"}, "y": 50},
74+
"tickLength": 26,
75+
"tickColor": INK_SOFT,
76+
"lineColor": INK_SOFT,
77+
"lineWidth": 0,
78+
"labels": {
79+
"rotation": "auto",
80+
"style": {"fontSize": "44px", "color": INK_SOFT, "fontWeight": "500"},
81+
"distance": 40,
82+
},
83+
"title": {"text": "Performance (%)", "style": {"fontSize": "44px", "color": INK_SOFT}, "y": -120},
8284
"plotBands": [
83-
{"from": min_value, "to": thresholds[0], "color": "#9467BD", "thickness": 40}, # Low zone (purple)
84-
{"from": thresholds[0], "to": thresholds[1], "color": "#FFD43B", "thickness": 40}, # Mid zone (yellow)
85-
{"from": thresholds[1], "to": max_value, "color": "#306998", "thickness": 40}, # High zone (blue)
85+
{
86+
"from": min_value,
87+
"to": thresholds[0],
88+
"color": ZONE_LOW,
89+
"thickness": 70,
90+
"outerRadius": "100%",
91+
"innerRadius": "92%",
92+
},
93+
{
94+
"from": thresholds[0],
95+
"to": thresholds[1],
96+
"color": ZONE_MID,
97+
"thickness": 70,
98+
"outerRadius": "100%",
99+
"innerRadius": "92%",
100+
},
101+
{
102+
"from": thresholds[1],
103+
"to": max_value,
104+
"color": ZONE_HIGH,
105+
"thickness": 70,
106+
"outerRadius": "100%",
107+
"innerRadius": "92%",
108+
},
86109
],
87110
},
88111
"series": [
@@ -91,36 +114,37 @@
91114
"data": [value],
92115
"tooltip": {"valueSuffix": "%"},
93116
"dataLabels": {
94-
"format": '<span style="font-size:64px;font-weight:bold">{y}</span>',
117+
"format": f'<span style="font-size:160px;font-weight:bold;color:{INK}">{{y}}</span>',
95118
"borderWidth": 0,
96-
"y": 120,
97-
"style": {"fontSize": "64px"},
119+
"backgroundColor": "transparent",
120+
"y": 180,
121+
"useHTML": True,
122+
"style": {"color": INK},
98123
},
99124
"dial": {
100-
"radius": "80%",
101-
"backgroundColor": "#1a3d5c",
102-
"baseWidth": 20,
125+
"radius": "82%",
126+
"backgroundColor": INK,
127+
"borderColor": INK,
128+
"baseWidth": 22,
129+
"topWidth": 4,
103130
"baseLength": "0%",
104131
"rearLength": "0%",
105132
},
106-
"pivot": {"backgroundColor": "#1a3d5c", "radius": 15},
133+
"pivot": {"backgroundColor": INK, "radius": 18, "borderWidth": 0},
107134
}
108135
],
109136
"tooltip": {"enabled": False},
110137
"credits": {"enabled": False},
111138
}
112139

113-
# Download Highcharts JS files for inline embedding
114-
highcharts_url = "https://code.highcharts.com/highcharts.js"
115-
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
116-
117-
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
140+
# Download Highcharts JS (inline embed required for headless Chrome)
141+
HC_BASE = "https://cdn.jsdelivr.net/npm/highcharts@11.4.8"
142+
with urllib.request.urlopen(f"{HC_BASE}/highcharts.js", timeout=30) as response:
118143
highcharts_js = response.read().decode("utf-8")
119-
120-
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
144+
with urllib.request.urlopen(f"{HC_BASE}/highcharts-more.js", timeout=30) as response:
121145
highcharts_more_js = response.read().decode("utf-8")
122146

123-
# Generate HTML with inline scripts
147+
# Generate HTML
124148
chart_options_json = json.dumps(chart_options)
125149
html_content = f"""<!DOCTYPE html>
126150
<html>
@@ -129,7 +153,7 @@
129153
<script>{highcharts_js}</script>
130154
<script>{highcharts_more_js}</script>
131155
</head>
132-
<body style="margin:0;">
156+
<body style="margin:0; background:{PAGE_BG};">
133157
<div id="container" style="width: 4800px; height: 2700px;"></div>
134158
<script>
135159
document.addEventListener('DOMContentLoaded', function() {{
@@ -139,33 +163,26 @@
139163
</body>
140164
</html>"""
141165

142-
# Write temp HTML file
143-
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
166+
# Save HTML artifact
167+
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
144168
f.write(html_content)
145-
temp_path = f.name
146169

147-
# Also save the HTML for interactive viewing
148-
with open("plot.html", "w", encoding="utf-8") as f:
170+
# Render PNG via headless Chrome
171+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
149172
f.write(html_content)
173+
temp_path = f.name
150174

151-
# Take screenshot with headless Chrome
152175
chrome_options = Options()
153176
chrome_options.add_argument("--headless")
154177
chrome_options.add_argument("--no-sandbox")
155178
chrome_options.add_argument("--disable-dev-shm-usage")
156179
chrome_options.add_argument("--disable-gpu")
157-
chrome_options.add_argument("--window-size=4800,2900")
180+
chrome_options.add_argument("--window-size=4800,2700")
158181

159182
driver = webdriver.Chrome(options=chrome_options)
160183
driver.get(f"file://{temp_path}")
161-
time.sleep(5) # Wait for chart to render
162-
driver.save_screenshot("plot_raw.png")
184+
time.sleep(5)
185+
driver.save_screenshot(f"plot-{THEME}.png")
163186
driver.quit()
164187

165-
# Crop to exact 4800x2700 dimensions
166-
img = Image.open("plot_raw.png")
167-
img_cropped = img.crop((0, 0, 4800, 2700))
168-
img_cropped.save("plot.png")
169-
Path("plot_raw.png").unlink()
170-
171-
Path(temp_path).unlink() # Clean up temp file
188+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)