diff --git a/plots/network-force-directed/implementations/python/pygal.py b/plots/network-force-directed/implementations/python/pygal.py
index 1c2cb7e55c..69132a7ffd 100644
--- a/plots/network-force-directed/implementations/python/pygal.py
+++ b/plots/network-force-directed/implementations/python/pygal.py
@@ -1,23 +1,42 @@
-""" pyplots.ai
+""" anyplot.ai
network-force-directed: Force-Directed Graph
-Library: pygal 3.1.0 | Python 3.13.11
-Quality: 92/100 | Created: 2025-12-17
+Library: pygal 3.1.0 | Python 3.14.4
+Quality: 83/100 | Created: 2026-04-26
"""
-import numpy as np
-import pygal
-from pygal.style import Style
+import sys
+from pathlib import Path
-# Set seed for reproducibility
+# Remove script directory from path to avoid name collision with pygal package
+_script_dir = str(Path(__file__).parent)
+sys.path = [p for p in sys.path if p != _script_dir]
+
+import os # noqa: E402
+
+import numpy as np # noqa: E402
+import pygal # noqa: E402
+from pygal.style import Style # noqa: E402
+
+
+# Theme-adaptive tokens
+THEME = os.getenv("ANYPLOT_THEME", "light")
+PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
+INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
+INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
+INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
+EDGE_COLOR = "#9A988F" if THEME == "light" else "#5A5852"
+
+OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")
+
+# Reproducibility
np.random.seed(42)
-# Data: A social network with 50 nodes in 3 communities
+# Data: A corporate social network with 50 nodes in 3 departments
# Demonstrates force-directed layout with clear community structure
nodes = []
edges = []
-# Create 3 communities
community_sizes = [18, 17, 15] # Total: 50 nodes
community_names = ["Engineering", "Marketing", "Sales"]
node_id = 0
@@ -50,18 +69,17 @@
bridge_edges = [(0, 18), (5, 20), (10, 25), (18, 35), (22, 40), (30, 45), (8, 38), (15, 48)]
edges.extend(bridge_edges)
-# Force-directed layout algorithm (Fruchterman-Reingold)
+# Force-directed layout (Fruchterman-Reingold)
n = len(nodes)
positions = np.random.rand(n, 2) * 2 - 1 # Initial random positions
-# Optimal distance parameter
-k = 0.5
-iterations = 200
+k = 0.95 # Optimal distance — larger to reduce dense-cluster node overlap
+iterations = 320
for iteration in range(iterations):
displacement = np.zeros((n, 2))
- # Repulsive forces between all node pairs (nodes push apart)
+ # Repulsive forces between all node pairs
for i in range(n):
for j in range(i + 1, n):
diff = positions[i] - positions[j]
@@ -70,7 +88,7 @@
displacement[i] += repulsive_force
displacement[j] -= repulsive_force
- # Attractive forces along edges (connected nodes pull together)
+ # Attractive forces along edges
for src, tgt in edges:
diff = positions[src] - positions[tgt]
dist = max(np.linalg.norm(diff), 0.01)
@@ -78,118 +96,100 @@
displacement[src] -= attractive_force
displacement[tgt] += attractive_force
- # Apply displacement with cooling (decreasing temperature)
+ # Apply displacement with cooling
temperature = 1 - iteration / iterations
for i in range(n):
disp_norm = np.linalg.norm(displacement[i])
if disp_norm > 0:
- # Limit movement by temperature
positions[i] += (displacement[i] / disp_norm) * min(disp_norm, 0.15 * temperature)
-# Normalize positions to [1, 11] range for pygal (with padding)
+# Normalize positions to a padded plotting range
pos_min = positions.min(axis=0)
pos_max = positions.max(axis=0)
positions = (positions - pos_min) / (pos_max - pos_min + 1e-6) * 10 + 1
pos = {node["id"]: positions[i] for i, node in enumerate(nodes)}
-# Calculate node degrees (number of connections)
+# Node degrees (for tooltip context)
degrees = {node["id"]: 0 for node in nodes}
for src, tgt in edges:
degrees[src] += 1
degrees[tgt] += 1
-# Community colors
-community_colors = ["#306998", "#FFD43B", "#FF6B6B"]
+# Style — first data series is the edge "Connections" (muted), then communities use Okabe-Ito 1..3
+community_colors = OKABE_ITO[: len(community_names)]
+series_colors = (EDGE_COLOR,) + community_colors
-# Custom style for the chart
custom_style = Style(
- background="white",
- plot_background="white",
- foreground="#333333",
- foreground_strong="#333333",
- foreground_subtle="#666666",
- colors=("#AAAAAA",) + tuple(community_colors),
+ background=PAGE_BG,
+ plot_background=PAGE_BG,
+ foreground=INK,
+ foreground_strong=INK,
+ foreground_subtle=INK_MUTED,
+ colors=series_colors,
title_font_size=72,
label_font_size=40,
major_label_font_size=36,
- legend_font_size=40,
+ legend_font_size=44,
value_font_size=32,
stroke_width=2,
- opacity=0.85,
+ opacity=0.9,
opacity_hover=1.0,
+ tooltip_font_size=28,
+ font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
)
-# Create XY chart
chart = pygal.XY(
width=4800,
height=2700,
style=custom_style,
- title="network-force-directed · pygal · pyplots.ai",
+ title="network-force-directed · pygal · anyplot.ai",
show_legend=True,
- x_title="",
- y_title="",
show_x_guides=False,
show_y_guides=False,
show_x_labels=False,
show_y_labels=False,
stroke=True,
- dots_size=25,
+ dots_size=28,
stroke_style={"width": 1.5, "linecap": "round"},
legend_at_bottom=True,
legend_at_bottom_columns=4,
+ legend_box_size=36,
+ margin=80,
range=(0, 12),
xrange=(0, 12),
)
-# Add edges as a single series with lines connecting pairs
-# Each edge is represented as two points connected, with None to break between edges
+# Edges as a single XY series with None breaks between segments
edge_points = []
for src, tgt in edges:
x1, y1 = pos[src]
x2, y2 = pos[tgt]
edge_points.append((x1, y1))
edge_points.append((x2, y2))
- edge_points.append(None) # Break the line for next edge
+ edge_points.append(None)
chart.add("Connections", edge_points, stroke=True, show_dots=False, fill=False)
-# Add nodes grouped by community
+# Nodes grouped by community — radius scales with node degree (visual encoding)
+# pygal supports per-point SVG attribute overrides via the "node" dict
+max_degree = max(degrees.values())
+min_radius, max_radius = 18, 52
for comm_idx, comm_name in enumerate(community_names):
comm_nodes = [node for node in nodes if node["community"] == comm_idx]
- # Create points with labels for tooltips showing degree
node_points = []
for node in comm_nodes:
x, y = pos[node["id"]]
degree = degrees[node["id"]]
+ radius = min_radius + (max_radius - min_radius) * (degree / max_degree)
label = f"Node {node['id']} | {degree} connections"
if degree >= 7:
label += " (Hub)"
- node_points.append({"value": (x, y), "label": label})
+ node_points.append({"value": (x, y), "label": label, "node": {"r": round(radius, 1)}})
chart.add(comm_name, node_points, stroke=False)
-# Save outputs
-chart.render_to_file("plot.svg")
-chart.render_to_png("plot.png")
-
-# Also save HTML for interactive version
-with open("plot.html", "w") as f:
- f.write(
- """
-
-
- network-force-directed · pygal · pyplots.ai
-
-
-
-
-
-
-
-"""
- )
+# Save outputs (theme-aware filenames)
+chart.render_to_file(f"plot-{THEME}.svg")
+chart.render_to_png(f"plot-{THEME}.png")
+
+with open(f"plot-{THEME}.html", "wb") as f:
+ f.write(chart.render())
diff --git a/plots/network-force-directed/metadata/python/pygal.yaml b/plots/network-force-directed/metadata/python/pygal.yaml
index a1425c0507..81658bfc3e 100644
--- a/plots/network-force-directed/metadata/python/pygal.yaml
+++ b/plots/network-force-directed/metadata/python/pygal.yaml
@@ -1,197 +1,244 @@
library: pygal
+language: python
specification_id: network-force-directed
created: 2025-12-17 10:04:33+00:00
-updated: 2025-12-17 10:04:33+00:00
-generated_by: claude-opus-4-5-20251101
-workflow_run: 20298900805
+updated: '2026-04-26T10:03:29Z'
+generated_by: claude-opus
+workflow_run: 24952933179
issue: 990
-python_version: 3.13.11
+python_version: 3.14.4
library_version: 3.1.0
-preview_url: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/pygal/plot.png
-preview_html: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/pygal/plot.html
-quality_score: 92
-impl_tags:
- dependencies: []
- techniques:
- - html-export
- patterns:
- - data-generation
- dataprep: []
- styling: []
+preview_url_light: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/python/pygal/plot-light.png
+preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/python/pygal/plot-dark.png
+preview_html_light: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/python/pygal/plot-light.html
+preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/network-force-directed/python/pygal/plot-dark.html
+quality_score: 83
review:
- strengths: []
- weaknesses: []
- improvements: []
- image_description: 'The plot displays a force-directed network graph with 50 nodes
- organized into 3 clearly visible communities. The title "network-force-directed
- · pygal · pyplots.ai" is displayed at the top. Nodes are colored by community:
- **blue** (Sales) cluster on the right side, **yellow** (Marketing) cluster in
- the lower-center, and **red/coral** (Engineering) cluster in the upper-left. Gray
- lines connect nodes representing edges/relationships. The communities are visually
- separated with sparse bridge connections between them. A legend at the bottom
- identifies the four series: "Connections", "Engineering", "Marketing", and "Sales".
- The layout shows the force-directed algorithm successfully clustering related
- nodes together.'
+ strengths:
+ - Complete Fruchterman-Reingold force-directed layout from scratch with no external
+ graph library
+ - 'Degree-based node sizing using pygal per-point SVG attribute overrides ({"node":
+ {"r": ...}}) — distinctive pygal feature'
+ - Three-community corporate social network (Engineering/Marketing/Sales) — realistic,
+ neutral scenario
+ - Full theme adaptation in both renders; all token assignments (background, foreground,
+ foreground_subtle) correctly derived from ANYPLOT_THEME
+ - 'Perfect code quality: seed set, clean imports, linear structure, appropriate
+ complexity; full output (PNG + SVG + HTML)'
+ weaknesses:
+ - 'First pygal series Connections uses EDGE_COLOR (#9A988F light / #5A5852 dark)
+ rather than #009E73 — palette position 1 is not the brand green; reorder series
+ so Engineering comes first'
+ - Some node crowding in dense Engineering and Marketing clusters; increase repulsion
+ constant k from 0.95 to 1.1 for better node separation
+ - Legend font appears small relative to canvas at rendered scale despite legend_font_size=44
+ being set
+ image_description: |-
+ Light render (plot-light.png):
+ Background: Warm off-white (#FAF8F1) — correct theme surface
+ Chrome: Title "network-force-directed · pygal · anyplot.ai" in dark text at top center — readable. No axis labels (appropriate for network graph). Legend at bottom with four entries (Connections, Engineering, Marketing, Sales) in dark text — readable.
+ Data: Three community clusters: Engineering (teal #009E73, right side), Marketing (orange #D55E00, lower-left), Sales (blue #0072B2, upper-center). Edges as light gray lines (#9A988F). Node sizes vary by degree — hub nodes noticeably larger.
+ Legibility verdict: PASS
+
+ Dark render (plot-dark.png):
+ Background: Near-black (#1A1A17) — correct theme surface
+ Chrome: Title in light off-white text at top center — clearly readable. Legend at bottom in light-colored text — readable. No dark-on-dark text issues detected.
+ Data: Community colors identical to light render — Engineering green, Marketing orange, Sales blue. Edges appear as lighter gray on dark background. Node sizing preserved.
+ Legibility verdict: PASS
criteria_checklist:
visual_quality:
- score: 32
- max: 35
+ score: 25
+ max: 30
items:
- id: VQ-01
- name: Axis labels
+ name: Text Legibility
score: 7
- max: 7
+ max: 8
passed: true
- comment: Appropriately hidden (not applicable for network graphs)
+ comment: Font sizes explicitly set (title=72, legend=44, labels=40); all text
+ readable in both themes
- id: VQ-02
- name: No overlapping text
- score: 6
+ name: No Overlap
+ score: 5
max: 6
passed: true
- comment: Clean layout, no text overlap
+ comment: Some node crowding in dense clusters inherent to 50-node network;
+ no text overlap
- id: VQ-03
- name: Color choice
+ name: Element Visibility
score: 5
- max: 5
+ max: 6
passed: true
- comment: Blue, yellow, red are distinguishable and colorblind-safe
+ comment: Nodes clearly visible with degree-based sizing; edges rendered as
+ clean gray lines
- id: VQ-04
- name: Element clarity
- score: 5
- max: 5
+ name: Color Accessibility
+ score: 2
+ max: 2
passed: true
- comment: Nodes are clearly visible with appropriate dot size (25)
+ comment: Okabe-Ito community colors (green/orange/blue); CVD-safe; muted edges
+ don't compete
- id: VQ-05
- name: Layout balance
- score: 4
- max: 5
- passed: true
- comment: Good proportions, communities well separated
- - id: VQ-06
- name: Grid subtlety
+ name: Layout & Canvas
score: 3
- max: 3
+ max: 4
passed: true
- comment: Grid appropriately disabled for network visualization
- - id: VQ-07
- name: Legend placement
+ comment: Network fills ~65-70% of canvas; slight wasted space at canvas edges
+ - id: VQ-06
+ name: Axis Labels & Title
score: 2
max: 2
passed: true
- comment: Legend at bottom, does not obscure data
- - id: VQ-08
- name: Image size
- score: 0
+ comment: Title correctly formatted; no axis labels appropriate for network
+ graph type
+ - id: VQ-07
+ name: Palette Compliance
+ score: 1
max: 2
+ passed: false
+ comment: 'Community colors correctly use Okabe-Ito; but first series Connections
+ uses EDGE_COLOR not #009E73; both theme backgrounds correct'
+ design_excellence:
+ score: 13
+ max: 20
+ items:
+ - id: DE-01
+ name: Aesthetic Sophistication
+ score: 5
+ max: 8
+ passed: true
+ comment: 'Above default: degree-based sizing, muted edges vs bright community
+ nodes creates hierarchy'
+ - id: DE-02
+ name: Visual Refinement
+ score: 4
+ max: 6
passed: true
- comment: 4800x2700 specified but aspect ratio appears slightly off
+ comment: No grid or axis decorations; clean margins; legend neat; above library
+ defaults
+ - id: DE-03
+ name: Data Storytelling
+ score: 4
+ max: 6
+ passed: true
+ comment: Three communities immediately apparent; hub nodes visually stand
+ out; bridge edges tell inter-community connectivity story
spec_compliance:
- score: 33
- max: 35
+ score: 14
+ max: 15
items:
- id: SC-01
- name: Correct plot type
- score: 10
- max: 10
+ name: Plot Type
+ score: 5
+ max: 5
passed: true
- comment: Force-directed network graph correctly implemented
+ comment: Force-directed graph with manual Fruchterman-Reingold implementation
- id: SC-02
- name: Data mapped correctly
- score: 7
- max: 7
+ name: Required Features
+ score: 3
+ max: 4
passed: true
- comment: Nodes and edges properly positioned via physics simulation
+ comment: Node degree scaling, edge representation, community structure present;
+ edge thickness for weights not implemented (spec optional)
- id: SC-03
- name: Required features present
- score: 7
- max: 7
+ name: Data Mapping
+ score: 3
+ max: 3
passed: true
- comment: Shows communities, connections, force-directed layout
+ comment: Correct network topology; three-community structure clearly shown
- id: SC-04
- name: Data range
- score: 4
- max: 4
- passed: true
- comment: All nodes visible with appropriate padding
- - id: SC-05
- name: Legend accuracy
- score: 2
- max: 4
- passed: true
- comment: Legend shows series but could be more descriptive
- - id: SC-06
- name: Title format
+ name: Title & Legend
score: 3
max: 3
passed: true
- comment: Uses `{spec-id} · {library} · pyplots.ai` format correctly
+ comment: Title network-force-directed · pygal · anyplot.ai correct; legend
+ labels correct
data_quality:
score: 14
max: 15
items:
- id: DQ-01
- name: Feature coverage
+ name: Feature Coverage
score: 5
max: 6
passed: true
- comment: Shows communities, varying node degrees, bridge connections; node
- size by degree mentioned in spec but not implemented
+ comment: 'Shows communities, hub nodes, bridge edges, degree variation; minor:
+ no strongly isolated peripheral nodes'
- id: DQ-02
- name: Realistic context
+ name: Realistic Context
score: 5
max: 5
passed: true
- comment: Social network with Engineering/Marketing/Sales teams is plausible
+ comment: Corporate social network (Engineering, Marketing, Sales) — realistic,
+ neutral scenario
- id: DQ-03
- name: Appropriate scale
+ name: Appropriate Scale
score: 4
max: 4
passed: true
- comment: 50 nodes is appropriate, edge density is reasonable
+ comment: 50 nodes, 30% intra-community edge probability, 8 bridge edges; all
+ plausible
code_quality:
- score: 13
- max: 15
+ score: 10
+ max: 10
items:
- id: CQ-01
- name: KISS structure
- score: 4
- max: 4
+ name: KISS Structure
+ score: 3
+ max: 3
passed: true
- comment: Sequential structure, no functions/classes
+ comment: 'Linear: imports -> tokens -> data -> layout -> plot -> save; no
+ functions or classes'
- id: CQ-02
name: Reproducibility
- score: 3
- max: 3
+ score: 2
+ max: 2
passed: true
- comment: Uses np.random.seed(42)
+ comment: np.random.seed(42)
- id: CQ-03
- name: Library idioms
- score: 3
- max: 3
+ name: Clean Imports
+ score: 2
+ max: 2
passed: true
- comment: Proper pygal XY chart usage with Style
+ comment: sys, pathlib.Path, os, numpy, pygal, pygal.style.Style — all used
- id: CQ-04
- name: Clean imports
+ name: Code Elegance
score: 2
max: 2
passed: true
- comment: Only numpy, pygal, and Style imported
+ comment: Force-directed layout computation appropriate complexity; clean readable
+ code
- id: CQ-05
- name: Helpful comments
- score: 0
- max: 1
- passed: true
- comment: Comments present but minimal
- - id: CQ-06
- name: No deprecated API
+ name: Output & API
score: 1
max: 1
passed: true
- comment: Current pygal API used
- - id: CQ-07
- name: Output correct
- score: 0
- max: 1
+ comment: Saves plot-{THEME}.png, .svg, .html
+ library_mastery:
+ score: 7
+ max: 10
+ items:
+ - id: LM-01
+ name: Idiomatic Usage
+ score: 4
+ max: 5
+ passed: true
+ comment: Uses pygal.XY, Style object for full theming, per-point node dict
+ for radius overrides, correct stroke/dots_size API
+ - id: LM-02
+ name: Distinctive Features
+ score: 3
+ max: 5
passed: true
- comment: Saves plot.png but also saves plot.svg and plot.html (extra files)
- verdict: APPROVED
+ comment: Per-point SVG attribute overrides for degree-based radii; None-break
+ technique for edges; interactive HTML export
+ verdict: REJECTED
+impl_tags:
+ dependencies: []
+ techniques:
+ - html-export
+ patterns:
+ - data-generation
+ - iteration-over-groups
+ dataprep: []
+ styling: []