Skip to content

Commit ab6804c

Browse files
feat(letsplot): implement network-directed (#2895)
## Implementation: `network-directed` - letsplot Implements the **letsplot** version of `network-directed`. **File:** `plots/network-directed/implementations/letsplot.py` **Parent Issue:** #2858 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608487718)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 60ad9e6 commit ab6804c

2 files changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
""" pyplots.ai
2+
network-directed: Directed Network Graph
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import os
8+
import shutil
9+
10+
import numpy as np
11+
import pandas as pd
12+
from lets_plot import (
13+
LetsPlot,
14+
aes,
15+
arrow,
16+
element_text,
17+
geom_point,
18+
geom_segment,
19+
geom_text,
20+
ggplot,
21+
ggsize,
22+
labs,
23+
scale_fill_manual,
24+
theme,
25+
theme_void,
26+
xlim,
27+
ylim,
28+
)
29+
from lets_plot.export import ggsave
30+
31+
32+
LetsPlot.setup_html()
33+
34+
np.random.seed(42)
35+
36+
# Define software package dependency network
37+
nodes = [
38+
{"id": "app", "label": "App", "group": "application"},
39+
{"id": "api", "label": "API", "group": "core"},
40+
{"id": "auth", "label": "Auth", "group": "core"},
41+
{"id": "db", "label": "Database", "group": "core"},
42+
{"id": "cache", "label": "Cache", "group": "infrastructure"},
43+
{"id": "config", "label": "Config", "group": "utility"},
44+
{"id": "logger", "label": "Logger", "group": "utility"},
45+
{"id": "utils", "label": "Utils", "group": "utility"},
46+
{"id": "http", "label": "HTTP Client", "group": "infrastructure"},
47+
{"id": "queue", "label": "Queue", "group": "infrastructure"},
48+
]
49+
50+
# Directed edges (source depends on target, arrow from source to target)
51+
edges = [
52+
("app", "api"),
53+
("app", "config"),
54+
("api", "auth"),
55+
("api", "db"),
56+
("api", "cache"),
57+
("api", "logger"),
58+
("auth", "db"),
59+
("auth", "config"),
60+
("auth", "logger"),
61+
("db", "config"),
62+
("db", "logger"),
63+
("cache", "config"),
64+
("cache", "logger"),
65+
("http", "config"),
66+
("http", "logger"),
67+
("queue", "config"),
68+
("queue", "logger"),
69+
("api", "http"),
70+
("api", "queue"),
71+
("utils", "logger"),
72+
]
73+
74+
# Create node positions using a hierarchical-like layout
75+
# Manually place nodes in layers for clear dependency visualization
76+
node_positions = {
77+
"app": (0.5, 1.0),
78+
"api": (0.5, 0.75),
79+
"auth": (0.15, 0.5),
80+
"db": (0.5, 0.5),
81+
"cache": (0.85, 0.5),
82+
"http": (0.05, 0.25),
83+
"queue": (0.3, 0.25),
84+
"config": (0.55, 0.25),
85+
"logger": (0.8, 0.25),
86+
"utils": (1.0, 0.75),
87+
}
88+
89+
# Build node dataframe
90+
node_df = pd.DataFrame(nodes)
91+
node_df["x"] = node_df["id"].map(lambda n: node_positions[n][0])
92+
node_df["y"] = node_df["id"].map(lambda n: node_positions[n][1])
93+
94+
# Color mapping for groups
95+
group_colors = {
96+
"application": "#306998", # Python Blue
97+
"core": "#FFD43B", # Python Yellow
98+
"infrastructure": "#22C55E", # Green
99+
"utility": "#A855F7", # Purple
100+
}
101+
node_df["color"] = node_df["group"].map(group_colors)
102+
103+
# Build edge dataframe with arrow endpoints
104+
edge_data = []
105+
for source, target in edges:
106+
x0, y0 = node_positions[source]
107+
x1, y1 = node_positions[target]
108+
109+
# Shorten edges to not overlap with nodes
110+
dx, dy = x1 - x0, y1 - y0
111+
length = np.sqrt(dx**2 + dy**2)
112+
if length > 0:
113+
# Shrink by node radius on each end
114+
shrink = 0.05
115+
x0_adj = x0 + (dx / length) * shrink
116+
y0_adj = y0 + (dy / length) * shrink
117+
x1_adj = x1 - (dx / length) * shrink * 1.8 # More shrink for arrow head
118+
y1_adj = y1 - (dy / length) * shrink * 1.8
119+
else:
120+
x0_adj, y0_adj, x1_adj, y1_adj = x0, y0, x1, y1
121+
122+
edge_data.append({"x": x0_adj, "y": y0_adj, "xend": x1_adj, "yend": y1_adj})
123+
124+
edge_df = pd.DataFrame(edge_data)
125+
126+
# Create the plot
127+
plot = (
128+
ggplot()
129+
# Draw edges with arrows
130+
+ geom_segment(
131+
aes(x="x", y="y", xend="xend", yend="yend"),
132+
data=edge_df,
133+
color="#64748B",
134+
size=1.2,
135+
arrow=arrow(length=12, type="closed"),
136+
alpha=0.7,
137+
)
138+
# Draw nodes
139+
+ geom_point(aes(x="x", y="y", fill="group"), data=node_df, size=22, shape=21, stroke=2.5, color="white")
140+
# Add node labels below nodes
141+
+ geom_text(
142+
aes(x="x", y="y", label="label"), data=node_df, size=12, color="#1E293B", fontface="bold", nudge_y=-0.06
143+
)
144+
# Color scale
145+
+ scale_fill_manual(values=["#306998", "#FFD43B", "#22C55E", "#A855F7"], name="Module Type")
146+
# Theme and styling
147+
+ theme_void()
148+
+ theme(
149+
plot_title=element_text(size=28, face="bold", hjust=0.5),
150+
legend_title=element_text(size=18),
151+
legend_text=element_text(size=16),
152+
legend_position="right",
153+
plot_margin=[60, 80, 80, 60],
154+
)
155+
+ labs(title="network-directed · letsplot · pyplots.ai")
156+
+ ggsize(1600, 900)
157+
+ xlim(-0.05, 1.15)
158+
+ ylim(0.1, 1.1)
159+
)
160+
161+
# Save as PNG and HTML
162+
ggsave(plot, "plot.png", scale=3)
163+
ggsave(plot, "plot.html")
164+
165+
# Move files from lets-plot-images subdirectory to current directory
166+
if os.path.exists("lets-plot-images/plot.png"):
167+
shutil.move("lets-plot-images/plot.png", "plot.png")
168+
if os.path.exists("lets-plot-images/plot.html"):
169+
shutil.move("lets-plot-images/plot.html", "plot.html")
170+
if os.path.exists("lets-plot-images") and not os.listdir("lets-plot-images"):
171+
os.rmdir("lets-plot-images")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
library: letsplot
2+
specification_id: network-directed
3+
created: '2025-12-30T23:57:51Z'
4+
updated: '2025-12-31T00:05:00Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608487718
7+
issue: 2858
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/network-directed/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/network-directed/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/network-directed/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent hierarchical layout clearly showing software dependency structure from
17+
top (App) to bottom (utilities)
18+
- Clean use of lets-plot ggplot2-style grammar with geom_segment arrows and geom_point
19+
for nodes
20+
- Well-chosen color palette distinguishing four module types (application, core,
21+
infrastructure, utility)
22+
- Proper edge shortening to prevent arrow overlap with nodes
23+
- Correct title format and professional styling with theme_void
24+
weaknesses:
25+
- Could demonstrate bidirectional edges (curved edges) as mentioned in spec for
26+
nodes with mutual connections
27+
- File handling code (os, shutil for moving files from lets-plot-images) adds unnecessary
28+
complexity
29+
- HTML export could leverage lets-plot interactive features like tooltips showing
30+
module details

0 commit comments

Comments
 (0)