Skip to content

Commit 4af13d4

Browse files
feat(matplotlib): implement circos-basic (#3058)
## Implementation: `circos-basic` - matplotlib Implements the **matplotlib** version of `circos-basic`. **File:** `plots/circos-basic/implementations/matplotlib.py` **Parent Issue:** #3005 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617639912)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 1c34c9a commit 4af13d4

2 files changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
""" pyplots.ai
2+
circos-basic: Circos Plot
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.patches as mpatches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
from matplotlib.path import Path
11+
12+
13+
# Data: Software module dependencies
14+
np.random.seed(42)
15+
16+
# Define segments (software modules)
17+
segments = ["Core", "API", "Database", "Auth", "Cache", "Queue", "Logger", "Config"]
18+
n_segments = len(segments)
19+
20+
# Segment sizes (relative importance/size of each module)
21+
segment_sizes = np.array([25, 20, 18, 15, 12, 10, 8, 6])
22+
segment_sizes = segment_sizes / segment_sizes.sum() * 360 # Convert to degrees
23+
24+
# Connection matrix (dependencies between modules)
25+
connections = [
26+
("Core", "API", 15),
27+
("Core", "Database", 12),
28+
("Core", "Logger", 8),
29+
("API", "Auth", 10),
30+
("API", "Cache", 8),
31+
("Database", "Cache", 6),
32+
("Database", "Logger", 5),
33+
("Auth", "Logger", 4),
34+
("Queue", "Logger", 7),
35+
("Queue", "Database", 5),
36+
("Config", "Core", 9),
37+
("Config", "Logger", 3),
38+
("Cache", "Logger", 4),
39+
("API", "Queue", 6),
40+
]
41+
42+
# Colors for each segment (colorblind-safe palette)
43+
colors = [
44+
"#306998", # Python Blue
45+
"#FFD43B", # Python Yellow
46+
"#2E8B57", # Sea Green
47+
"#DC143C", # Crimson
48+
"#9370DB", # Medium Purple
49+
"#20B2AA", # Light Sea Green
50+
"#FF8C00", # Dark Orange
51+
"#708090", # Slate Gray
52+
]
53+
54+
# Create figure (square for circular plot)
55+
fig, ax = plt.subplots(figsize=(12, 12))
56+
ax.set_aspect("equal")
57+
ax.axis("off")
58+
59+
# Calculate segment positions
60+
gap = 2 # Gap between segments in degrees
61+
total_gap = gap * n_segments
62+
available = 360 - total_gap
63+
segment_angles = segment_sizes / 360 * available
64+
65+
# Calculate start and end angles for each segment
66+
starts = []
67+
ends = []
68+
current = 90 # Start at top
69+
70+
for angle in segment_angles:
71+
starts.append(current)
72+
ends.append(current - angle)
73+
current = current - angle - gap
74+
75+
segment_dict = {name: i for i, name in enumerate(segments)}
76+
77+
# Draw outer ring segments
78+
r_outer = 1.0
79+
r_inner = 0.85
80+
n_arc_points = 50
81+
82+
for i in range(n_segments):
83+
start, end = starts[i], ends[i]
84+
theta1_rad = np.radians(end)
85+
theta2_rad = np.radians(start)
86+
theta = np.linspace(theta1_rad, theta2_rad, n_arc_points)
87+
88+
# Outer arc
89+
x_outer = r_outer * np.cos(theta)
90+
y_outer = r_outer * np.sin(theta)
91+
# Inner arc (reversed)
92+
x_inner = r_inner * np.cos(theta[::-1])
93+
y_inner = r_inner * np.sin(theta[::-1])
94+
# Combine into closed polygon
95+
x = np.concatenate([x_outer, x_inner])
96+
y = np.concatenate([y_outer, y_inner])
97+
ax.fill(x, y, color=colors[i], alpha=0.9, edgecolor="white", linewidth=1)
98+
99+
# Add segment label
100+
mid_angle = np.radians((start + end) / 2)
101+
label_r = r_outer + 0.08
102+
lx = label_r * np.cos(mid_angle)
103+
ly = label_r * np.sin(mid_angle)
104+
ax.text(lx, ly, segments[i], fontsize=18, fontweight="bold", ha="center", va="center", color=colors[i])
105+
106+
# Draw inner data track (simulated importance values)
107+
track_data = np.random.uniform(0.3, 1.0, n_segments)
108+
r_track_outer = 0.82
109+
r_track_inner = 0.70
110+
111+
for i in range(n_segments):
112+
start, end = starts[i], ends[i]
113+
track_height = (r_track_outer - r_track_inner) * track_data[i]
114+
theta1_rad = np.radians(end)
115+
theta2_rad = np.radians(start)
116+
theta = np.linspace(theta1_rad, theta2_rad, n_arc_points)
117+
118+
x_outer = (r_track_inner + track_height) * np.cos(theta)
119+
y_outer = (r_track_inner + track_height) * np.sin(theta)
120+
x_inner = r_track_inner * np.cos(theta[::-1])
121+
y_inner = r_track_inner * np.sin(theta[::-1])
122+
x = np.concatenate([x_outer, x_inner])
123+
y = np.concatenate([y_outer, y_inner])
124+
ax.fill(x, y, color=colors[i], alpha=0.6, edgecolor="none")
125+
126+
# Draw connections (ribbons)
127+
max_value = max(c[2] for c in connections)
128+
r_ribbon = r_inner - 0.02
129+
130+
for source, target, value in connections:
131+
idx1 = segment_dict[source]
132+
idx2 = segment_dict[target]
133+
134+
# Calculate positions within segments
135+
mid1 = np.radians((starts[idx1] + ends[idx1]) / 2)
136+
mid2 = np.radians((starts[idx2] + ends[idx2]) / 2)
137+
138+
# Ribbon width proportional to value
139+
width_factor = value / max_value * 0.15
140+
141+
# Points for segment 1
142+
angle1_start = mid1 - width_factor
143+
angle1_end = mid1 + width_factor
144+
x1_start = r_ribbon * np.cos(angle1_start)
145+
y1_start = r_ribbon * np.sin(angle1_start)
146+
x1_end = r_ribbon * np.cos(angle1_end)
147+
y1_end = r_ribbon * np.sin(angle1_end)
148+
149+
# Points for segment 2
150+
angle2_start = mid2 - width_factor
151+
angle2_end = mid2 + width_factor
152+
x2_start = r_ribbon * np.cos(angle2_start)
153+
y2_start = r_ribbon * np.sin(angle2_start)
154+
x2_end = r_ribbon * np.cos(angle2_end)
155+
y2_end = r_ribbon * np.sin(angle2_end)
156+
157+
# Control points at center for bezier curves
158+
ctrl_factor = 0.3
159+
ctrl1_x = ctrl_factor * (x1_start + x2_end) / 2
160+
ctrl1_y = ctrl_factor * (y1_start + y2_end) / 2
161+
ctrl2_x = ctrl_factor * (x1_end + x2_start) / 2
162+
ctrl2_y = ctrl_factor * (y1_end + y2_start) / 2
163+
164+
# Path vertices
165+
verts = [
166+
(x1_start, y1_start),
167+
(ctrl1_x, ctrl1_y),
168+
(x2_end, y2_end),
169+
(x2_start, y2_start),
170+
(ctrl2_x, ctrl2_y),
171+
(x1_end, y1_end),
172+
(x1_start, y1_start),
173+
]
174+
codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY]
175+
176+
path = Path(verts, codes)
177+
patch = mpatches.PathPatch(path, facecolor=colors[idx1], alpha=0.5, edgecolor="none")
178+
ax.add_patch(patch)
179+
180+
# Title
181+
ax.set_title("circos-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20)
182+
183+
# Set limits with padding
184+
ax.set_xlim(-1.4, 1.4)
185+
ax.set_ylim(-1.4, 1.4)
186+
187+
# Legend (outside the plot)
188+
legend_elements = [mpatches.Patch(facecolor=colors[i], label=segments[i], alpha=0.9) for i in range(n_segments)]
189+
ax.legend(
190+
handles=legend_elements,
191+
loc="lower right",
192+
fontsize=14,
193+
frameon=True,
194+
fancybox=True,
195+
framealpha=0.9,
196+
ncol=1,
197+
bbox_to_anchor=(1.35, 0.0),
198+
title="Modules",
199+
title_fontsize=16,
200+
)
201+
202+
plt.tight_layout()
203+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: matplotlib
2+
specification_id: circos-basic
3+
created: '2025-12-31T11:08:24Z'
4+
updated: '2025-12-31T11:19:11Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617639912
7+
issue: 3005
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/circos-basic/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/circos-basic/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent circular layout with proper segment proportions and visual gaps
17+
- Bezier curve ribbons correctly show connection magnitude through width
18+
- Inner data track adds additional information layer as specified
19+
- Clean colorblind-safe palette with 8 distinct, identifiable colors
20+
- Professional-quality output suitable for publication
21+
- Well-structured, readable code following KISS principles
22+
weaknesses:
23+
- Figure uses 12x12 square format instead of recommended 3600x3600 pixels (12*300=3600,
24+
so dimensions are correct but explicit figsize comment would help)
25+
- Legend positioned outside main canvas area requiring bbox_inches=tight - could
26+
be integrated more elegantly
27+
- Could use matplotlib built-in Wedge/Arc patches for cleaner segment drawing

0 commit comments

Comments
 (0)