Skip to content

Commit cedb8f0

Browse files
update(chord-basic): plotly — comprehensive review (#5213)
## Summary Updated **plotly** implementation for **chord-basic**. **Changes:** Comprehensive review — code quality, data choice, visual design, spec compliance, library feature usage. ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- 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 e5703d2 commit cedb8f0

File tree

2 files changed

+346
-191
lines changed

2 files changed

+346
-191
lines changed
Lines changed: 197 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
""" 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: 88/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
@@ -25,136 +24,255 @@
2524
]
2625
)
2726

28-
# Colors for each continent (Python Blue first, then colorblind-safe palette)
29-
colors = ["#306998", "#FFD43B", "#2E8B57", "#DC143C", "#9370DB", "#FF8C00"]
27+
# Colors: Python Blue first, then colorblind-safe palette
28+
# Replaced green (#2E8B57) with teal (#00B4D8) for deuteranopia accessibility
29+
colors = ["#306998", "#FFD43B", "#00B4D8", "#DC143C", "#9370DB", "#FF8C00"]
30+
colors_dim = [
31+
"rgba(48,105,152,0.4)",
32+
"rgba(255,212,59,0.4)",
33+
"rgba(0,180,216,0.4)",
34+
"rgba(220,20,60,0.4)",
35+
"rgba(147,112,219,0.4)",
36+
"rgba(255,140,0,0.4)",
37+
]
3038

3139
# Calculate totals for each continent
3240
totals = flow_matrix.sum(axis=0) + flow_matrix.sum(axis=1)
3341
total_flow = flow_matrix.sum()
3442

43+
# Identify the dominant corridor for storytelling emphasis
44+
max_flow_idx = np.unravel_index(np.argmax(flow_matrix + flow_matrix.T), flow_matrix.shape)
45+
dominant_src, dominant_tgt = max_flow_idx
46+
dominant_flow = flow_matrix[dominant_src, dominant_tgt] + flow_matrix[dominant_tgt, dominant_src]
47+
3548
# Calculate arc positions around the circle
3649
gap = 0.02
3750
arc_starts = []
3851
arc_ends = []
3952
current_pos = 0
40-
for _, total in enumerate(totals):
53+
for total in totals:
4154
arc_starts.append(current_pos)
4255
arc_ends.append(current_pos + (total / total_flow) * (1 - n * gap))
4356
current_pos = arc_ends[-1] + gap
4457

4558
# Create figure
4659
fig = go.Figure()
4760

48-
# Draw outer arcs for each continent
61+
# Draw outer arcs with gradient-like layered effect
4962
for i in range(n):
50-
# Generate arc points (outer)
5163
angles_outer = np.linspace(2 * np.pi * arc_starts[i] - np.pi / 2, 2 * np.pi * arc_ends[i] - np.pi / 2, 100)
64+
# Outer ring (thicker, slightly transparent for depth)
65+
x_o = 1.02 * np.cos(angles_outer)
66+
y_o = 1.02 * np.sin(angles_outer)
67+
angles_rev = angles_outer[::-1]
68+
x_i = 0.98 * np.cos(angles_rev)
69+
y_i = 0.98 * np.sin(angles_rev)
70+
71+
fig.add_trace(
72+
go.Scatter(
73+
x=np.concatenate([x_o, x_i]),
74+
y=np.concatenate([y_o, y_i]),
75+
fill="toself",
76+
fillcolor=colors_dim[i],
77+
line={"color": "rgba(255,255,255,0)", "width": 0},
78+
hoverinfo="skip",
79+
showlegend=False,
80+
)
81+
)
82+
83+
# Inner ring (solid color, main arc)
5284
x_outer = 1.0 * np.cos(angles_outer)
5385
y_outer = 1.0 * np.sin(angles_outer)
54-
55-
# Generate arc points (inner, reversed)
56-
angles_inner = np.linspace(2 * np.pi * arc_ends[i] - np.pi / 2, 2 * np.pi * arc_starts[i] - np.pi / 2, 100)
57-
x_inner = 0.95 * np.cos(angles_inner)
58-
y_inner = 0.95 * np.sin(angles_inner)
86+
x_inner = 0.94 * np.cos(angles_rev)
87+
y_inner = 0.94 * np.sin(angles_rev)
5988

6089
fig.add_trace(
6190
go.Scatter(
6291
x=np.concatenate([x_outer, x_inner]),
6392
y=np.concatenate([y_outer, y_inner]),
6493
fill="toself",
6594
fillcolor=colors[i],
66-
line=dict(color="white", width=1),
67-
hoverinfo="text",
68-
text=f"{continents[i]}<br>Total flow: {totals[i]}",
95+
line={"color": "white", "width": 0.5},
96+
hovertemplate=(
97+
f"<b>{continents[i]}</b><br>"
98+
f"Outgoing: {int(flow_matrix[i].sum())}M<br>"
99+
f"Incoming: {int(flow_matrix[:, i].sum())}M<br>"
100+
f"Total: {int(totals[i])}M people"
101+
"<extra></extra>"
102+
),
69103
name=continents[i],
70104
showlegend=True,
105+
legendgroup=continents[i],
71106
)
72107
)
73108

74-
# Draw chords between continents
75-
shapes = []
109+
# Draw chords with enhanced visibility and storytelling
110+
min_chord_width = 0.008 # Minimum visual width for thin chords
76111
for i in range(n):
77112
src_pos = arc_starts[i]
78113
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-
)
114+
if i == j or flow_matrix[i, j] == 0:
115+
continue
88116

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-
)
117+
flow = flow_matrix[i, j]
118+
chord_width = max((flow / total_flow) * (1 - n * gap), min_chord_width)
119+
120+
# Highlight dominant corridor with higher opacity
121+
is_dominant = (i == dominant_src and j == dominant_tgt) or (i == dominant_tgt and j == dominant_src)
122+
opacity = 0.72 if is_dominant else 0.45
123+
line_width = 1.0 if is_dominant else 0.3
124+
125+
# Target position offset based on prior incoming flows
126+
tgt_base = arc_starts[j]
127+
tgt_offset = sum(
128+
max((flow_matrix[k, j] / total_flow) * (1 - n * gap), min_chord_width)
129+
for k in range(i)
130+
if flow_matrix[k, j] > 0
131+
)
111132

112-
shapes.append(
113-
dict(type="path", path=path, fillcolor=colors[i], opacity=0.6, line=dict(color=colors[i], width=0.5))
133+
# Source arc endpoints
134+
src_angle1 = 2 * np.pi * src_pos - np.pi / 2
135+
src_angle2 = 2 * np.pi * (src_pos + chord_width) - np.pi / 2
136+
sx1, sy1 = 0.94 * np.cos(src_angle1), 0.94 * np.sin(src_angle1)
137+
sx2, sy2 = 0.94 * np.cos(src_angle2), 0.94 * np.sin(src_angle2)
138+
139+
# Target arc endpoints
140+
tgt_start = tgt_base + tgt_offset
141+
tgt_end = tgt_start + chord_width
142+
tgt_angle1 = 2 * np.pi * tgt_start - np.pi / 2
143+
tgt_angle2 = 2 * np.pi * tgt_end - np.pi / 2
144+
tx1, ty1 = 0.94 * np.cos(tgt_angle1), 0.94 * np.sin(tgt_angle1)
145+
tx2, ty2 = 0.94 * np.cos(tgt_angle2), 0.94 * np.sin(tgt_angle2)
146+
147+
# Build chord path with smoother bezier curves
148+
src_angles = np.linspace(src_angle1, src_angle2, 20)
149+
src_x = 0.94 * np.cos(src_angles)
150+
src_y = 0.94 * np.sin(src_angles)
151+
152+
t = np.linspace(0, 1, 40)
153+
bez1_x = (1 - t) ** 2 * sx2 + 2 * (1 - t) * t * 0 + t**2 * tx1
154+
bez1_y = (1 - t) ** 2 * sy2 + 2 * (1 - t) * t * 0 + t**2 * ty1
155+
156+
tgt_angles = np.linspace(tgt_angle1, tgt_angle2, 20)
157+
tgt_x = 0.94 * np.cos(tgt_angles)
158+
tgt_y = 0.94 * np.sin(tgt_angles)
159+
160+
bez2_x = (1 - t) ** 2 * tx2 + 2 * (1 - t) * t * 0 + t**2 * sx1
161+
bez2_y = (1 - t) ** 2 * ty2 + 2 * (1 - t) * t * 0 + t**2 * sy1
162+
163+
chord_x = np.concatenate([src_x, bez1_x, tgt_x, bez2_x])
164+
chord_y = np.concatenate([src_y, bez1_y, tgt_y, bez2_y])
165+
166+
fig.add_trace(
167+
go.Scatter(
168+
x=chord_x,
169+
y=chord_y,
170+
fill="toself",
171+
fillcolor=colors[i],
172+
opacity=opacity,
173+
line={"color": colors[i], "width": line_width},
174+
hovertemplate=(
175+
f"<b>{continents[i]}{continents[j]}</b><br>"
176+
f"Flow: {flow}M people<br>"
177+
f"Share: {flow / total_flow * 100:.1f}% of total"
178+
"<extra></extra>"
179+
),
180+
showlegend=False,
181+
hoveron="fills",
114182
)
183+
)
115184

116-
src_pos += chord_width
185+
src_pos += chord_width
117186

118-
# Add continent labels around the perimeter
187+
# Add continent labels around the perimeter (horizontal for clarity)
119188
for i in range(n):
120189
mid_pos = (arc_starts[i] + arc_ends[i]) / 2
121190
angle = 2 * np.pi * mid_pos - np.pi / 2
122-
label_radius = 1.12
191+
label_radius = 1.16
192+
193+
lx = label_radius * np.cos(angle)
194+
ly = label_radius * np.sin(angle)
195+
angle_deg = np.degrees(angle) % 360
123196

124-
# Rotate text for readability
125-
text_angle_deg = np.degrees(angle)
126-
if 90 < text_angle_deg < 270 or -270 < text_angle_deg < -90:
127-
text_angle_deg += 180
128-
rotation = -text_angle_deg + 90 if -90 < np.degrees(angle) < 90 else -text_angle_deg - 90
197+
# Anchor text toward the circle center for clean alignment
198+
if 45 < angle_deg < 135:
199+
xanchor, yanchor = "center", "bottom"
200+
elif 135 <= angle_deg < 225:
201+
xanchor, yanchor = "right", "middle"
202+
elif 225 <= angle_deg < 315:
203+
xanchor, yanchor = "center", "top"
204+
else:
205+
xanchor, yanchor = "left", "middle"
129206

130207
fig.add_annotation(
131-
x=label_radius * np.cos(angle),
132-
y=label_radius * np.sin(angle),
133-
text=f"<b>{continents[i]}</b>",
134-
font=dict(size=18, color=colors[i]),
208+
x=lx,
209+
y=ly,
210+
text=f"<b>{continents[i]}</b> <span style='font-size:17px;color:#888'>{int(totals[i])}M</span>",
211+
font={"size": 20, "color": colors[i], "family": "Arial, Helvetica, sans-serif"},
135212
showarrow=False,
136-
textangle=rotation,
213+
xanchor=xanchor,
214+
yanchor=yanchor,
137215
)
138216

139-
# Layout
140-
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",
217+
# Subtitle annotation for storytelling
218+
fig.add_annotation(
219+
text=(
220+
f"Europe–Asia corridor dominates at <b>{dominant_flow}M</b> combined flow"
221+
" · Chord width proportional to flow magnitude"
146222
),
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"),
223+
xref="paper",
224+
yref="paper",
225+
x=0.5,
226+
y=0.955,
227+
showarrow=False,
228+
font={"size": 17, "color": "#666666", "family": "Arial, Helvetica, sans-serif"},
229+
xanchor="center",
230+
)
231+
232+
# Layout with refined styling
233+
fig.update_layout(
234+
title={
235+
"text": "Migration Flows Between Continents · chord-basic · plotly · pyplots.ai",
236+
"font": {"size": 28, "color": "#222222", "family": "Arial Black, Arial, sans-serif"},
237+
"x": 0.5,
238+
"xanchor": "center",
239+
"y": 0.98,
240+
},
241+
xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "showline": False, "range": [-1.5, 1.5]},
242+
yaxis={
243+
"showgrid": False,
244+
"zeroline": False,
245+
"showticklabels": False,
246+
"showline": False,
247+
"range": [-1.6, 1.4],
248+
"scaleanchor": "x",
249+
},
150250
template="plotly_white",
151251
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),
252+
legend={
253+
"font": {"size": 18, "family": "Arial, Helvetica, sans-serif"},
254+
"title": {"text": "<b>Continents</b>", "font": {"size": 18, "color": "#444"}},
255+
"x": 0.98,
256+
"y": 0.02,
257+
"xanchor": "right",
258+
"yanchor": "bottom",
259+
"bgcolor": "rgba(255,255,255,0.9)",
260+
"bordercolor": "#ddd",
261+
"borderwidth": 1,
262+
"tracegroupgap": 6,
263+
"itemsizing": "constant",
264+
},
265+
margin={"l": 20, "r": 20, "t": 80, "b": 15},
154266
plot_bgcolor="white",
155-
paper_bgcolor="white",
267+
paper_bgcolor="#FAFAFA",
268+
hovermode="closest",
269+
hoverlabel={
270+
"bgcolor": "white",
271+
"bordercolor": "#ccc",
272+
"font": {"size": 16, "family": "Arial, Helvetica, sans-serif", "color": "#333"},
273+
},
156274
)
157275

158276
# Save outputs
159-
fig.write_image("plot.png", width=1600, height=900, scale=3)
277+
fig.write_image("plot.png", width=1200, height=1200, scale=3)
160278
fig.write_html("plot.html", include_plotlyjs="cdn")

0 commit comments

Comments
 (0)