Skip to content

Commit 95a47d9

Browse files
update(chord-basic): matplotlib — comprehensive review
Comprehensive review and improvement of matplotlib chord diagram implementation.
1 parent f514b51 commit 95a47d9

3 files changed

Lines changed: 81 additions & 115 deletions

File tree

plots/chord-basic/implementations/matplotlib.py

Lines changed: 73 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
chord-basic: Basic Chord Diagram
3-
Library: matplotlib 3.10.8 | Python 3.13.11
4-
Quality: 98/100 | Created: 2025-12-14
3+
Library: matplotlib 3.10.8 | Python 3.14
4+
Quality: /100 | Updated: 2026-04-06
55
"""
66

77
import matplotlib.patches as mpatches
@@ -11,11 +11,9 @@
1111

1212

1313
# Data: Migration flows between continents (in millions)
14-
np.random.seed(42)
1514
entities = ["Africa", "Asia", "Europe", "N. America", "S. America", "Oceania"]
1615
n = len(entities)
1716

18-
# Flow matrix (row=source, col=target)
1917
flow_matrix = np.array(
2018
[
2119
[0, 12, 8, 5, 2, 1], # From Africa
@@ -27,157 +25,123 @@
2725
]
2826
)
2927

30-
# Colors for each entity (colorblind-safe palette)
31-
colors = ["#306998", "#FFD43B", "#4ECDC4", "#FF6B6B", "#95E1A3", "#DDA0DD"]
28+
# Colorblind-safe palette starting with Python Blue
29+
colors = ["#306998", "#E69F00", "#009E73", "#D55E00", "#56B4E9", "#CC79A7"]
3230

33-
# Calculate totals for each entity (sum of outgoing + incoming)
31+
# Calculate entity totals and arc geometry
3432
totals = flow_matrix.sum(axis=1) + flow_matrix.sum(axis=0)
3533
total_flow = totals.sum()
34+
gap_deg = 3
35+
available_deg = 360 - gap_deg * n
36+
arc_spans = (totals / total_flow) * available_deg
3637

37-
# Gap between entity arcs (in degrees)
38-
gap = 3
39-
total_gap = gap * n
40-
available_degrees = 360 - total_gap
41-
42-
# Calculate arc spans for each entity
43-
arc_spans = (totals / total_flow) * available_degrees
44-
45-
# Calculate start angles for each entity arc
38+
# Start angles (clockwise from top)
4639
start_angles = np.zeros(n)
47-
current_angle = 90 # Start from top
40+
angle = 90
4841
for i in range(n):
49-
start_angles[i] = current_angle
50-
current_angle -= arc_spans[i] + gap
42+
start_angles[i] = angle
43+
angle -= arc_spans[i] + gap_deg
5144

52-
# Create figure
45+
# Plot
5346
fig, ax = plt.subplots(figsize=(16, 9), subplot_kw={"aspect": "equal"})
54-
ax.set_xlim(-1.5, 1.5)
55-
ax.set_ylim(-1.3, 1.3)
47+
ax.set_xlim(-1.55, 1.55)
48+
ax.set_ylim(-1.35, 1.35)
5649
ax.axis("off")
5750

58-
# Draw entity arcs on the outer ring
5951
radius = 1.0
6052
arc_width = 0.08
53+
inner_r = radius - arc_width
6154

55+
# Draw outer arcs
6256
for i in range(n):
6357
theta1 = start_angles[i] - arc_spans[i]
6458
theta2 = start_angles[i]
65-
66-
# Draw outer arc as a wedge
6759
wedge = mpatches.Wedge(
6860
(0, 0), radius, theta1, theta2, width=arc_width, facecolor=colors[i], edgecolor="white", linewidth=2
6961
)
7062
ax.add_patch(wedge)
7163

72-
# Add entity label
73-
mid_angle = (theta1 + theta2) / 2
74-
label_radius = radius + 0.12
75-
label_x = label_radius * np.cos(np.radians(mid_angle))
76-
label_y = label_radius * np.sin(np.radians(mid_angle))
64+
# Label placement
65+
mid = np.radians((theta1 + theta2) / 2)
66+
lx, ly = (radius + 0.12) * np.cos(mid), (radius + 0.12) * np.sin(mid)
67+
mid_deg = np.degrees(mid) % 360
68+
ha = (
69+
"center"
70+
if mid_deg < 15 or mid_deg > 345 or 165 < mid_deg < 195
71+
else ("right" if 90 < mid_deg < 270 else "left")
72+
)
73+
ax.text(lx, ly, entities[i], fontsize=18, fontweight="bold", ha=ha, va="center", color=colors[i])
7774

78-
# Rotate text to align with arc
79-
rotation = mid_angle
80-
if mid_angle > 90 or mid_angle < -90:
81-
rotation = mid_angle + 180
82-
if 90 < mid_angle < 270 or -270 < mid_angle < -90:
83-
ha = "right"
84-
else:
85-
ha = "left"
86-
if abs(mid_angle) < 10 or abs(mid_angle - 180) < 10 or abs(mid_angle + 180) < 10:
87-
ha = "center"
75+
# Track angular position within each arc for chord placement
76+
arc_cursors = start_angles.copy()
77+
unit_angles = arc_spans / totals
8878

89-
ax.text(label_x, label_y, entities[i], fontsize=18, fontweight="bold", ha=ha, va="center", color=colors[i])
79+
# Sort flows by magnitude (draw largest last for visual hierarchy)
80+
flows = [(i, j, flow_matrix[i, j]) for i in range(n) for j in range(n) if i != j and flow_matrix[i, j] > 0]
81+
flows.sort(key=lambda f: f[2])
9082

83+
# Pre-compute chord positions to avoid cursor interference from draw order
84+
chord_params = []
85+
pos_cursors = start_angles.copy()
86+
for i in range(n):
87+
for j in range(n):
88+
if i != j and flow_matrix[i, j] > 0:
89+
flow = flow_matrix[i, j]
90+
src_span = flow * unit_angles[i]
91+
src_end = pos_cursors[i]
92+
src_start = src_end - src_span
93+
pos_cursors[i] = src_start
9194

92-
def draw_chord(ax, start1, end1, start2, end2, color, alpha=0.65):
93-
"""Draw a chord between two arcs using Bezier curves."""
94-
inner_radius = radius - arc_width
95+
tgt_span = flow * unit_angles[j]
96+
tgt_end = pos_cursors[j]
97+
tgt_start = tgt_end - tgt_span
98+
pos_cursors[j] = tgt_start
9599

96-
# Convert angles to radians
97-
s1, e1 = np.radians(start1), np.radians(end1)
98-
s2, e2 = np.radians(start2), np.radians(end2)
100+
chord_params.append((src_start, src_end, tgt_start, tgt_end, colors[i], flow))
99101

100-
# Create path points
101-
n_arc_points = 20
102+
# Sort by flow magnitude so largest chords render on top
103+
chord_params.sort(key=lambda c: c[5])
102104

103-
# First arc (source)
104-
arc1_angles = np.linspace(s1, e1, n_arc_points)
105-
arc1_points = np.column_stack([inner_radius * np.cos(arc1_angles), inner_radius * np.sin(arc1_angles)])
105+
# Draw chords using cubic Bezier paths
106+
n_arc_pts = 30
107+
ctrl_factor = 0.25
106108

107-
# Second arc (target)
108-
arc2_angles = np.linspace(s2, e2, n_arc_points)
109-
arc2_points = np.column_stack([inner_radius * np.cos(arc2_angles), inner_radius * np.sin(arc2_angles)])
109+
for src_start, src_end, tgt_start, tgt_end, color, flow in chord_params:
110+
s1, e1 = np.radians(src_start), np.radians(src_end)
111+
s2, e2 = np.radians(tgt_start), np.radians(tgt_end)
110112

111-
# Control points for Bezier curves (through center with some offset)
112-
ctrl_factor = 0.3
113+
arc1_t = np.linspace(s1, e1, n_arc_pts)
114+
arc1 = np.column_stack([inner_r * np.cos(arc1_t), inner_r * np.sin(arc1_t)])
113115

114-
# Build path: arc1 -> bezier to arc2 -> arc2 -> bezier back to arc1
115-
verts = []
116-
codes = []
116+
arc2_t = np.linspace(s2, e2, n_arc_pts)
117+
arc2 = np.column_stack([inner_r * np.cos(arc2_t), inner_r * np.sin(arc2_t)])
117118

118-
# Start at first point of arc1
119-
verts.append(arc1_points[0])
120-
codes.append(Path.MOVETO)
119+
# Build closed path: arc1 → bezier → arc2 → bezier → close
120+
verts = [arc1[0]]
121+
codes = [Path.MOVETO]
121122

122-
# Arc1 points
123-
for pt in arc1_points[1:]:
123+
for pt in arc1[1:]:
124124
verts.append(pt)
125125
codes.append(Path.LINETO)
126126

127-
# Bezier curve from end of arc1 to start of arc2
128-
ctrl1 = arc1_points[-1] * ctrl_factor
129-
ctrl2 = arc2_points[0] * ctrl_factor
130-
verts.extend([ctrl1, ctrl2, arc2_points[0]])
127+
verts.extend([arc1[-1] * ctrl_factor, arc2[0] * ctrl_factor, arc2[0]])
131128
codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
132129

133-
# Arc2 points
134-
for pt in arc2_points[1:]:
130+
for pt in arc2[1:]:
135131
verts.append(pt)
136132
codes.append(Path.LINETO)
137133

138-
# Bezier curve from end of arc2 back to start of arc1
139-
ctrl3 = arc2_points[-1] * ctrl_factor
140-
ctrl4 = arc1_points[0] * ctrl_factor
141-
verts.extend([ctrl3, ctrl4, arc1_points[0]])
134+
verts.extend([arc2[-1] * ctrl_factor, arc1[0] * ctrl_factor, arc1[0]])
142135
codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
143136

144-
path = Path(verts, codes)
145-
patch = mpatches.PathPatch(path, facecolor=color, edgecolor="none", alpha=alpha)
137+
# Scale alpha by flow magnitude for visual depth
138+
alpha = 0.45 + 0.25 * (flow / flow_matrix.max())
139+
patch = mpatches.PathPatch(Path(verts, codes), facecolor=color, edgecolor=color, linewidth=0.3, alpha=alpha)
146140
ax.add_patch(patch)
147141

148-
149-
# Track position within each entity arc for placing chords
150-
arc_positions = {}
151-
for i in range(n):
152-
arc_positions[i] = {"out": start_angles[i], "in": start_angles[i]}
153-
154-
# Calculate the angular span each flow unit represents for each entity
155-
unit_angles = arc_spans / totals
156-
157-
# Draw chords for each flow
158-
for i in range(n):
159-
for j in range(n):
160-
if i != j and flow_matrix[i, j] > 0:
161-
flow = flow_matrix[i, j]
162-
163-
# Calculate chord width at source (outgoing from entity i)
164-
source_span = flow * unit_angles[i]
165-
source_start = arc_positions[i]["out"] - source_span
166-
source_end = arc_positions[i]["out"]
167-
arc_positions[i]["out"] = source_start
168-
169-
# Calculate chord width at target (incoming to entity j)
170-
target_span = flow * unit_angles[j]
171-
target_start = arc_positions[j]["in"] - target_span
172-
target_end = arc_positions[j]["in"]
173-
arc_positions[j]["in"] = target_start
174-
175-
# Draw the chord (use source color)
176-
draw_chord(ax, source_start, source_end, target_start, target_end, colors[i])
177-
178142
# Title
179143
ax.set_title(
180-
"Continental Migration Flows · chord-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20
144+
"Continental Migration Flows · chord-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=30
181145
)
182146

183147
plt.tight_layout()

plots/chord-basic/metadata/matplotlib.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
library: matplotlib
22
specification_id: chord-basic
33
created: 2025-12-14 19:45:02+00:00
4-
updated: 2025-12-14 19:45:02+00:00
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: 20213174710
77
issue: 858
8-
python_version: 3.13.11
8+
python_version: '3.14'
99
library_version: 3.10.8
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/chord-basic/matplotlib/plot.png
1111
preview_html: null
12-
quality_score: 98
12+
quality_score: null
1313
impl_tags:
1414
dependencies: []
1515
techniques:

plots/chord-basic/specification.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Basic Chord Diagram
66

77
# Specification tracking
88
created: 2025-12-14T19:33:31Z
9-
updated: 2025-12-14T19:33:31Z
9+
updated: 2026-04-06T12:00:00Z
1010
issue: 858
1111
suggested: MarkusNeusinger
1212

@@ -16,11 +16,13 @@ tags:
1616
- chord
1717
- flow
1818
data_type:
19+
- categorical
20+
- numeric
1921
- network
2022
- relational
2123
domain:
2224
- general
2325
features:
2426
- basic
2527
- circular
26-
- connection-visualization
28+
- proportional

0 commit comments

Comments
 (0)