{"spec_id":"circos-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ncircos-basic: Circos Plot\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette (first series is always #009E73)\nIMPRINT = [\n    \"#009E73\",  # bluish green (brand)\n    \"#C475FD\",  # vermillion\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # reddish purple\n    \"#AE3030\",  # orange\n    \"#2ABCCD\",  # sky blue\n]\n\n# Data - Trade flows between world regions\nflows = [\n    (\"Asia\", \"Europe\", 85),\n    (\"Asia\", \"North America\", 72),\n    (\"Asia\", \"Middle East\", 45),\n    (\"Asia\", \"Africa\", 28),\n    (\"Europe\", \"North America\", 55),\n    (\"Europe\", \"Asia\", 48),\n    (\"Europe\", \"Africa\", 32),\n    (\"Europe\", \"South America\", 22),\n    (\"North America\", \"Asia\", 42),\n    (\"North America\", \"Europe\", 38),\n    (\"North America\", \"South America\", 35),\n    (\"South America\", \"Europe\", 28),\n    (\"South America\", \"North America\", 25),\n    (\"South America\", \"Asia\", 18),\n    (\"Middle East\", \"Asia\", 65),\n    (\"Middle East\", \"Europe\", 42),\n    (\"Africa\", \"Europe\", 38),\n    (\"Africa\", \"Asia\", 22),\n]\n\n# Unique segments\nsegments = list(dict.fromkeys([f[0] for f in flows] + [f[1] for f in flows]))\n\n# Map segments to Okabe-Ito colors\ncolors = {seg: IMPRINT[i % len(IMPRINT)] for i, seg in enumerate(segments)}\n\n# Calculate total flow for each segment\nsegment_totals = dict.fromkeys(segments, 0)\nfor src, tgt, val in flows:\n    segment_totals[src] += val\n    segment_totals[tgt] += val\n\ntotal_flow = sum(segment_totals.values())\n\n# Calculate arc positions\ngap_angle = 0.08\ntotal_gap = gap_angle * len(segments)\navailable_angle = 2 * np.pi - total_gap\n\nsegment_arcs = {}\ncurrent_angle = -np.pi / 2\nfor segment in segments:\n    arc_size = (segment_totals[segment] / total_flow) * available_angle\n    segment_arcs[segment] = {\n        \"start\": current_angle,\n        \"end\": current_angle + arc_size,\n        \"mid\": current_angle + arc_size / 2,\n    }\n    current_angle += arc_size + gap_angle\n\n# Radii for the circos plot\nouter_radius = 1.0\ninner_radius = 0.92\nchord_radius = 0.88\ntrack_outer = 0.82\ntrack_inner = 0.72\n\n# Build outer arc segments\narc_data = []\nn_arc_points = 80\narc_id = 0\nfor segment in segments:\n    arc = segment_arcs[segment]\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n\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                \"segment\": segment,\n                \"arc_id\": f\"arc_{arc_id}\",\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                \"segment\": segment,\n                \"arc_id\": f\"arc_{arc_id}\",\n            }\n        )\n    arc_id += 1\n\narc_df = pd.DataFrame(arc_data)\n\n# Build inner data track\ntrack_data = []\ntrack_id = 0\nfor segment in segments:\n    arc = segment_arcs[segment]\n    angles = np.linspace(arc[\"start\"], arc[\"end\"], n_arc_points)\n\n    for angle in angles:\n        track_data.append(\n            {\n                \"x\": track_outer * np.cos(angle),\n                \"y\": track_outer * np.sin(angle),\n                \"segment\": segment,\n                \"track_id\": f\"track_{track_id}\",\n            }\n        )\n    for angle in reversed(angles):\n        track_data.append(\n            {\n                \"x\": track_inner * np.cos(angle),\n                \"y\": track_inner * np.sin(angle),\n                \"segment\": segment,\n                \"track_id\": f\"track_{track_id}\",\n            }\n        )\n    track_id += 1\n\ntrack_df = pd.DataFrame(track_data)\n\n# Track segment offsets for chord placement\nsegment_offsets = {s: segment_arcs[s][\"start\"] for s in segments}\n\n# Build ribbon/chord polygons\nchord_data = []\nchord_id = 0\nn_bezier = 50\n\nfor src, tgt, val in flows:\n    src_width = (val / total_flow) * available_angle * 0.5\n    tgt_width = (val / total_flow) * available_angle * 0.5\n\n    src_start = segment_offsets[src]\n    src_end = src_start + src_width\n    segment_offsets[src] = src_end + 0.005\n\n    tgt_start = segment_offsets[tgt]\n    tgt_end = tgt_start + tgt_width\n    segment_offsets[tgt] = tgt_end + 0.005\n\n    polygon_x = []\n    polygon_y = []\n\n    # Source arc\n    src_angles = np.linspace(src_start, src_end, 15)\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 from source to target\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\n    tgt_angles = np.linspace(tgt_start, tgt_end, 15)\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 back from target to 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\n    chord_id += 1\n\nchord_df = pd.DataFrame(chord_data)\n\n# Segment labels\nlabel_data = []\nlabel_radius = 1.15\nfor segment in segments:\n    arc = segment_arcs[segment]\n    mid_angle = arc[\"mid\"]\n    label_data.append(\n        {\n            \"x\": label_radius * np.cos(mid_angle),\n            \"y\": label_radius * np.sin(mid_angle),\n            \"label\": segment,\n            \"segment\": segment,\n        }\n    )\n\nlabel_df = pd.DataFrame(label_data)\n\n# Circular gridlines\ngrid_rows = []\nfor radius in [0.5, 0.7]:\n    grid_angles = np.linspace(0, 2 * np.pi, 100)\n    for angle in grid_angles:\n        grid_rows.append({\"x\": radius * np.cos(angle), \"y\": radius * np.sin(angle), \"radius\": radius})\n\ngrid_df = pd.DataFrame(grid_rows)\n\n# Build the circos plot\nplot = (\n    ggplot()\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"radius\"), data=grid_df, color=INK_SOFT, size=0.3, alpha=0.15)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"chord_id\", fill=\"source\"), data=chord_df, alpha=0.5, color=PAGE_BG, size=0.15\n    )\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"track_id\", fill=\"segment\"), data=track_df, alpha=0.4, color=PAGE_BG, size=0.3\n    )\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=\"arc_id\", fill=\"segment\"), data=arc_df, alpha=0.95, color=PAGE_BG, size=0.8)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=14, color=INK, fontweight=\"bold\")\n    + scale_fill_manual(values=colors, name=\"Region\")\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-1.6, 1.6), expand=(0, 0))\n    + scale_y_continuous(limits=(-1.5, 1.6), expand=(0, 0))\n    + labs(title=\"circos-basic · plotnine · anyplot.ai\")\n    + theme(\n        figure_size=(12, 12),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=24, ha=\"center\", fontweight=\"bold\", margin={\"b\": 20}, color=INK),\n        plot_margin=0.08,\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        legend_title=element_text(size=16, color=INK),\n        legend_text=element_text(size=14, color=INK_SOFT),\n        legend_position=\"right\",\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=300)\n"}