Skip to content

Commit 622fa3b

Browse files
feat(highcharts): implement scatter-marginal (#6132)
## Implementation: `scatter-marginal` - python/highcharts Implements the **python/highcharts** version of `scatter-marginal`. **File:** `plots/scatter-marginal/implementations/python/highcharts.py` **Parent Issue:** #2005 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25592907546)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 2198f80 commit 622fa3b

2 files changed

Lines changed: 289 additions & 188 deletions

File tree

plots/scatter-marginal/implementations/python/highcharts.py

Lines changed: 118 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import http.server
8+
import os
9+
import shutil
10+
import socketserver
711
import tempfile
12+
import threading
813
import time
914
import urllib.request
10-
from pathlib import Path
1115

1216
import numpy as np
1317
from highcharts_core.chart import Chart
@@ -18,12 +22,26 @@
1822
from selenium.webdriver.chrome.options import Options
1923

2024

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+
2141
# Data - bivariate normal with correlation (realistic sensor data scenario)
2242
np.random.seed(42)
2343
n_points = 150
24-
# Temperature sensor readings (Celsius)
2544
temperature = np.random.randn(n_points) * 5 + 25
26-
# Humidity readings (%) - correlated with temperature
2745
humidity = -1.2 * temperature + 80 + np.random.randn(n_points) * 8
2846

2947
# Axis ranges for alignment
@@ -40,9 +58,26 @@
4058
y_hist, y_edges = np.histogram(humidity, bins=n_bins, range=(y_min - y_padding, y_max + y_padding))
4159

4260
# 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.")
4681

4782
# Shared axis bounds for alignment
4883
scatter_x_min = x_min - x_padding
@@ -68,7 +103,7 @@
68103
"type": "scatter",
69104
"width": main_width,
70105
"height": main_height,
71-
"backgroundColor": "#ffffff",
106+
"backgroundColor": PAGE_BG,
72107
"marginTop": margin_top,
73108
"marginRight": margin_right,
74109
"marginBottom": margin_bottom,
@@ -81,45 +116,45 @@
81116
main_chart.options.caption = {"text": ""}
82117

83118
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}},
86121
"min": scatter_x_min,
87122
"max": scatter_x_max,
88123
"gridLineWidth": 1,
89-
"gridLineColor": "#e0e0e0",
124+
"gridLineColor": GRID,
90125
"gridLineDashStyle": "Dash",
91126
"lineWidth": 2,
92-
"lineColor": "#333333",
127+
"lineColor": INK_SOFT,
93128
"tickInterval": 5,
129+
"tickColor": INK_SOFT,
94130
}
95131

96132
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}},
99135
"min": scatter_y_min,
100136
"max": scatter_y_max,
101137
"gridLineWidth": 1,
102-
"gridLineColor": "#e0e0e0",
138+
"gridLineColor": GRID,
103139
"gridLineDashStyle": "Dash",
104140
"lineWidth": 2,
105-
"lineColor": "#333333",
141+
"lineColor": INK_SOFT,
142+
"tickColor": INK_SOFT,
106143
}
107144

108145
main_chart.options.legend = {"enabled": False}
109146
main_chart.options.credits = {"enabled": False}
110147
main_chart.options.exporting = {"enabled": False}
111148

112149
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}}
116151
}
117152

118153
# Add scatter series
119154
scatter_series = ScatterSeries()
120155
scatter_series.data = [[float(xi), float(yi)] for xi, yi in zip(temperature, humidity, strict=True)]
121156
scatter_series.name = "Sensor Readings"
122-
scatter_series.color = "#306998"
157+
scatter_series.color = BRAND
123158
main_chart.add_series(scatter_series)
124159

125160
# Create top histogram (X marginal) - using column chart with continuous x-axis
@@ -130,7 +165,7 @@
130165
"type": "column",
131166
"width": main_width,
132167
"height": top_height,
133-
"backgroundColor": "#ffffff",
168+
"backgroundColor": PAGE_BG,
134169
"marginTop": 100,
135170
"marginRight": margin_right,
136171
"marginBottom": 0,
@@ -139,8 +174,8 @@
139174
}
140175

141176
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},
144179
"align": "center",
145180
}
146181
top_chart.options.subtitle = {"text": ""}
@@ -159,9 +194,9 @@
159194

