Skip to content

Commit 19bd632

Browse files
update(chord-basic): plotly — comprehensive review
Comprehensive review and improvement of plotly chord diagram implementation.
1 parent f514b51 commit 19bd632

2 files changed

Lines changed: 87 additions & 67 deletions

File tree

plots/chord-basic/implementations/plotly.py

Lines changed: 82 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
chord-basic: Basic Chord Diagram
3-
Library: plotly 6.5.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: plotly 6.5.2 | Python 3.14
4+
Quality: /100 | Updated: 2026-04-06
55
"""
66

77
import numpy as np
88
import plotly.graph_objects as go
99

1010

11-
# Data: Migration flows between 6 continents (bidirectional)
11+
# Data: Migration flows between 6 continents (bidirectional, millions of people)
1212
continents = ["Africa", "Asia", "Europe", "N. America", "S. America", "Oceania"]
1313
n = len(continents)
1414

1515
# Flow matrix (row = source, col = target) - realistic migration patterns
16-
np.random.seed(42)
1716
flow_matrix = np.array(
1817
[
1918
[0, 15, 25, 10, 5, 3], # Africa to others
@@ -37,7 +36,7 @@
3736
arc_starts = []
3837
arc_ends = []
3938
current_pos = 0
40-
for _, total in enumerate(totals):
39+
for total in totals:
4140
arc_starts.append(current_pos)
4241
arc_ends.append(current_pos + (total / total_flow) * (1 - n * gap))
4342
current_pos = arc_ends[-1] + gap
@@ -47,12 +46,10 @@
4746

4847
# Draw outer arcs for each continent
4948
for i in range(n):
50-
# Generate arc points (outer)
5149
angles_outer = np.linspace(2 * np.pi * arc_starts[i] - np.pi / 2, 2 * np.pi * arc_ends[i] - np.pi / 2, 100)
5250
x_outer = 1.0 * np.cos(angles_outer)
5351
y_outer = 1.0 * np.sin(angles_outer)
5452

55-
# Generate arc points (inner, reversed)
5653
angles_inner = np.linspace(2 * np.pi * arc_ends[i] - np.pi / 2, 2 * np.pi * arc_starts[i] - np.pi / 2, 100)
5754
x_inner = 0.95 * np.cos(angles_inner)
5855
y_inner = 0.95 * np.sin(angles_inner)
@@ -63,65 +60,87 @@
6360
y=np.concatenate([y_outer, y_inner]),
6461
fill="toself",
6562
fillcolor=colors[i],
66-
line=dict(color="white", width=1),
67-
hoverinfo="text",
68-
text=f"{continents[i]}<br>Total flow: {totals[i]}",
63+
line={"color": "white", "width": 1},
64+
hovertemplate=(f"<b>{continents[i]}</b><br>Total flow: {int(totals[i])}M people<extra></extra>"),
6965
name=continents[i],
7066
showlegend=True,
7167
)
7268
)
7369

74-
# Draw chords between continents
75-
shapes = []
70+
# Draw chords as interactive traces (not shapes) for hover support
7671
for i in range(n):
7772
src_pos = arc_starts[i]
7873
for j in range(n):
79-
if i != j and flow_matrix[i, j] > 0:
80-
flow = flow_matrix[i, j]
81-
chord_width = (flow / total_flow) * (1 - n * gap)
82-
83-
# Calculate target position offset
84-
tgt_base = arc_starts[j]
85-
tgt_offset = sum(
86-
(flow_matrix[k, j] / total_flow) * (1 - n * gap) for k in range(i) if flow_matrix[k, j] > 0
87-
)
88-
89-
# Calculate chord endpoints (source)
90-
src_angle1 = 2 * np.pi * src_pos - np.pi / 2
91-
src_angle2 = 2 * np.pi * (src_pos + chord_width) - np.pi / 2
92-
x1 = 0.95 * np.cos(src_angle1)
93-
y1 = 0.95 * np.sin(src_angle1)
94-
x2 = 0.95 * np.cos(src_angle2)
95-
y2 = 0.95 * np.sin(src_angle2)
96-
97-
# Calculate chord endpoints (target)
98-
tgt_start = tgt_base + tgt_offset
99-
tgt_end = tgt_start + chord_width
100-
tgt_angle1 = 2 * np.pi * tgt_start - np.pi / 2
101-
tgt_angle2 = 2 * np.pi * tgt_end - np.pi / 2
102-
x3 = 0.95 * np.cos(tgt_angle1)
103-
y3 = 0.95 * np.sin(tgt_angle1)
104-
x4 = 0.95 * np.cos(tgt_angle2)
105-
y4 = 0.95 * np.sin(tgt_angle2)
106-
107-
# SVG path with quadratic bezier curves through center
108-
path = (
109-
f"M {x1},{y1} Q 0,0 {x3},{y3} A 0.95,0.95 0 0,1 {x4},{y4} Q 0,0 {x2},{y2} A 0.95,0.95 0 0,1 {x1},{y1} Z"
110-
)
111-
112-
shapes.append(
113-
dict(type="path", path=path, fillcolor=colors[i], opacity=0.6, line=dict(color=colors[i], width=0.5))
74+
if i == j or flow_matrix[i, j] == 0:
75+
continue
76+
77+
flow = flow_matrix[i, j]
78+
chord_width = (flow / total_flow) * (1 - n * gap)
79+
80+
# Target position offset based on prior incoming flows
81+
tgt_base = arc_starts[j]
82+
tgt_offset = sum((flow_matrix[k, j] / total_flow) * (1 - n * gap) for k in range(i) if flow_matrix[k, j] > 0)
83+
84+
# Source arc endpoints
85+
src_angle1 = 2 * np.pi * src_pos - np.pi / 2
86+
src_angle2 = 2 * np.pi * (src_pos + chord_width) - np.pi / 2
87+
sx1, sy1 = 0.95 * np.cos(src_angle1), 0.95 * np.sin(src_angle1)
88+
sx2, sy2 = 0.95 * np.cos(src_angle2), 0.95 * np.sin(src_angle2)
89+
90+
# Target arc endpoints
91+
tgt_start = tgt_base + tgt_offset
92+
tgt_end = tgt_start + chord_width
93+
tgt_angle1 = 2 * np.pi * tgt_start - np.pi / 2
94+
tgt_angle2 = 2 * np.pi * tgt_end - np.pi / 2
95+
tx1, ty1 = 0.95 * np.cos(tgt_angle1), 0.95 * np.sin(tgt_angle1)
96+
tx2, ty2 = 0.95 * np.cos(tgt_angle2), 0.95 * np.sin(tgt_angle2)
97+
98+
# Build chord path: source arc -> bezier to target -> target arc -> bezier back
99+
# Source side arc points
100+
src_angles = np.linspace(src_angle1, src_angle2, 20)
101+
src_x = 0.95 * np.cos(src_angles)
102+
src_y = 0.95 * np.sin(src_angles)
103+
104+
# Bezier from source end to target start (through center)
105+
t = np.linspace(0, 1, 30)
106+
bez1_x = (1 - t) ** 2 * sx2 + 2 * (1 - t) * t * 0 + t**2 * tx1
107+
bez1_y = (1 - t) ** 2 * sy2 + 2 * (1 - t) * t * 0 + t**2 * ty1
108+
109+
# Target side arc points
110+
tgt_angles = np.linspace(tgt_angle1, tgt_angle2, 20)
111+
tgt_x = 0.95 * np.cos(tgt_angles)
112+
tgt_y = 0.95 * np.sin(tgt_angles)
113+
114+
# Bezier from target end back to source start (through center)
115+
bez2_x = (1 - t) ** 2 * tx2 + 2 * (1 - t) * t * 0 + t**2 * sx1
116+
bez2_y = (1 - t) ** 2 * ty2 + 2 * (1 - t) * t * 0 + t**2 * sy1
117+
118+
# Combine into closed polygon
119+
chord_x = np.concatenate([src_x, bez1_x, tgt_x, bez2_x])
120+
chord_y = np.concatenate([src_y, bez1_y, tgt_y, bez2_y])
121+
122+
fig.add_trace(
123+
go.Scatter(
124+
x=chord_x,
125+
y=chord_y,
126+
fill="toself",
127+
fillcolor=colors[i],
128+
opacity=0.55,
129+
line={"color": colors[i], "width": 0.5},
130+
hovertemplate=(f"<b>{continents[i]}{continents[j]}</b><br>Flow: {flow}M people<extra></extra>"),
131+
showlegend=False,
132+
hoveron="fills",
114133
)
134+
)
115135

116-
src_pos += chord_width
136+
src_pos += chord_width
117137

118138
# Add continent labels around the perimeter
119139
for i in range(n):
120140
mid_pos = (arc_starts[i] + arc_ends[i]) / 2
121141
angle = 2 * np.pi * mid_pos - np.pi / 2
122142
label_radius = 1.12
123143

124-
# Rotate text for readability
125144
text_angle_deg = np.degrees(angle)
126145
if 90 < text_angle_deg < 270 or -270 < text_angle_deg < -90:
127146
text_angle_deg += 180
@@ -131,28 +150,29 @@
131150
x=label_radius * np.cos(angle),
132151
y=label_radius * np.sin(angle),
133152
text=f"<b>{continents[i]}</b>",
134-
font=dict(size=18, color=colors[i]),
153+
font={"size": 22, "color": colors[i]},
135154
showarrow=False,
136155
textangle=rotation,
137156
)
138157

139158
# Layout
140159
fig.update_layout(
141-
title=dict(
142-
text="Migration Flows Between Continents · chord-basic · plotly · pyplots.ai",
143-
font=dict(size=28),
144-
x=0.5,
145-
xanchor="center",
146-
),
147-
shapes=shapes,
148-
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, range=[-1.4, 1.4]),
149-
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False, range=[-1.4, 1.4], scaleanchor="x"),
160+
title={
161+
"text": "Migration Flows Between Continents · chord-basic · plotly · pyplots.ai",
162+
"font": {"size": 30, "color": "#333333"},
163+
"x": 0.5,
164+
"xanchor": "center",
165+
"y": 0.97,
166+
},
167+
xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "range": [-1.35, 1.35]},
168+
yaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "range": [-1.35, 1.35], "scaleanchor": "x"},
150169
template="plotly_white",
151170
showlegend=True,
152-
legend=dict(font=dict(size=16), x=1.02, y=0.5, yanchor="middle"),
153-
margin=dict(l=50, r=150, t=100, b=50),
171+
legend={"font": {"size": 18}, "x": 1.01, "y": 0.5, "yanchor": "middle", "tracegroupgap": 5},
172+
margin={"l": 30, "r": 160, "t": 80, "b": 30},
154173
plot_bgcolor="white",
155174
paper_bgcolor="white",
175+
hovermode="closest",
156176
)
157177

158178
# Save outputs

plots/chord-basic/metadata/plotly.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
library: plotly
22
specification_id: chord-basic
33
created: '2025-12-23T10:01:12Z'
4-
updated: '2025-12-23T10:06:59Z'
5-
generated_by: claude-opus-4-5-20251101
4+
updated: '2026-04-06T20:26:19+00:00'
5+
generated_by: claude-opus-4-6
66
workflow_run: 20457530538
77
issue: 0
8-
python_version: 3.13.11
9-
library_version: 6.5.0
8+
python_version: '3.14'
9+
library_version: 6.5.2
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/chord-basic/plotly/plot.png
1111
preview_html: https://storage.googleapis.com/pyplots-images/plots/chord-basic/plotly/plot.html
12-
quality_score: 91
12+
quality_score: null
1313
impl_tags:
1414
dependencies: []
1515
techniques:

0 commit comments

Comments
 (0)