{"spec_id":"circos-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncircos-basic: Circos Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-15\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\n\nscript_dir = str(Path(__file__).parent.absolute())\nsys.path = [p for p in sys.path if p != script_dir and p != \"\"]\n\nimport os\nimport time\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\nnp.random.seed(42)\n\nregions = [\"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Africa\", \"Oceania\"]\nn_regions = len(regions)\n\nflow_matrix = np.array(\n    [\n        [0, 45, 52, 18, 15, 22],\n        [38, 0, 35, 12, 20, 8],\n        [48, 42, 0, 28, 10, 15],\n        [15, 18, 25, 0, 8, 5],\n        [12, 25, 8, 10, 0, 3],\n        [20, 10, 18, 6, 4, 0],\n    ]\n)\n\nsegment_sizes = flow_matrix.sum(axis=0) + flow_matrix.sum(axis=1)\ntrack_values = np.array([4.2, 1.8, 2.5, 1.5, 3.8, 2.2])\n\ntotal_size = segment_sizes.sum()\ngap = 0.03\ntotal_gap = gap * n_regions\navailable_angle = 2 * np.pi - total_gap\n\nsegment_angles = []\ncurrent_angle = 0\nfor size in segment_sizes:\n    angle_span = (size / total_size) * available_angle\n    start = current_angle\n    end = current_angle + angle_span\n    segment_angles.append((start, end))\n    current_angle = end + gap\n\np = figure(\n    width=3600,\n    height=3600,\n    title=\"circos-basic · bokeh · anyplot.ai\",\n    x_range=(-1.5, 1.5),\n    y_range=(-1.5, 1.5),\n    tools=\"\",\n    toolbar_location=None,\n)\n\np.title.text_font_size = \"36pt\"\np.title.align = \"center\"\np.title.text_color = INK\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\nouter_radius = 1.0\ninner_radius = 0.85\ntrack_outer = 0.82\ntrack_inner = 0.70\nribbon_radius = 0.65\n\nfor i, (start, end) in enumerate(segment_angles):\n    theta = np.linspace(start, end, 50)\n    outer_x = outer_radius * np.cos(theta)\n    outer_y = outer_radius * np.sin(theta)\n    inner_x = inner_radius * np.cos(theta[::-1])\n    inner_y = inner_radius * np.sin(theta[::-1])\n\n    xs = np.concatenate([outer_x, inner_x, [outer_x[0]]])\n    ys = np.concatenate([outer_y, inner_y, [outer_y[0]]])\n\n    source = ColumnDataSource(data={\"xs\": [xs], \"ys\": [ys]})\n    color = IMPRINT[i % len(IMPRINT)]\n    p.patches(xs=\"xs\", ys=\"ys\", source=source, fill_color=color, line_color=INK_SOFT, line_width=1, alpha=0.85)\n\n    mid_angle = (start + end) / 2\n    label_radius = outer_radius + 0.12\n    label_x = label_radius * np.cos(mid_angle)\n    label_y = label_radius * np.sin(mid_angle)\n\n    angle = mid_angle * 180 / np.pi\n    if 90 < angle < 270:\n        angle += 180\n\n    p.text(\n        x=[label_x],\n        y=[label_y],\n        text=[regions[i]],\n        text_font_size=\"20pt\",\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_color=INK,\n        angle=[np.radians(angle - 90)],\n    )\n\nmax_track = track_values.max()\nmin_track = track_values.min()\ntrack_range = max_track - min_track\n\nfor i, (start, end) in enumerate(segment_angles):\n    norm_val = (track_values[i] - min_track) / track_range if track_range > 0 else 0.5\n    bar_radius = track_inner + norm_val * (track_outer - track_inner)\n\n    theta = np.linspace(start, end, 30)\n    outer_x = bar_radius * np.cos(theta)\n    outer_y = bar_radius * np.sin(theta)\n    inner_x = track_inner * np.cos(theta[::-1])\n    inner_y = track_inner * np.sin(theta[::-1])\n\n    xs = np.concatenate([outer_x, inner_x, [outer_x[0]]])\n    ys = np.concatenate([outer_y, inner_y, [outer_y[0]]])\n\n    source = ColumnDataSource(data={\"xs\": [xs], \"ys\": [ys]})\n    color = IMPRINT[i % len(IMPRINT)]\n    p.patches(xs=\"xs\", ys=\"ys\", source=source, fill_color=color, line_color=None, alpha=0.4)\n\ntrack_ref_theta = np.linspace(0, 2 * np.pi, 100)\ntrack_ref_x = track_inner * np.cos(track_ref_theta)\ntrack_ref_y = track_inner * np.sin(track_ref_theta)\np.line(track_ref_x, track_ref_y, line_color=INK_SOFT, line_width=1, line_alpha=0.2)\n\nflow_threshold = 15\n\nfor i in range(n_regions):\n    for j in range(i + 1, n_regions):\n        flow_ij = flow_matrix[i, j]\n        flow_ji = flow_matrix[j, i]\n        total_flow = flow_ij + flow_ji\n\n        if total_flow < flow_threshold:\n            continue\n\n        start_i, end_i = segment_angles[i]\n        seg_span_i = end_i - start_i\n        ribbon_width_i = (total_flow / segment_sizes[i]) * seg_span_i * 0.8\n\n        start_j, end_j = segment_angles[j]\n        seg_span_j = end_j - start_j\n        ribbon_width_j = (total_flow / segment_sizes[j]) * seg_span_j * 0.8\n\n        mid_i = (start_i + end_i) / 2\n        mid_j = (start_j + end_j) / 2\n\n        theta_i_start = mid_i - ribbon_width_i / 2\n        theta_i_end = mid_i + ribbon_width_i / 2\n\n        theta_j_start = mid_j - ribbon_width_j / 2\n        theta_j_end = mid_j + ribbon_width_j / 2\n\n        n_curve = 30\n        t = np.linspace(0, 1, n_curve)\n        ctrl_x, ctrl_y = 0, 0\n\n        x1_start = ribbon_radius * np.cos(theta_i_start)\n        y1_start = ribbon_radius * np.sin(theta_i_start)\n        x1_end = ribbon_radius * np.cos(theta_j_start)\n        y1_end = ribbon_radius * np.sin(theta_j_start)\n\n        curve1_x = (1 - t) ** 2 * x1_start + 2 * (1 - t) * t * ctrl_x + t**2 * x1_end\n        curve1_y = (1 - t) ** 2 * y1_start + 2 * (1 - t) * t * ctrl_y + t**2 * y1_end\n\n        arc_j_theta = np.linspace(theta_j_start, theta_j_end, 10)\n        arc_j_x = ribbon_radius * np.cos(arc_j_theta)\n        arc_j_y = ribbon_radius * np.sin(arc_j_theta)\n\n        x2_start = ribbon_radius * np.cos(theta_j_end)\n        y2_start = ribbon_radius * np.sin(theta_j_end)\n        x2_end = ribbon_radius * np.cos(theta_i_end)\n        y2_end = ribbon_radius * np.sin(theta_i_end)\n\n        curve2_x = (1 - t) ** 2 * x2_start + 2 * (1 - t) * t * ctrl_x + t**2 * x2_end\n        curve2_y = (1 - t) ** 2 * y2_start + 2 * (1 - t) * t * ctrl_y + t**2 * y2_end\n\n        arc_i_theta = np.linspace(theta_i_end, theta_i_start, 10)\n        arc_i_x = ribbon_radius * np.cos(arc_i_theta)\n        arc_i_y = ribbon_radius * np.sin(arc_i_theta)\n\n        ribbon_x = np.concatenate([curve1_x, arc_j_x, curve2_x, arc_i_x])\n        ribbon_y = np.concatenate([curve1_y, arc_j_y, curve2_y, arc_i_y])\n\n        ribbon_color = IMPRINT[i % len(IMPRINT)]\n\n        source = ColumnDataSource(data={\"xs\": [ribbon_x], \"ys\": [ribbon_y]})\n        p.patches(\n            xs=\"xs\",\n            ys=\"ys\",\n            source=source,\n            fill_color=ribbon_color,\n            line_color=ribbon_color,\n            line_width=0.5,\n            alpha=0.45,\n        )\n\nlegend_x = 1.15\nlegend_y_start = 0.8\nlegend_spacing = 0.15\n\nfor i, region in enumerate(regions):\n    y_pos = legend_y_start - i * legend_spacing\n    color = IMPRINT[i % len(IMPRINT)]\n    p.rect(x=[legend_x], y=[y_pos], width=0.08, height=0.08, fill_color=color, line_color=None)\n    p.text(\n        x=[legend_x + 0.12],\n        y=[y_pos],\n        text=[region],\n        text_font_size=\"16pt\",\n        text_align=\"left\",\n        text_baseline=\"middle\",\n        text_color=INK_SOFT,\n    )\n\np.text(\n    x=[-0.35],\n    y=[-0.20],\n    text=[\"Inner track: GDP Growth (%)\"],\n    text_font_size=\"18pt\",\n    text_color=INK_SOFT,\n    text_align=\"center\",\n)\n\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 3600, 3600\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}