{"spec_id":"network-directed","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nnetwork-directed: Directed Network Graph\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\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\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette (first series is #009E73)\nIMPRINT = [\n    \"#009E73\",  # Brand green (entry point)\n    \"#C475FD\",  # Vermillion (core)\n    \"#4467A3\",  # Blue (data)\n    \"#BD8233\",  # Reddish purple (helpers)\n]\n\n# Data: Software module dependencies (arrows show import direction)\nnp.random.seed(42)\n\nnodes = [\n    {\"id\": 0, \"label\": \"main\", \"group\": 0},\n    {\"id\": 1, \"label\": \"api\", \"group\": 1},\n    {\"id\": 2, \"label\": \"auth\", \"group\": 1},\n    {\"id\": 3, \"label\": \"database\", \"group\": 1},\n    {\"id\": 4, \"label\": \"models\", \"group\": 2},\n    {\"id\": 5, \"label\": \"utils\", \"group\": 3},\n    {\"id\": 6, \"label\": \"config\", \"group\": 3},\n    {\"id\": 7, \"label\": \"logging\", \"group\": 3},\n    {\"id\": 8, \"label\": \"cache\", \"group\": 1},\n    {\"id\": 9, \"label\": \"router\", \"group\": 1},\n    {\"id\": 10, \"label\": \"middleware\", \"group\": 1},\n    {\"id\": 11, \"label\": \"validators\", \"group\": 2},\n    {\"id\": 12, \"label\": \"schemas\", \"group\": 2},\n]\n\n# Directed edges with bidirectional pairs\nedges = [\n    (0, 1, False),  # main -> api\n    (0, 6, False),  # main -> config\n    (0, 7, False),  # main -> logging\n    (1, 2, False),  # api -> auth\n    (1, 9, False),  # api -> router\n    (1, 10, False),  # api -> middleware\n    (2, 3, False),  # auth -> database\n    (2, 5, False),  # auth -> utils\n    (3, 4, False),  # database -> models\n    (3, 8, False),  # database -> cache\n    (4, 12, False),  # models -> schemas\n    (5, 7, False),  # utils -> logging\n    (6, 7, False),  # config -> logging\n    (8, 7, False),  # cache -> logging\n    (9, 10, False),  # router -> middleware\n    (9, 11, False),  # router -> validators\n    (10, 2, False),  # middleware -> auth\n    (11, 12, False),  # validators -> schemas\n    (12, 5, True),  # schemas <-> utils (bidirectional)\n]\n\n# Circular layout\nn_nodes = len(nodes)\nangles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)\nradius = 3\nnode_x = radius * np.cos(angles)\nnode_y = radius * np.sin(angles)\n\n# Create figure\nfig = go.Figure()\n\n# Add edges as lines with arrows\nfor idx, edge_data in enumerate(edges):\n    if len(edge_data) == 3:\n        source, target, bidirectional = edge_data\n    else:\n        source, target = edge_data\n        bidirectional = False\n\n    x0, y0 = node_x[source], node_y[source]\n    x1, y1 = node_x[target], node_y[target]\n\n    # Calculate direction vector\n    dx, dy = x1 - x0, y1 - y0\n    length = np.sqrt(dx**2 + dy**2)\n    if length > 0:\n        dx, dy = dx / length, dy / length\n\n    # Node radius for adjustment\n    node_radius = 0.35\n\n    # For bidirectional edges, use curved paths\n    if bidirectional:\n        # Create a curved path using intermediate points\n        # Control point offset for the curve\n        perp_x, perp_y = -dy, dx\n        offset = 0.5\n\n        # Start and end points adjusted for node radius\n        x0_adj = x0 + dx * node_radius\n        y0_adj = y0 + dy * node_radius\n        x1_adj = x1 - dx * node_radius\n        y1_adj = y1 - dy * node_radius\n\n        # Midpoint\n        mid_x = (x0_adj + x1_adj) / 2 + perp_x * offset\n        mid_y = (y0_adj + y1_adj) / 2 + perp_y * offset\n\n        # Create curved edge\n        t = np.linspace(0, 1, 20)\n        curve_x = []\n        curve_y = []\n        for ti in t:\n            # Quadratic Bezier curve\n            bx = (1 - ti) ** 2 * x0_adj + 2 * (1 - ti) * ti * mid_x + ti**2 * x1_adj\n            by = (1 - ti) ** 2 * y0_adj + 2 * (1 - ti) * ti * mid_y + ti**2 * y1_adj\n            curve_x.append(bx)\n            curve_y.append(by)\n\n        fig.add_trace(\n            go.Scatter(\n                x=curve_x,\n                y=curve_y,\n                mode=\"lines\",\n                line=dict(width=2, color=INK_SOFT),\n                hoverinfo=\"none\",\n                showlegend=False,\n            )\n        )\n    else:\n        # Straight edge\n        x0_adj = x0 + dx * node_radius\n        y0_adj = y0 + dy * node_radius\n        x1_adj = x1 - dx * node_radius\n        y1_adj = y1 - dy * node_radius\n\n        fig.add_trace(\n            go.Scatter(\n                x=[x0_adj, x1_adj],\n                y=[y0_adj, y1_adj],\n                mode=\"lines\",\n                line=dict(width=2, color=INK_SOFT),\n                hoverinfo=\"none\",\n                showlegend=False,\n            )\n        )\n\n# Add arrowheads\nfor idx, edge_data in enumerate(edges):\n    if len(edge_data) == 3:\n        source, target, bidirectional = edge_data\n    else:\n        source, target = edge_data\n        bidirectional = False\n\n    x0, y0 = node_x[source], node_y[source]\n    x1, y1 = node_x[target], node_y[target]\n\n    dx, dy = x1 - x0, y1 - y0\n    length = np.sqrt(dx**2 + dy**2)\n    if length > 0:\n        dx, dy = dx / length, dy / length\n\n    node_radius = 0.4\n    ax = x1 - dx * node_radius\n    ay = y1 - dy * node_radius\n\n    fig.add_annotation(\n        x=ax,\n        y=ay,\n        ax=x1 - dx * (node_radius + 0.25),\n        ay=y1 - dy * (node_radius + 0.25),\n        xref=\"x\",\n        yref=\"y\",\n        axref=\"x\",\n        ayref=\"y\",\n        showarrow=True,\n        arrowhead=2,\n        arrowsize=1.5,\n        arrowwidth=2,\n        arrowcolor=INK_SOFT,\n    )\n\n    # For bidirectional edges, add second arrow in opposite direction\n    if bidirectional:\n        bx = x0 + dx * node_radius\n        by = y0 + dy * node_radius\n        fig.add_annotation(\n            x=bx,\n            y=by,\n            ax=x0 + dx * (node_radius + 0.25),\n            ay=y0 + dy * (node_radius + 0.25),\n            xref=\"x\",\n            yref=\"y\",\n            axref=\"x\",\n            ayref=\"y\",\n            showarrow=True,\n            arrowhead=2,\n            arrowsize=1.5,\n            arrowwidth=2,\n            arrowcolor=INK_SOFT,\n        )\n\n# Add nodes by group\ngroup_names = [\"Entry\", \"Core\", \"Data\", \"Helpers\"]\nfor group_idx in range(len(IMPRINT)):\n    group_nodes = [n for n in nodes if n[\"group\"] == group_idx]\n    group_x = [node_x[n[\"id\"]] for n in group_nodes]\n    group_y = [node_y[n[\"id\"]] for n in group_nodes]\n    group_labels = [n[\"label\"] for n in group_nodes]\n\n    fig.add_trace(\n        go.Scatter(\n            x=group_x,\n            y=group_y,\n            mode=\"markers+text\",\n            marker=dict(size=18, color=IMPRINT[group_idx], line=dict(width=1.5, color=INK_SOFT)),\n            text=group_labels,\n            textposition=\"middle center\",\n            textfont=dict(size=14, color=INK, family=\"monospace\"),\n            name=group_names[group_idx],\n            hovertemplate=\"<b>%{text}</b><br>Group: \" + group_names[group_idx] + \"<extra></extra>\",\n        )\n    )\n\n# Update layout\nfig.update_layout(\n    title=dict(text=\"network-directed · plotly · anyplot.ai\", font=dict(size=28, color=INK), x=0.5, xanchor=\"center\"),\n    xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title=\"\", range=[-4.2, 4.2]),\n    yaxis=dict(\n        showgrid=False, zeroline=False, showticklabels=False, title=\"\", range=[-4.2, 4.2], scaleanchor=\"x\", scaleratio=1\n    ),\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    showlegend=True,\n    legend=dict(\n        title=dict(text=\"Module Groups\", font=dict(size=18, color=INK)),\n        font=dict(size=16, color=INK_SOFT),\n        x=1.02,\n        y=0.5,\n        yanchor=\"middle\",\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n    ),\n    margin=dict(l=50, r=200, t=100, b=50),\n    hovermode=\"closest\",\n)\n\n# Save outputs\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}