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