-
Notifications
You must be signed in to change notification settings - Fork 0
update(chord-basic): matplotlib — comprehensive review #5212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
95a47d9
update(chord-basic): matplotlib — comprehensive review
MarkusNeusinger c853944
chore(matplotlib): update quality score 88 and review feedback for ch…
github-actions[bot] f8957fc
fix(matplotlib): address review feedback for chord-basic
github-actions[bot] 177fb1f
chore(matplotlib): update quality score 87 and review feedback for ch…
github-actions[bot] 2becacf
fix(matplotlib): address review feedback for chord-basic
github-actions[bot] 8d570b3
chore(matplotlib): update quality score 90 and review feedback for ch…
github-actions[bot] b28c43f
Merge branch 'main' into implementation/chord-basic/matplotlib
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| """ 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: 90/100 | Created: 2026-04-06 | ||
| """ | ||
|
|
||
| import matplotlib.patches as mpatches | ||
|
|
@@ -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 | ||
|
|
@@ -27,158 +25,172 @@ | |
| ] | ||
| ) | ||
|
|
||
| # 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 | ||
|
|
||
| # Create figure | ||
| 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) | ||
| 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") | ||
|
|
||
| # Draw entity arcs on the outer ring | ||
| radius = 1.0 | ||
| arc_width = 0.08 | ||
| 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] | ||
|
|
||
| # 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=20, 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() | ||
| 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
|
||
|
|
||
| # 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 | ||
|
|
||
| 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 = max(flow * unit_angles[j], min_chord_deg) | ||
| 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 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, | ||
| }, | ||
| ) | ||
|
|
||
| # 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 | ||
| # Title and subtitle | ||
| 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=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="white") | ||
| plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="#FAFAFA") | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arc_cursorsis 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.