|
1 | 1 | """ pyplots.ai |
2 | 2 | arc-basic: Basic Arc Diagram |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: plotly 6.5.2 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
|
13 | 13 | n_nodes = len(nodes) |
14 | 14 |
|
15 | 15 | # Edges: pairs of (source_idx, target_idx, weight) |
16 | | -# Demonstrates short-range and long-range connections with varying weights |
17 | 16 | edges = [ |
18 | | - (0, 1, 3), # Alice-Bob (neighbors, strong connection) |
19 | | - (0, 3, 2), # Alice-David (medium distance) |
| 17 | + (0, 1, 3), # Alice-Bob (neighbors, strong) |
| 18 | + (0, 3, 2), # Alice-David |
20 | 19 | (1, 2, 2), # Bob-Carol |
21 | | - (2, 4, 3), # Carol-Eve |
| 20 | + (2, 4, 3), # Carol-Eve (strong) |
22 | 21 | (3, 5, 2), # David-Frank |
23 | 22 | (4, 6, 2), # Eve-Grace |
24 | | - (5, 7, 3), # Frank-Henry |
25 | | - (0, 8, 1), # Alice-Iris (long arc) |
26 | | - (2, 9, 2), # Carol-Jack (long arc) |
27 | | - (1, 4, 2), # Bob-Eve (medium arc) |
28 | | - (3, 7, 1), # David-Henry (long arc) |
| 23 | + (5, 7, 3), # Frank-Henry (strong) |
| 24 | + (0, 8, 1), # Alice-Iris (long, weak) |
| 25 | + (2, 9, 2), # Carol-Jack (long) |
| 26 | + (1, 4, 2), # Bob-Eve |
| 27 | + (3, 7, 1), # David-Henry (long, weak) |
29 | 28 | (6, 9, 2), # Grace-Jack |
30 | 29 | ] |
31 | 30 |
|
32 | 31 | # Node positions along horizontal axis |
33 | 32 | x_positions = np.linspace(0, 10, n_nodes) |
34 | 33 |
|
| 34 | +# Weight-based styling: color intensity, opacity, and width encode connection strength |
| 35 | +arc_styles = { |
| 36 | + 1: {"color": "rgba(100, 149, 194, 0.50)", "width": 3.0, "label": "Weak (1)"}, |
| 37 | + 2: {"color": "rgba(48, 105, 152, 0.65)", "width": 3.5, "label": "Medium (2)"}, |
| 38 | + 3: {"color": "rgba(20, 66, 110, 0.90)", "width": 5.0, "label": "Strong (3)"}, |
| 39 | +} |
| 40 | + |
35 | 41 | # Create figure |
36 | 42 | fig = go.Figure() |
37 | 43 |
|
| 44 | +# Track weight per trace for interactive filtering |
| 45 | +trace_weights = [] |
| 46 | + |
38 | 47 | # Draw arcs as smooth parabolic curves |
39 | 48 | for src, tgt, weight in edges: |
40 | | - x_src = x_positions[src] |
41 | | - x_tgt = x_positions[tgt] |
42 | | - |
43 | | - # Arc height proportional to distance between nodes |
44 | | - distance = abs(tgt - src) |
45 | | - arc_height = distance * 0.4 |
| 49 | + x_src, x_tgt = x_positions[src], x_positions[tgt] |
| 50 | + arc_height = abs(tgt - src) * 0.45 |
46 | 51 |
|
47 | | - # Create smooth arc using multiple points |
48 | 52 | t = np.linspace(0, 1, 50) |
49 | 53 | x_arc = x_src + t * (x_tgt - x_src) |
50 | | - y_arc = arc_height * 4 * t * (1 - t) # Parabolic arc |
51 | | - |
52 | | - # Line width based on weight |
53 | | - line_width = 2 + weight * 1.5 |
| 54 | + y_arc = arc_height * 4 * t * (1 - t) |
54 | 55 |
|
| 56 | + style = arc_styles[weight] |
| 57 | + distance = abs(tgt - src) |
55 | 58 | fig.add_trace( |
56 | 59 | go.Scatter( |
57 | 60 | x=x_arc, |
58 | 61 | y=y_arc, |
59 | 62 | mode="lines", |
60 | | - line={"width": line_width, "color": "#306998"}, |
61 | | - opacity=0.6, |
62 | | - hoverinfo="skip", |
| 63 | + line={"width": style["width"], "color": style["color"]}, |
| 64 | + hovertemplate=( |
| 65 | + f"<b>{nodes[src]} \u2014 {nodes[tgt]}</b><br>" |
| 66 | + f"Weight: <b>{weight}</b> ({style['label'].split(' ')[0].lower()})<br>" |
| 67 | + f"Distance: {distance} positions<extra></extra>" |
| 68 | + ), |
63 | 69 | showlegend=False, |
64 | 70 | ) |
65 | 71 | ) |
| 72 | + trace_weights.append(weight) |
| 73 | + |
| 74 | +# Weight legend using dummy traces |
| 75 | +for w in [3, 2, 1]: |
| 76 | + style = arc_styles[w] |
| 77 | + fig.add_trace( |
| 78 | + go.Scatter( |
| 79 | + x=[None], |
| 80 | + y=[None], |
| 81 | + mode="lines", |
| 82 | + line={"width": style["width"], "color": style["color"]}, |
| 83 | + name=style["label"], |
| 84 | + showlegend=True, |
| 85 | + ) |
| 86 | + ) |
| 87 | + trace_weights.append(w) |
| 88 | + |
| 89 | +# Compute per-node connection counts for rich hover |
| 90 | +conn_count = [0] * n_nodes |
| 91 | +for src, tgt, _ in edges: |
| 92 | + conn_count[src] += 1 |
| 93 | + conn_count[tgt] += 1 |
66 | 94 |
|
67 | 95 | # Draw nodes on horizontal axis |
68 | 96 | fig.add_trace( |
69 | 97 | go.Scatter( |
70 | 98 | x=x_positions, |
71 | | - y=[0] * n_nodes, |
| 99 | + y=np.zeros(n_nodes), |
72 | 100 | mode="markers+text", |
73 | | - marker={"size": 24, "color": "#FFD43B", "line": {"width": 3, "color": "#306998"}}, |
| 101 | + marker={"size": 26, "color": "#FFD43B", "line": {"width": 2.5, "color": "#306998"}}, |
74 | 102 | text=nodes, |
75 | 103 | textposition="bottom center", |
76 | | - textfont={"size": 18, "color": "#306998"}, |
77 | | - hovertemplate="%{text}<extra></extra>", |
| 104 | + textfont={"size": 22, "color": "#2a2a2a"}, |
| 105 | + customdata=np.array(conn_count), |
| 106 | + hovertemplate="<b>%{text}</b><br>Connections: %{customdata}<extra></extra>", |
78 | 107 | showlegend=False, |
79 | 108 | ) |
80 | 109 | ) |
| 110 | +trace_weights.append(0) # 0 = always visible |
81 | 111 |
|
82 | | -# Draw horizontal baseline |
83 | | -fig.add_trace( |
84 | | - go.Scatter( |
85 | | - x=[x_positions[0] - 0.3, x_positions[-1] + 0.3], |
86 | | - y=[0, 0], |
87 | | - mode="lines", |
88 | | - line={"width": 3, "color": "#888888"}, |
89 | | - hoverinfo="skip", |
90 | | - showlegend=False, |
91 | | - ) |
| 112 | +# Subtle horizontal baseline |
| 113 | +fig.add_shape( |
| 114 | + type="line", x0=x_positions[0] - 0.3, x1=x_positions[-1] + 0.3, y0=0, y1=0, line={"width": 1.5, "color": "#CCCCCC"} |
92 | 115 | ) |
93 | 116 |
|
| 117 | +# Annotate longest-range arc as focal point |
| 118 | +mid_x = (x_positions[0] + x_positions[8]) / 2 |
| 119 | +peak_y = abs(8 - 0) * 0.45 |
| 120 | +fig.add_annotation( |
| 121 | + x=mid_x, |
| 122 | + y=peak_y, |
| 123 | + text="longest range", |
| 124 | + showarrow=True, |
| 125 | + arrowhead=2, |
| 126 | + arrowwidth=1.5, |
| 127 | + arrowcolor="#666666", |
| 128 | + ax=55, |
| 129 | + ay=-25, |
| 130 | + font={"size": 20, "color": "#555555", "family": "Arial"}, |
| 131 | + bgcolor="rgba(255, 255, 255, 0.7)", |
| 132 | + borderpad=4, |
| 133 | +) |
| 134 | + |
| 135 | +# Build interactive filter buttons (Plotly-distinctive updatemenus) |
| 136 | +filter_options = [("All", {1, 2, 3}), ("Strong", {3}), ("Medium", {2}), ("Weak", {1})] |
| 137 | +buttons = [] |
| 138 | +for label, keep in filter_options: |
| 139 | + visible = [True if tw == 0 else (tw in keep) for tw in trace_weights] |
| 140 | + buttons.append({"label": label, "method": "update", "args": [{"visible": visible}]}) |
| 141 | + |
94 | 142 | # Layout |
95 | 143 | fig.update_layout( |
96 | | - title={"text": "arc-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"}, |
97 | | - xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": None, "range": [-0.5, 10.5]}, |
98 | | - yaxis={ |
99 | | - "showgrid": False, |
100 | | - "zeroline": False, |
101 | | - "showticklabels": False, |
102 | | - "title": None, |
103 | | - "range": [-1.5, 4], |
104 | | - "scaleanchor": "x", |
105 | | - "scaleratio": 0.5, |
| 144 | + title={ |
| 145 | + "text": ( |
| 146 | + "arc-basic \u00b7 plotly \u00b7 pyplots.ai" |
| 147 | + "<br><span style='font-size:18px;color:#777777;font-weight:normal'>" |
| 148 | + "Character interactions in a story narrative</span>" |
| 149 | + ), |
| 150 | + "font": {"size": 30, "color": "#2a2a2a"}, |
| 151 | + "x": 0.5, |
| 152 | + "xanchor": "center", |
| 153 | + "y": 0.97, |
106 | 154 | }, |
| 155 | + xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "showline": False, "range": [-0.5, 10.5]}, |
| 156 | + yaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "showline": False, "range": [-0.7, 4.1]}, |
| 157 | + hovermode="closest", |
| 158 | + hoverlabel={"bgcolor": "white", "font_size": 16, "font_color": "#306998", "bordercolor": "#306998"}, |
107 | 159 | template="plotly_white", |
108 | 160 | plot_bgcolor="white", |
109 | | - margin={"l": 50, "r": 50, "t": 100, "b": 80}, |
| 161 | + paper_bgcolor="white", |
| 162 | + margin={"l": 30, "r": 30, "t": 90, "b": 30}, |
| 163 | + legend={ |
| 164 | + "title": {"text": "Connection Strength", "font": {"size": 18, "color": "#2a2a2a"}}, |
| 165 | + "font": {"size": 16}, |
| 166 | + "x": 0.98, |
| 167 | + "y": 0.98, |
| 168 | + "xanchor": "right", |
| 169 | + "yanchor": "top", |
| 170 | + "bgcolor": "rgba(255, 255, 255, 0.85)", |
| 171 | + "bordercolor": "#CCCCCC", |
| 172 | + "borderwidth": 1, |
| 173 | + }, |
| 174 | + updatemenus=[ |
| 175 | + { |
| 176 | + "type": "buttons", |
| 177 | + "direction": "right", |
| 178 | + "x": 0.02, |
| 179 | + "y": 0.98, |
| 180 | + "xanchor": "left", |
| 181 | + "yanchor": "top", |
| 182 | + "buttons": buttons, |
| 183 | + "showactive": True, |
| 184 | + "bgcolor": "rgba(255, 255, 255, 0.85)", |
| 185 | + "bordercolor": "#CCCCCC", |
| 186 | + "font": {"size": 14, "color": "#306998"}, |
| 187 | + "pad": {"r": 8, "t": 8}, |
| 188 | + } |
| 189 | + ], |
110 | 190 | ) |
111 | 191 |
|
112 | 192 | # Save outputs |
113 | 193 | fig.write_image("plot.png", width=1600, height=900, scale=3) |
114 | | -fig.write_html("plot.html", include_plotlyjs="cdn") |
| 194 | +fig.write_html( |
| 195 | + "plot.html", |
| 196 | + include_plotlyjs="cdn", |
| 197 | + config={ |
| 198 | + "displaylogo": False, |
| 199 | + "modeBarButtonsToRemove": ["lasso2d", "select2d"], |
| 200 | + "toImageButtonOptions": {"width": 4800, "height": 2700, "scale": 1}, |
| 201 | + }, |
| 202 | +) |
0 commit comments