{"spec_id":"arc-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\narc-basic: Basic Arc Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-30\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nfrom PIL import Image\n\n\n# Drop script directory from sys.path so the `altair` package resolves, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Data: Character interactions in a story chapter\nnp.random.seed(42)\n\nnodes = [\"Alice\", \"Bob\", \"Carol\", \"David\", \"Eve\", \"Frank\", \"Grace\", \"Henry\", \"Iris\", \"Jack\"]\nn_nodes = len(nodes)\n\nedges = [\n    (0, 1, 3),  # Alice-Bob (strong)\n    (0, 3, 2),  # Alice-David\n    (1, 2, 2),  # Bob-Carol\n    (2, 4, 1),  # Carol-Eve\n    (3, 5, 2),  # David-Frank\n    (4, 6, 1),  # Eve-Grace\n    (0, 7, 1),  # Alice-Henry (long-range)\n    (1, 5, 2),  # Bob-Frank\n    (2, 3, 3),  # Carol-David (strong)\n    (5, 8, 1),  # Frank-Iris\n    (6, 9, 2),  # Grace-Jack\n    (0, 9, 1),  # Alice-Jack (longest range)\n    (3, 7, 2),  # David-Henry\n    (7, 8, 1),  # Henry-Iris\n    (8, 9, 2),  # Iris-Jack\n]\n\nx_positions = np.linspace(0, 100, n_nodes)\ny_baseline = 0\n\nconnection_count = [0] * n_nodes\nfor s, e, w in edges:\n    connection_count[s] += w\n    connection_count[e] += w\n\nnodes_df = pd.DataFrame({\"x\": x_positions, \"y\": [y_baseline] * n_nodes, \"name\": nodes, \"connections\": connection_count})\n\n# Build arc paths as semicircular curves\narc_data = []\npoints_per_arc = 50\nmax_span = max(abs(e - s) for s, e, _ in edges)\n\nfor edge_id, (start, end, weight) in enumerate(edges):\n    x_start = x_positions[start]\n    x_end = x_positions[end]\n    span = abs(end - start)\n    height = 7 * span\n\n    angles = np.linspace(0, np.pi, points_per_arc)\n    x_center = (x_start + x_end) / 2\n    radius_x = abs(x_end - x_start) / 2\n    radius_y = height / 2\n\n    pair = f\"{nodes[start]}–{nodes[end]}\"\n    for i, angle in enumerate(angles):\n        arc_data.append(\n            {\n                \"edge_id\": edge_id,\n                \"x\": x_center - radius_x * np.cos(angle),\n                \"y\": y_baseline + radius_y * np.sin(angle),\n                \"weight\": weight,\n                \"pair\": pair,\n                \"order\": i,\n            }\n        )\n\narcs_df = pd.DataFrame(arc_data)\n\nmax_arc_height = 7 * max_span / 2\ny_domain = [-8, max_arc_height + 4]\nx_domain = [-4, 104]\n\nhover = alt.selection_point(on=\"pointerover\", empty=False, fields=[\"edge_id\"])\n\n# Imprint sequential palette for arc weight: brand green → blue (imprint_seq)\nARC_LOW = \"#009E73\"  # Imprint position 1 — brand green (weak)\nARC_MID = \"#22828B\"  # midpoint of imprint_seq\nARC_HIGH = \"#4467A3\"  # Imprint position 3 — blue (strong)\n\n# Arcs: weight drives color (Imprint sequential), thickness, and opacity\narcs = (\n    alt.Chart(arcs_df)\n    .mark_line()\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=x_domain)),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=y_domain)),\n        detail=\"edge_id:N\",\n        strokeWidth=alt.StrokeWidth(\n            \"weight:Q\",\n            scale=alt.Scale(domain=[1, 3], range=[1.5, 5]),\n            legend=alt.Legend(\n                title=\"Interaction Strength\",\n                titleFontSize=12,\n                labelFontSize=10,\n                orient=\"top-left\",\n                offset=10,\n                values=[1, 2, 3],\n                symbolStrokeWidth=3,\n                labelExpr=\"datum.value == 1 ? 'Weak' : datum.value == 2 ? 'Moderate' : 'Strong'\",\n            ),\n        ),\n        strokeOpacity=alt.condition(\n            hover,\n            alt.value(0.95),\n            alt.StrokeOpacity(\"weight:Q\", scale=alt.Scale(domain=[1, 3], range=[0.5, 0.9]), legend=None),\n        ),\n        color=alt.Color(\"weight:Q\", scale=alt.Scale(domain=[1, 2, 3], range=[ARC_LOW, ARC_MID, ARC_HIGH]), legend=None),\n        tooltip=[alt.Tooltip(\"pair:N\", title=\"Connection\"), alt.Tooltip(\"weight:Q\", title=\"Strength\")],\n    )\n    .add_params(hover)\n)\n\n# Nodes: Imprint ochre fill, size proportional to total connection weight\nnode_points = (\n    alt.Chart(nodes_df)\n    .mark_circle(fill=\"#BD8233\", stroke=INK, strokeWidth=2.0)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=x_domain)),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=y_domain)),\n        size=alt.Size(\"connections:Q\", scale=alt.Scale(domain=[2, 11], range=[100, 400]), legend=None),\n        tooltip=[alt.Tooltip(\"name:N\", title=\"Character\"), alt.Tooltip(\"connections:Q\", title=\"Total Weight\")],\n    )\n)\n\n# Node labels below baseline — theme-adaptive ink color\nnode_labels = (\n    alt.Chart(nodes_df)\n    .mark_text(dy=22, fontSize=16, fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"name:N\")\n)\n\ntitle_str = \"arc-basic · python · altair · anyplot.ai\"\n\nchart = (\n    alt.layer(arcs, node_points, node_labels)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 10, \"right\": 10, \"top\": 10, \"bottom\": 30},\n        title=alt.Title(title_str, fontSize=16, anchor=\"middle\", offset=10, color=INK),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, titleColor=INK, labelColor=INK_SOFT, padding=10)\n)\n\n# Save — canvas contract: 3200 × 1800 (landscape)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}