diff --git a/plots/chord-basic/implementations/matplotlib.py b/plots/chord-basic/implementations/matplotlib.py index 5bac8fdb48..a811edb914 100644 --- a/plots/chord-basic/implementations/matplotlib.py +++ b/plots/chord-basic/implementations/matplotlib.py @@ -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]) +# 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") diff --git a/plots/chord-basic/metadata/matplotlib.yaml b/plots/chord-basic/metadata/matplotlib.yaml index 510da20ac8..6e80b5d8af 100644 --- a/plots/chord-basic/metadata/matplotlib.yaml +++ b/plots/chord-basic/metadata/matplotlib.yaml @@ -1,31 +1,232 @@ 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:47:55Z' +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: 90 impl_tags: dependencies: [] techniques: - bezier-curves - patches - - manual-ticks + - annotations patterns: - data-generation - matrix-construction - iteration-over-groups dataprep: [] styling: - - alpha-blending - minimal-chrome + - alpha-blending + - edge-highlighting review: - strengths: [] - weaknesses: [] + strengths: + - Excellent data storytelling with subtitle, annotated top flows, and alpha/linewidth + hierarchy + - Custom Bezier chord rendering with proper angular positioning and pre-computed + cursor tracking + - Colorblind-safe palette with color-coordinated entity labels + - Clean, well-structured code with smart geometry calculations + weaknesses: + - Annotation boxes float without visual connection (arrows/lines) to the specific + chords they describe + - Smallest chords (Oceania connections) are very faint at ~0.15 alpha improvements: [] verdict: APPROVED + image_description: 'The plot displays a chord diagram on a light gray (#FAFAFA) + background with 6 continents arranged clockwise from the top: Africa (dark blue), + Asia (amber/gold), Europe (teal/green), N. America (burnt orange), S. America + (light blue), and Oceania (pink/mauve). Each entity has a colored outer arc segment + proportional to its total flow, with white edge separators between segments and + 3-degree gaps. Chords connect entities through the interior using cubic Bezier + curves, colored by source entity with alpha and linewidth scaled by flow magnitude + — the largest chords (Asia→Europe at 15M) are bold and opaque while smaller flows + (Oceania connections) are faint and thin. Three annotation boxes with rounded + corners highlight the top flows: "Asia → Europe: 15M" at bottom, "Africa → Asia: + 12M" at upper right, and "N. America → Europe: 12M" at left. A subtitle in italic + gray reads "Asia–Europe corridor dominates global flows." The title follows the + required format: "Continental Migration Flows · chord-basic · matplotlib · pyplots.ai".' + criteria_checklist: + visual_quality: + score: 27 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set (title 24pt, labels 20pt bold, subtitle + 16pt). Annotations at 12-14pt slightly small but readable. + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: Entity labels well-positioned with smart alignment logic. No text + overlap. Chord crossings inherent to chart type. + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Alpha/linewidth scaling creates good hierarchy. Minimum chord degree + ensures small flows visible, though Oceania connections quite faint. + - id: VQ-04 + name: Color Accessibility + score: 4 + max: 4 + passed: true + comment: Colorblind-safe Wong palette with good contrast on light background. + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Square 12x12 canvas ideal for circular diagram. Chart fills canvas + well. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Axes correctly hidden for chord diagram. Title descriptive with context. + design_excellence: + score: 16 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Custom colorblind-safe palette, color-matched entity labels, warm + background, white edge separators, alpha/linewidth scaling. Clearly above + defaults. + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Axes off, custom background, white edge lines, 3-degree gap spacing, + generous whitespace. + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: Strong subtitle, annotated top 3 flows with styled callout boxes, + alpha/linewidth hierarchy for visual emphasis. + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct chord diagram with perimeter arcs and Bezier-curve chords. + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Distinct colors, proportional chord width, bidirectional flows, 6 + continents. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Flow matrix correctly maps source to target with proper angular allocation. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title follows required format. Color-matched entity labels serve + as legend. + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows bidirectional asymmetric flows, varying magnitudes 1-15M, all + 6 entities interconnected. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Continental migration flows in millions — realistic, neutral, comprehensible. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Values 1-15M plausible for continental migration flows. + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Linear flow: imports, data, geometry, plot, save. No functions or + classes.' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Fully deterministic with hardcoded flow matrix. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: 'All imports used: matplotlib.patches, pyplot, numpy, Path.' + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Smart pre-computation of chord positions. Clean Bezier path construction. + Appropriate complexity. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png with dpi=300, bbox_inches=tight. + library_mastery: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Proper use of matplotlib low-level rendering API (fig/ax, add_patch) + — correct approach for custom chord diagram. + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Leverages Path with CURVE4 Bezier curves, mpatches.Wedge, PathPatch + — distinctive matplotlib geometry features. diff --git a/plots/chord-basic/specification.yaml b/plots/chord-basic/specification.yaml index 34c47b8560..696903da2d 100644 --- a/plots/chord-basic/specification.yaml +++ b/plots/chord-basic/specification.yaml @@ -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 @@ -16,6 +16,8 @@ tags: - chord - flow data_type: + - categorical + - numeric - network - relational domain: @@ -23,4 +25,4 @@ tags: features: - basic - circular - - connection-visualization + - proportional