Directed Network Graph — Plotly

A directed network graph visualizes relationships between entities using nodes connected by edges with arrows, indicating the direction of relationships or flow. Unlike undirected graphs, directed graphs reveal asymmetric relationships such as dependencies, hierarchies, or information flow. The arrows clearly communicate which entity points to which, making cause-and-effect relationships and directional dependencies immediately visible.

Directed Network Graph rendered with Plotly

Python source (Plotly)

""" anyplot.ai
network-directed: Directed Network Graph
Library: plotly 6.7.0 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-14
"""

import os

import numpy as np
import plotly.graph_objects as go


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Okabe-Ito palette (first series is #009E73)
IMPRINT = [
    "#009E73",  # Brand green (entry point)
    "#C475FD",  # Vermillion (core)
    "#4467A3",  # Blue (data)
    "#BD8233",  # Reddish purple (helpers)
]

# Data: Software module dependencies (arrows show import direction)
np.random.seed(42)

nodes = [
    {"id": 0, "label": "main", "group": 0},
    {"id": 1, "label": "api", "group": 1},
    {"id": 2, "label": "auth", "group": 1},
    {"id": 3, "label": "database", "group": 1},
    {"id": 4, "label": "models", "group": 2},
    {"id": 5, "label": "utils", "group": 3},
    {"id": 6, "label": "config", "group": 3},
    {"id": 7, "label": "logging", "group": 3},
    {"id": 8, "label": "cache", "group": 1},
    {"id": 9, "label": "router", "group": 1},
    {"id": 10, "label": "middleware", "group": 1},
    {"id": 11, "label": "validators", "group": 2},
    {"id": 12, "label": "schemas", "group": 2},
]

# Directed edges with bidirectional pairs
edges = [
    (0, 1, False),  # main -> api
    (0, 6, False),  # main -> config
    (0, 7, False),  # main -> logging
    (1, 2, False),  # api -> auth
    (1, 9, False),  # api -> router
    (1, 10, False),  # api -> middleware
    (2, 3, False),  # auth -> database
    (2, 5, False),  # auth -> utils
    (3, 4, False),  # database -> models
    (3, 8, False),  # database -> cache
    (4, 12, False),  # models -> schemas
    (5, 7, False),  # utils -> logging
    (6, 7, False),  # config -> logging
    (8, 7, False),  # cache -> logging
    (9, 10, False),  # router -> middleware
    (9, 11, False),  # router -> validators
    (10, 2, False),  # middleware -> auth
    (11, 12, False),  # validators -> schemas
    (12, 5, True),  # schemas <-> utils (bidirectional)
]

# Circular layout
n_nodes = len(nodes)
angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)
radius = 3
node_x = radius * np.cos(angles)
node_y = radius * np.sin(angles)

# Create figure
fig = go.Figure()

# Add edges as lines with arrows
for idx, edge_data in enumerate(edges):
    if len(edge_data) == 3:
        source, target, bidirectional = edge_data
    else:
        source, target = edge_data
        bidirectional = False

    x0, y0 = node_x[source], node_y[source]
    x1, y1 = node_x[target], node_y[target]

    # Calculate direction vector
    dx, dy = x1 - x0, y1 - y0
    length = np.sqrt(dx**2 + dy**2)
    if length > 0:
        dx, dy = dx / length, dy / length

    # Node radius for adjustment
    node_radius = 0.35

    # For bidirectional edges, use curved paths
    if bidirectional:
        # Create a curved path using intermediate points
        # Control point offset for the curve
        perp_x, perp_y = -dy, dx
        offset = 0.5

        # Start and end points adjusted for node radius
        x0_adj = x0 + dx * node_radius
        y0_adj = y0 + dy * node_radius
        x1_adj = x1 - dx * node_radius
        y1_adj = y1 - dy * node_radius

        # Midpoint
        mid_x = (x0_adj + x1_adj) / 2 + perp_x * offset
        mid_y = (y0_adj + y1_adj) / 2 + perp_y * offset

        # Create curved edge
        t = np.linspace(0, 1, 20)
        curve_x = []
        curve_y = []
        for ti in t:
            # Quadratic Bezier curve
            bx = (1 - ti) ** 2 * x0_adj + 2 * (1 - ti) * ti * mid_x + ti**2 * x1_adj
            by = (1 - ti) ** 2 * y0_adj + 2 * (1 - ti) * ti * mid_y + ti**2 * y1_adj
            curve_x.append(bx)
            curve_y.append(by)

        fig.add_trace(
            go.Scatter(
                x=curve_x,
                y=curve_y,
                mode="lines",
                line=dict(width=2, color=INK_SOFT),
                hoverinfo="none",
                showlegend=False,
            )
        )
    else:
        # Straight edge
        x0_adj = x0 + dx * node_radius
        y0_adj = y0 + dy * node_radius
        x1_adj = x1 - dx * node_radius
        y1_adj = y1 - dy * node_radius

        fig.add_trace(
            go.Scatter(
                x=[x0_adj, x1_adj],
                y=[y0_adj, y1_adj],
                mode="lines",
                line=dict(width=2, color=INK_SOFT),
                hoverinfo="none",
                showlegend=False,
            )
        )

# Add arrowheads
for idx, edge_data in enumerate(edges):
    if len(edge_data) == 3:
        source, target, bidirectional = edge_data
    else:
        source, target = edge_data
        bidirectional = False

    x0, y0 = node_x[source], node_y[source]
    x1, y1 = node_x[target], node_y[target]

    dx, dy = x1 - x0, y1 - y0
    length = np.sqrt(dx**2 + dy**2)
    if length > 0:
        dx, dy = dx / length, dy / length

    node_radius = 0.4
    ax = x1 - dx * node_radius
    ay = y1 - dy * node_radius

    fig.add_annotation(
        x=ax,
        y=ay,
        ax=x1 - dx * (node_radius + 0.25),
        ay=y1 - dy * (node_radius + 0.25),
        xref="x",
        yref="y",
        axref="x",
        ayref="y",
        showarrow=True,
        arrowhead=2,
        arrowsize=1.5,
        arrowwidth=2,
        arrowcolor=INK_SOFT,
    )

    # For bidirectional edges, add second arrow in opposite direction
    if bidirectional:
        bx = x0 + dx * node_radius
        by = y0 + dy * node_radius
        fig.add_annotation(
            x=bx,
            y=by,
            ax=x0 + dx * (node_radius + 0.25),
            ay=y0 + dy * (node_radius + 0.25),
            xref="x",
            yref="y",
            axref="x",
            ayref="y",
            showarrow=True,
            arrowhead=2,
            arrowsize=1.5,
            arrowwidth=2,
            arrowcolor=INK_SOFT,
        )

# Add nodes by group
group_names = ["Entry", "Core", "Data", "Helpers"]
for group_idx in range(len(IMPRINT)):
    group_nodes = [n for n in nodes if n["group"] == group_idx]
    group_x = [node_x[n["id"]] for n in group_nodes]
    group_y = [node_y[n["id"]] for n in group_nodes]
    group_labels = [n["label"] for n in group_nodes]

    fig.add_trace(
        go.Scatter(
            x=group_x,
            y=group_y,
            mode="markers+text",
            marker=dict(size=18, color=IMPRINT[group_idx], line=dict(width=1.5, color=INK_SOFT)),
            text=group_labels,
            textposition="middle center",
            textfont=dict(size=14, color=INK, family="monospace"),
            name=group_names[group_idx],
            hovertemplate="<b>%{text}</b><br>Group: " + group_names[group_idx] + "<extra></extra>",
        )
    )

# Update layout
fig.update_layout(
    title=dict(text="network-directed · plotly · anyplot.ai", font=dict(size=28, color=INK), x=0.5, xanchor="center"),
    xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title="", range=[-4.2, 4.2]),
    yaxis=dict(
        showgrid=False, zeroline=False, showticklabels=False, title="", range=[-4.2, 4.2], scaleanchor="x", scaleratio=1
    ),
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    showlegend=True,
    legend=dict(
        title=dict(text="Module Groups", font=dict(size=18, color=INK)),
        font=dict(size=16, color=INK_SOFT),
        x=1.02,
        y=0.5,
        yanchor="middle",
        bgcolor=ELEVATED_BG,
        bordercolor=INK_SOFT,
        borderwidth=1,
    ),
    margin=dict(l=50, r=200, t=100, b=50),
    hovermode="closest",
)

# Save outputs
fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Directed Network Graph on anyplot.ai.

Other implementations