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.

""" anyplot.ai
network-directed: Directed Network Graph
Library: altair 6.1.0 | Python 3.13.13
Quality: 92/100 | Updated: 2026-05-14
"""
import os
import sys
from pathlib import Path
# Avoid name collision with script file named altair.py
script_dir = str(Path(__file__).parent)
if script_dir in sys.path:
sys.path.remove(script_dir)
import altair as alt
import numpy as np
import pandas as pd
# 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"
np.random.seed(42)
# Data: Software package dependency graph
nodes = [
{"id": "app", "label": "App", "group": "main"},
{"id": "api", "label": "API", "group": "core"},
{"id": "auth", "label": "Auth", "group": "core"},
{"id": "database", "label": "Database", "group": "core"},
{"id": "cache", "label": "Cache", "group": "service"},
{"id": "logger", "label": "Logger", "group": "util"},
{"id": "config", "label": "Config", "group": "util"},
{"id": "utils", "label": "Utils", "group": "util"},
{"id": "models", "label": "Models", "group": "data"},
{"id": "schemas", "label": "Schemas", "group": "data"},
{"id": "router", "label": "Router", "group": "core"},
{"id": "middleware", "label": "Middleware", "group": "core"},
]
# Directed edges: (source, target) - arrows point from source to target
edges = [
("app", "api"),
("app", "auth"),
("app", "router"),
("api", "database"),
("api", "cache"),
("api", "models"),
("auth", "database"),
("auth", "cache"),
("auth", "logger"),
("database", "config"),
("database", "logger"),
("cache", "config"),
("cache", "logger"),
("router", "middleware"),
("router", "api"),
("middleware", "auth"),
("middleware", "logger"),
("models", "schemas"),
("models", "utils"),
("schemas", "utils"),
("logger", "config"),
("utils", "config"),
("api", "auth"),
("cache", "database"),
]
# Okabe-Ito palette for groups
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"]
group_colors = {
"main": IMPRINT[0], # Brand green
"core": IMPRINT[1], # Vermillion
"service": IMPRINT[2], # Blue
"util": IMPRINT[3], # Reddish purple
"data": IMPRINT[4], # Orange
}
# Node positions using hierarchical layout based on dependency depth
depths = {"app": 0}
for _ in range(len(nodes)):
for source, target in edges:
if source in depths:
current_depth = depths.get(target, -1)
depths[target] = max(current_depth, depths[source] + 1)
for node in nodes:
if node["id"] not in depths:
depths[node["id"]] = 0
depth_groups = {}
for node_id, depth in depths.items():
if depth not in depth_groups:
depth_groups[depth] = []
depth_groups[depth].append(node_id)
positions = {}
max_depth = max(depths.values()) if depths else 0
for depth, node_ids in depth_groups.items():
n_nodes = len(node_ids)
for i, node_id in enumerate(node_ids):
x = depth / max(max_depth, 1)
y = (i + 0.5) / n_nodes
positions[node_id] = (x, y)
# Create node DataFrame
node_df = pd.DataFrame(
[
{
"id": n["id"],
"label": n["label"],
"group": n["group"],
"x": positions[n["id"]][0],
"y": positions[n["id"]][1],
}
for n in nodes
]
)
# Identify bidirectional edge pairs
edge_set = set(edges)
bidirectional_pairs = set()
for source, target in edges:
if (target, source) in edge_set:
bidirectional_pairs.add(tuple(sorted([source, target])))
# Create edge DataFrame with arrow coordinates
edge_data = []
curved_edge_data = []
for source, target in edges:
sx, sy = positions[source]
tx, ty = positions[target]
is_bidirectional = tuple(sorted([source, target])) in bidirectional_pairs
dx, dy = tx - sx, ty - sy
length = np.sqrt(dx**2 + dy**2)
if length > 0:
offset = 0.03
sx_adj = sx + dx / length * offset
sy_adj = sy + dy / length * offset
tx_adj = tx - dx / length * offset
ty_adj = ty - dy / length * offset
else:
sx_adj, sy_adj = sx, sy
tx_adj, ty_adj = tx, ty
if is_bidirectional:
perp_x, perp_y = -dy / length * 0.05, dx / length * 0.05
mid_x, mid_y = (sx + tx) / 2 + perp_x, (sy + ty) / 2 + perp_y
for t in np.linspace(0, 1, 10):
t_next = min(t + 0.1, 1)
bx1 = (1 - t) ** 2 * sx_adj + 2 * (1 - t) * t * mid_x + t**2 * tx_adj
by1 = (1 - t) ** 2 * sy_adj + 2 * (1 - t) * t * mid_y + t**2 * ty_adj
bx2 = (1 - t_next) ** 2 * sx_adj + 2 * (1 - t_next) * t_next * mid_x + t_next**2 * tx_adj
by2 = (1 - t_next) ** 2 * sy_adj + 2 * (1 - t_next) * t_next * mid_y + t_next**2 * ty_adj
curved_edge_data.append({"x": bx1, "y": by1, "x2": bx2, "y2": by2, "edge_id": f"{source}-{target}"})
else:
edge_data.append({"source": source, "target": target, "x": sx_adj, "y": sy_adj, "x2": tx_adj, "y2": ty_adj})
edge_df = pd.DataFrame(edge_data)
curved_edge_df = pd.DataFrame(curved_edge_data) if curved_edge_data else pd.DataFrame(columns=["x", "y", "x2", "y2"])
# Create arrow head data
arrow_data = []
for source, target in edges:
sx, sy = positions[source]
tx, ty = positions[target]
dx, dy = tx - sx, ty - sy
length = np.sqrt(dx**2 + dy**2)
if length > 0:
is_bidirectional = tuple(sorted([source, target])) in bidirectional_pairs
if is_bidirectional:
perp_x, perp_y = -dy / length * 0.05, dx / length * 0.05
mid_x, mid_y = (sx + tx) / 2 + perp_x, (sy + ty) / 2 + perp_y
t = 0.95
offset = 0.03
sx_adj = sx + dx / length * offset
sy_adj = sy + dy / length * offset
tx_adj = tx - dx / length * offset
ty_adj = ty - dy / length * offset
ax = (1 - t) ** 2 * sx_adj + 2 * (1 - t) * t * mid_x + t**2 * tx_adj
ay = (1 - t) ** 2 * sy_adj + 2 * (1 - t) * t * mid_y + t**2 * ty_adj
t_prev = 0.9
ax_prev = (1 - t_prev) ** 2 * sx_adj + 2 * (1 - t_prev) * t_prev * mid_x + t_prev**2 * tx_adj
ay_prev = (1 - t_prev) ** 2 * sy_adj + 2 * (1 - t_prev) * t_prev * mid_y + t_prev**2 * ty_adj
angle = np.degrees(np.arctan2(ay - ay_prev, ax - ax_prev))
else:
offset = 0.04
ax = tx - dx / length * offset
ay = ty - dy / length * offset
angle = np.degrees(np.arctan2(dy, dx))
arrow_data.append({"x": ax, "y": ay, "angle": angle})
arrow_df = pd.DataFrame(arrow_data)
# Add colors to node dataframe
node_df["color"] = node_df["group"].map(group_colors)
# Create the visualization
edges_chart = (
alt.Chart(edge_df)
.mark_rule(strokeWidth=2, opacity=0.6, color=INK_SOFT)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[-0.1, 1.1]), axis=None),
y=alt.Y("y:Q", scale=alt.Scale(domain=[-0.05, 1.05]), axis=None),
x2="x2:Q",
y2="y2:Q",
)
)
curved_edges_chart = (
alt.Chart(curved_edge_df)
.mark_rule(strokeWidth=2, opacity=0.6, color=INK_SOFT)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[-0.1, 1.1]), axis=None),
y=alt.Y("y:Q", scale=alt.Scale(domain=[-0.05, 1.05]), axis=None),
x2="x2:Q",
y2="y2:Q",
)
)
# Arrow heads as triangular points
arrows_chart = (
alt.Chart(arrow_df)
.mark_point(shape="triangle", size=150, filled=True, color=INK_SOFT, opacity=0.8)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[-0.1, 1.1])),
y=alt.Y("y:Q", scale=alt.Scale(domain=[-0.05, 1.05])),
angle=alt.Angle("angle:Q"),
)
)
# Nodes as circles
nodes_chart = (
alt.Chart(node_df)
.mark_circle(size=800, stroke=PAGE_BG, strokeWidth=2)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[-0.1, 1.1])),
y=alt.Y("y:Q", scale=alt.Scale(domain=[-0.05, 1.05])),
color=alt.Color(
"group:N",
scale=alt.Scale(domain=list(group_colors.keys()), range=list(group_colors.values())),
legend=alt.Legend(title="Module Type", titleFontSize=18, labelFontSize=16, orient="right"),
),
tooltip=["label:N", "group:N"],
)
)
# Node labels
labels_chart = (
alt.Chart(node_df)
.mark_text(fontSize=18, fontWeight="bold", dy=-28, color=INK)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[-0.1, 1.1])),
y=alt.Y("y:Q", scale=alt.Scale(domain=[-0.05, 1.05])),
text="label:N",
)
)
# Combine all layers
chart = (
(edges_chart + curved_edges_chart + arrows_chart + nodes_chart + labels_chart)
.properties(
width=1600,
height=900,
background=PAGE_BG,
title=alt.Title(text="network-directed · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK),
)
.configure_view(fill=PAGE_BG, stroke=INK_SOFT)
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
)
# Save as PNG and HTML
script_dir = Path(__file__).parent
output_png = script_dir / f"plot-{THEME}.png"
output_html = script_dir / f"plot-{THEME}.html"
chart.save(str(output_png), scale_factor=3.0)
chart.save(str(output_html))
Part of Directed Network Graph on anyplot.ai.