{"spec_id":"chord-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nchord-basic: Basic Chord Diagram\nLibrary: plotnine 0.15.7 | Python 3.13.13\nQuality: 93/100 | Created: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    theme,\n    theme_void,\n)\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\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette — abstract entities, canonical order, first series #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data — annual migration flows between 6 continents (thousands of people / year)\n# Square directed matrix: M[i, j] = flow FROM entity i TO entity j (diagonal = 0)\ncontinents = [\"Africa\", \"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Oceania\"]\nflow_matrix = np.array(\n    [\n        [0, 35, 90, 40, 5, 8],  # Africa -> ...\n        [20, 0, 110, 130, 10, 45],  # Asia -> ...\n        [25, 30, 0, 70, 15, 30],  # Europe -> ...\n        [10, 35, 40, 0, 25, 12],  # N. America -> ...\n        [4, 8, 55, 60, 0, 5],  # S. America -> ...\n        [3, 25, 20, 15, 2, 0],  # Oceania -> ...\n    ],\n    dtype=float,\n)\nn = len(continents)\n\n# Chord layout — each entity occupies an arc proportional to its total outflow,\n# subdivided into one slice per destination (d3-style chord geometry)\ngroup_value = flow_matrix.sum(axis=1)\ntotal = group_value.sum()\npad = 0.05  # angular gap between adjacent entity arcs (radians)\nscale = (2 * np.pi - n * pad) / total\n\nsub_start = np.zeros((n, n))\nsub_end = np.zeros((n, n))\ngroup_a0 = np.zeros(n)\ngroup_a1 = np.zeros(n)\n\nangle = np.pi / 2  # start at the top of the circle\nfor i in range(n):\n    group_a0[i] = angle\n    cursor = angle\n    for j in range(n):\n        sub_start[i, j] = cursor\n        cursor += flow_matrix[i, j] * scale\n        sub_end[i, j] = cursor\n    group_a1[i] = cursor\n    angle = cursor + pad\n\n# Radii: ribbons attach at R_IN; the entity arc band sits between R_IN and R_OUT\nR_IN = 1.0\nR_OUT = 1.08\nR_LABEL = 1.22\n\n# Outer entity arc bands (filled annular sectors)\narc_x, arc_y, arc_gid, arc_entity = [], [], [], []\nfor i in range(n):\n    span = group_a1[i] - group_a0[i]\n    steps = max(8, int(span / 0.02))\n    outer = np.linspace(group_a0[i], group_a1[i], steps)\n    inner = outer[::-1]\n    xs = np.concatenate([R_OUT * np.cos(outer), R_IN * np.cos(inner)])\n    ys = np.concatenate([R_OUT * np.sin(outer), R_IN * np.sin(inner)])\n    arc_x.extend(xs)\n    arc_y.extend(ys)\n    arc_gid.extend([f\"arc{i}\"] * len(xs))\n    arc_entity.extend([continents[i]] * len(xs))\n\narcs_df = pd.DataFrame({\"x\": arc_x, \"y\": arc_y, \"gid\": arc_gid, \"entity\": arc_entity})\n\n# Ribbons — one per entity pair, ends curve through the circle centre.\n# The dominant flow direction gives the ribbon its colour (its source entity).\nrib_x, rib_y, rib_gid, rib_entity = [], [], [], []\nt = np.linspace(0.0, 1.0, 28)  # quadratic Bezier parameter (control point = origin)\nfor i in range(n):\n    for j in range(i + 1, n):\n        if flow_matrix[i, j] == 0 and flow_matrix[j, i] == 0:\n            continue\n        src, dst = (i, j) if flow_matrix[i, j] >= flow_matrix[j, i] else (j, i)\n\n        s0, s1 = sub_start[src, dst], sub_end[src, dst]\n        d0, d1 = sub_start[dst, src], sub_end[dst, src]\n\n        # source arc\n        s_arc = np.linspace(s0, s1, 16)\n        px = list(R_IN * np.cos(s_arc))\n        py = list(R_IN * np.sin(s_arc))\n        # Bezier from source end to destination start (curving through origin)\n        x0, y0 = R_IN * np.cos(s1), R_IN * np.sin(s1)\n        x1, y1 = R_IN * np.cos(d0), R_IN * np.sin(d0)\n        px.extend((1 - t) ** 2 * x0 + t**2 * x1)\n        py.extend((1 - t) ** 2 * y0 + t**2 * y1)\n        # destination arc\n        d_arc = np.linspace(d0, d1, 16)\n        px.extend(R_IN * np.cos(d_arc))\n        py.extend(R_IN * np.sin(d_arc))\n        # Bezier back from destination end to source start\n        x0, y0 = R_IN * np.cos(d1), R_IN * np.sin(d1)\n        x1, y1 = R_IN * np.cos(s0), R_IN * np.sin(s0)\n        px.extend((1 - t) ** 2 * x0 + t**2 * x1)\n        py.extend((1 - t) ** 2 * y0 + t**2 * y1)\n\n        rid = f\"rib{src}_{dst}\"\n        rib_x.extend(px)\n        rib_y.extend(py)\n        rib_gid.extend([rid] * len(px))\n        rib_entity.extend([continents[src]] * len(px))\n\nribbons_df = pd.DataFrame({\"x\": rib_x, \"y\": rib_y, \"gid\": rib_gid, \"entity\": rib_entity})\n\n# Entity labels at the arc midpoints\nmid = (group_a0 + group_a1) / 2\nlabels_df = pd.DataFrame({\"x\": R_LABEL * np.cos(mid), \"y\": R_LABEL * np.sin(mid), \"entity\": continents})\n\n# Keep the legend / fill order stable\ncat = pd.Categorical(continents, categories=continents, ordered=True)\narcs_df[\"entity\"] = pd.Categorical(arcs_df[\"entity\"], categories=continents)\nribbons_df[\"entity\"] = pd.Categorical(ribbons_df[\"entity\"], categories=continents)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_polygon(arcs_df, aes(x=\"x\", y=\"y\", group=\"gid\", fill=\"entity\"), color=PAGE_BG, size=0.2)\n    + geom_polygon(ribbons_df, aes(x=\"x\", y=\"y\", group=\"gid\", fill=\"entity\"), color=PAGE_BG, size=0.15, alpha=0.6)\n    + geom_text(labels_df, aes(x=\"x\", y=\"y\", label=\"entity\"), color=INK, size=6.5, fontweight=\"bold\")\n    + scale_fill_manual(values=IMPRINT_PALETTE, limits=list(cat.categories))\n    + coord_fixed(ratio=1, xlim=(-1.4, 1.4), ylim=(-1.4, 1.4))\n    + labs(title=\"chord-basic · python · plotnine · anyplot.ai\", fill=\"Continent\")\n    + theme_void()\n    + theme(\n        figure_size=(6, 6),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_key=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),\n        plot_title=element_text(size=12, color=INK, ha=\"center\"),\n        legend_title=element_text(size=10, color=INK),\n        legend_text=element_text(size=9, color=INK_SOFT),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\", verbose=False)\n"}