diff --git a/plots/network-weighted/implementations/plotly.py b/plots/network-weighted/implementations/plotly.py new file mode 100644 index 0000000000..4f3497eac5 --- /dev/null +++ b/plots/network-weighted/implementations/plotly.py @@ -0,0 +1,243 @@ +""" pyplots.ai +network-weighted: Weighted Network Graph with Edge Thickness +Library: plotly 6.5.1 | Python 3.13.11 +Quality: 92/100 | Created: 2026-01-08 +""" + +import numpy as np +import plotly.graph_objects as go + + +# Data - Trade network between countries (billions USD) +np.random.seed(42) + +# Define nodes (countries) +countries = [ + "USA", + "China", + "Germany", + "Japan", + "UK", + "France", + "Canada", + "Mexico", + "Brazil", + "India", + "Australia", + "S. Korea", + "Netherlands", + "Italy", + "Spain", +] +n_nodes = len(countries) +node_idx = {name: i for i, name in enumerate(countries)} + +# Create weighted edges (trade relationships) +edges = [ + # Major trade routes (high weight) + ("USA", "China", 580), + ("USA", "Canada", 620), + ("USA", "Mexico", 550), + ("China", "Japan", 320), + ("China", "S. Korea", 280), + ("China", "Germany", 190), + ("Germany", "France", 180), + ("Germany", "Netherlands", 210), + ("Germany", "Italy", 140), + ("UK", "Germany", 130), + ("UK", "USA", 140), + ("UK", "Netherlands", 90), + ("Japan", "USA", 200), + ("Japan", "S. Korea", 85), + # Medium trade routes + ("France", "Italy", 95), + ("France", "Spain", 110), + ("Spain", "Italy", 50), + ("Canada", "Mexico", 40), + ("Brazil", "USA", 75), + ("Brazil", "China", 100), + ("India", "USA", 90), + ("India", "China", 115), + ("India", "UK", 35), + ("Australia", "China", 145), + ("Australia", "Japan", 55), + ("Australia", "S. Korea", 45), + # Lower trade routes + ("Netherlands", "UK", 65), + ("S. Korea", "USA", 120), + ("Mexico", "China", 70), +] + +# Compute force-directed layout (Fruchterman-Reingold algorithm) +pos = np.random.rand(n_nodes, 2) * 2 - 1 +k = 0.5 +for _ in range(200): + displacement = np.zeros((n_nodes, 2)) + # Repulsive forces + for i in range(n_nodes): + diff = pos[i] - pos + dist = np.sqrt((diff**2).sum(axis=1)) + dist = np.where(dist < 0.01, 0.01, dist) + rep_force = k**2 / dist + rep_force[i] = 0 + displacement[i] += (diff * rep_force[:, np.newaxis]).sum(axis=0) + # Attractive forces along edges + for source, target, weight in edges: + i, j = node_idx[source], node_idx[target] + diff = pos[j] - pos[i] + dist = np.sqrt((diff**2).sum()) + if dist > 0.01: + attr_force = dist**2 / k * (1 + weight / 200) + displacement[i] += diff / dist * attr_force + displacement[j] -= diff / dist * attr_force + # Update positions + length = np.sqrt((displacement**2).sum(axis=1)) + length = np.where(length < 0.01, 0.01, length) + pos += displacement / length[:, np.newaxis] * min(0.1, k) + +# Normalize positions with margin for labels and annotation +pos = (pos - pos.min(axis=0)) / (pos.max(axis=0) - pos.min(axis=0)) +pos = pos * 1.6 - 0.8 # Scale to [-0.8, 0.8] +# Center the layout +pos = pos - pos.mean(axis=0) +node_positions = {countries[i]: pos[i] for i in range(n_nodes)} + +# Calculate weighted degree for node sizing +weighted_degree = dict.fromkeys(countries, 0) +for source, target, weight in edges: + weighted_degree[source] += weight + weighted_degree[target] += weight + +node_sizes = [20 + (weighted_degree[node] / 40) for node in countries] + +# Create edge traces with varying thickness +edge_traces = [] +min_weight = min(w for _, _, w in edges) +max_weight = max(w for _, _, w in edges) + +for source, target, weight in edges: + x0, y0 = node_positions[source] + x1, y1 = node_positions[target] + # Scale width: 2 to 18 based on weight + normalized = (weight - min_weight) / (max_weight - min_weight) + line_width = 2 + normalized * 16 + # Color alpha based on weight (darker = stronger) + alpha = 0.4 + normalized * 0.5 + edge_traces.append( + go.Scatter( + x=[x0, x1, None], + y=[y0, y1, None], + mode="lines", + line={"width": line_width, "color": f"rgba(48, 105, 152, {alpha})"}, + hoverinfo="text", + text=f"{source} ↔ {target}: ${weight}B", + showlegend=False, + ) + ) + +# Create node trace +node_x = [node_positions[node][0] for node in countries] +node_y = [node_positions[node][1] for node in countries] + +# Calculate smart label positions to avoid overlap with explicit handling +# for known problematic pairs: Japan/S.Korea and Italy/France +label_positions = [] + +for i, node in enumerate(countries): + x, y = node_positions[node] + # Find nearby nodes and adjust position + nearby_above = 0 + nearby_below = 0 + nearby_left = 0 + nearby_right = 0 + for j, other in enumerate(countries): + if i != j: + ox, oy = node_positions[other] + dx, dy = x - ox, y - oy + dist = np.sqrt(dx**2 + dy**2) + if dist < 0.35: + if dy > 0: + nearby_below += 1 + else: + nearby_above += 1 + if dx > 0: + nearby_left += 1 + else: + nearby_right += 1 + + # Handle specific known close pairs to avoid overlap + if node == "Japan": + pos_choice = "top right" + elif node == "S. Korea": + pos_choice = "bottom left" + elif node == "Italy": + pos_choice = "top left" + elif node == "France": + pos_choice = "bottom right" + elif nearby_above > nearby_below: + pos_choice = "bottom center" + elif nearby_left > nearby_right: + pos_choice = "middle right" + elif nearby_right > nearby_left: + pos_choice = "middle left" + else: + pos_choice = "top center" + label_positions.append(pos_choice) + +node_trace = go.Scatter( + x=node_x, + y=node_y, + mode="markers+text", + marker={"size": node_sizes, "color": "#FFD43B", "line": {"width": 2, "color": "#306998"}}, + text=countries, + textposition=label_positions, + textfont={"size": 16, "color": "#333333"}, + hoverinfo="text", + hovertext=[f"{c}
Trade Volume: ${weighted_degree[c]}B" for c in countries], + showlegend=False, +) + +# Create figure +fig = go.Figure() + +# Add edges first (behind nodes) +for trace in edge_traces: + fig.add_trace(trace) + +# Add nodes +fig.add_trace(node_trace) + +# Add weight scale annotation (positioned at top-left to avoid cutoff) +fig.add_annotation( + x=0.01, + y=0.99, + xref="paper", + yref="paper", + text="Edge Thickness = Trade Volume
35B USD (thin) to 620B USD (thick)", + showarrow=False, + font={"size": 18, "color": "#333333", "family": "Arial"}, + align="left", + xanchor="left", + yanchor="top", + bgcolor="rgba(255,255,255,0.95)", + bordercolor="#999999", + borderwidth=1, + borderpad=10, +) + +# Update layout +fig.update_layout( + title={ + "text": "network-weighted · plotly · pyplots.ai", "font": {"size": 28, "color": "#333333"}, "x": 0.5, "xanchor": "center" + }, + xaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": ""}, + yaxis={"showgrid": False, "zeroline": False, "showticklabels": False, "title": ""}, + template="plotly_white", + showlegend=False, + margin={"l": 80, "r": 80, "t": 100, "b": 80}, + plot_bgcolor="white", +) + +# Save outputs +fig.write_image("plot.png", width=1600, height=900, scale=3) +fig.write_html("plot.html") diff --git a/plots/network-weighted/metadata/plotly.yaml b/plots/network-weighted/metadata/plotly.yaml new file mode 100644 index 0000000000..03662044a7 --- /dev/null +++ b/plots/network-weighted/metadata/plotly.yaml @@ -0,0 +1,222 @@ +library: plotly +specification_id: network-weighted +created: '2026-01-08T15:58:57Z' +updated: '2026-01-08T16:26:11Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 20822849491 +issue: 3290 +python_version: 3.13.11 +library_version: 6.5.1 +preview_url: https://storage.googleapis.com/pyplots-images/plots/network-weighted/plotly/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/network-weighted/plotly/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/network-weighted/plotly/plot.html +quality_score: 92 +review: + strengths: + - Clear edge thickness differentiation showing varying trade volumes from thin (35B) + to thick (620B USD) + - Well-implemented force-directed layout with weight-influenced attraction keeping + high-trade partners close (USA-China-Canada cluster) + - Effective node sizing based on weighted degree - USA and China correctly appear + as the largest hubs + - Smart label positioning avoids overlap through explicit handling of problematic + pairs (Japan/S.Korea, Italy/France) + - Informative weight scale annotation box explaining edge thickness interpretation + - Clean data representing a realistic international trade network scenario + weaknesses: + - The network is positioned more toward the right side of the canvas, leaving significant + whitespace on the left - better centering would improve layout balance + image_description: |- + The plot displays a weighted network graph of international trade relationships between 15 countries. Nodes are represented as yellow circular markers with dark blue (#306998) borders, sized proportionally to their weighted degree (total trade volume). USA and China appear as the largest nodes, reflecting their position as major trade hubs. + + Edges connect trading partners with varying thickness representing trade volume in billions of USD. The thickest edges (dark blue, high opacity) connect USA-Canada (~620B), USA-Mexico (~550B), and USA-China (~580B), clearly visible as prominent thick lines. Thinner, more transparent edges represent smaller trade relationships like Spain-Italy (50B) and India-UK (35B). + + The title "network-weighted · plotly · pyplots.ai" is centered at the top in dark gray text (size 28). A legend box in the top-left corner explains "Edge Thickness = Trade Volume" with the range "35B USD (thin) to 620B USD (thick)" on a white background with subtle border. + + Country labels are displayed in dark gray (#333333) with size 16 font, positioned intelligently around each node to avoid overlap - Japan appears "top right", S. Korea "bottom left", Italy "top left", and France "bottom right" with other nodes using contextual positioning based on nearby neighbors. + + The layout uses a force-directed algorithm where high-trade partners are pulled closer together, creating a visible USA-China-Mexico-Canada cluster on the right side. European countries (Germany, France, Italy, Spain, Netherlands, UK) form a cluster on the left-center. Asian-Pacific countries (Japan, S. Korea, Australia) connect primarily through China. + + Background is clean white with no grid lines (appropriate for network graphs). The overall layout fills approximately 60-70% of the canvas with the network slightly positioned toward the right. + criteria_checklist: + visual_quality: + score: 36 + max: 40 + items: + - id: VQ-01 + name: Text Legibility + score: 10 + max: 10 + passed: true + comment: Title at 28pt, node labels at 16pt, annotation text at 18pt - all + clearly readable at 4800x2700 + - id: VQ-02 + name: No Overlap + score: 8 + max: 8 + passed: true + comment: Smart label positioning with explicit handling for close pairs (Japan/S.Korea, + Italy/France) prevents all text overlap + - id: VQ-03 + name: Element Visibility + score: 8 + max: 8 + passed: true + comment: Node sizes (20-55px) appropriately scaled by weighted degree; edge + widths (2-18px) provide clear visual differentiation + - id: VQ-04 + name: Color Accessibility + score: 5 + max: 5 + passed: true + comment: Single blue color scheme for edges with alpha variation (0.4-0.9); + yellow nodes with blue borders - no colorblind issues + - id: VQ-05 + name: Layout Balance + score: 3 + max: 5 + passed: true + comment: Network fills ~60-70% of canvas but positioned slightly right of + center; left side has more whitespace + - id: VQ-06 + name: Axis Labels + score: 0 + max: 2 + passed: true + comment: N/A for network graph - axes correctly hidden (no labels needed) + - id: VQ-07 + name: Grid & Legend + score: 2 + max: 2 + passed: true + comment: No grid (appropriate for network); annotation box with weight scale + well-positioned in top-left with clear styling + spec_compliance: + score: 25 + max: 25 + items: + - id: SC-01 + name: Plot Type + score: 8 + max: 8 + passed: true + comment: Correct weighted network graph with edge thickness mapping + - id: SC-02 + name: Data Mapping + score: 5 + max: 5 + passed: true + comment: Nodes=countries, edges=trade relationships, weight=trade volume correctly + mapped to edge thickness + - id: SC-03 + name: Required Features + score: 5 + max: 5 + passed: true + comment: 'All spec features: edge thickness for weight, node sizing by weighted + degree, weight scale annotation, force-directed layout with weight influence' + - id: SC-04 + name: Data Range + score: 3 + max: 3 + passed: true + comment: All 15 nodes and 29 edges visible within plot bounds + - id: SC-05 + name: Legend Accuracy + score: 2 + max: 2 + passed: true + comment: Weight scale annotation accurately describes edge thickness range + (35B-620B USD) + - id: SC-06 + name: Title Format + score: 2 + max: 2 + passed: true + comment: 'Uses exact format: network-weighted · plotly · pyplots.ai' + data_quality: + score: 20 + max: 20 + items: + - id: DQ-01 + name: Feature Coverage + score: 8 + max: 8 + passed: true + comment: 'Shows full range: high-weight edges (USA-Canada 620B), medium (France-Spain + 110B), low (India-UK 35B); large hubs (USA, China) vs peripheral nodes (Spain, + Australia)' + - id: DQ-02 + name: Realistic Context + score: 7 + max: 7 + passed: true + comment: International trade network is real-world relevant; trade volumes + approximate actual relationships (USA-China, USA-Canada top partners) + - id: DQ-03 + name: Appropriate Scale + score: 5 + max: 5 + passed: true + comment: Trade values in billions USD (35B-620B) are realistic for major bilateral + trade flows + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Follows imports → data → layout computation → edge traces → node + trace → figure → save structure; no functions/classes + - id: CQ-02 + name: Reproducibility + score: 3 + max: 3 + passed: true + comment: np.random.seed(42) ensures reproducible force-directed layout + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only numpy and plotly.graph_objects imported, both used + - id: CQ-04 + name: No Deprecated API + score: 1 + max: 1 + passed: true + comment: Uses current Plotly 6.x API + - id: CQ-05 + name: Output Correct + score: 1 + max: 1 + passed: true + comment: Saves as plot.png and plot.html + library_features: + score: 5 + max: 5 + items: + - id: LF-01 + name: Distinctive Features + score: 5 + max: 5 + passed: true + comment: 'Uses Plotly-specific features: hover tooltips with formatted text + (trade pairs + values), individual edge traces for per-edge styling, annotations + with paper coordinates, HTML export for interactivity' + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - hover-tooltips + - html-export + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending