Skip to content

Commit f2f01a1

Browse files
feat(plotly): implement network-weighted (#3306)
## Implementation: `network-weighted` - plotly Implements the **plotly** version of `network-weighted`. **File:** `plots/network-weighted/implementations/plotly.py` **Parent Issue:** #3290 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20822849491)* --------- 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 41b24dd commit f2f01a1

2 files changed

Lines changed: 465 additions & 0 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
""" pyplots.ai
2+
network-weighted: Weighted Network Graph with Edge Thickness
3+
Library: plotly 6.5.1 | Python 3.13.11
4+
Quality: 92/100 | Created: 2026-01-08
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data - Trade network between countries (billions USD)
12+
np.random.seed(42)
13+
14+
# Define nodes (countries)
15+
countries = [
16+
"USA",
17+
"China",
18+
"Germany",
19+
"Japan",
20+
"UK",
21+
"France",
22+
"Canada",
23+
"Mexico",
24+
"Brazil",
25+
"India",
26+
"Australia",
27+
"S. Korea",
28+
"Netherlands",
29+
"Italy",
30+
"Spain",
31+
]
32+
n_nodes = len(countries)
33+
node_idx = {name: i for i, name in enumerate(countries)}
34+
35+
# Create weighted edges (trade relationships)
36+
edges = [
37+
# Major trade routes (high weight)
38+
("USA", "China", 580),
39+
("USA", "Canada", 620),
40+
("USA", "Mexico", 550),
41+
("China", "Japan", 320),
42+
("China", "S. Korea", 280),
43+
("China", "Germany", 190),
44+
("Germany", "France", 180),
45+
("Germany", "Netherlands", 210),
46+
("Germany", "Italy", 140),
47+
("UK", "Germany", 130),
48+
("UK", "USA", 140),
49+
("UK", "Netherlands", 90),
50+
("Japan", "USA", 200),
51+
("Japan", "S. Korea", 85),
52+
# Medium trade routes
53+
("France", "Italy", 95),
54+
("France", "Spain", 110),
55+
("Spain", "Italy", 50),
56+
("Canada", "Mexico", 40),
57+
("Brazil", "USA", 75),
58+
("Brazil", "China", 100),
59+
("India", "USA", 90),
60+
("India", "China", 115),
61+
("India", "UK", 35),
62+
("Australia", "China", 145),
63+
("Australia", "Japan", 55),
64+
("Australia", "S. Korea", 45),
65+
# Lower trade routes
66+
("Netherlands", "UK", 65),
67+
("S. Korea", "USA", 120),
68+
("Mexico", "China", 70),
69+
]
70+
71+
# Compute force-directed layout (Fruchterman-Reingold algorithm)
72+
pos = np.random.rand(n_nodes, 2) * 2 - 1
73+
k = 0.5
74+
for _ in range(200):
75+
displacement = np.zeros((n_nodes, 2))
76+
# Repulsive forces
77+
for i in range(n_nodes):
78+
diff = pos[i] - pos
79+
dist = np.sqrt((diff**2).sum(axis=1))
80+
dist = np.where(dist < 0.01, 0.01, dist)
81+
rep_force = k**2 / dist
82+
rep_force[i] = 0
83+
displacement[i] += (diff * rep_force[:, np.newaxis]).sum(axis=0)
84+
# Attractive forces along edges
85+
for source, target, weight in edges:
86+
i, j = node_idx[source], node_idx[target]
87+
diff = pos[j] - pos[i]
88+
dist = np.sqrt((diff**2).sum())
89+
if dist > 0.01:
90+
attr_force = dist**2 / k * (1 + weight / 200)
91+
displacement[i] += diff / dist * attr_force
92+
displacement[j] -= diff / dist * attr_force
93+
# Update positions
94+
length = np.sqrt((displacement**2).sum(axis=1))
95+
length = np.where(length < 0.01, 0.01, length)
96+
pos += displacement / length[:, np.newaxis] * min(0.1, k)
97+
98+
# Normalize positions with margin for labels and annotation
99+
pos = (pos - pos.min(axis=0)) / (pos.max(axis=0) - pos.min(axis=0))
100+
pos = pos * 1.6 - 0.8 # Scale to [-0.8, 0.8]
101+
# Center the layout
102+
pos = pos - pos.mean(axis=0)
103+
node_positions = {countries[i]: pos[i] for i in range(n_nodes)}
104+
105+
# Calculate weighted degree for node sizing
106+
weighted_degree = dict.fromkeys(countries, 0)
107+
for source, target, weight in edges:
108+
weighted_degree[source] += weight
109+
weighted_degree[target] += weight
110+
111+
node_sizes = [20 + (weighted_degree[node] / 40) for node in countries]
112+
113+
# Create edge traces with varying thickness
114+
edge_traces = []
115+
min_weight = min(w for _, _, w in edges)
116+
max_weight = max(w for _, _, w in edges)
117+
118+
for source, target, weight in edges:
119+
x0, y0 = node_positions[source]
120+
x1, y1 = node_positions[target]
121+
# Scale width: 2 to 18 based on weight
122+
normalized = (weight - min_weight) / (max_weight - min_weight)
123+
line_width = 2 + normalized * 16
124+
# Color alpha based on weight (darker = stronger)
125+
alpha = 0.4 + normalized * 0.5
126+
edge_traces.append(
127+
go.Scatter(
128+
x=[x0, x1, None],
129+
y=[y0, y1, None],
130+
mode="lines",
131+
line={"width": line_width, "color": f"rgba(48, 105, 152, {alpha})"},
132+
hoverinfo="text",
133+
text=f"{source}{target}: ${weight}B",
134+
showlegend=False,
135+
)
136+
)
137+
138+
# Create node trace
139+
node_x = [node_positions[node][0] for node in countries]
140+
node_y = [node_positions[node][1] for node in countries]
141+
142+
# Calculate smart label positions to avoid overlap with explicit handling
143+
# for known problematic pairs: Japan/S.Korea and Italy/France
144+
label_positions = []
145+
146+
for i, node in enumerate(countries):
147+
x, y = node_positions[node]
148+
# Find nearby nodes and adjust position
149+
nearby_above = 0
150+
nearby_below = 0
151+
nearby_left = 0
152+
nearby_right = 0
153+
for j, other in enumerate(countries):
154+
if i != j:
155+
ox, oy = node_positions[other]
156+
dx, dy = x - ox, y - oy
157+
dist = np.sqrt(dx**2 + dy**2)
158+
if dist < 0.35:
159+
if dy > 0:
160+
nearby_below += 1
161+
else:
162+
nearby_above += 1
163+
if dx > 0:
164+
nearby_left += 1
165+
else:
166+
nearby_right += 1
167+
168+
# Handle specific known close pairs to avoid overlap
169+
if node == "Japan":
170+
pos_choice = "top right"
171+
elif node == "S. Korea":
172+
pos_choice = "bottom left"
173+
elif node == "Italy":
174+
pos_choice = "top left"
175+
elif node == "France":
176+
pos_choice = "bottom right"
177+
elif nearby_above > nearby_below:
178+
pos_choice = "bottom center"
179+
elif nearby_left > nearby_right:
180+
pos_choice = "middle right"
181+
elif nearby_right > nearby_left:
182+
pos_choice = "middle left"
183+
else:
184+
pos_choice = "top center"
185+
label_positions.append(pos_choice)
186+
187+
node_trace = go.Scatter(
188+
x=node_x,
189+
y=node_y,
190+
mode="markers+text",
191+
marker={"size": node_sizes, "color": "#FFD43B", "line": {"width": 2, "color": "#306998"}},
192+
text=countries,
193+
textposition=label_positions,
194+
textfont={"size": 16, "color": "#333333"},
195+
hoverinfo="text",
196+
hovertext=[f"{c}<br>Trade Volume: ${weighted_degree[c]}B" for c in countries],
197+
showlegend=False,
198+
)
199+
200+
# Create figure
201+
fig = go.Figure()
202+
203+
# Add edges first (behind nodes)
204+
for trace in edge_traces:
205+
fig.add_trace(trace)
206+
207+
# Add nodes
208+
fig.add_trace(node_trace)
209+
210+
# Add weight scale annotation (positioned at top-left to avoid cutoff)
211+
fig.add_annotation(
212+
x=0.01,
213+
y=0.99,
214+
xref="paper",
215+
yref="paper",
216+
text="Edge Thickness = Trade Volume<br>35B USD (thin) to 620B USD (thick)",
217+
showarrow=False,
218+
font={"size": 18, "color": "#333333", "family": "Arial"},
219+
align="left",
220+
xanchor="left",
221+
yanchor="top",
222+
bgcolor="rgba(255,255,255,0.95)",
223+
bordercolor="#999999",
224+
borderwidth=1,
225+
borderpad=10,
226+
)
227+
228+
# Update layout
229+
fig.update_layout(
230+
title={
231+
"text": "network-weighted · plotly · pyplots.ai", "font": {"size": 28, "color": "#333333"}, "x": 0.5, "xanchor": "center"
232+
},
233+
xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": ""},
234+
yaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": ""},
235+
template="plotly_white",
236+
showlegend=False,
237+
margin={"l": 80, "r": 80, "t": 100, "b": 80},
238+
plot_bgcolor="white",
239+
)
240+
241+
# Save outputs
242+
fig.write_image("plot.png", width=1600, height=900, scale=3)
243+
fig.write_html("plot.html")

0 commit comments

Comments
 (0)