| 258 | |
| 259 | |
| 260 | def _html_script(nodes_json: str, edges_json: str, legend_json: str) -> str: |
| 261 | return f"""<script> |
| 262 | const RAW_NODES = {nodes_json}; |
| 263 | const RAW_EDGES = {edges_json}; |
| 264 | const LEGEND = {legend_json}; |
| 265 | |
| 266 | // HTML-escape helper — prevents XSS when injecting graph data into innerHTML |
| 267 | function esc(s) {{ |
| 268 | return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); |
| 269 | }} |
| 270 | |
| 271 | // Build vis datasets |
| 272 | const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({{ |
| 273 | id: n.id, label: n.label, color: n.color, size: n.size, |
| 274 | font: n.font, title: n.title, |
| 275 | _community: n.community, _community_name: n.community_name, |
| 276 | _source_file: n.source_file, _file_type: n.file_type, _degree: n.degree, |
| 277 | }}))); |
| 278 | |
| 279 | const edgesDS = new vis.DataSet(RAW_EDGES.map((e, i) => ({{ |
| 280 | id: i, from: e.from, to: e.to, |
| 281 | label: '', |
| 282 | title: e.title, |
| 283 | dashes: e.dashes, |
| 284 | width: e.width, |
| 285 | color: e.color, |
| 286 | arrows: {{ to: {{ enabled: true, scaleFactor: 0.5 }} }}, |
| 287 | }}))); |
| 288 | |
| 289 | const container = document.getElementById('graph'); |
| 290 | const network = new vis.Network(container, {{ nodes: nodesDS, edges: edgesDS }}, {{ |
| 291 | physics: {{ |
| 292 | enabled: true, |
| 293 | solver: 'forceAtlas2Based', |
| 294 | forceAtlas2Based: {{ |
| 295 | gravitationalConstant: -60, |
| 296 | centralGravity: 0.005, |
| 297 | springLength: 120, |
| 298 | springConstant: 0.08, |
| 299 | damping: 0.4, |
| 300 | avoidOverlap: 0.8, |
| 301 | }}, |
| 302 | stabilization: {{ iterations: 200, fit: true }}, |
| 303 | }}, |
| 304 | interaction: {{ |
| 305 | hover: true, |
| 306 | tooltipDelay: 100, |
| 307 | hideEdgesOnDrag: true, |
| 308 | navigationButtons: false, |
| 309 | keyboard: false, |
| 310 | }}, |
| 311 | nodes: {{ shape: 'dot', borderWidth: 1.5 }}, |
| 312 | edges: {{ smooth: {{ type: 'continuous', roundness: 0.2 }}, selectionWidth: 3 }}, |
| 313 | }}); |
| 314 | |
| 315 | network.once('stabilizationIterationsDone', () => {{ |
| 316 | network.setOptions({{ physics: {{ enabled: false }} }}); |
| 317 | }}); |