{"spec_id":"network-transport-static","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nnetwork-transport-static: Static Transport Network Diagram\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme-adaptive 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\"\nRULE = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Okabe-Ito palette for route types\nIMPRINT = {\n    \"express\": \"#009E73\",  # Position 1 - brand green\n    \"regional\": \"#C475FD\",  # Position 2 - vermillion\n    \"local\": \"#4467A3\",  # Position 3 - blue\n}\n\nnp.random.seed(42)\n\n# Data: Regional rail network stations with coordinates\nstations = [\n    {\"id\": \"A\", \"label\": \"Central Station\", \"x\": 0.5, \"y\": 0.9},\n    {\"id\": \"B\", \"label\": \"North Park\", \"x\": 0.2, \"y\": 0.7},\n    {\"id\": \"C\", \"label\": \"East Harbor\", \"x\": 0.8, \"y\": 0.75},\n    {\"id\": \"D\", \"label\": \"West Gate\", \"x\": 0.1, \"y\": 0.5},\n    {\"id\": \"E\", \"label\": \"Downtown\", \"x\": 0.5, \"y\": 0.55},\n    {\"id\": \"F\", \"label\": \"Airport\", \"x\": 0.9, \"y\": 0.5},\n    {\"id\": \"G\", \"label\": \"University\", \"x\": 0.3, \"y\": 0.35},\n    {\"id\": \"H\", \"label\": \"Tech Campus\", \"x\": 0.7, \"y\": 0.35},\n    {\"id\": \"I\", \"label\": \"South Valley\", \"x\": 0.2, \"y\": 0.15},\n    {\"id\": \"J\", \"label\": \"Industrial Zone\", \"x\": 0.5, \"y\": 0.1},\n    {\"id\": \"K\", \"label\": \"Beach Resort\", \"x\": 0.85, \"y\": 0.15},\n]\n\n# Create station lookup\nstation_lookup = {s[\"id\"]: s for s in stations}\n\n# Routes: Train services with times (route_type for color coding)\nroutes = [\n    {\"source\": \"A\", \"target\": \"B\", \"route_id\": \"RE 10\", \"dep\": \"06:15\", \"arr\": \"06:32\", \"type\": \"regional\"},\n    {\"source\": \"A\", \"target\": \"C\", \"route_id\": \"RE 12\", \"dep\": \"06:20\", \"arr\": \"06:38\", \"type\": \"regional\"},\n    {\"source\": \"A\", \"target\": \"E\", \"route_id\": \"EX 01\", \"dep\": \"06:00\", \"arr\": \"06:12\", \"type\": \"express\"},\n    {\"source\": \"B\", \"target\": \"D\", \"route_id\": \"RE 10\", \"dep\": \"06:35\", \"arr\": \"06:52\", \"type\": \"regional\"},\n    {\"source\": \"B\", \"target\": \"E\", \"route_id\": \"LO 05\", \"dep\": \"07:00\", \"arr\": \"07:18\", \"type\": \"local\"},\n    {\"source\": \"C\", \"target\": \"F\", \"route_id\": \"EX 02\", \"dep\": \"06:42\", \"arr\": \"06:55\", \"type\": \"express\"},\n    {\"source\": \"C\", \"target\": \"E\", \"route_id\": \"RE 12\", \"dep\": \"06:45\", \"arr\": \"07:02\", \"type\": \"regional\"},\n    {\"source\": \"D\", \"target\": \"G\", \"route_id\": \"RE 10\", \"dep\": \"06:55\", \"arr\": \"07:12\", \"type\": \"regional\"},\n    {\"source\": \"E\", \"target\": \"G\", \"route_id\": \"LO 05\", \"dep\": \"07:22\", \"arr\": \"07:38\", \"type\": \"local\"},\n    {\"source\": \"E\", \"target\": \"H\", \"route_id\": \"RE 15\", \"dep\": \"07:10\", \"arr\": \"07:28\", \"type\": \"regional\"},\n    {\"source\": \"E\", \"target\": \"F\", \"route_id\": \"EX 01\", \"dep\": \"06:15\", \"arr\": \"06:35\", \"type\": \"express\"},\n    {\"source\": \"F\", \"target\": \"H\", \"route_id\": \"LO 08\", \"dep\": \"07:00\", \"arr\": \"07:20\", \"type\": \"local\"},\n    {\"source\": \"F\", \"target\": \"K\", \"route_id\": \"RE 18\", \"dep\": \"07:30\", \"arr\": \"07:55\", \"type\": \"regional\"},\n    {\"source\": \"G\", \"target\": \"I\", \"route_id\": \"RE 10\", \"dep\": \"07:15\", \"arr\": \"07:32\", \"type\": \"regional\"},\n    {\"source\": \"G\", \"target\": \"J\", \"route_id\": \"LO 05\", \"dep\": \"07:42\", \"arr\": \"08:00\", \"type\": \"local\"},\n    {\"source\": \"H\", \"target\": \"J\", \"route_id\": \"RE 15\", \"dep\": \"07:32\", \"arr\": \"07:48\", \"type\": \"regional\"},\n    {\"source\": \"H\", \"target\": \"K\", \"route_id\": \"LO 08\", \"dep\": \"07:25\", \"arr\": \"07:50\", \"type\": \"local\"},\n    {\"source\": \"I\", \"target\": \"J\", \"route_id\": \"LO 09\", \"dep\": \"07:40\", \"arr\": \"07:58\", \"type\": \"local\"},\n    {\"source\": \"J\", \"target\": \"K\", \"route_id\": \"RE 18\", \"dep\": \"08:05\", \"arr\": \"08:25\", \"type\": \"regional\"},\n]\n\n# Create figure\nfig = go.Figure()\n\n# Track edge counts between station pairs for offset calculation\nedge_counts = {}\n\n\ndef get_edge_offset(source, target, route_idx):\n    key = tuple(sorted([source, target]))\n    if key not in edge_counts:\n        edge_counts[key] = 0\n    offset_idx = edge_counts[key]\n    edge_counts[key] += 1\n    if offset_idx == 0:\n        return 0\n    sign = 1 if offset_idx % 2 == 1 else -1\n    magnitude = ((offset_idx + 1) // 2) * 0.04\n    return sign * magnitude\n\n\n# Track which route types we've added to legend\nlegend_items = set()\n\n# Draw edges (routes) with arrows\nfor i, route in enumerate(routes):\n    src = station_lookup[route[\"source\"]]\n    tgt = station_lookup[route[\"target\"]]\n\n    x0, y0 = src[\"x\"], src[\"y\"]\n    x1, y1 = tgt[\"x\"], tgt[\"y\"]\n\n    offset = get_edge_offset(route[\"source\"], route[\"target\"], i)\n\n    dx = x1 - x0\n    dy = y1 - y0\n    length = np.sqrt(dx**2 + dy**2)\n    px, py = -dy / length, dx / length\n\n    x0_off = x0 + px * offset\n    y0_off = y0 + py * offset\n    x1_off = x1 + px * offset\n    y1_off = y1 + py * offset\n\n    shrink = 0.035\n    x0_draw = x0_off + shrink * dx / length\n    y0_draw = y0_off + shrink * dy / length\n    x1_draw = x1_off - shrink * dx / length\n    y1_draw = y1_off - shrink * dy / length\n\n    color = IMPRINT[route[\"type\"]]\n    show_in_legend = route[\"type\"] not in legend_items\n    legend_items.add(route[\"type\"])\n\n    # Draw edge line\n    fig.add_trace(\n        go.Scatter(\n            x=[x0_draw, x1_draw],\n            y=[y0_draw, y1_draw],\n            mode=\"lines\",\n            line=dict(color=color, width=3),\n            hoverinfo=\"text\",\n            hovertext=f\"{route['route_id']}: {src['label']} → {tgt['label']}<br>{route['dep']} → {route['arr']}\",\n            showlegend=show_in_legend,\n            name=route[\"type\"].capitalize(),\n            legendgroup=route[\"type\"],\n        )\n    )\n\n    # Add arrow annotation\n    fig.add_annotation(\n        x=x1_draw,\n        y=y1_draw,\n        ax=x0_draw,\n        ay=y0_draw,\n        xref=\"x\",\n        yref=\"y\",\n        axref=\"x\",\n        ayref=\"y\",\n        showarrow=True,\n        arrowhead=2,\n        arrowsize=1.5,\n        arrowwidth=2.5,\n        arrowcolor=color,\n        standoff=0,\n    )\n\n    # Add route label at midpoint\n    mx = (x0_draw + x1_draw) / 2\n    my = (y0_draw + y1_draw) / 2\n\n    # Increase label offset in central area to reduce overlap\n    label_offset = 0.035\n    mx_label = mx + px * label_offset\n    my_label = my + py * label_offset\n\n    angle = np.degrees(np.arctan2(dy, dx))\n    if angle > 90:\n        angle -= 180\n    elif angle < -90:\n        angle += 180\n\n    fig.add_annotation(\n        x=mx_label,\n        y=my_label,\n        text=f\"<b>{route['route_id']}</b><br>{route['dep']}→{route['arr']}\",\n        showarrow=False,\n        font=dict(size=12, color=color),\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n        borderpad=2,\n        textangle=-angle,\n    )\n\n# Draw station nodes\nnode_x = [s[\"x\"] for s in stations]\nnode_y = [s[\"y\"] for s in stations]\nnode_labels = [s[\"label\"] for s in stations]\nnode_ids = [s[\"id\"] for s in stations]\n\nfig.add_trace(\n    go.Scatter(\n        x=node_x,\n        y=node_y,\n        mode=\"markers+text\",\n        marker=dict(size=40, color=ELEVATED_BG, line=dict(color=INK, width=3)),\n        text=node_ids,\n        textfont=dict(size=16, color=INK, family=\"Arial Black\"),\n        textposition=\"middle center\",\n        hoverinfo=\"text\",\n        hovertext=[f\"<b>{nid}</b>: {label}\" for nid, label in zip(node_ids, node_labels)],\n        showlegend=False,\n    )\n)\n\n# Add station name labels below nodes\nfor station in stations:\n    fig.add_annotation(\n        x=station[\"x\"],\n        y=station[\"y\"] - 0.055,\n        text=f\"<b>{station['label']}</b>\",\n        showarrow=False,\n        font=dict(size=14, color=INK),\n        bgcolor=ELEVATED_BG,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n        borderpad=3,\n    )\n\n\n# Update layout\nfig.update_layout(\n    title=dict(\n        text=\"network-transport-static · python · plotly · anyplot.ai\",\n        font=dict(size=28, color=INK),\n        x=0.5,\n        xanchor=\"center\",\n    ),\n    xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, range=[-0.05, 1.05]),\n    yaxis=dict(\n        showgrid=False, zeroline=False, showticklabels=False, range=[-0.02, 1.02], scaleanchor=\"x\", scaleratio=1\n    ),\n    plot_bgcolor=PAGE_BG,\n    paper_bgcolor=PAGE_BG,\n    font=dict(color=INK),\n    margin=dict(l=40, r=40, t=80, b=40),\n    showlegend=True,\n    legend=dict(x=0.02, y=0.98, bgcolor=\"rgba(0,0,0,0)\", borderwidth=0),\n)\n\n# Save outputs\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}