Skip to content

Commit de72986

Browse files
feat(highcharts): implement network-directed (#2891)
## Implementation: `network-directed` - highcharts Implements the **highcharts** version of `network-directed`. **File:** `plots/network-directed/implementations/highcharts.py` **Parent Issue:** #2858 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608487760)* --------- 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 8ce914f commit de72986

2 files changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
""" pyplots.ai
2+
network-directed: Directed Network Graph
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 90/100 | Created: 2025-12-30
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
from selenium import webdriver
14+
from selenium.webdriver.chrome.options import Options
15+
16+
17+
# Data - Software module dependencies showing import direction
18+
nodes = [
19+
{"id": "main", "name": "main"},
20+
{"id": "api", "name": "api"},
21+
{"id": "auth", "name": "auth"},
22+
{"id": "database", "name": "database"},
23+
{"id": "models", "name": "models"},
24+
{"id": "utils", "name": "utils"},
25+
{"id": "config", "name": "config"},
26+
{"id": "cache", "name": "cache"},
27+
{"id": "logging", "name": "logging"},
28+
{"id": "validation", "name": "validation"},
29+
{"id": "routes", "name": "routes"},
30+
{"id": "middleware", "name": "middleware"},
31+
]
32+
33+
# Directed edges (source -> target) showing import dependencies
34+
edges = [
35+
("main", "api"),
36+
("main", "config"),
37+
("main", "logging"),
38+
("api", "routes"),
39+
("api", "middleware"),
40+
("routes", "auth"),
41+
("routes", "database"),
42+
("routes", "models"),
43+
("routes", "validation"),
44+
("middleware", "auth"),
45+
("middleware", "logging"),
46+
("auth", "database"),
47+
("auth", "cache"),
48+
("auth", "utils"),
49+
("database", "models"),
50+
("database", "config"),
51+
("database", "logging"),
52+
("models", "validation"),
53+
("cache", "config"),
54+
("cache", "logging"),
55+
("utils", "config"),
56+
("validation", "utils"),
57+
]
58+
59+
# Fixed node positions for reproducibility (arranged in hierarchical layers)
60+
# Spread nodes more to fill canvas better
61+
node_positions = {
62+
"main": (2400, 350),
63+
"api": (1400, 700),
64+
"config": (2400, 700),
65+
"logging": (3400, 700),
66+
"routes": (1000, 1100),
67+
"middleware": (1800, 1100),
68+
"auth": (600, 1550),
69+
"database": (1400, 1550),
70+
"models": (2200, 1550),
71+
"validation": (3400, 1550),
72+
"cache": (1000, 2000),
73+
"utils": (3000, 2000),
74+
}
75+
76+
# Format data for Highcharts networkgraph with fixed positions
77+
nodes_data = [{"id": n["id"], "name": n["name"]} for n in nodes]
78+
links_data = [{"from": src, "to": tgt} for src, tgt in edges]
79+
80+
# Download Highcharts JS files
81+
highcharts_url = "https://code.highcharts.com/highcharts.js"
82+
networkgraph_url = "https://code.highcharts.com/modules/networkgraph.js"
83+
84+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
85+
highcharts_js = response.read().decode("utf-8")
86+
87+
with urllib.request.urlopen(networkgraph_url, timeout=30) as response:
88+
networkgraph_js = response.read().decode("utf-8")
89+
90+
# Build data as JSON
91+
nodes_json = json.dumps(nodes_data)
92+
links_json = json.dumps(links_data)
93+
positions_json = json.dumps(node_positions)
94+
edges_json = json.dumps(edges)
95+
96+
# Create HTML with inline scripts - using fixed positions for reproducibility
97+
# Arrows are drawn as separate SVG elements after the chart renders
98+
html_content = f"""<!DOCTYPE html>
99+
<html>
100+
<head>
101+
<meta charset="utf-8">
102+
<script>{highcharts_js}</script>
103+
<script>{networkgraph_js}</script>
104+
<style>
105+
body {{ margin: 0; padding: 0; }}
106+
#container {{ width: 4800px; height: 2700px; }}
107+
</style>
108+
</head>
109+
<body>
110+
<div id="container"></div>
111+
<script>
112+
var nodePositions = {positions_json};
113+
var nodesData = {nodes_json};
114+
var linksData = {links_json};
115+
var edgeList = {edges_json};
116+
117+
var chart = Highcharts.chart('container', {{
118+
chart: {{
119+
type: 'networkgraph',
120+
width: 4800,
121+
height: 2700,
122+
backgroundColor: '#ffffff',
123+
marginTop: 200,
124+
marginBottom: 250,
125+
marginLeft: 350,
126+
marginRight: 350,
127+
events: {{
128+
load: function() {{
129+
var chart = this;
130+
var series = chart.series[0];
131+
132+
// Set fixed positions for all nodes
133+
series.nodes.forEach(function(node) {{
134+
var pos = nodePositions[node.id];
135+
if (pos) {{
136+
node.plotX = pos[0];
137+
node.plotY = pos[1];
138+
node.fixedPosition = true;
139+
}}
140+
}});
141+
series.isDirty = true;
142+
chart.redraw();
143+
144+
// Draw arrows after positions are set
145+
setTimeout(function() {{
146+
drawArrows(chart, series, nodePositions, edgeList);
147+
}}, 300);
148+
}}
149+
}}
150+
}},
151+
title: {{
152+
text: 'network-directed · highcharts · pyplots.ai',
153+
style: {{
154+
fontSize: '56px',
155+
fontWeight: 'bold'
156+
}}
157+
}},
158+
subtitle: {{
159+
text: 'Software Module Dependencies (arrows show import direction)',
160+
style: {{
161+
fontSize: '36px'
162+
}}
163+
}},
164+
credits: {{
165+
enabled: false
166+
}},
167+
plotOptions: {{
168+
networkgraph: {{
169+
layoutAlgorithm: {{
170+
enableSimulation: false,
171+
initialPositions: 'circle'
172+
}},
173+
link: {{
174+
width: 6,
175+
color: '#306998'
176+
}},
177+
dataLabels: {{
178+
enabled: true,
179+
linkFormat: '',
180+
style: {{
181+
fontSize: '32px',
182+
fontWeight: 'bold',
183+
textOutline: '4px white'
184+
}}
185+
}}
186+
}}
187+
}},
188+
series: [{{
189+
type: 'networkgraph',
190+
draggable: false,
191+
marker: {{
192+
radius: 50
193+
}},
194+
dataLabels: {{
195+
enabled: true,
196+
style: {{
197+
fontSize: '32px',
198+
fontWeight: 'bold',
199+
textOutline: '4px white'
200+
}}
201+
}},
202+
nodes: nodesData,
203+
data: linksData,
204+
color: '#FFD43B'
205+
}}]
206+
}});
207+
208+
// Draw arrow heads at end of each edge
209+
function drawArrows(chart, series, positions, edges) {{
210+
var renderer = chart.renderer;
211+
var nodeRadius = 50;
212+
var arrowSize = 20;
213+
214+
edges.forEach(function(edge) {{
215+
var fromId = edge[0];
216+
var toId = edge[1];
217+
var fromPos = positions[fromId];
218+
var toPos = positions[toId];
219+
220+
if (!fromPos || !toPos) return;
221+
222+
// Calculate direction vector
223+
var dx = toPos[0] - fromPos[0];
224+
var dy = toPos[1] - fromPos[1];
225+
var len = Math.sqrt(dx * dx + dy * dy);
226+
227+
if (len === 0) return;
228+
229+
// Normalize
230+
var nx = dx / len;
231+
var ny = dy / len;
232+
233+
// Arrow tip position (at edge of target node)
234+
var tipX = toPos[0] - nx * (nodeRadius + 5);
235+
var tipY = toPos[1] - ny * (nodeRadius + 5);
236+
237+
// Arrow base positions (perpendicular to direction)
238+
var baseX1 = tipX - nx * arrowSize - ny * arrowSize * 0.6;
239+
var baseY1 = tipY - ny * arrowSize + nx * arrowSize * 0.6;
240+
var baseX2 = tipX - nx * arrowSize + ny * arrowSize * 0.6;
241+
var baseY2 = tipY - ny * arrowSize - nx * arrowSize * 0.6;
242+
243+
// Offset for chart position
244+
var offsetX = chart.plotLeft;
245+
var offsetY = chart.plotTop;
246+
247+
// Draw filled triangle arrow
248+
renderer.path([
249+
'M', tipX + offsetX, tipY + offsetY,
250+
'L', baseX1 + offsetX, baseY1 + offsetY,
251+
'L', baseX2 + offsetX, baseY2 + offsetY,
252+
'Z'
253+
])
254+
.attr({{
255+
fill: '#306998',
256+
'stroke-width': 0,
257+
zIndex: 10
258+
}})
259+
.add();
260+
}});
261+
}}
262+
</script>
263+
</body>
264+
</html>"""
265+
266+
# Write temp HTML file
267+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
268+
f.write(html_content)
269+
temp_path = f.name
270+
271+
# Setup headless Chrome
272+
chrome_options = Options()
273+
chrome_options.add_argument("--headless")
274+
chrome_options.add_argument("--no-sandbox")
275+
chrome_options.add_argument("--disable-dev-shm-usage")
276+
chrome_options.add_argument("--disable-gpu")
277+
chrome_options.add_argument("--window-size=4800,2700")
278+
279+
# Take screenshot
280+
driver = webdriver.Chrome(options=chrome_options)
281+
driver.get(f"file://{temp_path}")
282+
time.sleep(6)
283+
driver.save_screenshot("plot.png")
284+
driver.quit()
285+
286+
# Also save the interactive HTML version
287+
Path("plot.html").write_text(html_content, encoding="utf-8")
288+
289+
# Clean up temp file
290+
Path(temp_path).unlink()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: highcharts
2+
specification_id: network-directed
3+
created: '2025-12-30T23:56:36Z'
4+
updated: '2025-12-31T00:13:39Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608487760
7+
issue: 2858
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/network-directed/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/network-directed/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/network-directed/highcharts/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Clear hierarchical layout showing software module dependencies in logical layers
17+
- Properly implemented directional arrows using custom SVG rendering
18+
- Good colorblind-safe color scheme (yellow nodes, blue edges)
19+
- Fixed node positions ensure reproducibility across runs
20+
- Correct title format following spec requirements
21+
- Realistic software architecture example that demonstrates practical use case
22+
weaknesses:
23+
- Node labels are readable but slightly small for the 4800x2700 canvas; increasing
24+
font size would improve legibility
25+
- Significant empty space on left and right margins - nodes could be spread wider
26+
to better utilize canvas

0 commit comments

Comments
 (0)