{"spec_id":"map-connection-lines","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nmap-connection-lines: Connection Lines Map (Origin-Destination)\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove cwd temporarily to resolve the pygal.py → pygal library name conflict\n_cwd = sys.path[0] if sys.path else None\nif _cwd:\n    sys.path.remove(_cwd)\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nif _cwd:\n    sys.path.insert(0, _cwd)\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nOCEAN_BG = \"#E8F3F9\" if THEME == \"light\" else \"#0C1822\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nGRID_COLOR = \"#1A1A1722\" if THEME == \"light\" else \"#F0EFE822\"\n\n# Data - Major international flight routes with passenger volumes (thousands/year)\nnp.random.seed(42)\n\nairports = {\n    \"JFK\": (40.64, -73.78, \"New York\"),\n    \"LAX\": (33.94, -118.41, \"Los Angeles\"),\n    \"LHR\": (51.47, -0.46, \"London\"),\n    \"CDG\": (49.01, 2.55, \"Paris\"),\n    \"DXB\": (25.25, 55.36, \"Dubai\"),\n    \"HND\": (35.55, 139.78, \"Tokyo\"),\n    \"SIN\": (1.36, 103.99, \"Singapore\"),\n    \"SYD\": (-33.95, 151.18, \"Sydney\"),\n    \"HKG\": (22.31, 113.91, \"Hong Kong\"),\n    \"FRA\": (50.03, 8.57, \"Frankfurt\"),\n    \"ORD\": (41.98, -87.90, \"Chicago\"),\n    \"PEK\": (40.08, 116.58, \"Beijing\"),\n    \"GRU\": (-23.43, -46.47, \"Sao Paulo\"),\n    \"JNB\": (-26.13, 28.23, \"Johannesburg\"),\n}\n\nroutes = [\n    (\"JFK\", \"LHR\", 4200),\n    (\"JFK\", \"CDG\", 2100),\n    (\"LAX\", \"HND\", 3500),\n    (\"LAX\", \"SYD\", 1800),\n    (\"LHR\", \"DXB\", 3800),\n    (\"LHR\", \"HKG\", 2900),\n    (\"LHR\", \"JFK\", 4200),\n    (\"CDG\", \"JFK\", 2100),\n    (\"DXB\", \"SIN\", 3200),\n    (\"DXB\", \"LHR\", 3800),\n    (\"HND\", \"SIN\", 2400),\n    (\"HND\", \"LAX\", 3500),\n    (\"SIN\", \"SYD\", 2800),\n    (\"SIN\", \"HKG\", 3100),\n    (\"HKG\", \"LHR\", 2900),\n    (\"HKG\", \"SIN\", 3100),\n    (\"FRA\", \"JFK\", 2600),\n    (\"FRA\", \"DXB\", 2200),\n    (\"ORD\", \"LHR\", 2800),\n    (\"ORD\", \"FRA\", 1900),\n    (\"PEK\", \"LAX\", 2300),\n    (\"PEK\", \"FRA\", 1700),\n    (\"GRU\", \"JFK\", 1500),\n    (\"GRU\", \"LHR\", 1200),\n    (\"JNB\", \"DXB\", 1400),\n    (\"JNB\", \"LHR\", 1100),\n    (\"SYD\", \"LAX\", 1800),\n    (\"SYD\", \"SIN\", 2800),\n]\n\nroute_data = []\nfor origin, dest, volume in routes:\n    o_lat, o_lon, o_city = airports[origin]\n    d_lat, d_lon, d_city = airports[dest]\n    route_data.append(\n        {\n            \"origin_lat\": o_lat,\n            \"origin_lon\": o_lon,\n            \"dest_lat\": d_lat,\n            \"dest_lon\": d_lon,\n            \"volume\": volume,\n            \"origin_city\": o_city,\n            \"dest_city\": d_city,\n        }\n    )\n\ncoastlines = [\n    [(-125, 50), (-124, 45), (-122, 38), (-117, 33), (-110, 32), (-105, 28)],\n    [(-67, 45), (-70, 42), (-74, 40), (-76, 37), (-80, 32), (-81, 28), (-82, 25)],\n    [(-82, 25), (-85, 30), (-90, 30), (-95, 28), (-97, 26), (-105, 28)],\n    [(-125, 50), (-130, 55), (-141, 60), (-150, 61), (-165, 55), (-168, 65)],\n    [(-45, 60), (-40, 65), (-35, 70), (-25, 72), (-20, 65), (-30, 60), (-45, 60)],\n    [(-35, -6), (-38, -13), (-42, -23), (-48, -28), (-53, -33), (-58, -38), (-66, -55)],\n    [(-78, 10), (-80, 0), (-81, -5), (-77, -15), (-72, -30), (-75, -45), (-66, -55)],\n    [(-10, 36), (-9, 42), (-5, 44), (0, 43), (3, 43), (5, 47), (3, 51)],\n    [(3, 51), (5, 53), (8, 55), (10, 58), (18, 60), (25, 66), (28, 70)],\n    [(-10, 36), (0, 37), (10, 43), (18, 40), (23, 37), (26, 35), (30, 31)],\n    [(-17, 14), (-15, 12), (-13, 10), (-5, 5), (0, 6), (5, 4), (10, 5)],\n    [(30, 31), (34, 30), (42, 14), (48, 8), (45, 0), (40, -5), (35, -22), (27, -34)],\n    [(27, -34), (20, -34), (17, -30), (12, -17), (10, 5)],\n    [(30, 31), (35, 32), (42, 30), (50, 27), (55, 25), (60, 25)],\n    [(60, 25), (66, 24), (72, 22), (72, 8), (80, 8), (88, 22), (90, 22)],\n    [(90, 22), (100, 14), (104, 2), (102, -5), (106, -7), (110, -8)],\n    [(120, 32), (122, 37), (124, 40), (130, 43), (135, 44), (141, 45)],\n    [(100, 22), (106, 22), (110, 20), (117, 24), (120, 32)],\n    [(130, 32), (132, 34), (135, 35), (140, 36), (141, 41), (145, 44)],\n    [\n        (113, -22),\n        (130, -14),\n        (145, -15),\n        (150, -23),\n        (153, -28),\n        (150, -38),\n        (142, -38),\n        (130, -32),\n        (117, -35),\n        (113, -22),\n    ],\n    [(173, -41), (175, -37), (178, -38), (177, -44), (170, -46), (168, -45), (173, -41)],\n]\n\n# Plot - colors ordered: routes first (pos 0-2 = brand green/lavender/blue),\n# then airports (pos 3), then coastlines last (pos 4 = INK_MUTED)\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=OCEAN_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=GRID_COLOR,\n    guide_stroke_dasharray=\"2,6\",\n    colors=(\n        \"#009E73\",  # Routes < 2M (Imprint palette pos 0 — brand green, first series)\n        \"#C475FD\",  # Routes 2-3M (Imprint palette pos 1)\n        \"#4467A3\",  # Routes > 3M (Imprint palette pos 2)\n        INK,  # Airports (theme-adaptive endpoint markers)\n        INK_MUTED,  # Coastlines (geographic context, drawn last)\n    ),\n    opacity=0.65,\n    opacity_hover=0.95,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"map-connection-lines · python · pygal · anyplot.ai\",\n    x_title=\"Longitude (°)\",\n    y_title=\"Latitude (°)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    legend_box_size=28,\n    stroke=True,\n    dots_size=3,\n    show_x_guides=True,\n    show_y_guides=True,\n    explicit_size=True,\n    print_values=False,\n    xrange=(-180, 180),\n    range=(-60, 80),\n    margin=70,\n    margin_top=120,\n    margin_bottom=130,\n)\n\nn_segments = 20\n\n\ndef build_bezier_curves(route_list):\n    curves = []\n    for route in route_list:\n        o_lat, o_lon = route[\"origin_lat\"], route[\"origin_lon\"]\n        d_lat, d_lon = route[\"dest_lat\"], route[\"dest_lon\"]\n        label = f\"{route['origin_city']} → {route['dest_city']}: {route['volume']}K passengers\"\n        mid_lon = (o_lon + d_lon) / 2\n        mid_lat = (o_lat + d_lat) / 2\n        dx, dy = d_lon - o_lon, d_lat - o_lat\n        length = np.sqrt(dx * dx + dy * dy)\n        if length > 0:\n            perp_x, perp_y = -dy / length, dx / length\n            # Pacific-crossing routes: flip perpendicular to arc northward over the pole\n            if abs(d_lon - o_lon) > 150:\n                perp_x, perp_y = -perp_x, -perp_y\n            offset_amount = min(length * 0.15, 20.0)\n            ctrl_lon = mid_lon + perp_x * offset_amount\n            ctrl_lat = mid_lat + perp_y * offset_amount\n        else:\n            ctrl_lon, ctrl_lat = mid_lon, mid_lat\n        for i in range(n_segments + 1):\n            t = i / n_segments\n            lon = (1 - t) ** 2 * o_lon + 2 * (1 - t) * t * ctrl_lon + t**2 * d_lon\n            lat = (1 - t) ** 2 * o_lat + 2 * (1 - t) * t * ctrl_lat + t**2 * d_lat\n            curves.append({\"value\": (lon, lat), \"label\": label})\n        curves.append({\"value\": (None, None)})\n    return curves\n\n\nlow_routes = [r for r in route_data if r[\"volume\"] < 2000]\nmedium_routes = [r for r in route_data if 2000 <= r[\"volume\"] <= 3000]\nhigh_routes = [r for r in route_data if r[\"volume\"] > 3000]\n\n# Routes added first — occupy palette positions 0-2 (brand green, lavender, blue)\nchart.add(\n    \"Routes < 2M\",\n    build_bezier_curves(low_routes),\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 3, \"linecap\": \"round\", \"opacity\": 0.55},\n)\nchart.add(\n    \"Routes 2-3M\",\n    build_bezier_curves(medium_routes),\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 5, \"linecap\": \"round\", \"opacity\": 0.60},\n)\nchart.add(\n    \"Routes > 3M\",\n    build_bezier_curves(high_routes),\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 8, \"linecap\": \"round\", \"opacity\": 0.65},\n)\n\n# Airport markers — palette position 3 (INK, theme-adaptive)\nairport_points = []\nfor code, (lat, lon, city) in airports.items():\n    airport_points.append({\"value\": (lon, lat), \"label\": f\"{city} ({code})\"})\n\nchart.add(\"Airports\", airport_points, stroke=False, dots_size=16)\n\n# Coastlines last — palette position 4 (INK_MUTED), low opacity so routes show through\ncoastline_points = []\nfor coastline in coastlines:\n    for lon, lat in coastline:\n        coastline_points.append({\"value\": (lon, lat), \"label\": \"Coastline\"})\n    coastline_points.append({\"value\": (None, None)})\n\nchart.add(\"Coastlines\", coastline_points, stroke=True, show_dots=False, stroke_style={\"width\": 2, \"opacity\": 0.25})\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}