Circos Plot — Bokeh

A Circos plot is a circular visualization that displays data on concentric tracks arranged around a circle, with ribbons or arcs connecting related segments across the circular layout. Originally designed for genomic data visualization, it excels at showing relationships between segments while simultaneously displaying multiple data attributes on different tracks. The circular arrangement makes efficient use of space and reveals patterns in complex relational data.

Circos Plot rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
circos-basic: Circos Plot
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 94/100 | Updated: 2026-05-15
"""

import sys
from pathlib import Path


script_dir = str(Path(__file__).parent.absolute())
sys.path = [p for p in sys.path if p != script_dir and p != ""]

import os
import time

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]

np.random.seed(42)

regions = ["Asia", "Europe", "N. America", "S. America", "Africa", "Oceania"]
n_regions = len(regions)

flow_matrix = np.array(
    [
        [0, 45, 52, 18, 15, 22],
        [38, 0, 35, 12, 20, 8],
        [48, 42, 0, 28, 10, 15],
        [15, 18, 25, 0, 8, 5],
        [12, 25, 8, 10, 0, 3],
        [20, 10, 18, 6, 4, 0],
    ]
)

segment_sizes = flow_matrix.sum(axis=0) + flow_matrix.sum(axis=1)
track_values = np.array([4.2, 1.8, 2.5, 1.5, 3.8, 2.2])

total_size = segment_sizes.sum()
gap = 0.03
total_gap = gap * n_regions
available_angle = 2 * np.pi - total_gap

segment_angles = []
current_angle = 0
for size in segment_sizes:
    angle_span = (size / total_size) * available_angle
    start = current_angle
    end = current_angle + angle_span
    segment_angles.append((start, end))
    current_angle = end + gap

p = figure(
    width=3600,
    height=3600,
    title="circos-basic · bokeh · anyplot.ai",
    x_range=(-1.5, 1.5),
    y_range=(-1.5, 1.5),
    tools="",
    toolbar_location=None,
)

p.title.text_font_size = "36pt"
p.title.align = "center"
p.title.text_color = INK

p.xaxis.visible = False
p.yaxis.visible = False
p.xgrid.visible = False
p.ygrid.visible = False
p.outline_line_color = None
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

outer_radius = 1.0
inner_radius = 0.85
track_outer = 0.82
track_inner = 0.70
ribbon_radius = 0.65

for i, (start, end) in enumerate(segment_angles):
    theta = np.linspace(start, end, 50)
    outer_x = outer_radius * np.cos(theta)
    outer_y = outer_radius * np.sin(theta)
    inner_x = inner_radius * np.cos(theta[::-1])
    inner_y = inner_radius * np.sin(theta[::-1])

    xs = np.concatenate([outer_x, inner_x, [outer_x[0]]])
    ys = np.concatenate([outer_y, inner_y, [outer_y[0]]])

    source = ColumnDataSource(data={"xs": [xs], "ys": [ys]})
    color = IMPRINT[i % len(IMPRINT)]
    p.patches(xs="xs", ys="ys", source=source, fill_color=color, line_color=INK_SOFT, line_width=1, alpha=0.85)

    mid_angle = (start + end) / 2
    label_radius = outer_radius + 0.12
    label_x = label_radius * np.cos(mid_angle)
    label_y = label_radius * np.sin(mid_angle)

    angle = mid_angle * 180 / np.pi
    if 90 < angle < 270:
        angle += 180

    p.text(
        x=[label_x],
        y=[label_y],
        text=[regions[i]],
        text_font_size="20pt",
        text_align="center",
        text_baseline="middle",
        text_color=INK,
        angle=[np.radians(angle - 90)],
    )

max_track = track_values.max()
min_track = track_values.min()
track_range = max_track - min_track

for i, (start, end) in enumerate(segment_angles):
    norm_val = (track_values[i] - min_track) / track_range if track_range > 0 else 0.5
    bar_radius = track_inner + norm_val * (track_outer - track_inner)

    theta = np.linspace(start, end, 30)
    outer_x = bar_radius * np.cos(theta)
    outer_y = bar_radius * np.sin(theta)
    inner_x = track_inner * np.cos(theta[::-1])
    inner_y = track_inner * np.sin(theta[::-1])

    xs = np.concatenate([outer_x, inner_x, [outer_x[0]]])
    ys = np.concatenate([outer_y, inner_y, [outer_y[0]]])

    source = ColumnDataSource(data={"xs": [xs], "ys": [ys]})
    color = IMPRINT[i % len(IMPRINT)]
    p.patches(xs="xs", ys="ys", source=source, fill_color=color, line_color=None, alpha=0.4)

track_ref_theta = np.linspace(0, 2 * np.pi, 100)
track_ref_x = track_inner * np.cos(track_ref_theta)
track_ref_y = track_inner * np.sin(track_ref_theta)
p.line(track_ref_x, track_ref_y, line_color=INK_SOFT, line_width=1, line_alpha=0.2)

flow_threshold = 15

for i in range(n_regions):
    for j in range(i + 1, n_regions):
        flow_ij = flow_matrix[i, j]
        flow_ji = flow_matrix[j, i]
        total_flow = flow_ij + flow_ji

        if total_flow < flow_threshold:
            continue

        start_i, end_i = segment_angles[i]
        seg_span_i = end_i - start_i
        ribbon_width_i = (total_flow / segment_sizes[i]) * seg_span_i * 0.8

        start_j, end_j = segment_angles[j]
        seg_span_j = end_j - start_j
        ribbon_width_j = (total_flow / segment_sizes[j]) * seg_span_j * 0.8

        mid_i = (start_i + end_i) / 2
        mid_j = (start_j + end_j) / 2

        theta_i_start = mid_i - ribbon_width_i / 2
        theta_i_end = mid_i + ribbon_width_i / 2

        theta_j_start = mid_j - ribbon_width_j / 2
        theta_j_end = mid_j + ribbon_width_j / 2

        n_curve = 30
        t = np.linspace(0, 1, n_curve)
        ctrl_x, ctrl_y = 0, 0

        x1_start = ribbon_radius * np.cos(theta_i_start)
        y1_start = ribbon_radius * np.sin(theta_i_start)
        x1_end = ribbon_radius * np.cos(theta_j_start)
        y1_end = ribbon_radius * np.sin(theta_j_start)

        curve1_x = (1 - t) ** 2 * x1_start + 2 * (1 - t) * t * ctrl_x + t**2 * x1_end
        curve1_y = (1 - t) ** 2 * y1_start + 2 * (1 - t) * t * ctrl_y + t**2 * y1_end

        arc_j_theta = np.linspace(theta_j_start, theta_j_end, 10)
        arc_j_x = ribbon_radius * np.cos(arc_j_theta)
        arc_j_y = ribbon_radius * np.sin(arc_j_theta)

        x2_start = ribbon_radius * np.cos(theta_j_end)
        y2_start = ribbon_radius * np.sin(theta_j_end)
        x2_end = ribbon_radius * np.cos(theta_i_end)
        y2_end = ribbon_radius * np.sin(theta_i_end)

        curve2_x = (1 - t) ** 2 * x2_start + 2 * (1 - t) * t * ctrl_x + t**2 * x2_end
        curve2_y = (1 - t) ** 2 * y2_start + 2 * (1 - t) * t * ctrl_y + t**2 * y2_end

        arc_i_theta = np.linspace(theta_i_end, theta_i_start, 10)
        arc_i_x = ribbon_radius * np.cos(arc_i_theta)
        arc_i_y = ribbon_radius * np.sin(arc_i_theta)

        ribbon_x = np.concatenate([curve1_x, arc_j_x, curve2_x, arc_i_x])
        ribbon_y = np.concatenate([curve1_y, arc_j_y, curve2_y, arc_i_y])

        ribbon_color = IMPRINT[i % len(IMPRINT)]

        source = ColumnDataSource(data={"xs": [ribbon_x], "ys": [ribbon_y]})
        p.patches(
            xs="xs",
            ys="ys",
            source=source,
            fill_color=ribbon_color,
            line_color=ribbon_color,
            line_width=0.5,
            alpha=0.45,
        )

legend_x = 1.15
legend_y_start = 0.8
legend_spacing = 0.15

for i, region in enumerate(regions):
    y_pos = legend_y_start - i * legend_spacing
    color = IMPRINT[i % len(IMPRINT)]
    p.rect(x=[legend_x], y=[y_pos], width=0.08, height=0.08, fill_color=color, line_color=None)
    p.text(
        x=[legend_x + 0.12],
        y=[y_pos],
        text=[region],
        text_font_size="16pt",
        text_align="left",
        text_baseline="middle",
        text_color=INK_SOFT,
    )

p.text(
    x=[-0.35],
    y=[-0.20],
    text=["Inner track: GDP Growth (%)"],
    text_font_size="18pt",
    text_color=INK_SOFT,
    text_align="center",
)

output_file(f"plot-{THEME}.html")
save(p)

W, H = 3600, 3600
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Circos Plot on anyplot.ai.

Other implementations