|
1 | 1 | """ pyplots.ai |
2 | 2 | arc-basic: Basic Arc Diagram |
3 | | -Library: matplotlib 3.10.8 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: matplotlib 3.10.8 | Python 3.14.3 |
| 4 | +Quality: 91/100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import matplotlib.colors as mcolors |
7 | 8 | import matplotlib.patches as mpatches |
| 9 | +import matplotlib.path as mpath |
8 | 10 | import matplotlib.pyplot as plt |
9 | 11 | import numpy as np |
10 | 12 |
|
11 | 13 |
|
12 | 14 | # Data: Character interactions in a story chapter |
13 | | -np.random.seed(42) |
14 | | - |
15 | 15 | nodes = ["Alice", "Bob", "Carol", "David", "Eve", "Frank", "Grace", "Henry", "Iris", "Jack"] |
16 | 16 | n_nodes = len(nodes) |
17 | 17 |
|
18 | | -# Edges: pairs of connected nodes with weights (start, end, weight) |
| 18 | +# Edges: (source_index, target_index, weight) |
19 | 19 | edges = [ |
20 | | - (0, 1, 3), # Alice-Bob (strong connection) |
| 20 | + (0, 1, 3), # Alice-Bob (strong) |
21 | 21 | (0, 3, 2), # Alice-David |
22 | 22 | (1, 2, 2), # Bob-Carol |
23 | 23 | (2, 4, 1), # Carol-Eve |
|
34 | 34 | (8, 9, 2), # Iris-Jack |
35 | 35 | ] |
36 | 36 |
|
37 | | -# Create plot |
| 37 | +weights = [w for _, _, w in edges] |
| 38 | +weight_min, weight_max = min(weights), max(weights) |
| 39 | + |
| 40 | +# Weighted node degrees for size variation (data storytelling) |
| 41 | +node_degrees = [0] * n_nodes |
| 42 | +for s, e, w in edges: |
| 43 | + node_degrees[s] += w |
| 44 | + node_degrees[e] += w |
| 45 | + |
| 46 | +# Truncated Blues colormap — avoids near-white so weight-1 arcs stay clearly visible |
| 47 | +blues_raw = plt.cm.Blues(np.linspace(0.35, 0.95, 256)) |
| 48 | +arc_cmap = mcolors.LinearSegmentedColormap.from_list("TruncBlues", blues_raw) |
| 49 | +norm = mcolors.Normalize(vmin=weight_min, vmax=weight_max) |
| 50 | + |
| 51 | +# Plot |
38 | 52 | fig, ax = plt.subplots(figsize=(16, 9)) |
39 | 53 |
|
40 | | -# Node positions along x-axis |
41 | | -x_positions = np.linspace(0.05, 0.95, n_nodes) |
42 | | -y_baseline = 0.15 |
| 54 | +x_positions = np.linspace(0.06, 0.90, n_nodes) |
| 55 | +y_baseline = 0.08 |
43 | 56 |
|
44 | | -# Draw arcs |
45 | | -for start, end, weight in edges: |
| 57 | +# Arcs via PathPatch with cubic Bézier curves (distinctive matplotlib feature) |
| 58 | +for start, end, weight in sorted(edges, key=lambda e: e[2]): |
46 | 59 | x_start = x_positions[start] |
47 | 60 | x_end = x_positions[end] |
48 | | - |
49 | | - # Arc height proportional to distance between nodes |
50 | 61 | distance = abs(end - start) |
51 | | - height = 0.07 * distance |
52 | | - |
53 | | - # Center and width of the arc |
54 | | - x_center = (x_start + x_end) / 2 |
55 | | - arc_width = abs(x_end - x_start) |
56 | | - |
57 | | - # Arc thickness based on weight |
58 | | - linewidth = 2.0 + weight * 1.2 |
59 | | - |
60 | | - # Semi-transparent arcs |
61 | | - alpha = 0.55 |
62 | | - |
63 | | - # Create arc using Arc patch |
64 | | - arc = mpatches.Arc( |
65 | | - (x_center, y_baseline), |
66 | | - width=arc_width, |
67 | | - height=height * 2, |
68 | | - angle=0, |
69 | | - theta1=0, |
70 | | - theta2=180, |
71 | | - color="#306998", |
72 | | - linewidth=linewidth, |
73 | | - alpha=alpha, |
| 62 | + peak = 0.065 * distance |
| 63 | + |
| 64 | + path = mpath.Path( |
| 65 | + [ |
| 66 | + (x_start, y_baseline), |
| 67 | + (x_start, y_baseline + peak * 1.35), |
| 68 | + (x_end, y_baseline + peak * 1.35), |
| 69 | + (x_end, y_baseline), |
| 70 | + ], |
| 71 | + [mpath.Path.MOVETO, mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4], |
74 | 72 | ) |
75 | | - ax.add_patch(arc) |
76 | 73 |
|
77 | | -# Draw nodes |
78 | | -ax.scatter(x_positions, [y_baseline] * n_nodes, s=500, c="#FFD43B", edgecolors="#306998", linewidths=2.5, zorder=5) |
79 | | - |
80 | | -# Add node labels |
81 | | -for x, name in zip(x_positions, nodes, strict=True): |
82 | | - ax.text(x, y_baseline - 0.06, name, ha="center", va="top", fontsize=16, fontweight="bold", color="#306998") |
83 | | - |
84 | | -# Styling |
85 | | -ax.set_xlim(-0.02, 1.02) |
86 | | -ax.set_ylim(-0.05, 0.85) |
87 | | -ax.set_aspect("equal") |
| 74 | + patch = mpatches.PathPatch( |
| 75 | + path, |
| 76 | + facecolor="none", |
| 77 | + edgecolor=arc_cmap(norm(weight)), |
| 78 | + linewidth=1.5 + weight * 1.8, |
| 79 | + alpha=0.8, |
| 80 | + capstyle="round", |
| 81 | + ) |
| 82 | + ax.add_patch(patch) |
| 83 | + |
| 84 | +# Node sizes proportional to weighted degree (reveals hub characters) |
| 85 | +max_degree = max(node_degrees) |
| 86 | +node_sizes = [300 + 350 * (d / max_degree) for d in node_degrees] |
| 87 | + |
| 88 | +# Highlight protagonist Alice with distinct accent color |
| 89 | +node_colors = ["#FF6B35" if i == 0 else "#FFD43B" for i in range(n_nodes)] |
| 90 | +node_edge_colors = ["#B8441A" if i == 0 else "#306998" for i in range(n_nodes)] |
| 91 | + |
| 92 | +ax.scatter( |
| 93 | + x_positions, |
| 94 | + [y_baseline] * n_nodes, |
| 95 | + s=node_sizes, |
| 96 | + c=node_colors, |
| 97 | + edgecolors=node_edge_colors, |
| 98 | + linewidths=2.5, |
| 99 | + zorder=5, |
| 100 | +) |
| 101 | + |
| 102 | +# Node labels with typographic hierarchy |
| 103 | +for i, (x, name) in enumerate(zip(x_positions, nodes, strict=True)): |
| 104 | + ax.text( |
| 105 | + x, |
| 106 | + y_baseline - 0.04, |
| 107 | + name, |
| 108 | + ha="center", |
| 109 | + va="top", |
| 110 | + fontsize=16, |
| 111 | + fontweight="heavy" if i == 0 else "bold", |
| 112 | + color="#306998", |
| 113 | + ) |
88 | 114 |
|
89 | | -# Remove axes |
| 115 | +# Colorbar for connection strength |
| 116 | +sm = plt.cm.ScalarMappable(cmap=arc_cmap, norm=norm) |
| 117 | +sm.set_array([]) |
| 118 | +cbar = fig.colorbar(sm, ax=ax, shrink=0.4, aspect=15, pad=0.02) |
| 119 | +cbar.set_label("Connection Strength", fontsize=16) |
| 120 | +cbar.set_ticks([1, 2, 3]) |
| 121 | +cbar.ax.tick_params(labelsize=16) |
| 122 | + |
| 123 | +# Subtle baseline |
| 124 | +ax.plot( |
| 125 | + [x_positions[0] - 0.02, x_positions[-1] + 0.02], |
| 126 | + [y_baseline, y_baseline], |
| 127 | + color="#306998", |
| 128 | + linewidth=0.8, |
| 129 | + alpha=0.2, |
| 130 | + zorder=1, |
| 131 | +) |
| 132 | + |
| 133 | +# Layout |
| 134 | +ax.set_xlim(-0.02, 0.98) |
| 135 | +ax.set_ylim(-0.06, 0.68) |
90 | 136 | ax.axis("off") |
91 | 137 |
|
92 | | -ax.set_title("Character Interactions · arc-basic · matplotlib · pyplots.ai", fontsize=24, pad=20) |
| 138 | +# Title with narrative subtitle |
| 139 | +ax.set_title( |
| 140 | + "Character Interactions \u00b7 arc-basic \u00b7 matplotlib \u00b7 pyplots.ai", |
| 141 | + fontsize=24, |
| 142 | + fontweight="medium", |
| 143 | + pad=36, |
| 144 | +) |
| 145 | + |
| 146 | +ax.text( |
| 147 | + 0.5, |
| 148 | + 1.01, |
| 149 | + "Node size reflects connection activity \u00b7 Alice (orange) is the central character", |
| 150 | + ha="center", |
| 151 | + va="bottom", |
| 152 | + fontsize=13, |
| 153 | + color="#888888", |
| 154 | + fontstyle="italic", |
| 155 | + transform=ax.transAxes, |
| 156 | +) |
93 | 157 |
|
94 | 158 | plt.tight_layout() |
95 | 159 | plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments