Skip to content

Commit 42f410c

Browse files
feat(plotly): implement network-directed (#2888)
## Implementation: `network-directed` - plotly Implements the **plotly** version of `network-directed`. **File:** `plots/network-directed/implementations/plotly.py` **Parent Issue:** #2858 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608483175)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c09c42a commit 42f410c

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
""" pyplots.ai
2+
network-directed: Directed Network Graph
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data: Software module dependencies (arrows show import direction)
12+
np.random.seed(42)
13+
14+
nodes = [
15+
{"id": 0, "label": "main", "group": "entry"},
16+
{"id": 1, "label": "api", "group": "core"},
17+
{"id": 2, "label": "auth", "group": "core"},
18+
{"id": 3, "label": "database", "group": "core"},
19+
{"id": 4, "label": "models", "group": "data"},
20+
{"id": 5, "label": "utils", "group": "helpers"},
21+
{"id": 6, "label": "config", "group": "helpers"},
22+
{"id": 7, "label": "logging", "group": "helpers"},
23+
{"id": 8, "label": "cache", "group": "core"},
24+
{"id": 9, "label": "router", "group": "core"},
25+
{"id": 10, "label": "middleware", "group": "core"},
26+
{"id": 11, "label": "validators", "group": "data"},
27+
{"id": 12, "label": "schemas", "group": "data"},
28+
]
29+
30+
# Directed edges: (source, target) - arrows point from source to target
31+
edges = [
32+
(0, 1), # main -> api
33+
(0, 6), # main -> config
34+
(0, 7), # main -> logging
35+
(1, 2), # api -> auth
36+
(1, 9), # api -> router
37+
(1, 10), # api -> middleware
38+
(2, 3), # auth -> database
39+
(2, 5), # auth -> utils
40+
(3, 4), # database -> models
41+
(3, 8), # database -> cache
42+
(4, 12), # models -> schemas
43+
(5, 7), # utils -> logging
44+
(6, 7), # config -> logging
45+
(8, 7), # cache -> logging
46+
(9, 10), # router -> middleware
47+
(9, 11), # router -> validators
48+
(10, 2), # middleware -> auth
49+
(11, 12), # validators -> schemas
50+
(12, 5), # schemas -> utils
51+
]
52+
53+
# Group colors
54+
group_colors = {
55+
"entry": "#306998", # Python Blue
56+
"core": "#FFD43B", # Python Yellow
57+
"data": "#4ECDC4", # Teal
58+
"helpers": "#95A5A6", # Gray
59+
}
60+
61+
# Circular layout for clear visualization
62+
n_nodes = len(nodes)
63+
angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)
64+
radius = 3
65+
node_x = radius * np.cos(angles)
66+
node_y = radius * np.sin(angles)
67+
68+
# Create figure
69+
fig = go.Figure()
70+
71+
# Add edges as lines with arrows using annotations
72+
for source, target in edges:
73+
x0, y0 = node_x[source], node_y[source]
74+
x1, y1 = node_x[target], node_y[target]
75+
76+
# Calculate direction vector
77+
dx, dy = x1 - x0, y1 - y0
78+
length = np.sqrt(dx**2 + dy**2)
79+
dx, dy = dx / length, dy / length
80+
81+
# Shorten edge to not overlap with nodes (node radius ~0.4)
82+
node_radius = 0.4
83+
x0_adj = x0 + dx * node_radius
84+
y0_adj = y0 + dy * node_radius
85+
x1_adj = x1 - dx * node_radius
86+
y1_adj = y1 - dy * node_radius
87+
88+
# Add edge line
89+
fig.add_trace(
90+
go.Scatter(
91+
x=[x0_adj, x1_adj],
92+
y=[y0_adj, y1_adj],
93+
mode="lines",
94+
line=dict(width=2, color="#666666"),
95+
hoverinfo="none",
96+
showlegend=False,
97+
)
98+
)
99+
100+
# Add arrowheads using annotations
101+
for source, target in edges:
102+
x0, y0 = node_x[source], node_y[source]
103+
x1, y1 = node_x[target], node_y[target]
104+
105+
# Calculate direction vector
106+
dx, dy = x1 - x0, y1 - y0
107+
length = np.sqrt(dx**2 + dy**2)
108+
dx, dy = dx / length, dy / length
109+
110+
# Arrow position (at target node edge)
111+
node_radius = 0.45
112+
ax = x1 - dx * node_radius
113+
ay = y1 - dy * node_radius
114+
115+
fig.add_annotation(
116+
x=x1 - dx * node_radius,
117+
y=y1 - dy * node_radius,
118+
ax=x1 - dx * (node_radius + 0.3),
119+
ay=y1 - dy * (node_radius + 0.3),
120+
xref="x",
121+
yref="y",
122+
axref="x",
123+
ayref="y",
124+
showarrow=True,
125+
arrowhead=2,
126+
arrowsize=2,
127+
arrowwidth=2,
128+
arrowcolor="#666666",
129+
)
130+
131+
# Add nodes by group for legend
132+
for group in ["entry", "core", "data", "helpers"]:
133+
group_nodes = [n for n in nodes if n["group"] == group]
134+
group_x = [node_x[n["id"]] for n in group_nodes]
135+
group_y = [node_y[n["id"]] for n in group_nodes]
136+
group_labels = [n["label"] for n in group_nodes]
137+
138+
fig.add_trace(
139+
go.Scatter(
140+
x=group_x,
141+
y=group_y,
142+
mode="markers+text",
143+
marker=dict(size=45, color=group_colors[group], line=dict(width=2, color="#333333")),
144+
text=group_labels,
145+
textposition="middle center",
146+
textfont=dict(size=14, color="#333333", family="Arial Black"),
147+
name=group.capitalize(),
148+
hovertemplate="<b>%{text}</b><br>Group: " + group + "<extra></extra>",
149+
)
150+
)
151+
152+
# Update layout
153+
fig.update_layout(
154+
title=dict(
155+
text="Software Module Dependencies · network-directed · plotly · pyplots.ai",
156+
font=dict(size=28, color="#333333"),
157+
x=0.5,
158+
xanchor="center",
159+
),
160+
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title="", range=[-4.5, 4.5]),
161+
yaxis=dict(
162+
showgrid=False, zeroline=False, showticklabels=False, title="", range=[-4.5, 4.5], scaleanchor="x", scaleratio=1
163+
),
164+
template="plotly_white",
165+
showlegend=True,
166+
legend=dict(
167+
title=dict(text="Module Groups", font=dict(size=18)),
168+
font=dict(size=16),
169+
x=1.02,
170+
y=0.5,
171+
yanchor="middle",
172+
bgcolor="rgba(255,255,255,0.9)",
173+
bordercolor="#CCCCCC",
174+
borderwidth=1,
175+
),
176+
margin=dict(l=50, r=180, t=100, b=50),
177+
plot_bgcolor="white",
178+
)
179+
180+
# Save as PNG (4800x2700 at scale=3)
181+
fig.write_image("plot.png", width=1600, height=900, scale=3)
182+
183+
# Save as HTML for interactivity
184+
fig.write_html("plot.html", include_plotlyjs=True, full_html=True)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: plotly
2+
specification_id: network-directed
3+
created: '2025-12-30T23:56:01Z'
4+
updated: '2025-12-31T00:02:42Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608483175
7+
issue: 2858
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/network-directed/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/network-directed/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/network-directed/plotly/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent circular layout algorithm that positions nodes evenly and prevents overlap
17+
- Clear arrow direction implementation using Plotly annotations with appropriate
18+
sizing
19+
- Well-chosen color scheme that distinguishes module groups effectively and is colorblind-accessible
20+
- Good use of Plotly hover template for interactive exploration
21+
- Title follows the exact spec-id format required
22+
- Realistic software dependency scenario that matches the spec example perfectly
23+
weaknesses:
24+
- Some longer node labels (middleware, validators, schemas) appear slightly cramped
25+
within their circular markers
26+
- The graph does not include bidirectional edges to demonstrate curved edge handling
27+
mentioned in spec notes

0 commit comments

Comments
 (0)