Skip to content

Commit d1ef570

Browse files
feat(highcharts): implement network-weighted (#3320)
## Implementation: `network-weighted` - highcharts Implements the **highcharts** version of `network-weighted`. **File:** `plots/network-weighted/implementations/highcharts.py` **Parent Issue:** #3290 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20822859689)* --------- 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 a388698 commit d1ef570

2 files changed

Lines changed: 534 additions & 0 deletions

File tree

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
""" pyplots.ai
2+
network-weighted: Weighted Network Graph with Edge Thickness
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 90/100 | Created: 2026-01-08
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
import numpy as np
14+
from selenium import webdriver
15+
from selenium.webdriver.chrome.options import Options
16+
17+
18+
# Data: Research collaboration network between university departments
19+
np.random.seed(42)
20+
21+
# Departments (nodes)
22+
departments = [
23+
{"id": "CS", "name": "Computer Science"},
24+
{"id": "MATH", "name": "Mathematics"},
25+
{"id": "PHYS", "name": "Physics"},
26+
{"id": "STAT", "name": "Statistics"},
27+
{"id": "EE", "name": "Electrical Eng."},
28+
{"id": "ME", "name": "Mechanical Eng."},
29+
{"id": "BIO", "name": "Biology"},
30+
{"id": "CHEM", "name": "Chemistry"},
31+
{"id": "ECON", "name": "Economics"},
32+
{"id": "PSYCH", "name": "Psychology"},
33+
{"id": "MED", "name": "Medicine"},
34+
{"id": "ENV", "name": "Environmental Sci."},
35+
]
36+
37+
# Collaboration edges with weights (number of joint publications)
38+
edges = [
39+
("CS", "MATH", 45),
40+
("CS", "STAT", 38),
41+
("CS", "EE", 52),
42+
("CS", "PHYS", 22),
43+
("MATH", "STAT", 41),
44+
("MATH", "PHYS", 35),
45+
("MATH", "ECON", 18),
46+
("PHYS", "EE", 28),
47+
("PHYS", "CHEM", 25),
48+
("STAT", "ECON", 32),
49+
("STAT", "PSYCH", 24),
50+
("STAT", "BIO", 19),
51+
("EE", "ME", 33),
52+
("BIO", "CHEM", 47),
53+
("BIO", "MED", 55),
54+
("CHEM", "ENV", 29),
55+
("MED", "PSYCH", 21),
56+
("MED", "BIO", 55),
57+
("ENV", "BIO", 26),
58+
("ECON", "PSYCH", 15),
59+
]
60+
61+
# Calculate weighted degree for node sizing
62+
weighted_degree = {d["id"]: 0 for d in departments}
63+
for src, tgt, w in edges:
64+
weighted_degree[src] += w
65+
weighted_degree[tgt] += w
66+
67+
# Normalize for marker size (bigger nodes = more collaborations)
68+
max_degree = max(weighted_degree.values())
69+
min_degree = min(weighted_degree.values())
70+
71+
# Colors for nodes - colorblind-safe palette
72+
colors = [
73+
"#306998",
74+
"#FFD43B",
75+
"#9467BD",
76+
"#17BECF",
77+
"#8C564B",
78+
"#E377C2",
79+
"#7F7F7F",
80+
"#BCBD22",
81+
"#1F77B4",
82+
"#FF7F0E",
83+
"#2CA02C",
84+
"#D62728",
85+
]
86+
87+
# Create nodes for Highcharts networkgraph
88+
nodes_data = []
89+
for i, dept in enumerate(departments):
90+
deg = weighted_degree[dept["id"]]
91+
# Scale marker size between 50 and 120 based on weighted degree
92+
marker_size = 50 + 70 * (deg - min_degree) / (max_degree - min_degree)
93+
nodes_data.append(
94+
{"id": dept["id"], "name": dept["name"], "marker": {"radius": marker_size}, "color": colors[i % len(colors)]}
95+
)
96+
97+
# Create links with width based on weight
98+
# Scale line width between 4 and 24 based on weight
99+
min_weight = min(w for _, _, w in edges)
100+
max_weight = max(w for _, _, w in edges)
101+
102+
links_data = []
103+
for src, tgt, weight in edges:
104+
width = 4 + 20 * (weight - min_weight) / (max_weight - min_weight)
105+
links_data.append({"from": src, "to": tgt, "width": round(width, 1)})
106+
107+
# Convert links and nodes to JSON for embedding
108+
links_json = json.dumps(links_data)
109+
nodes_json = json.dumps(nodes_data)
110+
111+
# Download Highcharts JS and networkgraph module
112+
highcharts_url = "https://code.highcharts.com/highcharts.js"
113+
networkgraph_url = "https://code.highcharts.com/modules/networkgraph.js"
114+
115+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
116+
highcharts_js = response.read().decode("utf-8")
117+
118+
with urllib.request.urlopen(networkgraph_url, timeout=30) as response:
119+
networkgraph_js = response.read().decode("utf-8")
120+
121+
# Generate HTML with inline scripts, weight legend, and custom link width rendering
122+
html_content = f"""<!DOCTYPE html>
123+
<html>
124+
<head>
125+
<meta charset="utf-8">
126+
<script>{highcharts_js}</script>
127+
<script>{networkgraph_js}</script>
128+
<style>
129+
body {{ margin: 0; padding: 0; }}
130+
#container {{ width: 4800px; height: 2700px; }}
131+
#legend {{
132+
position: absolute;
133+
bottom: 120px;
134+
left: 150px;
135+
background: rgba(255, 255, 255, 0.95);
136+
border: 3px solid #306998;
137+
border-radius: 12px;
138+
padding: 30px 40px;
139+
font-family: 'Segoe UI', Arial, sans-serif;
140+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
141+
}}
142+
#legend h3 {{
143+
margin: 0 0 20px 0;
144+
font-size: 36px;
145+
color: #333;
146+
font-weight: bold;
147+
}}
148+
.legend-item {{
149+
display: flex;
150+
align-items: center;
151+
margin: 16px 0;
152+
font-size: 28px;
153+
color: #444;
154+
}}
155+
.legend-line {{
156+
height: 0;
157+
margin-right: 20px;
158+
border-top-style: solid;
159+
border-top-color: #306998;
160+
}}
161+
.legend-line.thin {{ width: 80px; border-top-width: 4px; }}
162+
.legend-line.medium {{ width: 80px; border-top-width: 14px; }}
163+
.legend-line.thick {{ width: 80px; border-top-width: 24px; }}
164+
</style>
165+
</head>
166+
<body>
167+
<div id="container"></div>
168+
<div id="legend">
169+
<h3>Edge Weight (Publications)</h3>
170+
<div class="legend-item">
171+
<div class="legend-line thin"></div>
172+
<span>{min_weight} publications</span>
173+
</div>
174+
<div class="legend-item">
175+
<div class="legend-line medium"></div>
176+
<span>~{(min_weight + max_weight) // 2} publications</span>
177+
</div>
178+
<div class="legend-item">
179+
<div class="legend-line thick"></div>
180+
<span>{max_weight} publications</span>
181+
</div>
182+
</div>
183+
<script>
184+
var linksData = {links_json};
185+
var nodesData = {nodes_json};
186+
187+
Highcharts.chart('container', {{
188+
chart: {{
189+
type: 'networkgraph',
190+
width: 4800,
191+
height: 2700,
192+
backgroundColor: '#ffffff',
193+
marginTop: 180,
194+
marginBottom: 200,
195+
marginLeft: 400,
196+
marginRight: 400,
197+
events: {{
198+
load: function() {{
199+
// Center the network by adjusting plot area
200+
var chart = this;
201+
chart.plotBackground.attr({{
202+
fill: 'none'
203+
}});
204+
}}
205+
}}
206+
}},
207+
title: {{
208+
text: 'network-weighted · highcharts · pyplots.ai',
209+
style: {{ fontSize: '56px', fontWeight: 'bold' }}
210+
}},
211+
subtitle: {{
212+
text: 'University Department Collaboration Network',
213+
style: {{ fontSize: '36px', color: '#666666' }}
214+
}},
215+
plotOptions: {{
216+
networkgraph: {{
217+
layoutAlgorithm: {{
218+
enableSimulation: true,
219+
friction: -0.92,
220+
linkLength: 320,
221+
gravitationalConstant: 0.08,
222+
integration: 'verlet',
223+
approximation: 'none',
224+
initialPositions: 'circle',
225+
maxIterations: 3000,
226+
initialPositionRadius: 800
227+
}},
228+
link: {{
229+
color: '#306998'
230+
}},
231+
dataLabels: {{
232+
enabled: true,
233+
linkFormat: '',
234+
allowOverlap: false,
235+
style: {{
236+
fontSize: '36px',
237+
fontWeight: 'bold',
238+
textOutline: '4px white'
239+
}}
240+
}}
241+
}}
242+
}},
243+
series: [{{
244+
type: 'networkgraph',
245+
name: 'Collaborations',
246+
nodes: nodesData,
247+
data: linksData,
248+
dataLabels: {{
249+
enabled: true,
250+
linkFormat: '',
251+
format: '{{point.id}}',
252+
style: {{
253+
fontSize: '36px',
254+
fontWeight: 'bold',
255+
textOutline: '4px white'
256+
}}
257+
}},
258+
marker: {{
259+
radius: 70
260+
}}
261+
}}],
262+
credits: {{ enabled: false }},
263+
tooltip: {{
264+
enabled: true,
265+
style: {{ fontSize: '28px' }},
266+
formatter: function() {{
267+
if (this.point.isNode) {{
268+
return '<b>' + this.point.id + '</b>';
269+
}}
270+
var link = linksData.find(function(l) {{
271+
return (l.from === this.point.from && l.to === this.point.to) ||
272+
(l.from === this.point.to && l.to === this.point.from);
273+
}}, this);
274+
if (link) {{
275+
return this.point.from + ' - ' + this.point.to + ': <b>' + link.width.toFixed(1) + '</b>';
276+
}}
277+
return this.point.from + ' - ' + this.point.to;
278+
}}
279+
}}
280+
}}, function(chart) {{
281+
// After chart renders, update link widths based on data
282+
setTimeout(function() {{
283+
chart.series[0].points.forEach(function(point) {{
284+
if (!point.isNode && point.graphic) {{
285+
var linkData = linksData.find(function(l) {{
286+
return (l.from === point.from && l.to === point.to) ||
287+
(l.from === point.to && l.to === point.from);
288+
}});
289+
if (linkData) {{
290+
point.graphic.attr({{
291+
'stroke-width': linkData.width
292+
}});
293+
}}
294+
}}
295+
}});
296+
}}, 500);
297+
}});
298+
</script>
299+
</body>
300+
</html>"""
301+
302+
# Write temp HTML and take screenshot
303+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
304+
f.write(html_content)
305+
temp_path = f.name
306+
307+
# Also save as plot.html for interactive viewing
308+
with open("plot.html", "w", encoding="utf-8") as f:
309+
f.write(html_content)
310+
311+
chrome_options = Options()
312+
chrome_options.add_argument("--headless")
313+
chrome_options.add_argument("--no-sandbox")
314+
chrome_options.add_argument("--disable-dev-shm-usage")
315+
chrome_options.add_argument("--disable-gpu")
316+
chrome_options.add_argument("--window-size=4900,2800")
317+
318+
driver = webdriver.Chrome(options=chrome_options)
319+
driver.get(f"file://{temp_path}")
320+
time.sleep(10) # Wait for network simulation to settle
321+
driver.save_screenshot("plot.png")
322+
driver.quit()
323+
324+
Path(temp_path).unlink() # Clean up temp file

0 commit comments

Comments
 (0)