160195
top_chart.options.y_axis = {
161196
"title": {"text": "", "enabled": False},
162-
"labels": {"style": {"fontSize": "22px", "color": "#333333"}},
197+
"labels": {"style": {"fontSize": "22px", "color": INK_SOFT}},
163198
"gridLineWidth": 1,
164-
"gridLineColor": "#e0e0e0",
199+
"gridLineColor": GRID,
165200
"gridLineDashStyle": "Dash",
166201
"min": 0,
167202
}
@@ -176,7 +211,7 @@
176211
top_chart.options.plot_options = {
177212
"column": {
178213
"borderWidth": 1,
179-
"borderColor": "#306998",
214+
"borderColor": BRAND,
180215
"pointPadding": 0,
181216
"groupPadding": 0,
182217
"pointWidth": None,
@@ -188,7 +223,7 @@
188223
top_series = ColumnSeries()
189224
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))]
190225
top_series.name = "Temperature Distribution"
191-
top_series.color = "rgba(48, 105, 152, 0.5)"
226+
top_series.color = BRAND
192227
top_chart.add_series(top_series)
193228

194229
# Create right histogram (Y marginal) - using bar chart for horizontal bars
@@ -199,7 +234,7 @@
199234
"type": "bar",
200235
"width": right_width,
201236
"height": main_height,
202-
"backgroundColor": "#ffffff",
237+
"backgroundColor": PAGE_BG,
203238
"marginTop": margin_top,
204239
"marginRight": 120,
205240
"marginBottom": margin_bottom,
@@ -225,9 +260,9 @@
225260

226261
right_chart.options.y_axis = {
227262
"title": {"text": "", "enabled": False},
228-
"labels": {"style": {"fontSize": "22px", "color": "#333333"}},
263+
"labels": {"style": {"fontSize": "22px", "color": INK_SOFT}},
229264
"gridLineWidth": 1,
230-
"gridLineColor": "#e0e0e0",
265+
"gridLineColor": GRID,
231266
"gridLineDashStyle": "Dash",
232267
"opposite": True,
233268
"reversed": False,
@@ -244,7 +279,7 @@
244279
right_chart.options.plot_options = {
245280
"bar": {
246281
"borderWidth": 1,
247-
"borderColor": "#306998",
282+
"borderColor": BRAND,
248283
"pointPadding": 0,
249284
"groupPadding": 0,
250285
"pointRange": y_bin_width * 0.9,
@@ -255,7 +290,7 @@
255290
right_series = BarSeries()
256291
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))]
257292
right_series.name = "Humidity Distribution"
258-
right_series.color = "rgba(48, 105, 152, 0.5)"
293+
right_series.color = BRAND
259294
right_chart.add_series(right_series)
260295

261296
# Generate JS literals
@@ -268,19 +303,24 @@
268303
total_height = top_height + main_height
269304

270305
# 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+
271311
html_content = f"""<!DOCTYPE html>
272312
<html>
273313
<head>
274314
<meta charset="utf-8">
275-
<script>{highcharts_js}</script>
315+
{script_tag}
276316
<style>
277317
* {{
278318
margin: 0;
279319
padding: 0;
280320
box-sizing: border-box;
281321
}}
282322
body {{
283-
background: #ffffff;
323+
background: {PAGE_BG};
284324
width: {total_width}px;
285325
height: {total_height}px;
286326
overflow: hidden;
@@ -298,7 +338,7 @@
298338
left: {main_width}px;
299339
width: {right_width}px;
300340
height: {top_height}px;
301-
background: #ffffff;
341+
background: {PAGE_BG};
302342
}}
303343
#main-chart {{
304344
position: absolute;
@@ -322,7 +362,6 @@
322362
<div id="main-chart"></div>
323363
<div id="right-chart"></div>
324364
<script>
325-
// Override Highcharts defaults to remove "Chart title"
326365
Highcharts.setOptions({{
327366
lang: {{
328367
chartTitle: ''
@@ -338,26 +377,50 @@
338377
</body>
339378
</html>"""
340379

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:
343385
f.write(html_content)
344-
temp_path = f.name
345386

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:
348391
f.write(html_content)
349392

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}")
356393

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()
362424

363-
Path(temp_path).unlink() # Clean up temp file
425+
# Clean up
426+
shutil.rmtree(temp_dir, ignore_errors=True)

0 commit comments

Comments
 (0)