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