|
1 | 1 | """ pyplots.ai |
2 | 2 | arc-basic: Basic Arc Diagram |
3 | | -Library: pygal 3.1.0 | Python 3.13.11 |
4 | | -Quality: 94/100 | Created: 2025-12-17 |
| 3 | +Library: pygal 3.1.0 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Created: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import math |
|
39 | 39 |
|
40 | 40 | # Node positions along x-axis (1 to 10 range) |
41 | 41 | x_positions = np.linspace(1, 10, n_nodes) |
42 | | -y_baseline = 1.0 |
| 42 | +y_baseline = 0.5 |
43 | 43 |
|
44 | | -# Build color tuple: Python Blue for all arcs, Python Yellow for nodes |
45 | | -n_edges = len(edges) |
46 | | -colors = tuple(["#306998"] * n_edges + ["#FFD43B"]) |
| 44 | +# Color palette: distinct hues for immediate weight differentiation (colorblind-safe) |
| 45 | +arc_colors = {1: "#93C5E8", 2: "#D4770B", 3: "#08306B"} |
47 | 46 |
|
48 | | -# Custom style for the chart |
| 47 | +# Thickness: wide range for immediate visual distinction |
| 48 | +arc_widths = {1: 4, 2: 12, 3: 22} |
| 49 | + |
| 50 | +# Weight labels for tooltip context |
| 51 | +weight_labels = {1: "Weak", 2: "Moderate", 3: "Strong"} |
| 52 | + |
| 53 | +# Build colors tuple: legend entries first, then edges, then nodes |
| 54 | +colors = tuple( |
| 55 | + [arc_colors[3], arc_colors[2], arc_colors[1]] # Legend (series 1-3) |
| 56 | + + [arc_colors[w] for _, _, w in edges] # Edges (series 4-18) |
| 57 | + + ["#B8860B", "#FFD43B"] # Node outline + fill |
| 58 | +) |
| 59 | + |
| 60 | +# Custom style — clean white background, no borders |
49 | 61 | custom_style = Style( |
50 | 62 | background="white", |
51 | 63 | plot_background="white", |
52 | 64 | foreground="#333333", |
53 | | - foreground_strong="#306998", |
54 | | - foreground_subtle="#666666", |
| 65 | + foreground_strong="#08306B", |
| 66 | + foreground_subtle="transparent", |
55 | 67 | colors=colors, |
56 | 68 | title_font_size=72, |
57 | 69 | label_font_size=40, |
58 | 70 | major_label_font_size=40, |
59 | 71 | legend_font_size=40, |
60 | 72 | value_font_size=32, |
61 | 73 | stroke_width=3, |
62 | | - opacity=0.65, |
| 74 | + opacity=0.85, |
63 | 75 | opacity_hover=1.0, |
64 | 76 | ) |
65 | 77 |
|
66 | | -# Create XY chart |
| 78 | +# Create XY chart — fill=False prevents area filling under arcs |
67 | 79 | chart = pygal.XY( |
68 | 80 | width=4800, |
69 | 81 | height=2700, |
70 | 82 | style=custom_style, |
| 83 | + fill=False, |
71 | 84 | title="Character Interactions · arc-basic · pygal · pyplots.ai", |
72 | | - show_legend=False, |
| 85 | + show_legend=True, |
| 86 | + legend_at_bottom=True, |
| 87 | + legend_at_bottom_columns=3, |
| 88 | + legend_box_size=30, |
73 | 89 | x_title="", |
74 | 90 | y_title="", |
75 | 91 | show_x_guides=False, |
|
78 | 94 | show_y_labels=False, |
79 | 95 | stroke=True, |
80 | 96 | dots_size=0, |
81 | | - stroke_style={"width": 3, "linecap": "round"}, |
82 | | - range=(0, 6), |
| 97 | + stroke_style={"width": 6, "linecap": "round"}, |
| 98 | + range=(0, 4.6), |
83 | 99 | xrange=(0, 11), |
84 | | - x_labels=nodes, |
85 | | - x_labels_major_count=n_nodes, |
| 100 | + x_labels=[{"value": float(x_positions[i]), "label": nodes[i]} for i in range(n_nodes)], |
86 | 101 | truncate_label=-1, |
| 102 | + css=[ |
| 103 | + "file://style.css", |
| 104 | + "inline:.plot .background {fill: white; stroke: none !important;}", |
| 105 | + "inline:.axis .line {stroke: none !important;}", |
| 106 | + "inline:.axis .guides .line {stroke: none !important;}", |
| 107 | + "inline:.plot .axis {stroke: none !important;}", |
| 108 | + "inline:.series .line {fill: none !important;}", |
| 109 | + # Hide all legend entries after the 3 weight categories |
| 110 | + "inline:.legends > g:nth-child(n+4) {display: none !important;}", |
| 111 | + ], |
| 112 | + js=[], |
87 | 113 | ) |
88 | 114 |
|
| 115 | +# Add weight legend entries first (series 1-3, visible in legend) |
| 116 | +for w_val, w_label in [(3, "Strong"), (2, "Moderate"), (1, "Weak")]: |
| 117 | + chart.add( |
| 118 | + f"{w_label} connection", |
| 119 | + [None], |
| 120 | + stroke=True, |
| 121 | + show_dots=False, |
| 122 | + stroke_style={"width": arc_widths[w_val], "linecap": "round"}, |
| 123 | + ) |
| 124 | + |
89 | 125 | # Generate arc points for each edge |
90 | | -# Each arc is drawn as a series of points forming a semi-circle |
91 | | -arc_resolution = 30 # Number of points per arc |
| 126 | +arc_resolution = 50 |
92 | 127 |
|
93 | 128 | for start_idx, end_idx, weight in edges: |
94 | 129 | x_start = x_positions[start_idx] |
|
98 | 133 | x_center = (x_start + x_end) / 2 |
99 | 134 | arc_radius = abs(x_end - x_start) / 2 |
100 | 135 |
|
101 | | - # Arc height proportional to the distance between nodes |
| 136 | + # Arc height proportional to node distance |
102 | 137 | distance = abs(end_idx - start_idx) |
103 | 138 | height_scale = 0.4 * distance |
104 | 139 |
|
105 | 140 | # Generate arc points (semi-circle above baseline) |
106 | 141 | arc_points = [] |
107 | 142 | for i in range(arc_resolution + 1): |
108 | | - theta = math.pi * i / arc_resolution # 0 to pi |
| 143 | + theta = math.pi * i / arc_resolution |
109 | 144 | x = x_center - arc_radius * math.cos(theta) |
110 | 145 | y = y_baseline + height_scale * math.sin(theta) |
111 | | - arc_points.append((x, y)) |
| 146 | + arc_points.append( |
| 147 | + {"value": (x, y), "label": f"{nodes[start_idx]} ↔ {nodes[end_idx]} ({weight_labels[weight]})"} |
| 148 | + ) |
112 | 149 |
|
113 | | - # Line thickness based on weight |
114 | 150 | chart.add( |
115 | | - f"Arc {start_idx}-{end_idx}", |
116 | | - arc_points, |
117 | | - stroke=True, |
118 | | - show_dots=False, |
119 | | - fill=False, |
120 | | - stroke_style={"width": 2 + weight * 2, "linecap": "round"}, |
| 151 | + "", arc_points, stroke=True, show_dots=False, stroke_style={"width": arc_widths[weight], "linecap": "round"} |
121 | 152 | ) |
122 | 153 |
|
123 | | -# Add nodes as a separate series (last, so uses Python Yellow) |
124 | | -node_points = [] |
125 | | -for i, name in enumerate(nodes): |
126 | | - x = x_positions[i] |
127 | | - node_points.append({"value": (x, y_baseline), "label": name}) |
| 154 | +# Add node outline ring (dark goldenrod border effect) |
| 155 | +node_points = [ |
| 156 | + { |
| 157 | + "value": (float(x_positions[i]), y_baseline), |
| 158 | + "label": f"{nodes[i]} ({sum(1 for s, t, _ in edges if s == i or t == i)} connections)", |
| 159 | + } |
| 160 | + for i in range(n_nodes) |
| 161 | +] |
| 162 | +chart.add("", node_points, stroke=False, dots_size=42) |
128 | 163 |
|
129 | | -chart.add("Characters", node_points, stroke=False, dots_size=35) |
| 164 | +# Add node fill on top (Python Yellow) |
| 165 | +chart.add("", node_points, stroke=False, dots_size=32) |
130 | 166 |
|
131 | 167 | # Save outputs |
132 | 168 | chart.render_to_file("plot.svg") |
133 | 169 | chart.render_to_png("plot.png") |
134 | 170 |
|
135 | | -# Also save HTML for interactive version |
| 171 | +# Save HTML for interactive version with hover tooltips |
136 | 172 | with open("plot.html", "w") as f: |
137 | 173 | f.write( |
138 | 174 | """<!DOCTYPE html> |
139 | 175 | <html> |
140 | 176 | <head> |
141 | 177 | <title>Character Interactions · arc-basic · pygal · pyplots.ai</title> |
142 | 178 | <style> |
143 | | - body { margin: 0; padding: 20px; background: #f5f5f5; } |
| 179 | + body { margin: 0; padding: 20px; background: #f5f5f5; font-family: sans-serif; } |
144 | 180 | .container { max-width: 100%; margin: 0 auto; } |
145 | 181 | object { width: 100%; height: auto; } |
146 | 182 | </style> |
|
0 commit comments