{"spec_id":"network-transport-static","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nnetwork-transport-static: Static Transport Network Diagram\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    arrow,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\n\n\nLetsPlot.setup_html()\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 for route types\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: Regional rail network with stations and routes\nnp.random.seed(42)\n\n# Station data with x, y coordinates (positioned like a simplified rail map)\nstations = [\n    {\"id\": \"A\", \"label\": \"Central\", \"x\": 0.5, \"y\": 0.5},\n    {\"id\": \"B\", \"label\": \"North\", \"x\": 0.5, \"y\": 0.88},\n    {\"id\": \"C\", \"label\": \"East\", \"x\": 0.82, \"y\": 0.5},\n    {\"id\": \"D\", \"label\": \"South\", \"x\": 0.5, \"y\": 0.12},\n    {\"id\": \"E\", \"label\": \"West\", \"x\": 0.18, \"y\": 0.5},\n    {\"id\": \"F\", \"label\": \"Airport\", \"x\": 0.82, \"y\": 0.82},\n    {\"id\": \"G\", \"label\": \"University\", \"x\": 0.18, \"y\": 0.82},\n    {\"id\": \"H\", \"label\": \"Harbor\", \"x\": 0.82, \"y\": 0.18},\n    {\"id\": \"I\", \"label\": \"Tech Park\", \"x\": 0.18, \"y\": 0.18},\n    {\"id\": \"J\", \"label\": \"Stadium\", \"x\": 0.66, \"y\": 0.32},\n]\n\n# Route data: train connections with times\n# Includes multiple routes between same stations to demonstrate curved edges\nroutes = [\n    # Express routes (RE) - Central hub connections\n    {\"source\": \"A\", \"target\": \"B\", \"route_id\": \"RE1\", \"depart\": \"06:15\", \"arrive\": \"06:35\", \"type\": \"Express\"},\n    {\"source\": \"A\", \"target\": \"C\", \"route_id\": \"RE2\", \"depart\": \"06:30\", \"arrive\": \"06:55\", \"type\": \"Express\"},\n    {\"source\": \"A\", \"target\": \"D\", \"route_id\": \"RE3\", \"depart\": \"07:00\", \"arrive\": \"07:25\", \"type\": \"Express\"},\n    {\"source\": \"A\", \"target\": \"E\", \"route_id\": \"RE4\", \"depart\": \"07:15\", \"arrive\": \"07:40\", \"type\": \"Express\"},\n    # Regional routes (RB) - Connecting outer stations\n    {\"source\": \"B\", \"target\": \"F\", \"route_id\": \"RB1\", \"depart\": \"07:00\", \"arrive\": \"07:20\", \"type\": \"Regional\"},\n    {\"source\": \"B\", \"target\": \"G\", \"route_id\": \"RB2\", \"depart\": \"07:30\", \"arrive\": \"07:55\", \"type\": \"Regional\"},\n    {\"source\": \"C\", \"target\": \"F\", \"route_id\": \"RB3\", \"depart\": \"08:00\", \"arrive\": \"08:25\", \"type\": \"Regional\"},\n    {\"source\": \"C\", \"target\": \"H\", \"route_id\": \"RB4\", \"depart\": \"08:15\", \"arrive\": \"08:40\", \"type\": \"Regional\"},\n    {\"source\": \"D\", \"target\": \"H\", \"route_id\": \"RB5\", \"depart\": \"08:30\", \"arrive\": \"08:55\", \"type\": \"Regional\"},\n    {\"source\": \"D\", \"target\": \"I\", \"route_id\": \"RB6\", \"depart\": \"09:00\", \"arrive\": \"09:30\", \"type\": \"Regional\"},\n    {\"source\": \"E\", \"target\": \"G\", \"route_id\": \"RB7\", \"depart\": \"09:15\", \"arrive\": \"09:40\", \"type\": \"Regional\"},\n    {\"source\": \"E\", \"target\": \"I\", \"route_id\": \"RB8\", \"depart\": \"09:30\", \"arrive\": \"09:55\", \"type\": \"Regional\"},\n    # Local routes (S) - Short connections, including multiple routes to same destination\n    {\"source\": \"C\", \"target\": \"J\", \"route_id\": \"S1\", \"depart\": \"10:00\", \"arrive\": \"10:12\", \"type\": \"Local\"},\n    {\"source\": \"J\", \"target\": \"H\", \"route_id\": \"S2\", \"depart\": \"10:15\", \"arrive\": \"10:30\", \"type\": \"Local\"},\n    {\"source\": \"A\", \"target\": \"J\", \"route_id\": \"S3\", \"depart\": \"10:30\", \"arrive\": \"10:50\", \"type\": \"Local\"},\n    # Second Express route A→C to demonstrate offset edges\n    {\"source\": \"A\", \"target\": \"C\", \"route_id\": \"RE5\", \"depart\": \"12:30\", \"arrive\": \"12:55\", \"type\": \"Express\"},\n]\n\n# Create DataFrames\nstations_df = pd.DataFrame(stations)\n\n# Track route counts between station pairs for offset calculation\nroute_counts = {}\nfor r in routes:\n    key = (r[\"source\"], r[\"target\"])\n    route_counts[key] = route_counts.get(key, 0) + 1\n\nroute_index = {}\n\n# Build edge DataFrame with source/target coordinates and curve offsets\nstation_coords = {s[\"id\"]: (s[\"x\"], s[\"y\"]) for s in stations}\nedges_data = []\nfor r in routes:\n    src_x, src_y = station_coords[r[\"source\"]]\n    tgt_x, tgt_y = station_coords[r[\"target\"]]\n\n    # Track index for this station pair\n    key = (r[\"source\"], r[\"target\"])\n    idx = route_index.get(key, 0)\n    route_index[key] = idx + 1\n    total = route_counts[key]\n\n    # Shorten edges slightly so arrows don't overlap nodes\n    dx, dy = tgt_x - src_x, tgt_y - src_y\n    length = np.sqrt(dx**2 + dy**2)\n    offset = 0.045 / length if length > 0 else 0\n\n    # Calculate perpendicular offset for curved/offset edges\n    perp_x = -dy / length if length > 0 else 0\n    perp_y = dx / length if length > 0 else 0\n\n    # Apply perpendicular offset for multiple routes between same stations (increased for visibility)\n    if total > 1:\n        curve_offset = 0.05 * (idx - (total - 1) / 2)\n    else:\n        curve_offset = 0\n\n    # Offset labels perpendicular to edge direction (increased for clarity)\n    label_offset = 0.055\n\n    edges_data.append(\n        {\n            \"x\": src_x + dx * offset + perp_x * curve_offset,\n            \"y\": src_y + dy * offset + perp_y * curve_offset,\n            \"xend\": tgt_x - dx * offset + perp_x * curve_offset,\n            \"yend\": tgt_y - dy * offset + perp_y * curve_offset,\n            \"route_id\": r[\"route_id\"],\n            \"depart\": r[\"depart\"],\n            \"arrive\": r[\"arrive\"],\n            \"label\": f\"{r['route_id']} | {r['depart']} → {r['arrive']}\",\n            \"type\": r[\"type\"],\n            \"mid_x\": (src_x + tgt_x) / 2 + perp_x * (label_offset + curve_offset),\n            \"mid_y\": (src_y + tgt_y) / 2 + perp_y * (label_offset + curve_offset),\n            \"source_station\": next(s[\"label\"] for s in stations if s[\"id\"] == r[\"source\"]),\n            \"target_station\": next(s[\"label\"] for s in stations if s[\"id\"] == r[\"target\"]),\n        }\n    )\n\nedges_df = pd.DataFrame(edges_data)\n\n# Map route types to Okabe-Ito colors\nroute_type_order = [\"Express\", \"Regional\", \"Local\"]\nroute_colors = {route_type_order[i]: IMPRINT[i] for i in range(len(route_type_order))}\n\n# Create tooltip specs for interactive hover\nedge_tooltips = (\n    layer_tooltips()\n    .title(\"@route_id\")\n    .line(\"@source_station → @target_station\")\n    .line(\"Departs: @depart\")\n    .line(\"Arrives: @arrive\")\n    .line(\"Type: @type\")\n)\n\nstation_tooltips = layer_tooltips().title(\"@label\").line(\"Station ID: @id\")\n\n# Create the plot with interactive tooltips\nplot = (\n    ggplot()\n    # Draw edges as segments with arrows and tooltips\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\", color=\"type\"),\n        data=edges_df,\n        size=1.8,\n        alpha=0.85,\n        arrow=arrow(angle=25, length=12, type=\"closed\"),\n        tooltips=edge_tooltips,\n    )\n    # Draw edge labels (route and times) - larger for readability\n    + geom_text(aes(x=\"mid_x\", y=\"mid_y\", label=\"label\", color=\"type\"), data=edges_df, size=6)\n    # Draw station nodes with tooltips\n    + geom_point(\n        aes(x=\"x\", y=\"y\"),\n        data=stations_df,\n        size=12,\n        color=\"white\",\n        shape=21,\n        fill=IMPRINT[0],\n        stroke=2.5,\n        tooltips=station_tooltips,\n    )\n    # Draw station labels (adjusted position to avoid edge label overlap)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=stations_df, size=9, color=INK, fontface=\"bold\", nudge_y=-0.055)\n    # Color scale for route types\n    + scale_color_manual(values=route_colors, name=\"Route Type\")\n    # Styling\n    + labs(title=\"network-transport-static · python · letsplot · anyplot.ai\", x=\"\", y=\"\")\n    + theme_void()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=24, face=\"bold\", hjust=0.5, color=INK),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=16, color=INK),\n        legend_text=element_text(size=14, color=INK_SOFT),\n    )\n    + scale_x_continuous(limits=[0, 1])\n    + scale_y_continuous(limits=[0, 1])\n    + coord_fixed(ratio=1)\n    + ggsize(1600, 900)\n)\n\n# Save as PNG (scale 3x for 4800x2700)\nggsave(plot, f\"plot-{THEME}.png\", scale=3, path=\".\")\n\n# Save as HTML for interactivity (tooltips work in HTML)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}