{"spec_id":"network-weighted","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nnetwork-weighted: Weighted Network Graph with Edge Thickness\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\nBRAND = \"#009E73\"\n\n# Data: Trade network between 15 countries (billions USD annual trade volume)\nnp.random.seed(42)\n\n# Define nodes (15 countries/regions)\nnode_labels = [\n    \"USA\",\n    \"China\",\n    \"Germany\",\n    \"Japan\",\n    \"UK\",\n    \"France\",\n    \"Canada\",\n    \"Mexico\",\n    \"Brazil\",\n    \"India\",\n    \"S. Korea\",\n    \"Australia\",\n    \"Singapore\",\n    \"Netherlands\",\n    \"Switzerland\",\n]\nn_nodes = len(node_labels)\n\n# Generate weighted edges (trade relationships)\nedges = [\n    # USA trade partners\n    (0, 1, 580),\n    (0, 2, 180),\n    (0, 3, 220),\n    (0, 4, 130),\n    (0, 6, 620),\n    (0, 7, 680),\n    (0, 10, 170),\n    # China trade partners\n    (1, 3, 340),\n    (1, 10, 290),\n    (1, 2, 220),\n    (1, 11, 180),\n    (1, 12, 120),\n    (1, 9, 110),\n    # European connections\n    (2, 4, 160),\n    (2, 5, 180),\n    (2, 13, 200),\n    (2, 14, 140),\n    (4, 5, 110),\n    (5, 13, 90),\n    # Asian connections\n    (3, 10, 85),\n    (9, 12, 55),\n    (12, 11, 70),\n    # Americas\n    (6, 7, 80),\n    (0, 8, 95),\n    (8, 9, 40),\n]\n\n# Use force-directed layout for node positions\npositions = np.random.rand(n_nodes, 2) * 10\n\nfor _ in range(100):\n    forces = np.zeros((n_nodes, 2))\n\n    # Repulsion between all nodes\n    for i in range(n_nodes):\n        for j in range(i + 1, n_nodes):\n            diff = positions[i] - positions[j]\n            dist = np.linalg.norm(diff) + 0.1\n            force = diff / (dist**2) * 2\n            forces[i] += force\n            forces[j] -= force\n\n    # Attraction along edges (weighted)\n    for src, tgt, weight in edges:\n        diff = positions[tgt] - positions[src]\n        dist = np.linalg.norm(diff) + 0.1\n        force = diff * 0.01 * (weight / 200)\n        forces[src] += force\n        forces[tgt] -= force\n\n    positions += forces * 0.1\n\n# Center and scale positions\npositions -= positions.mean(axis=0)\npositions /= positions.max() * 1.2\n\nnode_x = positions[:, 0]\nnode_y = positions[:, 1]\n\n# Calculate weighted degree for node sizing\nweighted_degree = np.zeros(n_nodes)\nfor src, tgt, weight in edges:\n    weighted_degree[src] += weight\n    weighted_degree[tgt] += weight\n\n# Normalize node sizes\nmin_size = 30\nmax_size = 80\nnode_sizes = min_size + (weighted_degree - weighted_degree.min()) / (\n    weighted_degree.max() - weighted_degree.min() + 0.1\n) * (max_size - min_size)\n\n# Prepare edge data\nedge_x0, edge_y0, edge_x1, edge_y1 = [], [], [], []\nedge_widths = []\n\n# Normalize edge weights to line widths\nmax_weight = max(e[2] for e in edges)\nmin_weight = min(e[2] for e in edges)\n\nfor src, tgt, weight in edges:\n    edge_x0.append(node_x[src])\n    edge_y0.append(node_y[src])\n    edge_x1.append(node_x[tgt])\n    edge_y1.append(node_y[tgt])\n\n    # Scale width: thinnest = 2, thickest = 20\n    normalized = (weight - min_weight) / (max_weight - min_weight + 0.1)\n    edge_widths.append(2 + normalized * 18)\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"network-weighted · bokeh · anyplot.ai\",\n    x_axis_label=\"\",\n    y_axis_label=\"\",\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Remove axes and grid\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\np.outline_line_color = None\n\n# Theme-adaptive styling\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.title.text_color = INK\np.title.text_font_size = \"28pt\"\np.title.align = \"center\"\n\n# Set range with padding\npadding = 0.15\np.x_range = Range1d(node_x.min() - padding, node_x.max() + padding)\np.y_range = Range1d(node_y.min() - padding, node_y.max() + padding)\n\n# Draw edges with weighted line widths\nfor i in range(len(edge_x0)):\n    normalized = (edge_widths[i] - 2) / 18\n    alpha = 0.3 + normalized * 0.5\n    p.segment(\n        x0=[edge_x0[i]],\n        y0=[edge_y0[i]],\n        x1=[edge_x1[i]],\n        y1=[edge_y1[i]],\n        line_width=edge_widths[i],\n        line_color=BRAND,\n        line_alpha=alpha,\n        line_cap=\"round\",\n    )\n\n# Create node source with weighted degree\nnode_source = ColumnDataSource(\n    data={\n        \"x\": node_x,\n        \"y\": node_y,\n        \"size\": node_sizes,\n        \"labels\": node_labels,\n        \"weighted_degree\": [f\"{int(wd):,}\" for wd in weighted_degree],\n    }\n)\n\n# Draw nodes\nnodes_renderer = p.scatter(\n    x=\"x\", y=\"y\", source=node_source, size=\"size\", fill_color=BRAND, line_color=INK_SOFT, line_width=3, fill_alpha=0.85\n)\n\n# Add hover tool for interactivity\nhover = HoverTool(\n    renderers=[nodes_renderer], tooltips=[(\"Country\", \"@labels\"), (\"Total Trade (B USD)\", \"@weighted_degree\")]\n)\np.add_tools(hover)\n\n# Add node labels\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"labels\",\n    source=node_source,\n    text_font_size=\"16pt\",\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\np.add_layout(labels)\n\n# Add legend annotation for edge thickness\nlegend_x = node_x.min() - padding + 0.03\nlegend_y = node_y.max() + padding - 0.02\n\n# Legend title\np.text(\n    x=[legend_x],\n    y=[legend_y],\n    text=[\"Trade Volume (B USD)\"],\n    text_font_size=\"22pt\",\n    text_font_style=\"bold\",\n    text_color=INK,\n)\n\n# Legend lines showing weight scale\nlegend_weights = [min_weight, (min_weight + max_weight) / 2, max_weight]\nlegend_labels = [f\"{int(w)} B\" for w in legend_weights]\nlegend_widths = [4, 14, 26]\n\nfor i, (lw, label) in enumerate(zip(legend_widths, legend_labels, strict=True)):\n    y_pos = legend_y - 0.055 - i * 0.05\n    normalized = (lw - 2) / 18\n    alpha = 0.3 + normalized * 0.5\n    p.segment(\n        x0=[legend_x],\n        y0=[y_pos],\n        x1=[legend_x + 0.12],\n        y1=[y_pos],\n        line_width=lw,\n        line_color=BRAND,\n        line_alpha=alpha,\n        line_cap=\"round\",\n    )\n    p.text(\n        x=[legend_x + 0.14], y=[y_pos], text=[label], text_font_size=\"18pt\", text_baseline=\"middle\", text_color=INK_SOFT\n    )\n\n# Save HTML (interactive artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome for PNG\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}