{"spec_id":"chord-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nchord-basic: Basic Chord Diagram\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\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 palette — 6 hues for 6 entities (first series always #009E73)\nCOLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\nCOLORS_DIM = [\n    \"rgba(0,158,115,0.28)\",\n    \"rgba(196,117,253,0.28)\",\n    \"rgba(68,103,163,0.28)\",\n    \"rgba(189,130,51,0.28)\",\n    \"rgba(174,48,48,0.28)\",\n    \"rgba(42,188,205,0.28)\",\n]\n\n# Data: Migration flows between 6 continents (bidirectional, millions of people)\ncontinents = [\"Africa\", \"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Oceania\"]\nn = len(continents)\n\nflow_matrix = np.array(\n    [\n        [0, 5, 8, 3, 2, 1],  # Africa to others\n        [4, 0, 10, 7, 3, 5],  # Asia to others\n        [7, 12, 0, 8, 4, 3],  # Europe to others\n        [3, 6, 7, 0, 5, 2],  # N. America to others\n        [2, 3, 6, 7, 0, 1],  # S. America to others\n        [1, 4, 3, 2, 1, 0],  # Oceania to others\n    ]\n)\n\n# Totals per continent and dominant corridor for storytelling\ntotals = flow_matrix.sum(axis=0) + flow_matrix.sum(axis=1)\ntotal_flow = flow_matrix.sum()\n\nmax_flow_idx = np.unravel_index(np.argmax(flow_matrix + flow_matrix.T), flow_matrix.shape)\ndominant_src, dominant_tgt = max_flow_idx\ndominant_flow = flow_matrix[dominant_src, dominant_tgt] + flow_matrix[dominant_tgt, dominant_src]\n\n# Arc positions around the circle\ngap = 0.02\narc_starts = []\narc_ends = []\ncurrent_pos = 0\nfor total in totals:\n    arc_starts.append(current_pos)\n    arc_ends.append(current_pos + (total / total_flow) * (1 - n * gap))\n    current_pos = arc_ends[-1] + gap\n\n# Figure\nfig = go.Figure()\n\n# Outer translucent ring + inner solid arc for each continent\nfor i in range(n):\n    angles_outer = np.linspace(2 * np.pi * arc_starts[i] - np.pi / 2, 2 * np.pi * arc_ends[i] - np.pi / 2, 100)\n    angles_rev = angles_outer[::-1]\n\n    fig.add_trace(\n        go.Scatter(\n            x=np.concatenate([1.02 * np.cos(angles_outer), 0.98 * np.cos(angles_rev)]),\n            y=np.concatenate([1.02 * np.sin(angles_outer), 0.98 * np.sin(angles_rev)]),\n            fill=\"toself\",\n            fillcolor=COLORS_DIM[i],\n            line={\"color\": \"rgba(0,0,0,0)\", \"width\": 0},\n            hoverinfo=\"skip\",\n            showlegend=False,\n        )\n    )\n\n    fig.add_trace(\n        go.Scatter(\n            x=np.concatenate([np.cos(angles_outer), 0.94 * np.cos(angles_rev)]),\n            y=np.concatenate([np.sin(angles_outer), 0.94 * np.sin(angles_rev)]),\n            fill=\"toself\",\n            fillcolor=COLORS[i],\n            line={\"color\": PAGE_BG, \"width\": 0.5},\n            hovertemplate=(\n                f\"<b>{continents[i]}</b><br>\"\n                f\"Outgoing: {int(flow_matrix[i].sum())}M<br>\"\n                f\"Incoming: {int(flow_matrix[:, i].sum())}M<br>\"\n                f\"Total: {int(totals[i])}M people\"\n                \"<extra></extra>\"\n            ),\n            name=continents[i],\n            showlegend=True,\n            legendgroup=continents[i],\n        )\n    )\n\n# Chords — width proportional to flow magnitude\nmin_chord_width = 0.010\nfor i in range(n):\n    src_pos = arc_starts[i]\n    for j in range(n):\n        if i == j or flow_matrix[i, j] == 0:\n            continue\n\n        flow = flow_matrix[i, j]\n        chord_width = max((flow / total_flow) * (1 - n * gap), min_chord_width)\n\n        is_dominant = (i == dominant_src and j == dominant_tgt) or (i == dominant_tgt and j == dominant_src)\n        opacity = 0.75 if is_dominant else 0.50\n        line_width = 2.0 if is_dominant else 0.3\n\n        tgt_base = arc_starts[j]\n        tgt_offset = sum(\n            max((flow_matrix[k, j] / total_flow) * (1 - n * gap), min_chord_width)\n            for k in range(i)\n            if flow_matrix[k, j] > 0\n        )\n\n        src_angle1 = 2 * np.pi * src_pos - np.pi / 2\n        src_angle2 = 2 * np.pi * (src_pos + chord_width) - np.pi / 2\n        sx1, sy1 = 0.94 * np.cos(src_angle1), 0.94 * np.sin(src_angle1)\n        sx2, sy2 = 0.94 * np.cos(src_angle2), 0.94 * np.sin(src_angle2)\n\n        tgt_start = tgt_base + tgt_offset\n        tgt_end = tgt_start + chord_width\n        tgt_angle1 = 2 * np.pi * tgt_start - np.pi / 2\n        tgt_angle2 = 2 * np.pi * tgt_end - np.pi / 2\n        tx1, ty1 = 0.94 * np.cos(tgt_angle1), 0.94 * np.sin(tgt_angle1)\n        tx2, ty2 = 0.94 * np.cos(tgt_angle2), 0.94 * np.sin(tgt_angle2)\n\n        src_angles = np.linspace(src_angle1, src_angle2, 20)\n        t = np.linspace(0, 1, 40)\n        bez1_x = (1 - t) ** 2 * sx2 + 2 * (1 - t) * t * 0 + t**2 * tx1\n        bez1_y = (1 - t) ** 2 * sy2 + 2 * (1 - t) * t * 0 + t**2 * ty1\n\n        tgt_angles = np.linspace(tgt_angle1, tgt_angle2, 20)\n        bez2_x = (1 - t) ** 2 * tx2 + 2 * (1 - t) * t * 0 + t**2 * sx1\n        bez2_y = (1 - t) ** 2 * ty2 + 2 * (1 - t) * t * 0 + t**2 * sy1\n\n        chord_x = np.concatenate([0.94 * np.cos(src_angles), bez1_x, 0.94 * np.cos(tgt_angles), bez2_x])\n        chord_y = np.concatenate([0.94 * np.sin(src_angles), bez1_y, 0.94 * np.sin(tgt_angles), bez2_y])\n\n        fig.add_trace(\n            go.Scatter(\n                x=chord_x,\n                y=chord_y,\n                fill=\"toself\",\n                fillcolor=COLORS[i],\n                opacity=opacity,\n                line={\"color\": COLORS[i], \"width\": line_width},\n                hovertemplate=(\n                    f\"<b>{continents[i]} → {continents[j]}</b><br>\"\n                    f\"Flow: {flow}M people<br>\"\n                    f\"Share: {flow / total_flow * 100:.1f}% of total\"\n                    \"<extra></extra>\"\n                ),\n                showlegend=False,\n                hoveron=\"fills\",\n            )\n        )\n\n        src_pos += chord_width\n\n# Continent labels at perimeter\nfor i in range(n):\n    mid_pos = (arc_starts[i] + arc_ends[i]) / 2\n    angle = 2 * np.pi * mid_pos - np.pi / 2\n    lx = 1.17 * np.cos(angle)\n    ly = 1.17 * np.sin(angle)\n    angle_deg = np.degrees(angle) % 360\n\n    if 45 < angle_deg < 135:\n        xanchor, yanchor = \"center\", \"bottom\"\n    elif 135 <= angle_deg < 225:\n        xanchor, yanchor = \"right\", \"middle\"\n    elif 225 <= angle_deg < 315:\n        xanchor, yanchor = \"center\", \"top\"\n    else:\n        xanchor, yanchor = \"left\", \"middle\"\n\n    fig.add_annotation(\n        x=lx,\n        y=ly,\n        text=(f\"<b>{continents[i]}</b><span style='font-size:11px;color:{INK_SOFT}'> {int(totals[i])}M</span>\"),\n        font={\"size\": 14, \"color\": COLORS[i], \"family\": \"Arial, Helvetica, sans-serif\"},\n        showarrow=False,\n        xanchor=xanchor,\n        yanchor=yanchor,\n    )\n\n# Subtitle\nfig.add_annotation(\n    text=f\"Europe–Asia dominates at <b>{dominant_flow}M</b>  ·  chord width ∝ flow\",\n    xref=\"paper\",\n    yref=\"paper\",\n    x=0.5,\n    y=0.965,\n    showarrow=False,\n    font={\"size\": 14, \"color\": INK_MUTED, \"family\": \"Arial, Helvetica, sans-serif\"},\n    xanchor=\"center\",\n)\n\n# Title with scaled fontsize for long string\ntitle = \"Migration Flows Between Continents · chord-basic · python · plotly · anyplot.ai\"\ntitle_len = len(title)\ntitle_fontsize = round(16 * 67 / title_len) if title_len > 67 else 16\ntitle_fontsize = max(title_fontsize, 11)\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": title,\n        \"font\": {\"size\": title_fontsize, \"color\": INK, \"family\": \"Arial Black, Arial, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.99,\n        \"yanchor\": \"top\",\n    },\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False, \"range\": [-1.38, 1.38]},\n    yaxis={\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"showline\": False,\n        \"range\": [-1.38, 1.38],\n        \"scaleanchor\": \"x\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    showlegend=True,\n    legend={\n        \"font\": {\"size\": 11, \"family\": \"Arial, Helvetica, sans-serif\", \"color\": INK_SOFT},\n        \"title\": {\"text\": \"<b>Continents</b>\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"x\": 0.99,\n        \"y\": 0.01,\n        \"xanchor\": \"right\",\n        \"yanchor\": \"bottom\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"tracegroupgap\": 3,\n        \"itemsizing\": \"constant\",\n    },\n    margin={\"l\": 30, \"r\": 30, \"t\": 75, \"b\": 55},\n    hovermode=\"closest\",\n    hoverlabel={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"font\": {\"size\": 13, \"family\": \"Arial, Helvetica, sans-serif\", \"color\": INK},\n    },\n)\n\n# Save — square canvas (2400×2400) ideal for circular chord diagram\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}