{"spec_id":"arc-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\narc-basic: Basic Arc Diagram\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-30\n\"\"\"\n\nimport sys\n\n\nsys.path = sys.path[1:]  # Prevent self-import: script dir is removed so 'import plotly' finds the package\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens (see prompts/default-style-guide.md)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette positions 1→3 (green → lavender → blue)\narc_styles = {\n    1: {\"color\": \"rgba(0, 158, 115, 0.65)\", \"width\": 3.5, \"label\": \"Weak (1)\"},\n    2: {\"color\": \"rgba(196, 117, 253, 0.82)\", \"width\": 4.5, \"label\": \"Medium (2)\"},\n    3: {\"color\": \"rgba(68, 103, 163, 0.95)\", \"width\": 6.0, \"label\": \"Strong (3)\"},\n}\n\n# Data: Character interactions in a story narrative\nnodes = [\"Alice\", \"Bob\", \"Carol\", \"David\", \"Eve\", \"Frank\", \"Grace\", \"Henry\", \"Iris\", \"Jack\"]\nn_nodes = len(nodes)\nedges = [\n    (0, 1, 3),  # Alice-Bob (strong)\n    (0, 3, 2),  # Alice-David\n    (1, 2, 2),  # Bob-Carol\n    (2, 4, 3),  # Carol-Eve (strong)\n    (3, 5, 2),  # David-Frank\n    (4, 6, 2),  # Eve-Grace\n    (5, 7, 3),  # Frank-Henry (strong)\n    (0, 8, 1),  # Alice-Iris (long, weak)\n    (2, 9, 2),  # Carol-Jack (long)\n    (1, 4, 2),  # Bob-Eve\n    (3, 7, 1),  # David-Henry (long, weak)\n    (6, 9, 2),  # Grace-Jack\n]\nx_positions = np.linspace(0, 10, n_nodes)\n\n# Figure\nfig = go.Figure()\ntrace_weights = []\n\n# Parabolic arcs\nfor src, tgt, weight in edges:\n    x_src, x_tgt = x_positions[src], x_positions[tgt]\n    arc_height = abs(tgt - src) * 0.45\n    t = np.linspace(0, 1, 50)\n    x_arc = x_src + t * (x_tgt - x_src)\n    y_arc = arc_height * 4 * t * (1 - t)\n    style = arc_styles[weight]\n    distance = abs(tgt - src)\n    fig.add_trace(\n        go.Scatter(\n            x=x_arc,\n            y=y_arc,\n            mode=\"lines\",\n            line={\"width\": style[\"width\"], \"color\": style[\"color\"]},\n            hovertemplate=(\n                f\"<b>{nodes[src]} — {nodes[tgt]}</b><br>\"\n                f\"Weight: <b>{weight}</b> ({style['label'].split()[0].lower()})<br>\"\n                f\"Distance: {distance} positions<extra></extra>\"\n            ),\n            showlegend=False,\n        )\n    )\n    trace_weights.append(weight)\n\n# Dummy traces for legend entries\nfor w in [3, 2, 1]:\n    style = arc_styles[w]\n    fig.add_trace(\n        go.Scatter(\n            x=[None],\n            y=[None],\n            mode=\"lines\",\n            line={\"width\": style[\"width\"], \"color\": style[\"color\"]},\n            name=style[\"label\"],\n            showlegend=True,\n        )\n    )\n    trace_weights.append(w)\n\n# Per-node connection count for hover\nconn_count = [0] * n_nodes\nfor src, tgt, _ in edges:\n    conn_count[src] += 1\n    conn_count[tgt] += 1\n\n# Node markers on baseline\nfig.add_trace(\n    go.Scatter(\n        x=x_positions,\n        y=np.zeros(n_nodes),\n        mode=\"markers+text\",\n        marker={\"size\": 20, \"color\": ELEVATED_BG, \"line\": {\"width\": 2.5, \"color\": INK}},\n        text=nodes,\n        textposition=\"bottom center\",\n        textfont={\"size\": 14, \"color\": INK},\n        customdata=np.array(conn_count),\n        hovertemplate=\"<b>%{text}</b><br>Connections: %{customdata}<extra></extra>\",\n        showlegend=False,\n    )\n)\ntrace_weights.append(0)\n\n# Horizontal baseline\nfig.add_shape(\n    type=\"line\", x0=x_positions[0] - 0.3, x1=x_positions[-1] + 0.3, y0=0, y1=0, line={\"width\": 1.5, \"color\": INK_SOFT}\n)\n\n# Annotate the longest-range arc (Alice – Iris, distance = 8)\nmid_x = (x_positions[0] + x_positions[8]) / 2\npeak_y = abs(8 - 0) * 0.45  # arc peaks at t=0.5: height * 4 * 0.5 * 0.5 = height\nfig.add_annotation(\n    x=mid_x,\n    y=peak_y,\n    text=\"longest range\",\n    showarrow=True,\n    arrowhead=2,\n    arrowwidth=1.5,\n    arrowcolor=INK_SOFT,\n    ax=55,\n    ay=-25,\n    font={\"size\": 12, \"color\": INK_MUTED, \"family\": \"Arial\"},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderpad=4,\n)\n\n# Interactive filter buttons (Plotly updatemenus)\nfilter_options = [(\"All\", {1, 2, 3}), (\"Strong\", {3}), (\"Medium\", {2}), (\"Weak\", {1})]\nbuttons = []\nfor label, keep in filter_options:\n    visible = [True if tw == 0 else (tw in keep) for tw in trace_weights]\n    buttons.append({\"label\": label, \"method\": \"update\", \"args\": [{\"visible\": visible}]})\n\n# Title with subtitle\ntitle_text = \"Character Interactions · arc-basic · python · plotly · anyplot.ai\"\ntitle_fontsize = max(11, round(16 * 67 / len(title_text))) if len(title_text) > 67 else 16\ntitle_html = (\n    f\"<b>{title_text}</b>\"\n    f\"<br><span style='font-size:11px;color:{INK_MUTED};font-weight:normal'>\"\n    f\"Story narrative · 10 characters · 12 connections</span>\"\n)\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    template=\"plotly_white\",\n    font={\"color\": INK},\n    title={\n        \"text\": title_html,\n        \"font\": {\"size\": title_fontsize, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.97,\n    },\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False, \"range\": [-0.5, 10.5]},\n    yaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False, \"range\": [-0.45, 3.9]},\n    hovermode=\"closest\",\n    hoverlabel={\"bgcolor\": ELEVATED_BG, \"font_size\": 16, \"font_color\": INK, \"bordercolor\": INK_SOFT},\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    legend={\n        \"title\": {\"text\": \"Connection Strength\", \"font\": {\"size\": 14, \"color\": INK}},\n        \"font\": {\"size\": 11, \"color\": INK_SOFT},\n        \"x\": 0.98,\n        \"y\": 0.98,\n        \"xanchor\": \"right\",\n        \"yanchor\": \"top\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    updatemenus=[\n        {\n            \"type\": \"buttons\",\n            \"direction\": \"right\",\n            \"x\": 0.02,\n            \"y\": 0.98,\n            \"xanchor\": \"left\",\n            \"yanchor\": \"top\",\n            \"buttons\": buttons,\n            \"showactive\": THEME == \"light\",\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_SOFT,\n            \"font\": {\"size\": 11, \"color\": INK},\n            \"pad\": {\"r\": 8, \"t\": 8},\n        }\n    ],\n)\n\n# Save — canvas target: 3200 × 1800 px (landscape, width=800 height=450 scale=4)\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(\n    f\"plot-{THEME}.html\",\n    include_plotlyjs=\"cdn\",\n    config={\n        \"displaylogo\": False,\n        \"modeBarButtonsToRemove\": [\"lasso2d\", \"select2d\"],\n        \"toImageButtonOptions\": {\"width\": 3200, \"height\": 1800, \"scale\": 1},\n    },\n)\n"}