{"spec_id":"network-transport-static","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nnetwork-transport-static: Static Transport Network Diagram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-18\n\"\"\"\n\nimport os\nimport sys\nimport xml.etree.ElementTree as ET\n\n\n# Remove current directory from sys.path before importing to avoid circular import\n_script_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 cairosvg\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\n\n# Okabe-Ito palette (colorblind-safe)\nBRAND = \"#009E73\"  # Position 1 - brand green\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Route type colors (using Okabe-Ito positions)\nroute_colors = {\n    \"RE\": IMPRINT[0],  # Regional Express - brand green\n    \"EX\": IMPRINT[1],  # Express - vermillion\n    \"LO\": IMPRINT[2],  # Local - blue\n}\n\n# Station data: regional rail network\nstations = [\n    {\"id\": \"A\", \"label\": \"Central Station\", \"x\": 0.08, \"y\": 0.50},\n    {\"id\": \"B\", \"label\": \"North Park\", \"x\": 0.25, \"y\": 0.20},\n    {\"id\": \"C\", \"label\": \"East Harbor\", \"x\": 0.50, \"y\": 0.30},\n    {\"id\": \"D\", \"label\": \"South Gate\", \"x\": 0.25, \"y\": 0.80},\n    {\"id\": \"E\", \"label\": \"West Valley\", \"x\": 0.50, \"y\": 0.65},\n    {\"id\": \"F\", \"label\": \"Airport\", \"x\": 0.75, \"y\": 0.18},\n    {\"id\": \"G\", \"label\": \"University\", \"x\": 0.75, \"y\": 0.50},\n    {\"id\": \"H\", \"label\": \"Industrial Zone\", \"x\": 0.75, \"y\": 0.78},\n    {\"id\": \"I\", \"label\": \"Riverside\", \"x\": 0.50, \"y\": 0.88},\n    {\"id\": \"J\", \"label\": \"Tech Campus\", \"x\": 0.92, \"y\": 0.50},\n]\n\n# Routes: train services between stations\nroutes = [\n    {\"source\": \"A\", \"target\": \"B\", \"route_id\": \"RE 1\", \"dep\": \"06:00\", \"arr\": \"06:25\"},\n    {\"source\": \"A\", \"target\": \"D\", \"route_id\": \"RE 2\", \"dep\": \"06:15\", \"arr\": \"06:45\"},\n    {\"source\": \"B\", \"target\": \"C\", \"route_id\": \"RE 1\", \"dep\": \"06:30\", \"arr\": \"07:00\"},\n    {\"source\": \"B\", \"target\": \"F\", \"route_id\": \"EX 10\", \"dep\": \"07:00\", \"arr\": \"07:40\"},\n    {\"source\": \"C\", \"target\": \"F\", \"route_id\": \"RE 3\", \"dep\": \"07:10\", \"arr\": \"07:35\"},\n    {\"source\": \"C\", \"target\": \"G\", \"route_id\": \"RE 4\", \"dep\": \"07:20\", \"arr\": \"07:50\"},\n    {\"source\": \"D\", \"target\": \"E\", \"route_id\": \"RE 5\", \"dep\": \"06:50\", \"arr\": \"07:25\"},\n    {\"source\": \"D\", \"target\": \"I\", \"route_id\": \"LO 20\", \"dep\": \"07:00\", \"arr\": \"07:20\"},\n    {\"source\": \"E\", \"target\": \"G\", \"route_id\": \"RE 5\", \"dep\": \"07:30\", \"arr\": \"07:55\"},\n    {\"source\": \"E\", \"target\": \"H\", \"route_id\": \"RE 6\", \"dep\": \"07:35\", \"arr\": \"08:00\"},\n    {\"source\": \"E\", \"target\": \"I\", \"route_id\": \"LO 21\", \"dep\": \"07:40\", \"arr\": \"07:55\"},\n    {\"source\": \"F\", \"target\": \"G\", \"route_id\": \"EX 10\", \"dep\": \"07:45\", \"arr\": \"08:05\"},\n    {\"source\": \"G\", \"target\": \"J\", \"route_id\": \"EX 11\", \"dep\": \"08:10\", \"arr\": \"08:30\"},\n    {\"source\": \"G\", \"target\": \"H\", \"route_id\": \"RE 7\", \"dep\": \"08:00\", \"arr\": \"08:20\"},\n    {\"source\": \"H\", \"target\": \"I\", \"route_id\": \"RE 8\", \"dep\": \"08:25\", \"arr\": \"08:50\"},\n]\n\n# Create station lookup\nstation_lookup = {s[\"id\"]: s for s in stations}\n\n# Chart dimensions (16:9 landscape)\nWIDTH = 4800\nHEIGHT = 2700\n\n# Plotting area margins\nmargin_left = 150\nmargin_right = 150\nmargin_top = 200\nmargin_bottom = 200\n\nplot_width = WIDTH - margin_left - margin_right\nplot_height = HEIGHT - margin_top - margin_bottom\n\n\ndef normalize_to_svg(x_norm, y_norm):\n    \"\"\"Convert normalized (0-1) coordinates to SVG coordinates.\"\"\"\n    svg_x = margin_left + x_norm * plot_width\n    svg_y = margin_top + y_norm * plot_height\n    return svg_x, svg_y\n\n\n# Build SVG using ElementTree\nsvg_ns = \"http://www.w3.org/2000/svg\"\nET.register_namespace(\"\", svg_ns)\n\nsvg_root = ET.Element(\"svg\", xmlns=svg_ns, width=str(WIDTH), height=str(HEIGHT), viewBox=f\"0 0 {WIDTH} {HEIGHT}\")\nsvg_root.set(\"style\", f\"background-color: {PAGE_BG};\")\n\n# Definitions for arrow markers\ndefs = ET.SubElement(svg_root, \"defs\")\nfor prefix, color in route_colors.items():\n    marker = ET.SubElement(\n        defs,\n        \"marker\",\n        id=f\"arrow-{prefix}\",\n        markerWidth=\"12\",\n        markerHeight=\"12\",\n        refX=\"10\",\n        refY=\"4\",\n        orient=\"auto\",\n        markerUnits=\"strokeWidth\",\n    )\n    path = ET.SubElement(marker, \"path\")\n    path.set(\"d\", \"M0,0 L0,8 L12,4 z\")\n    path.set(\"fill\", color)\n\n# Add title\ntitle_elem = ET.SubElement(svg_root, \"text\")\ntitle_elem.set(\"x\", str(WIDTH / 2))\ntitle_elem.set(\"y\", \"80\")\ntitle_elem.set(\"text-anchor\", \"middle\")\ntitle_elem.set(\"fill\", INK)\ntitle_elem.set(\"font-size\", \"56\")\ntitle_elem.set(\"font-family\", \"sans-serif\")\ntitle_elem.set(\"font-weight\", \"bold\")\ntitle_elem.text = \"network-transport-static · python · pygal · anyplot.ai\"\n\n# Subtitle\nsubtitle_elem = ET.SubElement(svg_root, \"text\")\nsubtitle_elem.set(\"x\", str(WIDTH / 2))\nsubtitle_elem.set(\"y\", \"140\")\nsubtitle_elem.set(\"text-anchor\", \"middle\")\nsubtitle_elem.set(\"fill\", INK_SOFT)\nsubtitle_elem.set(\"font-size\", \"32\")\nsubtitle_elem.set(\"font-family\", \"sans-serif\")\nsubtitle_elem.text = \"Regional Rail Network - Morning Schedule\"\n\n# Plot background\nplot_bg = ET.SubElement(svg_root, \"rect\")\nplot_bg.set(\"x\", str(margin_left))\nplot_bg.set(\"y\", str(margin_top))\nplot_bg.set(\"width\", str(plot_width))\nplot_bg.set(\"height\", str(plot_height))\nplot_bg.set(\"fill\", PAGE_BG)\nplot_bg.set(\"stroke\", INK_MUTED)\nplot_bg.set(\"stroke-width\", \"2\")\nplot_bg.set(\"stroke-opacity\", \"0.3\")\n\n# Group for edges (routes)\nedges_g = ET.SubElement(svg_root, \"g\")\nedges_g.set(\"class\", \"routes\")\n\n# Track edge counts between station pairs for offset calculation\nedge_counts = {}\nfor route in routes:\n    key = tuple(sorted([route[\"source\"], route[\"target\"]]))\n    edge_counts[key] = edge_counts.get(key, 0) + 1\n\nedge_indices = {}\n\n# Draw edges (routes)\nfor route in routes:\n    src = station_lookup[route[\"source\"]]\n    tgt = station_lookup[route[\"target\"]]\n\n    x1, y1 = normalize_to_svg(src[\"x\"], src[\"y\"])\n    x2, y2 = normalize_to_svg(tgt[\"x\"], tgt[\"y\"])\n\n    # Get route color based on prefix\n    route_prefix = route[\"route_id\"].split()[0]\n    color = route_colors.get(route_prefix, BRAND)\n\n    # Calculate edge offset for multiple edges between same stations\n    key = tuple(sorted([route[\"source\"], route[\"target\"]]))\n    idx = edge_indices.get(key, 0)\n    edge_indices[key] = idx + 1\n    total = edge_counts[key]\n\n    # Calculate perpendicular offset\n    dx = x2 - x1\n    dy = y2 - y1\n    length = (dx**2 + dy**2) ** 0.5\n    if length > 0 and total > 1:\n        # Offset perpendicular to line\n        offset_amount = (idx - (total - 1) / 2) * 35\n        perp_x = -dy / length * offset_amount\n        perp_y = dx / length * offset_amount\n        x1 += perp_x\n        y1 += perp_y\n        x2 += perp_x\n        y2 += perp_y\n\n    # Shorten line to not overlap with node circles\n    node_radius = 45\n    if length > 0:\n        # Shorten from start\n        x1 += dx / length * node_radius\n        y1 += dy / length * node_radius\n        # Shorten from end\n        x2 -= dx / length * node_radius\n        y2 -= dy / length * node_radius\n\n    # Draw edge line with arrow\n    line = ET.SubElement(edges_g, \"line\")\n    line.set(\"x1\", f\"{x1:.1f}\")\n    line.set(\"y1\", f\"{y1:.1f}\")\n    line.set(\"x2\", f\"{x2:.1f}\")\n    line.set(\"y2\", f\"{y2:.1f}\")\n    line.set(\"stroke\", color)\n    line.set(\"stroke-width\", \"5\")\n    line.set(\"stroke-opacity\", \"0.8\")\n    line.set(\"marker-end\", f\"url(#arrow-{route_prefix})\")\n\n    # Edge label at midpoint\n    mid_x = (x1 + x2) / 2\n    mid_y = (y1 + y2) / 2\n\n    # Label background\n    label_text = f\"{route['route_id']} | {route['dep']} → {route['arr']}\"\n    label_bg = ET.SubElement(edges_g, \"rect\")\n    label_bg.set(\"x\", f\"{mid_x - 130:.1f}\")\n    label_bg.set(\"y\", f\"{mid_y - 18:.1f}\")\n    label_bg.set(\"width\", \"260\")\n    label_bg.set(\"height\", \"36\")\n    label_bg.set(\"fill\", ELEVATED_BG)\n    label_bg.set(\"fill-opacity\", \"0.95\")\n    label_bg.set(\"rx\", \"6\")\n    label_bg.set(\"stroke\", color)\n    label_bg.set(\"stroke-width\", \"1\")\n    label_bg.set(\"stroke-opacity\", \"0.5\")\n\n    # Label text\n    label_elem = ET.SubElement(edges_g, \"text\")\n    label_elem.set(\"x\", f\"{mid_x:.1f}\")\n    label_elem.set(\"y\", f\"{mid_y + 7:.1f}\")\n    label_elem.set(\"text-anchor\", \"middle\")\n    label_elem.set(\"fill\", color)\n    label_elem.set(\"font-size\", \"22\")\n    label_elem.set(\"font-family\", \"sans-serif\")\n    label_elem.set(\"font-weight\", \"600\")\n    label_elem.text = label_text\n\n    # Tooltip\n    title = ET.SubElement(line, \"title\")\n    title.text = f\"{route['route_id']}: {route['dep']} → {route['arr']}\"\n\n# Group for nodes (stations)\nnodes_g = ET.SubElement(svg_root, \"g\")\nnodes_g.set(\"class\", \"stations\")\n\n# Draw nodes (stations) on top\nfor station in stations:\n    x, y = normalize_to_svg(station[\"x\"], station[\"y\"])\n\n    # Node outer circle (border)\n    outer = ET.SubElement(nodes_g, \"circle\")\n    outer.set(\"cx\", f\"{x:.1f}\")\n    outer.set(\"cy\", f\"{y:.1f}\")\n    outer.set(\"r\", \"50\")\n    outer.set(\"fill\", BRAND)  # Okabe-Ito position 1\n    outer.set(\"stroke\", INK)\n    outer.set(\"stroke-width\", \"5\")\n\n    # Station ID inside node\n    id_elem = ET.SubElement(nodes_g, \"text\")\n    id_elem.set(\"x\", f\"{x:.1f}\")\n    id_elem.set(\"y\", f\"{y + 12:.1f}\")\n    id_elem.set(\"text-anchor\", \"middle\")\n    id_elem.set(\"fill\", PAGE_BG)\n    id_elem.set(\"font-size\", \"36\")\n    id_elem.set(\"font-family\", \"sans-serif\")\n    id_elem.set(\"font-weight\", \"bold\")\n    id_elem.text = station[\"id\"]\n\n    # Station name label below node\n    label_bg = ET.SubElement(nodes_g, \"rect\")\n    label_bg.set(\"x\", f\"{x - 110:.1f}\")\n    label_bg.set(\"y\", f\"{y + 60:.1f}\")\n    label_bg.set(\"width\", \"220\")\n    label_bg.set(\"height\", \"36\")\n    label_bg.set(\"fill\", ELEVATED_BG)\n    label_bg.set(\"fill-opacity\", \"0.95\")\n    label_bg.set(\"rx\", \"6\")\n    label_bg.set(\"stroke\", INK_MUTED)\n    label_bg.set(\"stroke-width\", \"1\")\n    label_bg.set(\"stroke-opacity\", \"0.3\")\n\n    label_elem = ET.SubElement(nodes_g, \"text\")\n    label_elem.set(\"x\", f\"{x:.1f}\")\n    label_elem.set(\"y\", f\"{y + 86:.1f}\")\n    label_elem.set(\"text-anchor\", \"middle\")\n    label_elem.set(\"fill\", INK)\n    label_elem.set(\"font-size\", \"24\")\n    label_elem.set(\"font-family\", \"sans-serif\")\n    label_elem.set(\"font-weight\", \"600\")\n    label_elem.text = station[\"label\"]\n\n    # Tooltip\n    title = ET.SubElement(outer, \"title\")\n    title.text = f\"{station['id']}: {station['label']}\"\n\n# Legend\nlegend_g = ET.SubElement(svg_root, \"g\")\nlegend_g.set(\"class\", \"legend\")\nlegend_x = WIDTH - 420\nlegend_y = HEIGHT - 180\n\n# Legend background\nlegend_bg = ET.SubElement(legend_g, \"rect\")\nlegend_bg.set(\"x\", str(legend_x - 30))\nlegend_bg.set(\"y\", str(legend_y - 50))\nlegend_bg.set(\"width\", \"400\")\nlegend_bg.set(\"height\", \"160\")\nlegend_bg.set(\"fill\", ELEVATED_BG)\nlegend_bg.set(\"fill-opacity\", \"0.95\")\nlegend_bg.set(\"stroke\", INK_MUTED)\nlegend_bg.set(\"stroke-width\", \"2\")\nlegend_bg.set(\"stroke-opacity\", \"0.3\")\nlegend_bg.set(\"rx\", \"10\")\n\n# Legend title\nlegend_title = ET.SubElement(legend_g, \"text\")\nlegend_title.set(\"x\", str(legend_x))\nlegend_title.set(\"y\", str(legend_y - 10))\nlegend_title.set(\"fill\", INK)\nlegend_title.set(\"font-size\", \"32\")\nlegend_title.set(\"font-family\", \"sans-serif\")\nlegend_title.set(\"font-weight\", \"bold\")\nlegend_title.text = \"Route Types\"\n\n# Legend items\nlegend_items = [(\"RE - Regional Express\", IMPRINT[0]), (\"EX - Express\", IMPRINT[1]), (\"LO - Local\", IMPRINT[2])]\n\nfor i, (label, color) in enumerate(legend_items):\n    ly = legend_y + 30 + i * 35\n\n    # Line symbol\n    line_sym = ET.SubElement(legend_g, \"line\")\n    line_sym.set(\"x1\", str(legend_x))\n    line_sym.set(\"y1\", str(ly))\n    line_sym.set(\"x2\", str(legend_x + 50))\n    line_sym.set(\"y2\", str(ly))\n    line_sym.set(\"stroke\", color)\n    line_sym.set(\"stroke-width\", \"5\")\n\n    # Arrow marker\n    arrow = ET.SubElement(legend_g, \"polygon\")\n    arrow.set(\"points\", f\"{legend_x + 52},{ly - 6} {legend_x + 52},{ly + 6} {legend_x + 64},{ly}\")\n    arrow.set(\"fill\", color)\n\n    # Label text\n    label_elem = ET.SubElement(legend_g, \"text\")\n    label_elem.set(\"x\", str(legend_x + 80))\n    label_elem.set(\"y\", str(ly + 8))\n    label_elem.set(\"fill\", INK_SOFT)\n    label_elem.set(\"font-size\", \"26\")\n    label_elem.set(\"font-family\", \"sans-serif\")\n    label_elem.text = label\n\n# Convert SVG to string\nsvg_output = ET.tostring(svg_root, encoding=\"unicode\")\n\n# Write HTML file\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>network-transport-static - pygal - anyplot.ai</title>\n    <style>\n        body {{ margin: 0; padding: 20px; background: {PAGE_BG}; font-family: sans-serif; }}\n        .container {{ max-width: 100%; overflow: auto; }}\n        svg {{ display: block; margin: 0 auto; box-shadow: 0 4px 20px rgba(0,0,0,0.15); }}\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        {svg_output}\n    </div>\n</body>\n</html>\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html_content)\n\n# Render to PNG using cairosvg\ncairosvg.svg2png(\n    bytestring=svg_output.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=WIDTH, output_height=HEIGHT\n)\n"}