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 - - - -
- - Force-directed network graph not supported - -
- -""" - ) +# 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: []