-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmatplotlib.py
More file actions
196 lines (165 loc) · 6.03 KB
/
matplotlib.py
File metadata and controls
196 lines (165 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
""" pyplots.ai
chord-basic: Basic Chord Diagram
Library: matplotlib 3.10.8 | Python 3.14
Quality: 90/100 | Created: 2026-04-06
"""
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.path import Path
# Data: Migration flows between continents (in millions)
entities = ["Africa", "Asia", "Europe", "N. America", "S. America", "Oceania"]
n = len(entities)
flow_matrix = np.array(
[
[0, 12, 8, 5, 2, 1], # From Africa
[8, 0, 15, 10, 3, 4], # From Asia
[3, 10, 0, 8, 4, 2], # From Europe
[2, 6, 12, 0, 7, 3], # From N. America
[1, 2, 5, 8, 0, 1], # From S. America
[0, 3, 2, 2, 1, 0], # From Oceania
]
)
# Colorblind-safe palette starting with Python Blue
colors = ["#306998", "#E69F00", "#009E73", "#D55E00", "#56B4E9", "#CC79A7"]
# Calculate entity totals and arc geometry
totals = flow_matrix.sum(axis=1) + flow_matrix.sum(axis=0)
total_flow = totals.sum()
gap_deg = 3
available_deg = 360 - gap_deg * n
arc_spans = (totals / total_flow) * available_deg
# Start angles (clockwise from top)
start_angles = np.zeros(n)
angle = 90
for i in range(n):
start_angles[i] = angle
angle -= arc_spans[i] + gap_deg
# Plot — square canvas for circular chart
fig, ax = plt.subplots(figsize=(12, 12), subplot_kw={"aspect": "equal"})
fig.set_facecolor("#FAFAFA")
ax.set_xlim(-1.45, 1.45)
ax.set_ylim(-1.45, 1.45)
ax.set_facecolor("#FAFAFA")
ax.axis("off")
radius = 1.0
arc_width = 0.09
inner_r = radius - arc_width
# Draw outer arcs
for i in range(n):
theta1 = start_angles[i] - arc_spans[i]
theta2 = start_angles[i]
wedge = mpatches.Wedge(
(0, 0), radius, theta1, theta2, width=arc_width, facecolor=colors[i], edgecolor="white", linewidth=2
)
ax.add_patch(wedge)
# Label placement
mid = np.radians((theta1 + theta2) / 2)
lx, ly = (radius + 0.12) * np.cos(mid), (radius + 0.12) * np.sin(mid)
mid_deg = np.degrees(mid) % 360
ha = (
"center"
if mid_deg < 15 or mid_deg > 345 or 165 < mid_deg < 195
else ("right" if 90 < mid_deg < 270 else "left")
)
ax.text(lx, ly, entities[i], fontsize=20, fontweight="bold", ha=ha, va="center", color=colors[i])
# Track angular position within each arc for chord placement
arc_cursors = start_angles.copy()
unit_angles = arc_spans / totals
# Sort flows by magnitude (draw largest last for visual hierarchy)
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]
flows.sort(key=lambda f: f[2])
# Pre-compute chord positions to avoid cursor interference from draw order
min_chord_deg = 1.5 # minimum angular span for visibility
chord_params = []
pos_cursors = start_angles.copy()
for i in range(n):
for j in range(n):
if i != j and flow_matrix[i, j] > 0:
flow = flow_matrix[i, j]
src_span = max(flow * unit_angles[i], min_chord_deg)
src_end = pos_cursors[i]
src_start = src_end - src_span
pos_cursors[i] = src_start
tgt_span = max(flow * unit_angles[j], min_chord_deg)
tgt_end = pos_cursors[j]
tgt_start = tgt_end - tgt_span
pos_cursors[j] = tgt_start
chord_params.append((src_start, src_end, tgt_start, tgt_end, colors[i], flow))
# Sort by flow magnitude so largest chords render on top
chord_params.sort(key=lambda c: c[5])
# Draw chords using cubic Bezier paths
n_arc_pts = 30
ctrl_factor = 0.25
for src_start, src_end, tgt_start, tgt_end, color, flow in chord_params:
s1, e1 = np.radians(src_start), np.radians(src_end)
s2, e2 = np.radians(tgt_start), np.radians(tgt_end)
arc1_t = np.linspace(s1, e1, n_arc_pts)
arc1 = np.column_stack([inner_r * np.cos(arc1_t), inner_r * np.sin(arc1_t)])
arc2_t = np.linspace(s2, e2, n_arc_pts)
arc2 = np.column_stack([inner_r * np.cos(arc2_t), inner_r * np.sin(arc2_t)])
# Build closed path: arc1 → bezier → arc2 → bezier → close
verts = [arc1[0]]
codes = [Path.MOVETO]
for pt in arc1[1:]:
verts.append(pt)
codes.append(Path.LINETO)
verts.extend([arc1[-1] * ctrl_factor, arc2[0] * ctrl_factor, arc2[0]])
codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
for pt in arc2[1:]:
verts.append(pt)
codes.append(Path.LINETO)
verts.extend([arc2[-1] * ctrl_factor, arc1[0] * ctrl_factor, arc1[0]])
codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
# Scale alpha and linewidth by flow magnitude for clear visual hierarchy
flow_ratio = flow / flow_matrix.max()
alpha = 0.15 + 0.65 * flow_ratio**0.7
lw = 0.3 + 1.2 * flow_ratio
patch = mpatches.PathPatch(Path(verts, codes), facecolor=color, edgecolor=color, linewidth=lw, alpha=alpha)
ax.add_patch(patch)
# Annotate the top 3 flows to create a clear data story
top_flows = sorted(
[(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],
key=lambda f: f[2],
reverse=True,
)[:3]
ann_positions = [(-0.55, -1.28), (0.55, 1.20), (-0.95, 0.60)]
for rank, (i, j, flow) in enumerate(top_flows):
ax_x, ax_y = ann_positions[rank]
label = f"{entities[i]} → {entities[j]}: {flow}M"
fs = 14 if rank == 0 else 12
ax.annotate(
label,
xy=(ax_x, ax_y),
fontsize=fs,
fontweight="bold" if rank == 0 else "medium",
ha="center",
va="center",
color="#333333",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": "white",
"edgecolor": colors[i],
"alpha": 0.92,
"linewidth": 1.5,
},
)
# Title and subtitle
ax.set_title(
"Continental Migration Flows · chord-basic · matplotlib · pyplots.ai",
fontsize=24,
fontweight="medium",
pad=40,
color="#333333",
)
ax.text(
0,
1.38,
"Asia–Europe corridor dominates global flows",
fontsize=16,
ha="center",
va="center",
color="#666666",
fontstyle="italic",
)
plt.tight_layout()
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="#FAFAFA")