{"spec_id":"network-weighted","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nnetwork-weighted: Weighted Network Graph with Edge Thickness\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 95/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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\"\n\n# Okabe-Ito palette - first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: Trade network between 15 countries (billions USD)\nnp.random.seed(42)\n\nnodes = [\n    {\"id\": 0, \"name\": \"USA\", \"group\": \"Americas\"},\n    {\"id\": 1, \"name\": \"China\", \"group\": \"Asia\"},\n    {\"id\": 2, \"name\": \"Germany\", \"group\": \"Europe\"},\n    {\"id\": 3, \"name\": \"Japan\", \"group\": \"Asia\"},\n    {\"id\": 4, \"name\": \"UK\", \"group\": \"Europe\"},\n    {\"id\": 5, \"name\": \"France\", \"group\": \"Europe\"},\n    {\"id\": 6, \"name\": \"India\", \"group\": \"Asia\"},\n    {\"id\": 7, \"name\": \"Italy\", \"group\": \"Europe\"},\n    {\"id\": 8, \"name\": \"Brazil\", \"group\": \"Americas\"},\n    {\"id\": 9, \"name\": \"Canada\", \"group\": \"Americas\"},\n    {\"id\": 10, \"name\": \"S. Korea\", \"group\": \"Asia\"},\n    {\"id\": 11, \"name\": \"Australia\", \"group\": \"Oceania\"},\n    {\"id\": 12, \"name\": \"Mexico\", \"group\": \"Americas\"},\n    {\"id\": 13, \"name\": \"Spain\", \"group\": \"Europe\"},\n    {\"id\": 14, \"name\": \"Netherlands\", \"group\": \"Europe\"},\n]\n\n# Trade relationships with weights (billions USD)\nedges = [\n    (0, 1, 560),\n    (0, 2, 185),\n    (0, 3, 210),\n    (0, 4, 140),\n    (0, 9, 580),\n    (0, 12, 490),\n    (1, 3, 320),\n    (1, 10, 280),\n    (1, 2, 175),\n    (1, 11, 145),\n    (2, 5, 165),\n    (2, 7, 130),\n    (2, 14, 195),\n    (2, 4, 125),\n    (3, 10, 85),\n    (3, 11, 75),\n    (4, 5, 95),\n    (4, 14, 85),\n    (5, 7, 80),\n    (5, 13, 75),\n    (6, 0, 95),\n    (6, 1, 110),\n    (8, 0, 60),\n    (8, 1, 115),\n    (9, 4, 25),\n    (10, 0, 120),\n    (11, 1, 190),\n    (12, 1, 45),\n    (13, 5, 55),\n    (14, 4, 70),\n]\n\nn_nodes = len(nodes)\n\n# Force-directed layout calculation\npos = np.random.rand(n_nodes, 2) * 2 - 1\nk = 0.3\n\nfor iteration in range(200):\n    disp = np.zeros((n_nodes, 2))\n    # Repulsive forces between all node pairs\n    for i in range(n_nodes):\n        for j in range(i + 1, n_nodes):\n            delta = pos[i] - pos[j]\n            dist = max(np.linalg.norm(delta), 0.01)\n            force = k * k / dist\n            disp[i] += (delta / dist) * force\n            disp[j] -= (delta / dist) * force\n\n    # Attractive forces along edges\n    for src, tgt, _weight in edges:\n        delta = pos[src] - pos[tgt]\n        dist = max(np.linalg.norm(delta), 0.01)\n        force = dist * dist / k\n        disp[src] -= (delta / dist) * force * 0.5\n        disp[tgt] += (delta / dist) * force * 0.5\n\n    # Apply displacement with cooling\n    temp = 0.1 * (1 - iteration / 200)\n    for i in range(n_nodes):\n        disp_len = max(np.linalg.norm(disp[i]), 0.01)\n        pos[i] += (disp[i] / disp_len) * min(disp_len, temp)\n\n    pos = np.clip(pos, -1, 1)\n\n# Calculate weighted degree for node sizing\nweighted_degree = dict.fromkeys(range(n_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# Build node dataframe\nnode_df = pd.DataFrame(nodes)\nnode_df[\"x\"] = pos[:, 0]\nnode_df[\"y\"] = pos[:, 1]\nnode_df[\"weighted_degree\"] = [weighted_degree[i] for i in range(n_nodes)]\n\n# Get weight range for scaling edge thickness\nmin_weight = min(e[2] for e in edges)\nmax_weight = max(e[2] for e in edges)\n\n# Build edge dataframe\nedge_data = []\nfor src, tgt, weight in edges:\n    edge_data.append({\"x\": pos[src, 0], \"y\": pos[src, 1], \"x2\": pos[tgt, 0], \"y2\": pos[tgt, 1], \"weight\": weight})\nedge_df = pd.DataFrame(edge_data)\n\n# Create edge layer with varying thickness\nedge_chart = (\n    alt.Chart(edge_df)\n    .mark_rule(opacity=0.4)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[-1.2, 1.2])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[-1.2, 1.2])),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        strokeWidth=alt.StrokeWidth(\n            \"weight:Q\",\n            scale=alt.Scale(domain=[min_weight, max_weight], range=[2, 14]),\n            legend=alt.Legend(title=\"Trade (B USD)\", titleFontSize=18, labelFontSize=16, orient=\"right\", offset=10),\n        ),\n        color=alt.value(INK_SOFT),\n    )\n)\n\n# Create node layer with size by weighted degree\nnode_chart = (\n    alt.Chart(node_df)\n    .mark_circle(stroke=INK_SOFT, strokeWidth=2)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[-1.2, 1.2])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[-1.2, 1.2])),\n        size=alt.Size(\n            \"weighted_degree:Q\",\n            scale=alt.Scale(domain=[min_degree, max_degree], range=[600, 3000]),\n            legend=alt.Legend(title=\"Total Trade\", titleFontSize=18, labelFontSize=16, orient=\"right\", offset=10),\n        ),\n        color=alt.Color(\n            \"group:N\",\n            scale=alt.Scale(domain=[\"Americas\", \"Europe\", \"Asia\", \"Oceania\"], range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Region\", titleFontSize=18, labelFontSize=16, orient=\"right\", symbolSize=600, offset=10\n            ),\n        ),\n        tooltip=[\"name:N\", \"group:N\", \"weighted_degree:Q\"],\n    )\n)\n\n# Create label layer for node names with adjusted positioning\nlabel_chart = (\n    alt.Chart(node_df)\n    .mark_text(fontSize=16, fontWeight=\"bold\", dy=-26, align=\"center\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-1.2, 1.2])),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-1.2, 1.2])),\n        text=\"name:N\",\n        color=alt.value(INK),\n    )\n)\n\n# Combine all layers with interactivity\nchart = (\n    (edge_chart + node_chart + label_chart)\n    .interactive()\n    .properties(\n        width=1600,\n        height=900,\n        title=alt.Title(\n            \"network-weighted · altair · anyplot.ai\",\n            fontSize=28,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"International Trade Network: Edge thickness shows bilateral trade volume (billions USD)\",\n            subtitleFontSize=18,\n        ),\n        background=PAGE_BG,\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, padding=10)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}