{"spec_id":"network-transport-static","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nnetwork-transport-static: Static Transport Network Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\nimport sys\n\n\n# Handle naming conflict: remove script dir from path before importing\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != script_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\nnp.random.seed(42)\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\"\n\n# Okabe-Ito palette (first series always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Station data - Regional rail network\nstations = [\n    {\"id\": \"A\", \"label\": \"Central Station\", \"x\": 400, \"y\": 450},\n    {\"id\": \"B\", \"label\": \"North Terminal\", \"x\": 400, \"y\": 100},\n    {\"id\": \"C\", \"label\": \"East Junction\", \"x\": 700, \"y\": 300},\n    {\"id\": \"D\", \"label\": \"West Gate\", \"x\": 100, \"y\": 300},\n    {\"id\": \"E\", \"label\": \"South Park\", \"x\": 400, \"y\": 750},\n    {\"id\": \"F\", \"label\": \"Airport\", \"x\": 750, \"y\": 100},\n    {\"id\": \"G\", \"label\": \"University\", \"x\": 700, \"y\": 600},\n    {\"id\": \"H\", \"label\": \"Harbor\", \"x\": 100, \"y\": 600},\n    {\"id\": \"I\", \"label\": \"Tech Campus\", \"x\": 550, \"y\": 200},\n    {\"id\": \"J\", \"label\": \"Old Town\", \"x\": 250, \"y\": 200},\n    {\"id\": \"K\", \"label\": \"Business District\", \"x\": 550, \"y\": 550},\n    {\"id\": \"L\", \"label\": \"Riverside\", \"x\": 250, \"y\": 550},\n]\n\nstations_df = pd.DataFrame(stations)\nstation_lookup = {s[\"id\"]: s for s in stations}\n\n# Route data - Train services with times\nroutes = [\n    # Express lines (longer routes)\n    {\"source\": \"A\", \"target\": \"B\", \"route\": \"RE1\", \"dep\": \"06:00\", \"arr\": \"06:25\", \"type\": \"Express\"},\n    {\"source\": \"B\", \"target\": \"A\", \"route\": \"RE1\", \"dep\": \"06:35\", \"arr\": \"07:00\", \"type\": \"Express\"},\n    {\"source\": \"A\", \"target\": \"F\", \"route\": \"RE2\", \"dep\": \"07:00\", \"arr\": \"07:45\", \"type\": \"Express\"},\n    {\"source\": \"F\", \"target\": \"A\", \"route\": \"RE2\", \"dep\": \"08:00\", \"arr\": \"08:45\", \"type\": \"Express\"},\n    {\"source\": \"A\", \"target\": \"E\", \"route\": \"RE3\", \"dep\": \"06:15\", \"arr\": \"06:40\", \"type\": \"Express\"},\n    {\"source\": \"E\", \"target\": \"A\", \"route\": \"RE3\", \"dep\": \"07:00\", \"arr\": \"07:25\", \"type\": \"Express\"},\n    # Regional lines\n    {\"source\": \"B\", \"target\": \"F\", \"route\": \"RB1\", \"dep\": \"07:15\", \"arr\": \"07:35\", \"type\": \"Regional\"},\n    {\"source\": \"F\", \"target\": \"B\", \"route\": \"RB1\", \"dep\": \"08:00\", \"arr\": \"08:20\", \"type\": \"Regional\"},\n    {\"source\": \"B\", \"target\": \"I\", \"route\": \"RB2\", \"dep\": \"06:30\", \"arr\": \"06:45\", \"type\": \"Regional\"},\n    {\"source\": \"I\", \"target\": \"C\", \"route\": \"RB2\", \"dep\": \"06:50\", \"arr\": \"07:05\", \"type\": \"Regional\"},\n    {\"source\": \"B\", \"target\": \"J\", \"route\": \"RB3\", \"dep\": \"07:00\", \"arr\": \"07:15\", \"type\": \"Regional\"},\n    {\"source\": \"J\", \"target\": \"D\", \"route\": \"RB3\", \"dep\": \"07:20\", \"arr\": \"07:35\", \"type\": \"Regional\"},\n    # Local services\n    {\"source\": \"A\", \"target\": \"K\", \"route\": \"S1\", \"dep\": \"06:10\", \"arr\": \"06:22\", \"type\": \"Local\"},\n    {\"source\": \"K\", \"target\": \"G\", \"route\": \"S1\", \"dep\": \"06:25\", \"arr\": \"06:40\", \"type\": \"Local\"},\n    {\"source\": \"G\", \"target\": \"C\", \"route\": \"S1\", \"dep\": \"06:45\", \"arr\": \"07:00\", \"type\": \"Local\"},\n    {\"source\": \"A\", \"target\": \"L\", \"route\": \"S2\", \"dep\": \"06:20\", \"arr\": \"06:32\", \"type\": \"Local\"},\n    {\"source\": \"L\", \"target\": \"H\", \"route\": \"S2\", \"dep\": \"06:35\", \"arr\": \"06:50\", \"type\": \"Local\"},\n    {\"source\": \"H\", \"target\": \"D\", \"route\": \"S2\", \"dep\": \"06:55\", \"arr\": \"07:10\", \"type\": \"Local\"},\n    {\"source\": \"D\", \"target\": \"A\", \"route\": \"S3\", \"dep\": \"07:30\", \"arr\": \"07:50\", \"type\": \"Local\"},\n    {\"source\": \"C\", \"target\": \"A\", \"route\": \"S4\", \"dep\": \"07:15\", \"arr\": \"07:35\", \"type\": \"Local\"},\n    {\"source\": \"E\", \"target\": \"K\", \"route\": \"S5\", \"dep\": \"08:00\", \"arr\": \"08:15\", \"type\": \"Local\"},\n    {\"source\": \"E\", \"target\": \"L\", \"route\": \"S6\", \"dep\": \"08:10\", \"arr\": \"08:25\", \"type\": \"Local\"},\n    {\"source\": \"K\", \"target\": \"A\", \"route\": \"S1\", \"dep\": \"07:30\", \"arr\": \"07:42\", \"type\": \"Local\"},\n    {\"source\": \"L\", \"target\": \"A\", \"route\": \"S2\", \"dep\": \"07:40\", \"arr\": \"07:52\", \"type\": \"Local\"},\n]\n\n# Build edge dataframe with coordinates\nedges_data = []\nfor i, r in enumerate(routes):\n    src = station_lookup[r[\"source\"]]\n    tgt = station_lookup[r[\"target\"]]\n\n    # Calculate offset for parallel routes (same source-target pair)\n    pair_key = tuple(sorted([r[\"source\"], r[\"target\"]]))\n    same_pair = [j for j, route in enumerate(routes) if tuple(sorted([route[\"source\"], route[\"target\"]])) == pair_key]\n    pair_index = same_pair.index(i)\n    offset = (pair_index - len(same_pair) / 2 + 0.5) * 25\n\n    # Calculate perpendicular offset\n    dx = tgt[\"x\"] - src[\"x\"]\n    dy = tgt[\"y\"] - src[\"y\"]\n    length = np.sqrt(dx**2 + dy**2) + 0.001\n    perp_x = -dy / length * offset\n    perp_y = dx / length * offset\n\n    # Shorten edges to not overlap with nodes\n    shrink = 45 / length\n    x1 = src[\"x\"] + dx * shrink + perp_x\n    y1 = src[\"y\"] + dy * shrink + perp_y\n    x2 = tgt[\"x\"] - dx * shrink + perp_x\n    y2 = tgt[\"y\"] - dy * shrink + perp_y\n\n    # Arrow position (near target)\n    arrow_pos = 0.75\n    ax = x1 + (x2 - x1) * arrow_pos\n    ay = y1 + (y2 - y1) * arrow_pos\n\n    # Label position (middle of edge)\n    lx = (x1 + x2) / 2\n    ly = (y1 + y2) / 2\n\n    edges_data.append(\n        {\n            \"x1\": x1,\n            \"y1\": y1,\n            \"x2\": x2,\n            \"y2\": y2,\n            \"ax\": ax,\n            \"ay\": ay,\n            \"lx\": lx,\n            \"ly\": ly,\n            \"route\": r[\"route\"],\n            \"dep\": r[\"dep\"],\n            \"arr\": r[\"arr\"],\n            \"type\": r[\"type\"],\n            \"label\": f\"{r['route']} | {r['dep']}→{r['arr']}\",\n            \"angle\": np.degrees(np.arctan2(y2 - y1, x2 - x1)),\n        }\n    )\n\nedges_df = pd.DataFrame(edges_data)\n\n# Node fill color (adapt to theme for better contrast)\nnode_fill = \"#FFFDF6\" if THEME == \"light\" else \"#3A3A35\"\n\n# Define scales with reversed Y axis (so North is at top, South at bottom)\nx_scale = alt.Scale(domain=[0, 850])\ny_scale = alt.Scale(domain=[0, 850], reverse=True)\n\n# Station nodes\nnodes = (\n    alt.Chart(stations_df)\n    .mark_circle(size=2000, stroke=INK_SOFT, strokeWidth=2)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), color=alt.value(node_fill))\n)\n\n# Station labels\nnode_labels = (\n    alt.Chart(stations_df)\n    .mark_text(fontSize=15, fontWeight=\"bold\", dy=-38)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), text=\"label:N\", color=alt.value(INK))\n)\n\n# Station ID in center\nnode_ids = (\n    alt.Chart(stations_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\")\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), text=\"id:N\", color=alt.value(IMPRINT[0]))\n)\n\n# Route edges (lines)\nedges = (\n    alt.Chart(edges_df)\n    .mark_rule(strokeWidth=3, opacity=0.8)\n    .encode(\n        x=alt.X(\"x1:Q\", scale=x_scale),\n        y=alt.Y(\"y1:Q\", scale=y_scale),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=alt.Color(\n            \"type:N\",\n            scale=alt.Scale(domain=[\"Express\", \"Regional\", \"Local\"], range=IMPRINT),\n            legend=alt.Legend(title=\"Route Type\", titleFontSize=16, labelFontSize=14, orient=\"right\"),\n        ),\n        tooltip=[\"route:N\", \"dep:N\", \"arr:N\", \"type:N\"],\n    )\n)\n\n# Arrow heads (triangles pointing in direction of travel)\nedges_df[\"angle_adjusted\"] = -edges_df[\"angle\"]\n\narrows = (\n    alt.Chart(edges_df)\n    .mark_point(shape=\"triangle\", size=220, filled=True, opacity=0.9)\n    .encode(\n        x=alt.X(\"ax:Q\", scale=x_scale),\n        y=alt.Y(\"ay:Q\", scale=y_scale),\n        angle=alt.Angle(\"angle_adjusted:Q\"),\n        color=alt.Color(\n            \"type:N\", scale=alt.Scale(domain=[\"Express\", \"Regional\", \"Local\"], range=IMPRINT), legend=None\n        ),\n    )\n)\n\n# Route labels (only show for a subset to avoid clutter)\nlabel_indices = [0, 2, 6, 12, 15, 18]\nlabels_df = edges_df.iloc[label_indices].copy()\n\nroute_labels = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=12, fontWeight=\"normal\", align=\"center\", baseline=\"middle\", dy=-14)\n    .encode(x=alt.X(\"lx:Q\", scale=x_scale), y=alt.Y(\"ly:Q\", scale=y_scale), text=\"label:N\", color=alt.value(INK_SOFT))\n)\n\n# Combine all layers\nchart = (\n    alt.layer(edges, arrows, nodes, node_labels, node_ids, route_labels)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"network-transport-static · python · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(grid=False, labels=False, ticks=False, domain=False, title=None)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save as PNG and HTML\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}