|
| 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