{"spec_id":"circos-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncircos-basic: Circos Plot\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prioritize venv's site-packages over current directory\nif sys.prefix not in sys.path:\n    import site\n\n    site_packages = site.getsitepackages()\n    if isinstance(site_packages, list):\n        sys.path = site_packages + sys.path\n    else:\n        sys.path.insert(0, site_packages)\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\"\n\n# Okabe-Ito palette with first series as brand\nIMPRINT = [\n    \"#009E73\",  # bluish green (brand)\n    \"#C475FD\",  # vermillion\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # reddish purple\n    \"#AE3030\",  # orange\n    \"#2ABCCD\",  # sky blue\n    \"#954477\",  # yellow\n    \"#1A1A1A\" if THEME == \"light\" else \"#E8E8E0\",  # neutral\n]\n\n# Data: Trade flows between regions\nnp.random.seed(42)\n\nsegments = [\"North America\", \"Europe\", \"East Asia\", \"South America\", \"Africa\", \"Middle East\", \"South Asia\", \"Oceania\"]\nn_segments = len(segments)\n\n# Segment sizes (proportional to economic importance)\nsegment_sizes = np.array([25, 30, 28, 10, 8, 12, 15, 6])\nsegment_sizes = segment_sizes / segment_sizes.sum() * 360\n\n# Connection matrix (trade flow values)\nconnections = np.array(\n    [\n        [0, 45, 60, 15, 5, 10, 8, 12],\n        [40, 0, 35, 12, 18, 25, 15, 8],\n        [55, 38, 0, 10, 12, 20, 30, 18],\n        [12, 10, 8, 0, 8, 3, 4, 5],\n        [6, 20, 10, 10, 0, 15, 6, 2],\n        [12, 28, 22, 4, 12, 0, 18, 5],\n        [10, 18, 35, 5, 8, 22, 0, 8],\n        [15, 10, 22, 6, 3, 6, 10, 0],\n    ]\n)\n\n# Calculate segment positions on circle\ngap = 2\ntotal_gap = gap * n_segments\navailable = 360 - total_gap\nsegment_angles = segment_sizes / segment_sizes.sum() * available\n\nstart_angles = np.zeros(n_segments)\nfor i in range(1, n_segments):\n    start_angles[i] = start_angles[i - 1] + segment_angles[i - 1] + gap\n\nfig = go.Figure()\n\n# Radii for visualization layers\nouter_r = 1.0\ninner_r = 0.85\nribbon_inner = 0.80\ntrack_r_outer = 0.78\ntrack_r_inner = 0.60\n\n# Draw outer segments (arcs)\nfor i in range(n_segments):\n    theta_start = start_angles[i]\n    theta_end = theta_start + segment_angles[i]\n\n    theta = np.linspace(np.radians(theta_start), np.radians(theta_end), 50)\n    theta_rev = theta[::-1]\n\n    x_outer = outer_r * np.cos(theta)\n    y_outer = outer_r * np.sin(theta)\n\n    x_inner = inner_r * np.cos(theta_rev)\n    y_inner = inner_r * np.sin(theta_rev)\n\n    x_arc = np.concatenate([x_outer, x_inner, [x_outer[0]]])\n    y_arc = np.concatenate([y_outer, y_inner, [y_outer[0]]])\n\n    fig.add_trace(\n        go.Scatter(\n            x=x_arc,\n            y=y_arc,\n            fill=\"toself\",\n            fillcolor=IMPRINT[i],\n            line={\"color\": INK_SOFT, \"width\": 1},\n            name=segments[i],\n            hoverinfo=\"name\",\n            showlegend=True,\n        )\n    )\n\n    # Add segment labels\n    mid_angle = np.radians((theta_start + theta_end) / 2)\n    label_r = outer_r + 0.12\n    label_x = label_r * np.cos(mid_angle)\n    label_y = label_r * np.sin(mid_angle)\n\n    text_angle = (theta_start + theta_end) / 2\n    if 90 < text_angle < 270:\n        text_angle = text_angle - 180\n\n    mid_deg = (theta_start + theta_end) / 2\n    if 45 < mid_deg < 135:\n        xanchor = \"center\"\n        yanchor = \"bottom\"\n    elif 225 < mid_deg < 315:\n        xanchor = \"center\"\n        yanchor = \"top\"\n    elif mid_deg <= 45 or mid_deg >= 315:\n        xanchor = \"left\"\n        yanchor = \"middle\"\n    else:\n        xanchor = \"right\"\n        yanchor = \"middle\"\n\n    fig.add_annotation(\n        x=label_x,\n        y=label_y,\n        text=segments[i],\n        showarrow=False,\n        font={\"size\": 20, \"color\": INK},\n        textangle=-text_angle,\n        xanchor=xanchor,\n        yanchor=yanchor,\n    )\n\n# Draw ribbons (connections between segments)\nmid_angles = start_angles + segment_angles / 2\nsegment_positions = np.zeros(n_segments)\n\nfor i in range(n_segments):\n    for j in range(i + 1, n_segments):\n        if connections[i, j] > 5:\n            max_conn = connections.max()\n            width_i = (connections[i, j] / max_conn) * segment_angles[i] * 0.3\n            width_j = (connections[i, j] / max_conn) * segment_angles[j] * 0.3\n\n            theta_i_start = start_angles[i] + segment_positions[i]\n            theta_i_end = theta_i_start + width_i\n            segment_positions[i] += width_i + 1\n\n            theta_j_start = start_angles[j] + segment_positions[j]\n            theta_j_end = theta_j_start + width_j\n            segment_positions[j] += width_j + 1\n\n            n_points = 30\n\n            theta_src = np.linspace(np.radians(theta_i_start), np.radians(theta_i_end), 10)\n            x_src = ribbon_inner * np.cos(theta_src)\n            y_src = ribbon_inner * np.sin(theta_src)\n\n            theta_tgt = np.linspace(np.radians(theta_j_start), np.radians(theta_j_end), 10)\n            x_tgt = ribbon_inner * np.cos(theta_tgt)\n            y_tgt = ribbon_inner * np.sin(theta_tgt)\n\n            t = np.linspace(0, 1, n_points)\n\n            cp1_x, cp1_y = 0.2 * x_src[-1], 0.2 * y_src[-1]\n            cp2_x, cp2_y = 0.2 * x_tgt[0], 0.2 * y_tgt[0]\n\n            curve1_x = (1 - t) ** 2 * x_src[-1] + 2 * (1 - t) * t * cp1_x + t**2 * x_tgt[0]\n            curve1_y = (1 - t) ** 2 * y_src[-1] + 2 * (1 - t) * t * cp1_y + t**2 * y_tgt[0]\n\n            cp3_x, cp3_y = 0.2 * x_tgt[-1], 0.2 * y_tgt[-1]\n            cp4_x, cp4_y = 0.2 * x_src[0], 0.2 * y_src[0]\n\n            curve2_x = (1 - t) ** 2 * x_tgt[-1] + 2 * (1 - t) * t * cp3_x + t**2 * x_src[0]\n            curve2_y = (1 - t) ** 2 * y_tgt[-1] + 2 * (1 - t) * t * cp3_y + t**2 * y_src[0]\n\n            x_ribbon = np.concatenate([x_src, curve1_x, x_tgt, curve2_x, [x_src[0]]])\n            y_ribbon = np.concatenate([y_src, curve1_y, y_tgt, curve2_y, [y_src[0]]])\n\n            # Blend colors inline\n            c1 = IMPRINT[i]\n            c2 = IMPRINT[j]\n            r1, g1, b1 = int(c1[1:3], 16), int(c1[3:5], 16), int(c1[5:7], 16)\n            r2, g2, b2 = int(c2[1:3], 16), int(c2[3:5], 16), int(c2[5:7], 16)\n            r = int(r1 * 0.5 + r2 * 0.5)\n            g = int(g1 * 0.5 + g2 * 0.5)\n            b = int(b1 * 0.5 + b2 * 0.5)\n            ribbon_color = f\"#{r:02x}{g:02x}{b:02x}\"\n\n            fig.add_trace(\n                go.Scatter(\n                    x=x_ribbon,\n                    y=y_ribbon,\n                    fill=\"toself\",\n                    fillcolor=ribbon_color,\n                    opacity=0.5,\n                    line={\"color\": INK_SOFT, \"width\": 0.5},\n                    hoverinfo=\"text\",\n                    hovertext=f\"{segments[i]} ↔ {segments[j]}: {connections[i, j]}\",\n                    showlegend=False,\n                )\n            )\n\n# Draw inner track (data bars)\ntrack_values = np.array([0.8, 0.95, 0.9, 0.4, 0.25, 0.5, 0.55, 0.3])\n\nfor i in range(n_segments):\n    theta_start = start_angles[i]\n    theta_end = theta_start + segment_angles[i]\n\n    theta = np.linspace(np.radians(theta_start), np.radians(theta_end), 30)\n    theta_rev = theta[::-1]\n\n    height = track_r_inner + (track_r_outer - track_r_inner) * track_values[i]\n\n    x_outer = height * np.cos(theta)\n    y_outer = height * np.sin(theta)\n    x_inner = track_r_inner * np.cos(theta_rev)\n    y_inner = track_r_inner * np.sin(theta_rev)\n\n    x_bar = np.concatenate([x_outer, x_inner, [x_outer[0]]])\n    y_bar = np.concatenate([y_outer, y_inner, [y_outer[0]]])\n\n    fig.add_trace(\n        go.Scatter(\n            x=x_bar,\n            y=y_bar,\n            fill=\"toself\",\n            fillcolor=IMPRINT[i],\n            opacity=0.6,\n            line={\"color\": INK_SOFT, \"width\": 0.5},\n            hoverinfo=\"text\",\n            hovertext=f\"{segments[i]} GDP Index: {track_values[i]:.2f}\",\n            showlegend=False,\n        )\n    )\n\n# Update layout with theme-adaptive colors\nfig.update_layout(\n    title={\"text\": \"circos-basic · plotly · anyplot.ai\", \"font\": {\"size\": 28, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    showlegend=True,\n    legend={\n        \"orientation\": \"h\",\n        \"yanchor\": \"bottom\",\n        \"y\": -0.15,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n        \"font\": {\"size\": 18, \"color\": INK_SOFT},\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    xaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"range\": [-1.5, 1.5], \"scaleanchor\": \"y\", \"scaleratio\": 1},\n    yaxis={\"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"range\": [-1.5, 1.5]},\n    plot_bgcolor=PAGE_BG,\n    paper_bgcolor=PAGE_BG,\n    margin={\"l\": 50, \"r\": 50, \"t\": 100, \"b\": 120},\n)\n\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nfig.write_image(os.path.join(script_dir, f\"plot-{THEME}.png\"), width=1600, height=900, scale=3)\nfig.write_html(os.path.join(script_dir, f\"plot-{THEME}.html\"), include_plotlyjs=\"cdn\")\n"}