|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import json |
| 8 | +import os |
8 | 9 | import tempfile |
9 | 10 | import time |
10 | 11 | import urllib.request |
11 | 12 | from pathlib import Path |
12 | 13 |
|
13 | | -from PIL import Image |
14 | 14 | from selenium import webdriver |
15 | 15 | from selenium.webdriver.chrome.options import Options |
16 | 16 |
|
17 | 17 |
|
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 |
20 | 33 | min_value = 0 |
21 | 34 | max_value = 100 |
22 | | -thresholds = [30, 70] # Red/Yellow/Green zones |
| 35 | +thresholds = [30, 70] |
23 | 36 |
|
24 | | -# Chart options for solid gauge |
| 37 | +# Chart options |
25 | 38 | chart_options = { |
26 | 39 | "chart": { |
27 | 40 | "type": "gauge", |
28 | 41 | "width": 4800, |
29 | 42 | "height": 2700, |
30 | | - "backgroundColor": "#ffffff", |
| 43 | + "backgroundColor": PAGE_BG, |
31 | 44 | "plotBackgroundColor": None, |
32 | 45 | "plotBorderWidth": 0, |
33 | 46 | "plotShadow": False, |
| 47 | + "spacingTop": 40, |
| 48 | + "spacingBottom": 40, |
| 49 | + "style": {"color": INK}, |
34 | 50 | }, |
35 | 51 | "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, |
39 | 55 | }, |
40 | 56 | "pane": { |
41 | 57 | "startAngle": -90, |
42 | 58 | "endAngle": 90, |
43 | | - "center": ["50%", "75%"], |
| 59 | + "center": ["50%", "78%"], |
44 | 60 | "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%"}], |
66 | 62 | }, |
67 | 63 | "yAxis": { |
68 | 64 | "min": min_value, |
69 | 65 | "max": max_value, |
70 | | - "minorTickInterval": "auto", |
| 66 | + "tickInterval": 10, |
| 67 | + "minorTickInterval": 5, |
71 | 68 | "minorTickWidth": 2, |
72 | | - "minorTickLength": 15, |
| 69 | + "minorTickLength": 16, |
73 | 70 | "minorTickPosition": "inside", |
74 | | - "minorTickColor": "#666", |
75 | | - "tickPixelInterval": 50, |
| 71 | + "minorTickColor": INK_SOFT, |
76 | 72 | "tickWidth": 3, |
77 | 73 | "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}, |
82 | 84 | "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 | + }, |
86 | 109 | ], |
87 | 110 | }, |
88 | 111 | "series": [ |
|
91 | 114 | "data": [value], |
92 | 115 | "tooltip": {"valueSuffix": "%"}, |
93 | 116 | "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>', |
95 | 118 | "borderWidth": 0, |
96 | | - "y": 120, |
97 | | - "style": {"fontSize": "64px"}, |
| 119 | + "backgroundColor": "transparent", |
| 120 | + "y": 180, |
| 121 | + "useHTML": True, |
| 122 | + "style": {"color": INK}, |
98 | 123 | }, |
99 | 124 | "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, |
103 | 130 | "baseLength": "0%", |
104 | 131 | "rearLength": "0%", |
105 | 132 | }, |
106 | | - "pivot": {"backgroundColor": "#1a3d5c", "radius": 15}, |
| 133 | + "pivot": {"backgroundColor": INK, "radius": 18, "borderWidth": 0}, |
107 | 134 | } |
108 | 135 | ], |
109 | 136 | "tooltip": {"enabled": False}, |
110 | 137 | "credits": {"enabled": False}, |
111 | 138 | } |
112 | 139 |
|
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: |
118 | 143 | 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: |
121 | 145 | highcharts_more_js = response.read().decode("utf-8") |
122 | 146 |
|
123 | | -# Generate HTML with inline scripts |
| 147 | +# Generate HTML |
124 | 148 | chart_options_json = json.dumps(chart_options) |
125 | 149 | html_content = f"""<!DOCTYPE html> |
126 | 150 | <html> |
|
129 | 153 | <script>{highcharts_js}</script> |
130 | 154 | <script>{highcharts_more_js}</script> |
131 | 155 | </head> |
132 | | -<body style="margin:0;"> |
| 156 | +<body style="margin:0; background:{PAGE_BG};"> |
133 | 157 | <div id="container" style="width: 4800px; height: 2700px;"></div> |
134 | 158 | <script> |
135 | 159 | document.addEventListener('DOMContentLoaded', function() {{ |
|
139 | 163 | </body> |
140 | 164 | </html>""" |
141 | 165 |
|
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: |
144 | 168 | f.write(html_content) |
145 | | - temp_path = f.name |
146 | 169 |
|
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: |
149 | 172 | f.write(html_content) |
| 173 | + temp_path = f.name |
150 | 174 |
|
151 | | -# Take screenshot with headless Chrome |
152 | 175 | chrome_options = Options() |
153 | 176 | chrome_options.add_argument("--headless") |
154 | 177 | chrome_options.add_argument("--no-sandbox") |
155 | 178 | chrome_options.add_argument("--disable-dev-shm-usage") |
156 | 179 | chrome_options.add_argument("--disable-gpu") |
157 | | -chrome_options.add_argument("--window-size=4800,2900") |
| 180 | +chrome_options.add_argument("--window-size=4800,2700") |
158 | 181 |
|
159 | 182 | driver = webdriver.Chrome(options=chrome_options) |
160 | 183 | 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") |
163 | 186 | driver.quit() |
164 | 187 |
|
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