|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | scatter-marginal: Scatter Plot with Marginal Distributions |
3 | | -Library: highcharts unknown | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-26 |
| 3 | +Library: highcharts unknown | Python 3.13.13 |
| 4 | +Quality: 95/100 | Updated: 2026-05-09 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import http.server |
| 8 | +import os |
| 9 | +import shutil |
| 10 | +import socketserver |
7 | 11 | import tempfile |
| 12 | +import threading |
8 | 13 | import time |
9 | 14 | import urllib.request |
10 | | -from pathlib import Path |
11 | 15 |
|
12 | 16 | import numpy as np |
13 | 17 | from highcharts_core.chart import Chart |
|
18 | 22 | from selenium.webdriver.chrome.options import Options |
19 | 23 |
|
20 | 24 |
|
| 25 | +try: |
| 26 | + import requests |
| 27 | + |
| 28 | + REQUESTS_AVAILABLE = True |
| 29 | +except ImportError: |
| 30 | + REQUESTS_AVAILABLE = False |
| 31 | + |
| 32 | +# Theme tokens |
| 33 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 34 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 35 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 36 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 37 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 38 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 39 | +BRAND = "#009E73" # Okabe-Ito position 1 |
| 40 | + |
21 | 41 | # Data - bivariate normal with correlation (realistic sensor data scenario) |
22 | 42 | np.random.seed(42) |
23 | 43 | n_points = 150 |
24 | | -# Temperature sensor readings (Celsius) |
25 | 44 | temperature = np.random.randn(n_points) * 5 + 25 |
26 | | -# Humidity readings (%) - correlated with temperature |
27 | 45 | humidity = -1.2 * temperature + 80 + np.random.randn(n_points) * 8 |
28 | 46 |
|
29 | 47 | # Axis ranges for alignment |
|
40 | 58 | y_hist, y_edges = np.histogram(humidity, bins=n_bins, range=(y_min - y_padding, y_max + y_padding)) |
41 | 59 |
|
42 | 60 | # Download Highcharts JS (required for headless Chrome) |
43 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
44 | | -with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
45 | | - highcharts_js = response.read().decode("utf-8") |
| 61 | +# Try multiple CDN sources |
| 62 | +cdns = ["https://cdn.jsdelivr.net/npm/highcharts@11.4.0/highcharts.min.js", "https://code.highcharts.com/highcharts.js"] |
| 63 | +highcharts_js = "" |
| 64 | +for cdn_url in cdns: |
| 65 | + try: |
| 66 | + if REQUESTS_AVAILABLE: |
| 67 | + response = requests.get(cdn_url, timeout=30) |
| 68 | + response.raise_for_status() |
| 69 | + highcharts_js = response.text |
| 70 | + break |
| 71 | + else: |
| 72 | + req = urllib.request.Request(cdn_url) |
| 73 | + req.add_header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") |
| 74 | + with urllib.request.urlopen(req, timeout=30) as response: |
| 75 | + highcharts_js = response.read().decode("utf-8") |
| 76 | + break |
| 77 | + except Exception: |
| 78 | + continue |
| 79 | +if not highcharts_js: |
| 80 | + print("Warning: Failed to download Highcharts JS from all CDNs. Using external CDN link in HTML.") |
46 | 81 |
|
47 | 82 | # Shared axis bounds for alignment |
48 | 83 | scatter_x_min = x_min - x_padding |
|
68 | 103 | "type": "scatter", |
69 | 104 | "width": main_width, |
70 | 105 | "height": main_height, |
71 | | - "backgroundColor": "#ffffff", |
| 106 | + "backgroundColor": PAGE_BG, |
72 | 107 | "marginTop": margin_top, |
73 | 108 | "marginRight": margin_right, |
74 | 109 | "marginBottom": margin_bottom, |
|
81 | 116 | main_chart.options.caption = {"text": ""} |
82 | 117 |
|
83 | 118 | main_chart.options.x_axis = { |
84 | | - "title": {"text": "Temperature (°C)", "style": {"fontSize": "32px", "color": "#333333"}}, |
85 | | - "labels": {"style": {"fontSize": "26px", "color": "#333333"}}, |
| 119 | + "title": {"text": "Temperature (°C)", "style": {"fontSize": "32px", "color": INK}}, |
| 120 | + "labels": {"style": {"fontSize": "26px", "color": INK_SOFT}}, |
86 | 121 | "min": scatter_x_min, |
87 | 122 | "max": scatter_x_max, |
88 | 123 | "gridLineWidth": 1, |
89 | | - "gridLineColor": "#e0e0e0", |
| 124 | + "gridLineColor": GRID, |
90 | 125 | "gridLineDashStyle": "Dash", |
91 | 126 | "lineWidth": 2, |
92 | | - "lineColor": "#333333", |
| 127 | + "lineColor": INK_SOFT, |
93 | 128 | "tickInterval": 5, |
| 129 | + "tickColor": INK_SOFT, |
94 | 130 | } |
95 | 131 |
|
96 | 132 | main_chart.options.y_axis = { |
97 | | - "title": {"text": "Relative Humidity (%)", "style": {"fontSize": "32px", "color": "#333333"}}, |
98 | | - "labels": {"style": {"fontSize": "26px", "color": "#333333"}}, |
| 133 | + "title": {"text": "Relative Humidity (%)", "style": {"fontSize": "32px", "color": INK}}, |
| 134 | + "labels": {"style": {"fontSize": "26px", "color": INK_SOFT}}, |
99 | 135 | "min": scatter_y_min, |
100 | 136 | "max": scatter_y_max, |
101 | 137 | "gridLineWidth": 1, |
102 | | - "gridLineColor": "#e0e0e0", |
| 138 | + "gridLineColor": GRID, |
103 | 139 | "gridLineDashStyle": "Dash", |
104 | 140 | "lineWidth": 2, |
105 | | - "lineColor": "#333333", |
| 141 | + "lineColor": INK_SOFT, |
| 142 | + "tickColor": INK_SOFT, |
106 | 143 | } |
107 | 144 |
|
108 | 145 | main_chart.options.legend = {"enabled": False} |
109 | 146 | main_chart.options.credits = {"enabled": False} |
110 | 147 | main_chart.options.exporting = {"enabled": False} |
111 | 148 |
|
112 | 149 | main_chart.options.plot_options = { |
113 | | - "scatter": { |
114 | | - "marker": {"radius": 12, "fillColor": "rgba(48, 105, 152, 0.45)", "lineWidth": 1, "lineColor": "#306998"} |
115 | | - } |
| 150 | + "scatter": {"marker": {"radius": 12, "fillColor": BRAND, "lineWidth": 1, "lineColor": PAGE_BG, "opacity": 0.7}} |
116 | 151 | } |
117 | 152 |
|
118 | 153 | # Add scatter series |
119 | 154 | scatter_series = ScatterSeries() |
120 | 155 | scatter_series.data = [[float(xi), float(yi)] for xi, yi in zip(temperature, humidity, strict=True)] |
121 | 156 | scatter_series.name = "Sensor Readings" |
122 | | -scatter_series.color = "#306998" |
| 157 | +scatter_series.color = BRAND |
123 | 158 | main_chart.add_series(scatter_series) |
124 | 159 |
|
125 | 160 | # Create top histogram (X marginal) - using column chart with continuous x-axis |
|
130 | 165 | "type": "column", |
131 | 166 | "width": main_width, |
132 | 167 | "height": top_height, |
133 | | - "backgroundColor": "#ffffff", |
| 168 | + "backgroundColor": PAGE_BG, |
134 | 169 | "marginTop": 100, |
135 | 170 | "marginRight": margin_right, |
136 | 171 | "marginBottom": 0, |
|
139 | 174 | } |
140 | 175 |
|
141 | 176 | top_chart.options.title = { |
142 | | - "text": "scatter-marginal · highcharts · pyplots.ai", |
143 | | - "style": {"fontSize": "42px", "fontWeight": "bold", "color": "#333333"}, |
| 177 | + "text": "scatter-marginal · highcharts · anyplot.ai", |
| 178 | + "style": {"fontSize": "42px", "fontWeight": "bold", "color": INK}, |
144 | 179 | "align": "center", |
145 | 180 | } |
146 | 181 | top_chart.options.subtitle = {"text": ""} |
|
159 | 194 |
|
160 | 195 | top_chart.options.y_axis = { |
161 | 196 | "title": {"text": "", "enabled": False}, |
162 | | - "labels": {"style": {"fontSize": "22px", "color": "#333333"}}, |
| 197 | + "labels": {"style": {"fontSize": "22px", "color": INK_SOFT}}, |
163 | 198 | "gridLineWidth": 1, |
164 | | - "gridLineColor": "#e0e0e0", |
| 199 | + "gridLineColor": GRID, |
165 | 200 | "gridLineDashStyle": "Dash", |
166 | 201 | "min": 0, |
167 | 202 | } |
|
176 | 211 | top_chart.options.plot_options = { |
177 | 212 | "column": { |
178 | 213 | "borderWidth": 1, |
179 | | - "borderColor": "#306998", |
| 214 | + "borderColor": BRAND, |
180 | 215 | "pointPadding": 0, |
181 | 216 | "groupPadding": 0, |
182 | 217 | "pointWidth": None, |
|
188 | 223 | top_series = ColumnSeries() |
189 | 224 | top_series.data = [{"x": float((x_edges[i] + x_edges[i + 1]) / 2), "y": int(x_hist[i])} for i in range(len(x_hist))] |
190 | 225 | top_series.name = "Temperature Distribution" |
191 | | -top_series.color = "rgba(48, 105, 152, 0.5)" |
| 226 | +top_series.color = BRAND |
192 | 227 | top_chart.add_series(top_series) |
193 | 228 |
|
194 | 229 | # Create right histogram (Y marginal) - using bar chart for horizontal bars |
|
199 | 234 | "type": "bar", |
200 | 235 | "width": right_width, |
201 | 236 | "height": main_height, |
202 | | - "backgroundColor": "#ffffff", |
| 237 | + "backgroundColor": PAGE_BG, |
203 | 238 | "marginTop": margin_top, |
204 | 239 | "marginRight": 120, |
205 | 240 | "marginBottom": margin_bottom, |
|
225 | 260 |
|
226 | 261 | right_chart.options.y_axis = { |
227 | 262 | "title": {"text": "", "enabled": False}, |
228 | | - "labels": {"style": {"fontSize": "22px", "color": "#333333"}}, |
| 263 | + "labels": {"style": {"fontSize": "22px", "color": INK_SOFT}}, |
229 | 264 | "gridLineWidth": 1, |
230 | | - "gridLineColor": "#e0e0e0", |
| 265 | + "gridLineColor": GRID, |
231 | 266 | "gridLineDashStyle": "Dash", |
232 | 267 | "opposite": True, |
233 | 268 | "reversed": False, |
|
244 | 279 | right_chart.options.plot_options = { |
245 | 280 | "bar": { |
246 | 281 | "borderWidth": 1, |
247 | | - "borderColor": "#306998", |
| 282 | + "borderColor": BRAND, |
248 | 283 | "pointPadding": 0, |
249 | 284 | "groupPadding": 0, |
250 | 285 | "pointRange": y_bin_width * 0.9, |
|
255 | 290 | right_series = BarSeries() |
256 | 291 | right_series.data = [{"x": float((y_edges[i] + y_edges[i + 1]) / 2), "y": int(y_hist[i])} for i in range(len(y_hist))] |
257 | 292 | right_series.name = "Humidity Distribution" |
258 | | -right_series.color = "rgba(48, 105, 152, 0.5)" |
| 293 | +right_series.color = BRAND |
259 | 294 | right_chart.add_series(right_series) |
260 | 295 |
|
261 | 296 | # Generate JS literals |
|
268 | 303 | total_height = top_height + main_height |
269 | 304 |
|
270 | 305 | # Create combined HTML with all three charts - seamless layout with no gaps |
| 306 | +if highcharts_js: |
| 307 | + script_tag = f"<script>{highcharts_js}</script>" |
| 308 | +else: |
| 309 | + script_tag = '<script src="https://code.highcharts.com/highcharts.js"></script>' |
| 310 | + |
271 | 311 | html_content = f"""<!DOCTYPE html> |
272 | 312 | <html> |
273 | 313 | <head> |
274 | 314 | <meta charset="utf-8"> |
275 | | - <script>{highcharts_js}</script> |
| 315 | + {script_tag} |
276 | 316 | <style> |
277 | 317 | * {{ |
278 | 318 | margin: 0; |
279 | 319 | padding: 0; |
280 | 320 | box-sizing: border-box; |
281 | 321 | }} |
282 | 322 | body {{ |
283 | | - background: #ffffff; |
| 323 | + background: {PAGE_BG}; |
284 | 324 | width: {total_width}px; |
285 | 325 | height: {total_height}px; |
286 | 326 | overflow: hidden; |
|
298 | 338 | left: {main_width}px; |
299 | 339 | width: {right_width}px; |
300 | 340 | height: {top_height}px; |
301 | | - background: #ffffff; |
| 341 | + background: {PAGE_BG}; |
302 | 342 | }} |
303 | 343 | #main-chart {{ |
304 | 344 | position: absolute; |
|
322 | 362 | <div id="main-chart"></div> |
323 | 363 | <div id="right-chart"></div> |
324 | 364 | <script> |
325 | | - // Override Highcharts defaults to remove "Chart title" |
326 | 365 | Highcharts.setOptions({{ |
327 | 366 | lang: {{ |
328 | 367 | chartTitle: '' |
|
338 | 377 | </body> |
339 | 378 | </html>""" |
340 | 379 |
|
341 | | -# Write temp HTML and take screenshot |
342 | | -with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
| 380 | +# Get current working directory before any changes |
| 381 | +cwd = os.getcwd() |
| 382 | + |
| 383 | +# Save HTML for interactive viewing |
| 384 | +with open(os.path.join(cwd, f"plot-{THEME}.html"), "w", encoding="utf-8") as f: |
343 | 385 | f.write(html_content) |
344 | | - temp_path = f.name |
345 | 386 |
|
346 | | -# Also save HTML for interactive viewing |
347 | | -with open("plot.html", "w", encoding="utf-8") as f: |
| 387 | +# Write temp HTML |
| 388 | +temp_dir = tempfile.mkdtemp() |
| 389 | +temp_path = os.path.join(temp_dir, "chart.html") |
| 390 | +with open(temp_path, "w", encoding="utf-8") as f: |
348 | 391 | f.write(html_content) |
349 | 392 |
|
350 | | -chrome_options = Options() |
351 | | -chrome_options.add_argument("--headless") |
352 | | -chrome_options.add_argument("--no-sandbox") |
353 | | -chrome_options.add_argument("--disable-dev-shm-usage") |
354 | | -chrome_options.add_argument("--disable-gpu") |
355 | | -chrome_options.add_argument(f"--window-size={total_width + 100},{total_height + 100}") |
356 | 393 |
|
357 | | -driver = webdriver.Chrome(options=chrome_options) |
358 | | -driver.get(f"file://{temp_path}") |
359 | | -time.sleep(5) # Wait for charts to render |
360 | | -driver.save_screenshot("plot.png") |
361 | | -driver.quit() |
| 394 | +# Start simple HTTP server in background thread |
| 395 | +class QuietHandler(http.server.SimpleHTTPRequestHandler): |
| 396 | + def log_message(self, format, *args): |
| 397 | + pass |
| 398 | + |
| 399 | + |
| 400 | +os.chdir(temp_dir) |
| 401 | + |
| 402 | +# Find an available port |
| 403 | +with socketserver.TCPServer(("127.0.0.1", 0), QuietHandler) as httpd: |
| 404 | + PORT = httpd.server_address[1] # Get the actual port assigned |
| 405 | + server_thread = threading.Thread(target=httpd.serve_forever, daemon=True) |
| 406 | + server_thread.start() |
| 407 | + |
| 408 | + time.sleep(1) # Give server time to start |
| 409 | + |
| 410 | + chrome_options = Options() |
| 411 | + chrome_options.add_argument("--headless") |
| 412 | + chrome_options.add_argument("--no-sandbox") |
| 413 | + chrome_options.add_argument("--disable-dev-shm-usage") |
| 414 | + chrome_options.add_argument("--disable-gpu") |
| 415 | + chrome_options.add_argument(f"--window-size={total_width + 100},{total_height + 100}") |
| 416 | + |
| 417 | + driver = webdriver.Chrome(options=chrome_options) |
| 418 | + driver.get(f"http://127.0.0.1:{PORT}/chart.html") |
| 419 | + time.sleep(10) # Wait for Highcharts to load and render |
| 420 | + driver.save_screenshot(os.path.join(cwd, f"plot-{THEME}.png")) |
| 421 | + driver.quit() |
| 422 | + |
| 423 | + httpd.shutdown() |
362 | 424 |
|
363 | | -Path(temp_path).unlink() # Clean up temp file |
| 425 | +# Clean up |
| 426 | +shutil.rmtree(temp_dir, ignore_errors=True) |
0 commit comments