{"spec_id":"network-weighted","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nnetwork-weighted: Weighted Network Graph with Edge Thickness\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport re\nimport sys\nfrom pathlib import Path\n\n\n# Work around filename/module name conflict\nscript_dir = Path(__file__).parent\nwhile str(script_dir) in sys.path:\n    sys.path.remove(str(script_dir))\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data: Trade network between countries (billions USD)\nnp.random.seed(42)\nnodes = {\n    \"USA\": {\"group\": 0},\n    \"CAN\": {\"group\": 0},\n    \"MEX\": {\"group\": 0},\n    \"BRA\": {\"group\": 0},\n    \"DEU\": {\"group\": 1},\n    \"FRA\": {\"group\": 1},\n    \"GBR\": {\"group\": 1},\n    \"ITA\": {\"group\": 1},\n    \"CHN\": {\"group\": 2},\n    \"JPN\": {\"group\": 2},\n    \"KOR\": {\"group\": 2},\n    \"IND\": {\"group\": 2},\n    \"AUS\": {\"group\": 3},\n}\n\n# Define edges with trade volume weights (billions USD)\nedges = [\n    (\"USA\", \"CAN\", 650),\n    (\"USA\", \"MEX\", 580),\n    (\"USA\", \"CHN\", 520),\n    (\"USA\", \"JPN\", 180),\n    (\"USA\", \"DEU\", 200),\n    (\"USA\", \"GBR\", 130),\n    (\"USA\", \"KOR\", 140),\n    (\"USA\", \"BRA\", 80),\n    (\"CAN\", \"CHN\", 75),\n    (\"CAN\", \"MEX\", 40),\n    (\"MEX\", \"CHN\", 90),\n    (\"DEU\", \"FRA\", 170),\n    (\"DEU\", \"GBR\", 120),\n    (\"DEU\", \"ITA\", 130),\n    (\"DEU\", \"CHN\", 200),\n    (\"FRA\", \"GBR\", 90),\n    (\"FRA\", \"ITA\", 80),\n    (\"FRA\", \"CHN\", 65),\n    (\"GBR\", \"CHN\", 95),\n    (\"CHN\", \"JPN\", 280),\n    (\"CHN\", \"KOR\", 250),\n    (\"CHN\", \"AUS\", 180),\n    (\"CHN\", \"IND\", 100),\n    (\"JPN\", \"KOR\", 70),\n    (\"JPN\", \"AUS\", 55),\n    (\"IND\", \"AUS\", 30),\n    (\"BRA\", \"DEU\", 20),\n]\n\n# Force-directed layout computation\nnode_list = list(nodes.keys())\nn = len(node_list)\nnode_idx = {name: i for i, name in enumerate(node_list)}\n\n# Initialize positions randomly\npos = np.random.rand(n, 2) * 2 - 1\n\nk = 1.5 / np.sqrt(n)  # Optimal distance\nt = 0.5  # Temperature (step size)\n\nfor _ in range(300):\n    disp = np.zeros((n, 2))\n\n    # Repulsive forces between all pairs\n    for i in range(n):\n        for j in range(i + 1, n):\n            delta = pos[i] - pos[j]\n            dist = max(np.linalg.norm(delta), 0.01)\n            force = k * k / dist * 1.5\n            direction = delta / dist\n            disp[i] += direction * force\n            disp[j] -= direction * force\n\n    # Attractive forces along edges (weighted)\n    for src, tgt, weight in edges:\n        i, j = node_idx[src], node_idx[tgt]\n        delta = pos[i] - pos[j]\n        dist = max(np.linalg.norm(delta), 0.01)\n        force = dist * dist / k * (0.8 + weight / 400)\n        direction = delta / dist\n        disp[i] -= direction * force\n        disp[j] += direction * force\n\n    # Apply displacement with temperature limiting\n    for i in range(n):\n        disp_norm = max(np.linalg.norm(disp[i]), 0.01)\n        pos[i] += disp[i] / disp_norm * min(disp_norm, t)\n\n    t *= 0.97\n\n# Normalize positions to [2, 10] for pygal\npos_min = pos.min(axis=0)\npos_max = pos.max(axis=0)\npos = (pos - pos_min) / (pos_max - pos_min + 1e-6) * 8 + 2\npositions = {name: pos[node_idx[name]] for name in node_list}\n\n# Compute weighted degree for node sizing\nweighted_degree = dict.fromkeys(nodes, 0)\nfor src, tgt, weight in edges:\n    weighted_degree[src] += weight\n    weighted_degree[tgt] += weight\n\nmax_degree = max(weighted_degree.values())\nmin_degree = min(weighted_degree.values())\n\n# Bin edges by weight for visual thickness representation\nedge_weights = [w for _, _, w in edges]\nmin_weight = min(edge_weights)\nmax_weight = max(edge_weights)\nweight_range = max_weight - min_weight\n\n# Create 4 weight bins for edge thickness visualization\nedge_bins = {\"low\": [], \"medium\": [], \"high\": [], \"very_high\": []}\n\nfor src, tgt, weight in edges:\n    norm_weight = (weight - min_weight) / weight_range if weight_range > 0 else 0.5\n    if norm_weight < 0.25:\n        edge_bins[\"low\"].append((src, tgt, weight))\n    elif norm_weight < 0.5:\n        edge_bins[\"medium\"].append((src, tgt, weight))\n    elif norm_weight < 0.75:\n        edge_bins[\"high\"].append((src, tgt, weight))\n    else:\n        edge_bins[\"very_high\"].append((src, tgt, weight))\n\n# Edge thickness and color mapping based on weight\nedge_styles = {\n    \"low\": {\"stroke\": INK_MUTED, \"stroke_width\": 3},\n    \"medium\": {\"stroke\": INK_SOFT, \"stroke_width\": 10},\n    \"high\": {\"stroke\": INK, \"stroke_width\": 18},\n    \"very_high\": {\"stroke\": IMPRINT[0], \"stroke_width\": 28},\n}\n\n# Custom style for pygal chart\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=2,\n    opacity=0.95,\n)\n\n# Create pygal XY chart for nodes\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"network-weighted · pygal · anyplot.ai\",\n    show_legend=True,\n    show_x_guides=False,\n    show_y_guides=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    stroke=False,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    range=(0, 12),\n    xrange=(0, 12),\n    print_labels=True,\n    print_values=False,\n    margin_bottom=250,\n    margin_top=150,\n    margin_left=150,\n    margin_right=150,\n)\n\n# Group nodes by region and add to chart with varying dot sizes\nregions = [[], [], [], []]\nregion_names = [\"Americas\", \"Europe\", \"Asia\", \"Oceania\"]\nfor name, data in nodes.items():\n    regions[data[\"group\"]].append(name)\n\nfor group_idx, region_nodes in enumerate(regions):\n    node_points = []\n    for name in region_nodes:\n        x, y = positions[name]\n        degree_norm = (\n            (weighted_degree[name] - min_degree) / (max_degree - min_degree) if max_degree > min_degree else 0.5\n        )\n        dot_size = 30 + degree_norm * 50\n        node_points.append({\"value\": (x, y), \"label\": name, \"node\": {\"r\": dot_size}})\n    chart.add(region_names[group_idx], node_points, dots_size=50)\n\n# Render chart to get SVG string\nsvg_content = chart.render().decode(\"utf-8\")\n\n# Post-process SVG to add edges with varying thickness before nodes\nseries_match = re.search(r\"(<g class=\\\"series)\", svg_content)\nif series_match:\n    insert_pos = series_match.start()\nelse:\n    insert_pos = svg_content.rfind(\"</svg>\")\n\n# Calculate SVG coordinate transformation\nsvg_margin = {\"top\": 150, \"right\": 150, \"bottom\": 250, \"left\": 150}\nsvg_width = 4800\nsvg_height = 2700\nplot_width = svg_width - svg_margin[\"left\"] - svg_margin[\"right\"]\nplot_height = svg_height - svg_margin[\"top\"] - svg_margin[\"bottom\"]\n\n# Build edge SVG elements\nedge_svg_parts = ['<g class=\"edges\">']\n\nfor weight_cat in [\"low\", \"medium\", \"high\", \"very_high\"]:\n    style = edge_styles[weight_cat]\n    for src, tgt, _weight in edge_bins[weight_cat]:\n        x1_data, y1_data = positions[src]\n        x2_data, y2_data = positions[tgt]\n\n        x1 = svg_margin[\"left\"] + (x1_data / 12) * plot_width\n        y1 = svg_margin[\"top\"] + (1 - y1_data / 12) * plot_height\n        x2 = svg_margin[\"left\"] + (x2_data / 12) * plot_width\n        y2 = svg_margin[\"top\"] + (1 - y2_data / 12) * plot_height\n\n        edge_svg_parts.append(\n            f'<line x1=\"{x1:.1f}\" y1=\"{y1:.1f}\" x2=\"{x2:.1f}\" y2=\"{y2:.1f}\" '\n            f'stroke=\"{style[\"stroke\"]}\" stroke-width=\"{style[\"stroke_width\"]}\" '\n            f'stroke-linecap=\"round\" opacity=\"0.7\"/>'\n        )\n\nedge_svg_parts.append(\"</g>\")\nedge_svg = \"\\n\".join(edge_svg_parts)\n\n# Insert edges into SVG\nsvg_content = svg_content[:insert_pos] + edge_svg + \"\\n\" + svg_content[insert_pos:]\n\n# Add node labels (country codes) on top of nodes\nlabel_svg_parts = ['<g class=\"node-labels\">']\nfor name in nodes.keys():\n    x_data, y_data = positions[name]\n    x = svg_margin[\"left\"] + (x_data / 12) * plot_width\n    y = svg_margin[\"top\"] + (1 - y_data / 12) * plot_height\n\n    # White stroke for contrast\n    label_svg_parts.append(\n        f'<text x=\"{x:.1f}\" y=\"{y + 18:.1f}\" text-anchor=\"middle\" '\n        f'font-family=\"system-ui, sans-serif\" font-size=\"50\" font-weight=\"bold\" '\n        f'fill=\"{PAGE_BG}\" stroke=\"{PAGE_BG}\" stroke-width=\"10\">{name}</text>'\n    )\n    # Main text in brand color\n    label_svg_parts.append(\n        f'<text x=\"{x:.1f}\" y=\"{y + 18:.1f}\" text-anchor=\"middle\" '\n        f'font-family=\"system-ui, sans-serif\" font-size=\"50\" font-weight=\"bold\" '\n        f'fill=\"{IMPRINT[0]}\">{name}</text>'\n    )\n\nlabel_svg_parts.append(\"</g>\")\nlabel_svg = \"\\n\".join(label_svg_parts)\n\n# Insert labels before closing </svg>\nsvg_content = svg_content.replace(\"</svg>\", label_svg + \"\\n</svg>\")\n\n# Add edge weight legend\nlegend_y = 2700 - 120\nlegend_x_start = 200\nlegend_items = [\n    (\"$20–178B\", edge_styles[\"low\"]),\n    (\"$178–335B\", edge_styles[\"medium\"]),\n    (\"$335–493B\", edge_styles[\"high\"]),\n    (\"$493–650B\", edge_styles[\"very_high\"]),\n]\n\nedge_legend_parts = ['<g class=\"edge-legend\">']\nedge_legend_parts.append(\n    f'<text x=\"{legend_x_start}\" y=\"{legend_y + 14}\" '\n    f'font-family=\"system-ui, sans-serif\" font-size=\"38\" font-weight=\"bold\" '\n    f'fill=\"{INK}\">Edge Thickness Scale:</text>'\n)\nlegend_x = legend_x_start + 420\nfor label, style in legend_items:\n    edge_legend_parts.append(\n        f'<line x1=\"{legend_x}\" y1=\"{legend_y}\" x2=\"{legend_x + 70}\" y2=\"{legend_y}\" '\n        f'stroke=\"{style[\"stroke\"]}\" stroke-width=\"{style[\"stroke_width\"]}\" '\n        f'stroke-linecap=\"round\" opacity=\"0.8\"/>'\n    )\n    edge_legend_parts.append(\n        f'<text x=\"{legend_x + 90}\" y=\"{legend_y + 14}\" '\n        f'font-family=\"system-ui, sans-serif\" font-size=\"34\" fill=\"{INK_SOFT}\">{label}</text>'\n    )\n    legend_x += 420\n\nedge_legend_parts.append(\"</g>\")\nedge_legend_svg = \"\\n\".join(edge_legend_parts)\n\n# Insert edge legend before closing </svg>\nsvg_content = svg_content.replace(\"</svg>\", edge_legend_svg + \"\\n</svg>\")\n\n# Convert modified SVG to PNG using cairosvg\ncairosvg.svg2png(bytestring=svg_content.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\n# Save interactive HTML version\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(\n        f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>network-weighted · pygal · anyplot.ai</title>\n    <style>\n        body {{\n            margin: 0;\n            padding: 20px;\n            background-color: {PAGE_BG};\n            font-family: system-ui, sans-serif;\n        }}\n        .container {{\n            max-width: 4800px;\n            margin: 0 auto;\n        }}\n        h1 {{\n            color: {INK};\n            text-align: center;\n            margin-bottom: 30px;\n        }}\n        .chart-container {{\n            display: flex;\n            justify-content: center;\n        }}\n        img {{\n            max-width: 100%;\n            height: auto;\n            border-radius: 8px;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n        }}\n        .info {{\n            color: {INK_SOFT};\n            text-align: center;\n            margin-top: 20px;\n            font-size: 16px;\n        }}\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <h1>network-weighted · pygal · anyplot.ai</h1>\n        <div class=\"chart-container\">\n            <img src=\"plot-{THEME}.png\" alt=\"Network graph with weighted edges\">\n        </div>\n        <div class=\"info\">\n            <p>Trade network visualization showing relationships between countries.</p>\n            <p>Edge thickness represents bilateral trade volume (billions USD).</p>\n            <p>Node size indicates total weighted degree (sum of connected edge weights).</p>\n        </div>\n    </div>\n</body>\n</html>\"\"\"\n    )\n"}