Skip to content

Commit 2dd92c8

Browse files
update(arc-basic): highcharts — new implementation (#4374)
## Summary New **highcharts** implementation for **arc-basic**. ### Changes - Created new highcharts implementation from scratch - Uses Highcharts arc diagram series type ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML created with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- 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 72c5b4e commit 2dd92c8

2 files changed

Lines changed: 476 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
""" pyplots.ai
2+
arc-basic: Basic Arc Diagram
3+
Library: highcharts 1.10.3 | Python 3.14.3
4+
Quality: 87/100 | Created: 2026-02-23
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
from PIL import Image
14+
from selenium import webdriver
15+
from selenium.webdriver.chrome.options import Options
16+
17+
18+
# Data: Character interactions in a story chapter
19+
nodes = ["Alice", "Bob", "Carol", "David", "Eve", "Frank", "Grace", "Henry", "Iris", "Jack"]
20+
21+
# Edges: (source, target, weight) — dialogue exchange count
22+
edges = [
23+
("Alice", "Bob", 5),
24+
("Alice", "David", 3),
25+
("Bob", "Carol", 3),
26+
("Carol", "Eve", 2),
27+
("David", "Frank", 3),
28+
("Eve", "Grace", 1),
29+
("Alice", "Henry", 2),
30+
("Bob", "Frank", 3),
31+
("Carol", "David", 4),
32+
("Frank", "Iris", 1),
33+
("Grace", "Jack", 2),
34+
("Alice", "Jack", 1),
35+
("David", "Henry", 3),
36+
("Henry", "Iris", 2),
37+
("Iris", "Jack", 3),
38+
]
39+
40+
# Node connection counts for marker sizing
41+
degree = dict.fromkeys(nodes, 0)
42+
for src, tgt, _ in edges:
43+
degree[src] += 1
44+
degree[tgt] += 1
45+
46+
# Colorblind-safe palette — Python Blue anchor with complementary tones
47+
node_colors = {
48+
"Alice": "#306998",
49+
"Bob": "#E8A317",
50+
"Carol": "#17BECF",
51+
"David": "#9467BD",
52+
"Eve": "#2CA02C",
53+
"Frank": "#D4652F",
54+
"Grace": "#8C564B",
55+
"Henry": "#1F77B4",
56+
"Iris": "#E377C2",
57+
"Jack": "#5DA88A",
58+
}
59+
60+
# Node config with degree-scaled markers (3-4x default for 4800x2700)
61+
nodes_data = []
62+
for name in nodes:
63+
nodes_data.append(
64+
{
65+
"id": name,
66+
"color": node_colors[name],
67+
"marker": {"radius": 95 + degree[name] * 14, "lineWidth": 5, "lineColor": "#ffffff"},
68+
}
69+
)
70+
71+
# Scale weights up for visual node sizing (arc diagram sizes nodes by total weight flow)
72+
weight_scale = 14
73+
links_data = [{"from": src, "to": tgt, "weight": w * weight_scale} for src, tgt, w in edges]
74+
75+
# Chart options (raw JS — highcharts_core doesn't support arcdiagram type)
76+
chart_options = {
77+
"chart": {
78+
"width": 4800,
79+
"height": 2700,
80+
"backgroundColor": "#ffffff",
81+
"marginTop": 150,
82+
"marginBottom": 10,
83+
"marginLeft": 200,
84+
"marginRight": 200,
85+
"spacingTop": 20,
86+
"spacingBottom": 0,
87+
},
88+
"title": {
89+
"text": "arc-basic \u00b7 highcharts \u00b7 pyplots.ai",
90+
"style": {"fontSize": "56px", "fontWeight": "bold", "color": "#333333"},
91+
"margin": 30,
92+
},
93+
"subtitle": {
94+
"text": "Character interactions — Dialogue exchanges between characters in a story chapter",
95+
"style": {"fontSize": "36px", "color": "#666666"},
96+
},
97+
"accessibility": {"enabled": False},
98+
"tooltip": {
99+
"style": {"fontSize": "32px"},
100+
"nodeFormat": "{point.name}: {point.sum} exchanges",
101+
"pointFormat": "{point.fromNode.name} \u2192 {point.toNode.name}: {point.weight} exchanges",
102+
},
103+
"series": [
104+
{
105+
"type": "arcdiagram",
106+
"name": "Interactions",
107+
"keys": ["from", "to", "weight"],
108+
"nodes": nodes_data,
109+
"data": links_data,
110+
"colorByPoint": True,
111+
"centeredLinks": True,
112+
"linkColorMode": "from",
113+
"linkOpacity": 0.5,
114+
"linkWeight": 18,
115+
"equalNodes": False,
116+
"nodeWidth": 110,
117+
"minLinkWidth": 8,
118+
"marker": {"radius": 110, "lineWidth": 5, "lineColor": "#ffffff"},
119+
"dataLabels": [
120+
{
121+
"enabled": True,
122+
"rotation": 0,
123+
"y": 80,
124+
"align": "center",
125+
"style": {
126+
"fontSize": "48px",
127+
"fontWeight": "bold",
128+
"textOutline": "3px #ffffff",
129+
"color": "#333333",
130+
},
131+
}
132+
],
133+
}
134+
],
135+
"legend": {"enabled": False},
136+
"credits": {"enabled": False},
137+
}
138+
139+
options_json = json.dumps(chart_options)
140+
141+
# Download Highcharts JS, sankey module (dependency), and arc-diagram module
142+
cache_dir = Path("/tmp")
143+
urls = {
144+
"highcharts": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/highcharts.js", cache_dir / "highcharts.js"),
145+
"sankey": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/sankey.js", cache_dir / "hc_sankey.js"),
146+
"arcdiagram": (
147+
"https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/arc-diagram.js",
148+
cache_dir / "hc_arc_diagram.js",
149+
),
150+
}
151+
js_scripts = {}
152+
for name, (url, cache_path) in urls.items():
153+
if cache_path.exists() and cache_path.stat().st_size > 1000:
154+
js_scripts[name] = cache_path.read_text(encoding="utf-8")
155+
else:
156+
for attempt in range(5):
157+
try:
158+
with urllib.request.urlopen(url, timeout=30) as resp:
159+
content = resp.read().decode("utf-8")
160+
cache_path.write_text(content, encoding="utf-8")
161+
js_scripts[name] = content
162+
break
163+
except urllib.error.HTTPError:
164+
time.sleep(3 * (attempt + 1))
165+
highcharts_js = js_scripts["highcharts"]
166+
sankey_js = js_scripts["sankey"]
167+
arcdiagram_js = js_scripts["arcdiagram"]
168+
169+
# Chart init JS: use formatter function to show only node names (no DOM manipulation)
170+
chart_init_js = f"""
171+
(function() {{
172+
var opts = {options_json};
173+
opts.series[0].dataLabels[0].formatter = function() {{
174+
return this.point.isNode ? this.point.name : '';
175+
}};
176+
Highcharts.chart('container', opts);
177+
}})();
178+
"""
179+
180+
# Build HTML with inline JS (no post-render DOM manipulation)
181+
html_content = f"""<!DOCTYPE html>
182+
<html>
183+
<head>
184+
<meta charset="utf-8">
185+
<script>{highcharts_js}</script>
186+
<script>{sankey_js}</script>
187+
<script>{arcdiagram_js}</script>
188+
</head>
189+
<body style="margin:0;">
190+
<div id="container" style="width: 4800px; height: 2700px;"></div>
191+
<script>{chart_init_js}</script>
192+
</body>
193+
</html>"""
194+
195+
# Save interactive HTML version (CDN links for standalone use)
196+
standalone_html = f"""<!DOCTYPE html>
197+
<html>
198+
<head>
199+
<meta charset="utf-8">
200+
<script src="https://cdn.jsdelivr.net/npm/highcharts@11.4.8/highcharts.js"></script>
201+
<script src="https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/sankey.js"></script>
202+
<script src="https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/arc-diagram.js"></script>
203+
</head>
204+
<body style="margin:0; overflow:auto;">
205+
<div id="container" style="width: 4800px; height: 2700px;"></div>
206+
<script>{chart_init_js}</script>
207+
</body>
208+
</html>"""
209+
210+
with open("plot.html", "w", encoding="utf-8") as f:
211+
f.write(standalone_html)
212+
213+
# Write temp HTML and take screenshot
214+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
215+
f.write(html_content)
216+
temp_path = f.name
217+
218+
chrome_options = Options()
219+
chrome_options.add_argument("--headless")
220+
chrome_options.add_argument("--no-sandbox")
221+
chrome_options.add_argument("--disable-dev-shm-usage")
222+
chrome_options.add_argument("--disable-gpu")
223+
chrome_options.add_argument("--window-size=4800,2900")
224+
225+
driver = webdriver.Chrome(options=chrome_options)
226+
driver.get(f"file://{temp_path}")
227+
time.sleep(5)
228+
driver.save_screenshot("plot_raw.png")
229+
driver.quit()
230+
231+
# Crop to exact 4800x2700 dimensions
232+
img = Image.open("plot_raw.png")
233+
img_cropped = img.crop((0, 0, 4800, 2700))
234+
img_cropped.save("plot.png")
235+
Path("plot_raw.png").unlink()
236+
237+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)