{"spec_id":"chord-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nchord-basic: Basic Chord Diagram\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 85/100 | Created: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom matplotlib.patches import PathPatch, Wedge\nfrom matplotlib.path import Path\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\n\n# Imprint palette — one distinct hue per continent, first entity always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\nsns.set_theme(style=\"white\", rc={\"figure.facecolor\": PAGE_BG, \"axes.facecolor\": PAGE_BG, \"text.color\": INK})\nsns.set_palette(IMPRINT_PALETTE)\n\n# Data — directed migration flows between 6 continents (thousands of people / year)\ncontinents = [\"Africa\", \"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Oceania\"]\nflows = np.array(\n    [\n        [0, 120, 200, 90, 20, 15],  # from Africa\n        [60, 0, 250, 180, 30, 90],  # from Asia\n        [80, 70, 0, 140, 50, 60],  # from Europe\n        [30, 60, 110, 0, 70, 25],  # from N. America\n        [10, 20, 130, 160, 0, 8],  # from S. America\n        [5, 70, 40, 20, 5, 0],  # from Oceania\n    ],\n    dtype=float,\n)\n\n# Geometry — lay each continent on an arc proportional to its total outgoing flow\nn = len(continents)\ngap = np.deg2rad(4)  # angular gap between continent arcs\ngroup_total = flows.sum(axis=1)\nscale = (2 * np.pi - n * gap) / flows.sum()\n\ngroup_start = np.zeros(n)\ncursor = np.pi / 2  # start at the top of the circle\nfor i in range(n):\n    group_start[i] = cursor\n    cursor += group_total[i] * scale + gap\n\n# Sub-arc spans: slice (i, j) reserves the angle for the i -> j flow on i's arc\nsub_start = np.zeros((n, n))\nsub_end = np.zeros((n, n))\nfor i in range(n):\n    a = group_start[i]\n    for j in range(n):\n        sub_start[i, j] = a\n        a += flows[i, j] * scale\n        sub_end[i, j] = a\n\n# Focal corridor — the largest bidirectional pair, highlighted to anchor the story\ncombined = flows + flows.T\nfocal = np.unravel_index(np.argmax(np.triu(combined, 1)), combined.shape)\n\n# Plot — square canvas: figsize=(6, 6) dpi=400 -> 2400 x 2400 px\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\nouter_r = 1.0\nring_w = 0.07\nribbon_r = outer_r - ring_w - 0.006\narc_res = 24  # points used to trace each circular arc segment\n\n# Ribbons — one chord per continent pair, width at each end = that direction's flow\nfor i in range(n):\n    for j in range(i + 1, n):\n        if flows[i, j] == 0 and flows[j, i] == 0:\n            continue\n\n        # colour the chord by the dominant migration direction's source\n        src = i if flows[i, j] >= flows[j, i] else j\n\n        a0, a1 = sub_start[i, j], sub_end[i, j]\n        b0, b1 = sub_start[j, i], sub_end[j, i]\n\n        verts = []\n        codes = []\n\n        # arc along i's slice: a0 -> a1\n        ts = np.linspace(a0, a1, arc_res)\n        arc_i = ribbon_r * np.column_stack([np.cos(ts), np.sin(ts)])\n        verts.append(arc_i[0])\n        codes.append(Path.MOVETO)\n        for p in arc_i[1:]:\n            verts.append(p)\n            codes.append(Path.LINETO)\n\n        # quadratic Bezier through the centre: a1 -> b0\n        verts.append((0.0, 0.0))\n        codes.append(Path.CURVE3)\n        verts.append((ribbon_r * np.cos(b0), ribbon_r * np.sin(b0)))\n        codes.append(Path.CURVE3)\n\n        # arc along j's slice: b0 -> b1\n        ts = np.linspace(b0, b1, arc_res)\n        arc_j = ribbon_r * np.column_stack([np.cos(ts), np.sin(ts)])\n        for p in arc_j[1:]:\n            verts.append(p)\n            codes.append(Path.LINETO)\n\n        # quadratic Bezier through the centre: b1 -> a0 (closes the ribbon)\n        verts.append((0.0, 0.0))\n        codes.append(Path.CURVE3)\n        verts.append((ribbon_r * np.cos(a0), ribbon_r * np.sin(a0)))\n        codes.append(Path.CURVE3)\n        verts.append((0.0, 0.0))\n        codes.append(Path.CLOSEPOLY)\n\n        # emphasise the focal corridor; let the rest recede so the story reads\n        is_focal = {i, j} == {focal[0], focal[1]}\n        ax.add_patch(\n            PathPatch(\n                Path(verts, codes),\n                facecolor=IMPRINT_PALETTE[src],\n                edgecolor=INK if is_focal else PAGE_BG,\n                linewidth=1.1 if is_focal else 0.4,\n                alpha=0.92 if is_focal else 0.5,\n            )\n        )\n\n# Outer ring — one solid arc per continent for identity, plus a label\nfor i in range(n):\n    theta1 = np.rad2deg(group_start[i])\n    theta2 = np.rad2deg(group_start[i] + group_total[i] * scale)\n    ax.add_patch(\n        Wedge(\n            (0, 0),\n            outer_r,\n            theta1,\n            theta2,\n            width=ring_w,\n            facecolor=IMPRINT_PALETTE[i],\n            edgecolor=PAGE_BG,\n            linewidth=1.0,\n        )\n    )\n\n    mid = np.deg2rad((theta1 + theta2) / 2)\n    ax.text(\n        1.16 * np.cos(mid),\n        1.16 * np.sin(mid),\n        continents[i],\n        ha=\"center\",\n        va=\"center\",\n        fontsize=10,\n        fontweight=\"medium\",\n        color=INK,\n    )\n\n# Corner annotations — the inscribed circle leaves the canvas corners empty, so\n# use them for the focal insight (top-left) and the flow-magnitude scale (bottom-left)\nfa, fb = continents[focal[0]], continents[focal[1]]\nax.text(\n    -1.28,\n    1.24,\n    f\"Largest corridor\\n{fa} ↔ {fb}: {int(combined[focal])}k / year\",\n    ha=\"left\",\n    va=\"top\",\n    fontsize=9,\n    fontweight=\"medium\",\n    color=INK,\n)\nax.text(\n    -1.28,\n    -1.24,\n    \"Ribbon & arc width ∝ annual\\nmigration (thousands of people)\",\n    ha=\"left\",\n    va=\"bottom\",\n    fontsize=8,\n    color=INK,\n    alpha=0.7,\n)\n\nax.set_xlim(-1.3, 1.3)\nax.set_ylim(-1.3, 1.3)\n\n# Title — scale fontsize off the 67-char baseline so the long title never overflows\ntitle = \"Global Migration Flows · chord-basic · python · seaborn · anyplot.ai\"\ntitle_fs = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\nax.set_title(title, fontsize=title_fs, fontweight=\"medium\", color=INK, pad=12)\n\n# Save — bbox_inches stays default (None) so figsize x dpi gives exactly 2400x2400\nfig.subplots_adjust(left=0.02, right=0.98, top=0.94, bottom=0.02)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}