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.

""" anyplot.ai
circos-basic: Circos Plot
Library: altair 6.1.0 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-15
"""
import os
import altair as alt
import numpy as np
import pandas as pd
# Theme tokens
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"
# Okabe-Ito categorical palette (first series always #009E73)
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]
# Data: Software module dependencies
np.random.seed(42)
# Define segments (software modules)
segments = ["Core", "API", "Database", "Auth", "Cache", "Queue", "Logger", "Config"]
n_segments = len(segments)
# Segment sizes (relative importance/size of each module)
segment_sizes = np.array([25, 20, 18, 15, 12, 10, 8, 6])
segment_sizes_normalized = segment_sizes / segment_sizes.sum()
# Connection matrix (dependencies between modules)
connections = [
("Core", "API", 15),
("Core", "Database", 12),
("Core", "Logger", 8),
("API", "Auth", 10),
("API", "Cache", 8),
("Database", "Cache", 6),
("Database", "Logger", 5),
("Auth", "Logger", 4),
("Queue", "Logger", 7),
("Queue", "Database", 5),
("Config", "Core", 9),
("Config", "Logger", 3),
("Cache", "Logger", 4),
("API", "Queue", 6),
]
# Inner track data (simulated importance/activity values)
track_data = np.random.uniform(0.3, 1.0, n_segments)
# Map segments to Okabe-Ito colors
segment_colors = {segments[i]: IMPRINT[i % len(IMPRINT)] for i in range(n_segments)}
# Darker shades for inner track (reduce lightness while maintaining hue)
inner_colors = {}
for i, seg in enumerate(segments):
color = IMPRINT[i % len(IMPRINT)]
# Create darker shade by reducing brightness
import colorsys
h, lightness, s = colorsys.rgb_to_hls(*bytes.fromhex(color[1:]))
darker = colorsys.hls_to_rgb(h, lightness * 0.6, s)
inner_colors[seg] = "#{:02x}{:02x}{:02x}".format(int(darker[0] * 255), int(darker[1] * 255), int(darker[2] * 255))
# Target output: 3600x3600 px (1:1 aspect ratio for circular plot) with scale_factor=3.0
# Internal canvas: 1200x1200 pixels
width = 1200
height = 1200
center_x = width / 2
center_y = height / 2
# Circle parameters
outer_radius = 400
inner_radius = 360
track_outer_radius = 340
track_inner_radius = 280
ribbon_radius = 270
# Calculate segment positions
gap = 0.05 # Gap between segments in radians
total_gap = gap * n_segments
available_angle = 2 * np.pi - total_gap
segment_angles = segment_sizes_normalized * available_angle
# Calculate start and end angles for each segment (starting at top)
start_angle = np.pi / 2
segment_arcs = {}
current_angle = start_angle
for i, name in enumerate(segments):
arc_angle = segment_angles[i]
segment_arcs[name] = {"start": current_angle, "end": current_angle - arc_angle, "angle": arc_angle, "idx": i}
current_angle = current_angle - arc_angle - gap
segment_dict = {name: i for i, name in enumerate(segments)}
# Create outer ring segments data
n_arc_points = 50
outer_ring_data = []
for name in segments:
arc = segment_arcs[name]
theta = np.linspace(arc["end"], arc["start"], n_arc_points)
# Outer arc (clockwise)
for j, angle in enumerate(theta):
outer_ring_data.append(
{
"segment": name,
"x": center_x + outer_radius * np.cos(angle),
"y": center_y + outer_radius * np.sin(angle),
"order": j,
"color": segment_colors[name],
}
)
# Inner arc (counter-clockwise to close the shape)
for j, angle in enumerate(reversed(theta)):
outer_ring_data.append(
{
"segment": name,
"x": center_x + inner_radius * np.cos(angle),
"y": center_y + inner_radius * np.sin(angle),
"order": n_arc_points + j,
"color": segment_colors[name],
}
)
outer_ring_df = pd.DataFrame(outer_ring_data)
# Create inner track data (concentric data track)
inner_track_data = []
for i, name in enumerate(segments):
arc = segment_arcs[name]
theta = np.linspace(arc["end"], arc["start"], n_arc_points)
# Height proportional to track data value
track_height = (track_outer_radius - track_inner_radius) * track_data[i]
actual_outer = track_inner_radius + track_height
# Outer arc
for j, angle in enumerate(theta):
inner_track_data.append(
{
"segment": name,
"x": center_x + actual_outer * np.cos(angle),
"y": center_y + actual_outer * np.sin(angle),
"order": j,
"value": track_data[i],
}
)
# Inner arc (counter-clockwise)
for j, angle in enumerate(reversed(theta)):
inner_track_data.append(
{
"segment": name,
"x": center_x + track_inner_radius * np.cos(angle),
"y": center_y + track_inner_radius * np.sin(angle),
"order": n_arc_points + j,
"value": track_data[i],
}
)
inner_track_df = pd.DataFrame(inner_track_data)
# Create ribbons (connections between segments)
max_value = max(c[2] for c in connections)
n_ribbon_points = 30
ribbons_data = []
ribbon_id = 0
for source, target, value in connections:
arc1 = segment_arcs[source]
arc2 = segment_arcs[target]
# Calculate positions at segment midpoints
mid1 = (arc1["start"] + arc1["end"]) / 2
mid2 = (arc2["start"] + arc2["end"]) / 2
# Ribbon width proportional to value (minimum 0.04 for visibility)
width_factor = max(0.04, value / max_value * 0.12)
# Points for source segment
angle1_start = mid1 - width_factor
angle1_end = mid1 + width_factor
# Points for target segment
angle2_start = mid2 - width_factor
angle2_end = mid2 + width_factor
ribbon_points = []
# Arc at source
src_angles = np.linspace(angle1_start, angle1_end, 8)
for angle in src_angles:
ribbon_points.append((center_x + ribbon_radius * np.cos(angle), center_y + ribbon_radius * np.sin(angle)))
# Bezier curve from source end to target start
for i in range(n_ribbon_points):
t = i / (n_ribbon_points - 1)
# Quadratic bezier with control point at center
x = (
(1 - t) ** 2 * (center_x + ribbon_radius * np.cos(angle1_end))
+ 2 * (1 - t) * t * center_x
+ t**2 * (center_x + ribbon_radius * np.cos(angle2_start))
)
y = (
(1 - t) ** 2 * (center_y + ribbon_radius * np.sin(angle1_end))
+ 2 * (1 - t) * t * center_y
+ t**2 * (center_y + ribbon_radius * np.sin(angle2_start))
)
ribbon_points.append((x, y))
# Arc at target
tgt_angles = np.linspace(angle2_start, angle2_end, 8)
for angle in tgt_angles:
ribbon_points.append((center_x + ribbon_radius * np.cos(angle), center_y + ribbon_radius * np.sin(angle)))
# Bezier curve from target end back to source start
for i in range(n_ribbon_points):
t = i / (n_ribbon_points - 1)
x = (
(1 - t) ** 2 * (center_x + ribbon_radius * np.cos(angle2_end))
+ 2 * (1 - t) * t * center_x
+ t**2 * (center_x + ribbon_radius * np.cos(angle1_start))
)
y = (
(1 - t) ** 2 * (center_y + ribbon_radius * np.sin(angle2_end))
+ 2 * (1 - t) * t * center_y
+ t**2 * (center_y + ribbon_radius * np.sin(angle1_start))
)
ribbon_points.append((x, y))
# Add points to dataframe
for pt_idx, (x, y) in enumerate(ribbon_points):
ribbons_data.append(
{
"ribbon_id": f"{source}-{target}-{ribbon_id}",
"source": source,
"target": target,
"value": value,
"x": x,
"y": y,
"order": pt_idx,
}
)
ribbon_id += 1
ribbons_df = pd.DataFrame(ribbons_data)
# Create segment labels data
labels_data = []
for name in segments:
arc = segment_arcs[name]
mid_angle = (arc["start"] + arc["end"]) / 2
label_radius = outer_radius + 45
labels_data.append(
{
"segment": name,
"x": center_x + label_radius * np.cos(mid_angle),
"y": center_y + label_radius * np.sin(mid_angle),
}
)
labels_df = pd.DataFrame(labels_data)
# Create outer ring chart
outer_ring_chart = (
alt.Chart(outer_ring_df)
.mark_line(filled=True, strokeWidth=1, stroke=INK_SOFT)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[0, width]), axis=None),
y=alt.Y("y:Q", scale=alt.Scale(domain=[0, height]), axis=None),
color=alt.Color(
"segment:N",
scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),
legend=alt.Legend(title="Modules", titleFontSize=18, labelFontSize=14, orient="right", symbolSize=200),
),
detail="segment:N",
order="order:Q",
)
)
# Create inner track chart with distinct styling
inner_track_chart = (
alt.Chart(inner_track_df)
.mark_line(filled=True, strokeWidth=2, stroke=INK_SOFT, opacity=0.85)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[0, width]), axis=None),
y=alt.Y("y:Q", scale=alt.Scale(domain=[0, height]), axis=None),
color=alt.Color(
"segment:N",
scale=alt.Scale(domain=list(inner_colors.keys()), range=list(inner_colors.values())),
legend=None,
),
detail="segment:N",
order="order:Q",
)
)
# Create ribbons chart
ribbons_chart = (
alt.Chart(ribbons_df)
.mark_line(filled=True, opacity=0.5, strokeWidth=0)
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[0, width]), axis=None),
y=alt.Y("y:Q", scale=alt.Scale(domain=[0, height]), axis=None),
color=alt.Color(
"source:N",
scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),
legend=None,
),
detail="ribbon_id:N",
order="order:Q",
tooltip=[
alt.Tooltip("source:N", title="From"),
alt.Tooltip("target:N", title="To"),
alt.Tooltip("value:Q", title="Dependency"),
],
)
)
# Create labels chart with larger font for 3600px canvas
labels_chart = (
alt.Chart(labels_df)
.mark_text(fontSize=22, fontWeight="bold")
.encode(
x=alt.X("x:Q", scale=alt.Scale(domain=[0, width])),
y=alt.Y("y:Q", scale=alt.Scale(domain=[0, height])),
text="segment:N",
color=alt.Color(
"segment:N",
scale=alt.Scale(domain=list(segment_colors.keys()), range=list(segment_colors.values())),
legend=None,
),
)
)
# Combine all layers
chart = (
alt.layer(ribbons_chart, inner_track_chart, outer_ring_chart, labels_chart)
.properties(
width=width,
height=height,
title=alt.Title(text="circos-basic · altair · anyplot.ai", fontSize=28, anchor="middle"),
background=PAGE_BG,
)
.configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)
.configure_title(color=INK, fontSize=28)
.configure_legend(
padding=15,
cornerRadius=5,
fillColor=ELEVATED_BG,
strokeColor=INK_SOFT,
strokeWidth=1,
titleFontSize=20,
labelFontSize=16,
symbolSize=250,
offset=20,
titleColor=INK,
labelColor=INK_SOFT,
)
)
# Save as PNG and HTML with theme-suffixed filenames
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
chart.save(f"plot-{THEME}.html")
Part of Circos Plot on anyplot.ai.