{"spec_id":"chord-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nchord-basic: Basic Chord Diagram\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-17\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\nfrom PIL import Image\n\n\nLetsPlot.setup_html()\n\n# Theme-adaptive chrome (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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nPAGE_BG_RGB = (250, 248, 241) if THEME == \"light\" else (26, 26, 23)\n\n# Imprint palette — 8 hues, theme-independent, hybrid-v3 sort. First series ALWAYS #009E73.\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Migration flow data between continents (bidirectional flows)\nflows = [\n    (\"Asia\", \"Europe\", 45),\n    (\"Asia\", \"North America\", 38),\n    (\"Asia\", \"Africa\", 12),\n    (\"Asia\", \"Oceania\", 16),\n    (\"Europe\", \"North America\", 28),\n    (\"Europe\", \"Asia\", 22),\n    (\"Europe\", \"Africa\", 15),\n    (\"Europe\", \"South America\", 10),\n    (\"Africa\", \"Europe\", 35),\n    (\"Africa\", \"Asia\", 18),\n    (\"Africa\", \"North America\", 8),\n    (\"North America\", \"Europe\", 20),\n    (\"North America\", \"Asia\", 15),\n    (\"North America\", \"South America\", 12),\n    (\"South America\", \"North America\", 25),\n    (\"South America\", \"Europe\", 18),\n    (\"Oceania\", \"Asia\", 14),\n    (\"Oceania\", \"Europe\", 8),\n]\n\n# Entities; first entity (Asia) gets Imprint brand green #009E73\nentities = list(dict.fromkeys([f[0] for f in flows] + [f[1] for f in flows]))\ncolors = IMPRINT_PALETTE[: len(entities)]\n\n# Calculate total flow for each entity (in + out)\nentity_totals = dict.fromkeys(entities, 0)\nfor src, tgt, val in flows:\n    entity_totals[src] += val\n    entity_totals[tgt] += val\n\n# Angular layout: proportional arc sizes with gaps\ntotal_flow = sum(entity_totals.values())\ngap_angle = 0.07\ntotal_gap = gap_angle * len(entities)\navailable_angle = 2 * math.pi - total_gap\n\nentity_arcs = {}\ncurrent_angle = math.pi / 2  # Start from top for better visual balance\nfor entity in entities:\n    arc_size = (entity_totals[entity] / total_flow) * available_angle\n    entity_arcs[entity] = {\"start\": current_angle, \"end\": current_angle + arc_size, \"mid\": current_angle + arc_size / 2}\n    current_angle += arc_size + gap_angle\n\n# Track offsets within each entity arc for chord placement\nentity_offsets = {e: entity_arcs[e][\"start\"] for e in entities}\n\n# Radius configuration\nouter_radius = 1.0\ninner_radius = 0.93\nchord_radius = 0.91\n\n# Build outer arc segments (the ring)\narc_data = []\nn_arc_points = 80\nfor entity in entities:\n    arc = entity_arcs[entity]\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n    for angle in angles:\n        arc_data.append(\n            {\n                \"x\": outer_radius * np.cos(angle),\n                \"y\": outer_radius * np.sin(angle),\n                \"entity\": entity,\n                \"arc_id\": f\"{entity}_arc\",\n            }\n        )\n    for angle in reversed(angles):\n        arc_data.append(\n            {\n                \"x\": inner_radius * np.cos(angle),\n                \"y\": inner_radius * np.sin(angle),\n                \"entity\": entity,\n                \"arc_id\": f\"{entity}_arc\",\n            }\n        )\n\narc_df = pd.DataFrame(arc_data)\n\n# Build chord polygons\nflow_values = [f[2] for f in flows]\nmin_flow = min(flow_values)\nmax_flow = max(flow_values)\n\nchord_data = []\nchord_id = 0\n\nfor src, tgt, val in flows:\n    src_width = (val / total_flow) * available_angle\n    tgt_width = (val / total_flow) * available_angle\n\n    src_start = entity_offsets[src]\n    src_end = src_start + src_width\n    entity_offsets[src] = src_end + 0.003\n\n    tgt_start = entity_offsets[tgt]\n    tgt_end = tgt_start + tgt_width\n    entity_offsets[tgt] = tgt_end + 0.003\n\n    n_bezier = 50\n    src_angles = np.linspace(src_start, src_end, 12)\n    tgt_angles = np.linspace(tgt_end, tgt_start, 12)\n\n    polygon_x = []\n    polygon_y = []\n\n    # Source arc at chord_radius\n    for angle in src_angles:\n        polygon_x.append(chord_radius * np.cos(angle))\n        polygon_y.append(chord_radius * np.sin(angle))\n\n    # Bezier curve source → target (pulled toward circle center)\n    src_end_x = chord_radius * np.cos(src_end)\n    src_end_y = chord_radius * np.sin(src_end)\n    tgt_start_x = chord_radius * np.cos(tgt_start)\n    tgt_start_y = chord_radius * np.sin(tgt_start)\n\n    for i in range(1, n_bezier):\n        t = i / n_bezier\n        x = (1 - t) ** 2 * src_end_x + 2 * (1 - t) * t * 0 + t**2 * tgt_start_x\n        y = (1 - t) ** 2 * src_end_y + 2 * (1 - t) * t * 0 + t**2 * tgt_start_y\n        polygon_x.append(x)\n        polygon_y.append(y)\n\n    # Target arc at chord_radius\n    for angle in tgt_angles:\n        polygon_x.append(chord_radius * np.cos(angle))\n        polygon_y.append(chord_radius * np.sin(angle))\n\n    # Bezier curve target → source\n    tgt_end_x = chord_radius * np.cos(tgt_end)\n    tgt_end_y = chord_radius * np.sin(tgt_end)\n    src_start_x = chord_radius * np.cos(src_start)\n    src_start_y = chord_radius * np.sin(src_start)\n\n    for i in range(1, n_bezier):\n        t = i / n_bezier\n        x = (1 - t) ** 2 * tgt_end_x + 2 * (1 - t) * t * 0 + t**2 * src_start_x\n        y = (1 - t) ** 2 * tgt_end_y + 2 * (1 - t) * t * 0 + t**2 * src_start_y\n        polygon_x.append(x)\n        polygon_y.append(y)\n\n    for x, y in zip(polygon_x, polygon_y, strict=False):\n        chord_data.append({\"x\": x, \"y\": y, \"chord_id\": f\"chord_{chord_id}\", \"source\": src, \"target\": tgt, \"value\": val})\n    chord_id += 1\n\nchord_df = pd.DataFrame(chord_data)\n\n# Three-tier chord split for visual hierarchy (alpha floor raised so weak flows stay legible)\nflow_threshold_high = min_flow + 0.66 * (max_flow - min_flow)\nflow_threshold_mid = min_flow + 0.33 * (max_flow - min_flow)\nchord_high = chord_df[chord_df[\"value\"] >= flow_threshold_high]\nchord_mid = chord_df[(chord_df[\"value\"] >= flow_threshold_mid) & (chord_df[\"value\"] < flow_threshold_high)]\nchord_low = chord_df[chord_df[\"value\"] < flow_threshold_mid]\n\n# Entity labels with adaptive positioning to avoid crowding near small arcs\nlabel_data = []\nfor entity in entities:\n    arc = entity_arcs[entity]\n    mid_angle = arc[\"mid\"]\n    arc_size = arc[\"end\"] - arc[\"start\"]\n    label_radius = 1.16 + max(0, 0.12 * (1.0 - arc_size / 0.7))\n    label_data.append({\"x\": label_radius * np.cos(mid_angle), \"y\": label_radius * np.sin(mid_angle), \"label\": entity})\n\nlabel_df = pd.DataFrame(label_data)\n\n# Tick marks connecting arcs to labels (visual refinement)\ntick_data = []\nfor entity in entities:\n    arc = entity_arcs[entity]\n    mid_angle = arc[\"mid\"]\n    tick_data.append(\n        {\n            \"x\": outer_radius * np.cos(mid_angle),\n            \"y\": outer_radius * np.sin(mid_angle),\n            \"xend\": 1.09 * np.cos(mid_angle),\n            \"yend\": 1.09 * np.sin(mid_angle),\n        }\n    )\n\ntick_df = pd.DataFrame(tick_data)\n\n# Annotation for dominant flow — data storytelling (positioned at center)\ntop_flow = max(flows, key=lambda f: f[2])\nannotation_df = pd.DataFrame(\n    [{\"x\": 0.0, \"y\": -0.02, \"label\": f\"Strongest: {top_flow[0]}→{top_flow[1]} ({top_flow[2]})\"}]\n)\n\n# Build the plot\nplot = (\n    ggplot()\n    # Low-magnitude chords (background layer) — alpha raised from 0.2 for legibility\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"chord_id\", fill=\"source\"),\n        data=chord_low,\n        alpha=0.38,\n        color=PAGE_BG,\n        size=0.15,\n        tooltips=layer_tooltips().line(\"@source → @target\").line(\"Flow|@value\"),\n    )\n    # Mid-magnitude chords\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"chord_id\", fill=\"source\"),\n        data=chord_mid,\n        alpha=0.58,\n        color=PAGE_BG,\n        size=0.2,\n        tooltips=layer_tooltips().line(\"@source → @target\").line(\"Flow|@value\"),\n    )\n    # High-magnitude chords (foreground, most prominent)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"chord_id\", fill=\"source\"),\n        data=chord_high,\n        alpha=0.82,\n        color=PAGE_BG,\n        size=0.25,\n        tooltips=layer_tooltips().line(\"@source → @target\").line(\"Flow|@value\"),\n    )\n    # Outer arc segments (the entity ring)\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=\"arc_id\", fill=\"entity\"), data=arc_df, alpha=0.95, color=PAGE_BG, size=0.5)\n    # Tick marks from arcs to labels\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=tick_df, color=INK_SOFT, size=0.6)\n    # Entity labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=9, color=INK, fontface=\"bold\")\n    # Dominant flow annotation for storytelling\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=annotation_df, size=6, color=INK_SOFT, fontface=\"bold\")\n    # Scales and coordinates\n    + scale_fill_manual(values=colors, name=\"Continent\")\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-1.6, 1.6))\n    + scale_y_continuous(limits=(-1.6, 1.6))\n    + labs(\n        title=\"chord-basic · python · letsplot · anyplot.ai\",\n        caption=\"Width proportional to migration flow magnitude  ·  Opacity indicates relative strength\",\n    )\n    + ggsize(600, 600)\n    + theme_void()\n    + theme(\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_caption=element_text(size=8, color=INK_MUTED, face=\"italic\"),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=11, face=\"bold\", color=INK),\n        legend_position=[0.5, 0.03],\n        legend_justification=[0.5, 0.0],\n        legend_direction=\"horizontal\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.5),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG, size=0),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG, size=0),\n        plot_margin=[24, 16, 12, 16],\n    )\n)\n\n# Save PNG (square: ggsize 600 × scale 4 = 2400×2400 px)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\n\n# Flatten any residual transparency onto the theme page background\nimg = Image.open(f\"plot-{THEME}.png\").convert(\"RGBA\")\nbg = Image.new(\"RGBA\", img.size, (*PAGE_BG_RGB, 255))\nImage.alpha_composite(bg, img).convert(\"RGB\").save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML export\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}