|
| 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") |
0 commit comments