{"spec_id":"chord-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nchord-basic: Basic Chord Diagram\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 93/100 | Created: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.path import Path\n\n\n# Theme-adaptive chrome (Imprint palette) — only chrome flips between themes,\n# the categorical data colors stay constant.\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — first series ALWAYS brand green (#009E73)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data: Migration flows between continents (in millions)\nentities = [\"Africa\", \"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Oceania\"]\nn = len(entities)\n\nflow_matrix = np.array(\n    [\n        [0, 12, 8, 5, 2, 1],  # From Africa\n        [8, 0, 15, 10, 3, 4],  # From Asia\n        [3, 10, 0, 8, 4, 2],  # From Europe\n        [2, 6, 12, 0, 7, 3],  # From N. America\n        [1, 2, 5, 8, 0, 1],  # From S. America\n        [0, 3, 2, 2, 1, 0],  # From Oceania\n    ]\n)\n\ncolors = IMPRINT_PALETTE[:n]\n\n# Calculate entity totals and arc geometry\ntotals = flow_matrix.sum(axis=1) + flow_matrix.sum(axis=0)\ntotal_flow = totals.sum()\ngap_deg = 3\navailable_deg = 360 - gap_deg * n\narc_spans = (totals / total_flow) * available_deg\n\n# Start angles (clockwise from top)\nstart_angles = np.zeros(n)\nangle = 90\nfor i in range(n):\n    start_angles[i] = angle\n    angle -= arc_spans[i] + gap_deg\n\n# Plot — square canvas (2400x2400) for the circular chart\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, subplot_kw={\"aspect\": \"equal\"})\nfig.set_facecolor(PAGE_BG)\nax.set_xlim(-1.6, 1.6)\nax.set_ylim(-1.6, 1.6)\nax.set_facecolor(PAGE_BG)\nax.axis(\"off\")\n\nradius = 1.0\narc_width = 0.09\ninner_r = radius - arc_width\n\n# Draw outer arcs — separators use the page bg so segments read cleanly on both themes\nfor i in range(n):\n    theta1 = start_angles[i] - arc_spans[i]\n    theta2 = start_angles[i]\n    wedge = mpatches.Wedge(\n        (0, 0), radius, theta1, theta2, width=arc_width, facecolor=colors[i], edgecolor=PAGE_BG, linewidth=2\n    )\n    ax.add_patch(wedge)\n\n    # Label placement — color-coordinated with the entity arc\n    mid = np.radians((theta1 + theta2) / 2)\n    lx, ly = (radius + 0.13) * np.cos(mid), (radius + 0.13) * np.sin(mid)\n    mid_deg = np.degrees(mid) % 360\n    ha = (\n        \"center\"\n        if mid_deg < 15 or mid_deg > 345 or 165 < mid_deg < 195\n        else (\"right\" if 90 < mid_deg < 270 else \"left\")\n    )\n    ax.text(lx, ly, entities[i], fontsize=12, fontweight=\"bold\", ha=ha, va=\"center\", color=colors[i])\n\n# Track angular position within each arc for chord placement\nunit_angles = arc_spans / totals\n\n# Pre-compute chord positions to avoid cursor interference from draw order\nmin_chord_deg = 1.5  # minimum angular span for visibility\nchord_params = []\npos_cursors = start_angles.copy()\nfor i in range(n):\n    for j in range(n):\n        if i != j and flow_matrix[i, j] > 0:\n            flow = flow_matrix[i, j]\n            src_span = max(flow * unit_angles[i], min_chord_deg)\n            src_end = pos_cursors[i]\n            src_start = src_end - src_span\n            pos_cursors[i] = src_start\n\n            tgt_span = max(flow * unit_angles[j], min_chord_deg)\n            tgt_end = pos_cursors[j]\n            tgt_start = tgt_end - tgt_span\n            pos_cursors[j] = tgt_start\n\n            chord_params.append(\n                {\n                    \"i\": i,\n                    \"j\": j,\n                    \"src_start\": src_start,\n                    \"src_end\": src_end,\n                    \"tgt_start\": tgt_start,\n                    \"tgt_end\": tgt_end,\n                    \"color\": colors[i],\n                    \"flow\": flow,\n                }\n            )\n\n# Sort by flow magnitude so largest chords render on top\nchord_params.sort(key=lambda c: c[\"flow\"])\n\n# Draw chords using cubic Bezier paths\nn_arc_pts = 30\nctrl_factor = 0.25\nflow_max = flow_matrix.max()\n\nfor c in chord_params:\n    s1, e1 = np.radians(c[\"src_start\"]), np.radians(c[\"src_end\"])\n    s2, e2 = np.radians(c[\"tgt_start\"]), np.radians(c[\"tgt_end\"])\n\n    arc1_t = np.linspace(s1, e1, n_arc_pts)\n    arc1 = np.column_stack([inner_r * np.cos(arc1_t), inner_r * np.sin(arc1_t)])\n\n    arc2_t = np.linspace(s2, e2, n_arc_pts)\n    arc2 = np.column_stack([inner_r * np.cos(arc2_t), inner_r * np.sin(arc2_t)])\n\n    # Build closed path: arc1 -> bezier -> arc2 -> bezier -> close\n    verts = [arc1[0]]\n    codes = [Path.MOVETO]\n\n    for pt in arc1[1:]:\n        verts.append(pt)\n        codes.append(Path.LINETO)\n\n    verts.extend([arc1[-1] * ctrl_factor, arc2[0] * ctrl_factor, arc2[0]])\n    codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])\n\n    for pt in arc2[1:]:\n        verts.append(pt)\n        codes.append(Path.LINETO)\n\n    verts.extend([arc2[-1] * ctrl_factor, arc1[0] * ctrl_factor, arc1[0]])\n    codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])\n\n    # Scale alpha and linewidth by flow magnitude for clear visual hierarchy.\n    # Floor raised so the smallest flows (e.g. Oceania links) stay legible.\n    flow_ratio = c[\"flow\"] / flow_max\n    alpha = 0.35 + 0.5 * flow_ratio**0.6\n    lw = 0.4 + 1.3 * flow_ratio\n    patch = mpatches.PathPatch(\n        Path(verts, codes), facecolor=c[\"color\"], edgecolor=c[\"color\"], linewidth=lw, alpha=alpha\n    )\n    ax.add_patch(patch)\n\n# Annotate the top 3 flows with leader lines connecting each box to its chord\ntop_flows = sorted(chord_params, key=lambda c: c[\"flow\"], reverse=True)[:3]\n\n# Box anchor positions (outside the ring, in open quadrants)\nbox_positions = [(0.0, -1.48), (1.05, 1.18), (-1.02, 0.95)]\nfor rank, c in enumerate(top_flows):\n    # Leader-line target: source-ribbon midpoint pulled just inside the arc\n    src_mid = np.radians((c[\"src_start\"] + c[\"src_end\"]) / 2)\n    tx, ty = inner_r * 0.88 * np.cos(src_mid), inner_r * 0.88 * np.sin(src_mid)\n\n    bx, by = box_positions[rank]\n    label = f\"{entities[c['i']]} → {entities[c['j']]}: {c['flow']}M\"\n    fs = 11 if rank == 0 else 10\n    ax.annotate(\n        label,\n        xy=(tx, ty),\n        xytext=(bx, by),\n        fontsize=fs,\n        fontweight=\"bold\" if rank == 0 else \"normal\",\n        ha=\"center\",\n        va=\"center\",\n        color=INK,\n        bbox={\n            \"boxstyle\": \"round,pad=0.35\",\n            \"facecolor\": ELEVATED_BG,\n            \"edgecolor\": c[\"color\"],\n            \"alpha\": 0.95,\n            \"linewidth\": 1.5,\n        },\n        arrowprops={\n            \"arrowstyle\": \"-\",\n            \"color\": c[\"color\"],\n            \"linewidth\": 1.3,\n            \"alpha\": 0.8,\n            \"connectionstyle\": \"arc3,rad=0.2\",\n        },\n    )\n\n# Title and subtitle — title fontsize scales with length to avoid overflow\ntitle = \"Continental Migration · chord-basic · matplotlib · anyplot.ai\"\ntitle_fs = max(8, round(13 * 60 / len(title))) if len(title) > 60 else 13\nax.set_title(title, fontsize=title_fs, fontweight=\"medium\", pad=16, color=INK)\nax.text(\n    0,\n    1.40,\n    \"Asia–Europe corridor dominates global flows\",\n    fontsize=11,\n    ha=\"center\",\n    va=\"center\",\n    color=INK_SOFT,\n    fontstyle=\"italic\",\n)\n\nfig.subplots_adjust(left=0.04, right=0.96, top=0.90, bottom=0.04)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}