Skip to content
182 changes: 73 additions & 109 deletions plots/chord-basic/implementations/matplotlib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
""" pyplots.ai
"""pyplots.ai
chord-basic: Basic Chord Diagram
Library: matplotlib 3.10.8 | Python 3.13.11
Quality: 98/100 | Created: 2025-12-14
Library: matplotlib 3.10.8 | Python 3.14
Quality: /100 | Updated: 2026-04-06
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header metadata looks malformed: Quality: /100 is missing the numeric score. This likely breaks any parsers that extract quality/created/updated info from the standard 4-line header. Fill in the quality score (or use the established pending pattern if it truly isn’t available).

Suggested change
Quality: /100 | Updated: 2026-04-06
Quality: pending | Updated: 2026-04-06

Copilot uses AI. Check for mistakes.
"""

import matplotlib.patches as mpatches
Expand All @@ -11,11 +11,9 @@


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

# Flow matrix (row=source, col=target)
flow_matrix = np.array(
[
[0, 12, 8, 5, 2, 1], # From Africa
Expand All @@ -27,157 +25,123 @@
]
)

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

# Calculate totals for each entity (sum of outgoing + incoming)
# 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

# Gap between entity arcs (in degrees)
gap = 3
total_gap = gap * n
available_degrees = 360 - total_gap

# Calculate arc spans for each entity
arc_spans = (totals / total_flow) * available_degrees

# Calculate start angles for each entity arc
# Start angles (clockwise from top)
start_angles = np.zeros(n)
current_angle = 90 # Start from top
angle = 90
for i in range(n):
start_angles[i] = current_angle
current_angle -= arc_spans[i] + gap
start_angles[i] = angle
angle -= arc_spans[i] + gap_deg

# Create figure
# Plot
fig, ax = plt.subplots(figsize=(16, 9), subplot_kw={"aspect": "equal"})
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.3, 1.3)
ax.set_xlim(-1.55, 1.55)
ax.set_ylim(-1.35, 1.35)
ax.axis("off")

# Draw entity arcs on the outer ring
radius = 1.0
arc_width = 0.08
inner_r = radius - arc_width

# Draw outer arcs
for i in range(n):
theta1 = start_angles[i] - arc_spans[i]
theta2 = start_angles[i]

# Draw outer arc as a wedge
wedge = mpatches.Wedge(
(0, 0), radius, theta1, theta2, width=arc_width, facecolor=colors[i], edgecolor="white", linewidth=2
)
ax.add_patch(wedge)

# Add entity label
mid_angle = (theta1 + theta2) / 2
label_radius = radius + 0.12
label_x = label_radius * np.cos(np.radians(mid_angle))
label_y = label_radius * np.sin(np.radians(mid_angle))
# 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=18, fontweight="bold", ha=ha, va="center", color=colors[i])

# Rotate text to align with arc
rotation = mid_angle
if mid_angle > 90 or mid_angle < -90:
rotation = mid_angle + 180
if 90 < mid_angle < 270 or -270 < mid_angle < -90:
ha = "right"
else:
ha = "left"
if abs(mid_angle) < 10 or abs(mid_angle - 180) < 10 or abs(mid_angle + 180) < 10:
ha = "center"
# Track angular position within each arc for chord placement
arc_cursors = start_angles.copy()
Comment on lines +77 to +78
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arc_cursors is assigned but never used. Since plots/ is excluded from Ruff checks, this won’t be caught automatically but still adds dead code and confusion. Remove it, or use it consistently for chord placement if that was the intent.

Suggested change
# Track angular position within each arc for chord placement
arc_cursors = start_angles.copy()
# Angular span per unit of flow for chord placement

Copilot uses AI. Check for mistakes.
unit_angles = arc_spans / totals

ax.text(label_x, label_y, entities[i], fontsize=18, fontweight="bold", ha=ha, va="center", color=colors[i])
# 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])
Comment on lines +81 to +83
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flows is computed and sorted but never used (the code renders based on chord_params instead). Remove flows and/or update the surrounding comment so it matches the actual draw-order logic.

Copilot uses AI. Check for mistakes.

# Pre-compute chord positions to avoid cursor interference from draw order
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 = flow * unit_angles[i]
src_end = pos_cursors[i]
src_start = src_end - src_span
pos_cursors[i] = src_start

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

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

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

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

# Second arc (target)
arc2_angles = np.linspace(s2, e2, n_arc_points)
arc2_points = np.column_stack([inner_radius * np.cos(arc2_angles), inner_radius * np.sin(arc2_angles)])
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)

# Control points for Bezier curves (through center with some offset)
ctrl_factor = 0.3
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)])

# Build path: arc1 -> bezier to arc2 -> arc2 -> bezier back to arc1
verts = []
codes = []
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)])

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

# Arc1 points
for pt in arc1_points[1:]:
for pt in arc1[1:]:
verts.append(pt)
codes.append(Path.LINETO)

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

# Arc2 points
for pt in arc2_points[1:]:
for pt in arc2[1:]:
verts.append(pt)
codes.append(Path.LINETO)

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

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


# Track position within each entity arc for placing chords
arc_positions = {}
for i in range(n):
arc_positions[i] = {"out": start_angles[i], "in": start_angles[i]}

# Calculate the angular span each flow unit represents for each entity
unit_angles = arc_spans / totals

# Draw chords for each flow
for i in range(n):
for j in range(n):
if i != j and flow_matrix[i, j] > 0:
flow = flow_matrix[i, j]

# Calculate chord width at source (outgoing from entity i)
source_span = flow * unit_angles[i]
source_start = arc_positions[i]["out"] - source_span
source_end = arc_positions[i]["out"]
arc_positions[i]["out"] = source_start

# Calculate chord width at target (incoming to entity j)
target_span = flow * unit_angles[j]
target_start = arc_positions[j]["in"] - target_span
target_end = arc_positions[j]["in"]
arc_positions[j]["in"] = target_start

# Draw the chord (use source color)
draw_chord(ax, source_start, source_end, target_start, target_end, colors[i])

# Title
ax.set_title(
"Continental Migration Flows · chord-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20
"Continental Migration Flows · chord-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=30
)

plt.tight_layout()
Expand Down
8 changes: 4 additions & 4 deletions plots/chord-basic/metadata/matplotlib.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
library: matplotlib
specification_id: chord-basic
created: 2025-12-14 19:45:02+00:00
updated: 2025-12-14 19:45:02+00:00
generated_by: claude-opus-4-5-20251101
updated: 2026-04-06T20:26:19+00:00
generated_by: claude-opus-4-6
workflow_run: 20213174710
issue: 858
python_version: 3.13.11
python_version: '3.14'
library_version: 3.10.8
preview_url: https://storage.googleapis.com/pyplots-images/plots/chord-basic/matplotlib/plot.png
preview_html: null
quality_score: 98
quality_score: null
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quality_score is set to null even though this metadata file has review.verdict: APPROVED. Across the repo, quality_score is consistently an integer (e.g., plots/band-basic/metadata/matplotlib.yaml:12), and null may break tooling that expects a numeric score (labels, dashboards, etc.). Set quality_score to the evaluated integer score for this implementation.

Suggested change
quality_score: null
quality_score: 90

Copilot uses AI. Check for mistakes.
impl_tags:
dependencies: []
techniques:
Expand Down
6 changes: 4 additions & 2 deletions plots/chord-basic/specification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: Basic Chord Diagram

# Specification tracking
created: 2025-12-14T19:33:31Z
updated: 2025-12-14T19:33:31Z
updated: 2026-04-06T12:00:00Z
issue: 858
suggested: MarkusNeusinger

Expand All @@ -16,11 +16,13 @@ tags:
- chord
- flow
data_type:
- categorical
- numeric
- network
- relational
domain:
- general
features:
- basic
- circular
- connection-visualization
- proportional
Loading