{"spec_id":"map-connection-lines","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nmap-connection-lines: Connection Lines Map (Origin-Destination)\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Created: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\nfrom collections import Counter\n\n\n# Prevent self-import: this file is named altair.py, so we must remove its\n# directory from sys.path before importing the altair package.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p and os.path.abspath(p) != _this_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\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\"\nMAP_LAND = \"#DDD9CC\" if THEME == \"light\" else \"#2A2A26\"\nPAGE_BG_TUPLE = (250, 248, 241) if THEME == \"light\" else (26, 26, 23)\n\nBRAND = \"#009E73\"\n\n# Data: major airports (avoiding trans-Pacific routes that cross the antimeridian)\nairports = {\n    \"JFK\": {\"lat\": 40.6, \"lon\": -73.8, \"city\": \"New York\"},\n    \"LAX\": {\"lat\": 33.9, \"lon\": -118.4, \"city\": \"Los Angeles\"},\n    \"ORD\": {\"lat\": 41.9, \"lon\": -87.9, \"city\": \"Chicago\"},\n    \"GRU\": {\"lat\": -23.5, \"lon\": -46.6, \"city\": \"Sao Paulo\"},\n    \"LHR\": {\"lat\": 51.5, \"lon\": -0.5, \"city\": \"London\"},\n    \"CDG\": {\"lat\": 49.0, \"lon\": 2.5, \"city\": \"Paris\"},\n    \"FRA\": {\"lat\": 50.0, \"lon\": 8.6, \"city\": \"Frankfurt\"},\n    \"DXB\": {\"lat\": 25.3, \"lon\": 55.4, \"city\": \"Dubai\"},\n    \"SIN\": {\"lat\": 1.4, \"lon\": 103.9, \"city\": \"Singapore\"},\n    \"HKG\": {\"lat\": 22.3, \"lon\": 113.9, \"city\": \"Hong Kong\"},\n    \"NRT\": {\"lat\": 35.8, \"lon\": 140.4, \"city\": \"Tokyo\"},\n    \"SYD\": {\"lat\": -33.9, \"lon\": 151.2, \"city\": \"Sydney\"},\n}\n\n# Flight routes (origin, destination, annual passengers in thousands)\nroutes_raw = [\n    (\"JFK\", \"LHR\", 3800),\n    (\"JFK\", \"CDG\", 2200),\n    (\"JFK\", \"FRA\", 1900),\n    (\"LAX\", \"LHR\", 2600),\n    (\"LAX\", \"CDG\", 1600),\n    (\"ORD\", \"LHR\", 1700),\n    (\"ORD\", \"FRA\", 1300),\n    (\"GRU\", \"LHR\", 1200),\n    (\"GRU\", \"CDG\", 900),\n    (\"LHR\", \"DXB\", 2900),\n    (\"CDG\", \"DXB\", 1400),\n    (\"FRA\", \"DXB\", 2200),\n    (\"DXB\", \"SIN\", 2400),\n    (\"DXB\", \"SYD\", 1900),\n    (\"DXB\", \"NRT\", 1600),\n    (\"LHR\", \"SIN\", 2200),\n    (\"LHR\", \"HKG\", 1800),\n    (\"LHR\", \"NRT\", 2100),\n    (\"FRA\", \"NRT\", 1600),\n    (\"CDG\", \"NRT\", 1500),\n    (\"NRT\", \"SIN\", 1700),\n    (\"NRT\", \"HKG\", 2100),\n    (\"NRT\", \"SYD\", 1300),\n    (\"SIN\", \"SYD\", 2100),\n    (\"HKG\", \"SIN\", 2500),\n]\n\n# Hub connectivity: count routes per airport\nconnectivity = Counter()\nfor origin, dest, _ in routes_raw:\n    connectivity[origin] += 1\n    connectivity[dest] += 1\n\n# Generate great circle arc points via spherical linear interpolation (SLERP)\nn_arc_points = 40\narc_records = []\nfor origin_code, dest_code, volume in routes_raw:\n    olat = airports[origin_code][\"lat\"]\n    olon = airports[origin_code][\"lon\"]\n    dlat = airports[dest_code][\"lat\"]\n    dlon = airports[dest_code][\"lon\"]\n\n    lat1_r = np.radians(olat)\n    lon1_r = np.radians(olon)\n    lat2_r = np.radians(dlat)\n    lon2_r = np.radians(dlon)\n\n    x1 = np.cos(lat1_r) * np.cos(lon1_r)\n    y1 = np.cos(lat1_r) * np.sin(lon1_r)\n    z1 = np.sin(lat1_r)\n\n    x2 = np.cos(lat2_r) * np.cos(lon2_r)\n    y2 = np.cos(lat2_r) * np.sin(lon2_r)\n    z2 = np.sin(lat2_r)\n\n    dot = float(np.clip(x1 * x2 + y1 * y2 + z1 * z2, -1, 1))\n    omega = np.arccos(dot)\n\n    for j, t in enumerate(np.linspace(0, 1, n_arc_points)):\n        if omega < 1e-10:\n            pt_lat, pt_lon = olat, olon\n        else:\n            sin_omega = np.sin(omega)\n            a = np.sin((1 - t) * omega) / sin_omega\n            b = np.sin(t * omega) / sin_omega\n            x = a * x1 + b * x2\n            y = a * y1 + b * y2\n            z = a * z1 + b * z2\n            pt_lat = float(np.degrees(np.arctan2(z, np.sqrt(x**2 + y**2))))\n            pt_lon = float(np.degrees(np.arctan2(y, x)))\n\n        arc_records.append(\n            {\n                \"latitude\": pt_lat,\n                \"longitude\": pt_lon,\n                \"route\": f\"{origin_code}-{dest_code}\",\n                \"volume\": volume,\n                \"order\": j,\n            }\n        )\n\narcs_df = pd.DataFrame(arc_records)\n\n# Airport markers dataframe with connectivity for size scaling\nairports_df = pd.DataFrame(\n    [\n        {\n            \"code\": code,\n            \"city\": info[\"city\"],\n            \"latitude\": info[\"lat\"],\n            \"longitude\": info[\"lon\"],\n            \"connections\": connectivity[code],\n        }\n        for code, info in airports.items()\n    ]\n)\n\n# Title (67 chars → fontSize=16, no scaling needed)\ntitle_str = \"Flight Routes · map-connection-lines · python · altair · anyplot.ai\"\nn_chars = len(title_str)\ntitle_fs = round(16 * 67 / n_chars) if n_chars > 67 else 16\n\n# World map base layer (110m resolution topojson)\nworld_url = \"https://cdn.jsdelivr.net/npm/vega-datasets@2/data/world-110m.json\"\n\nbase_map = alt.Chart(alt.topo_feature(world_url, \"countries\")).mark_geoshape(\n    fill=MAP_LAND, stroke=INK_SOFT, strokeWidth=0.3\n)\n\n# Arc connection lines — wider strokeWidth range makes volume differences impactful\narcs_layer = (\n    alt.Chart(arcs_df)\n    .mark_line(opacity=0.5)\n    .encode(\n        longitude=\"longitude:Q\",\n        latitude=\"latitude:Q\",\n        detail=\"route:N\",\n        order=\"order:O\",\n        color=alt.Color(\n            \"volume:Q\",\n            scale=alt.Scale(range=[\"#009E73\", \"#4467A3\"]),\n            legend=alt.Legend(\n                title=\"Passengers (k/yr)\",\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                symbolType=\"stroke\",\n                symbolSize=400,\n                symbolStrokeWidth=2,\n            ),\n        ),\n        strokeWidth=alt.StrokeWidth(\"volume:Q\", scale=alt.Scale(range=[0.3, 2.5]), legend=None),\n        tooltip=[alt.Tooltip(\"route:N\", title=\"Route\"), alt.Tooltip(\"volume:Q\", title=\"Passengers (k/yr)\")],\n    )\n)\n\n# Airport endpoint markers scaled by hub connectivity degree\nmarkers_layer = (\n    alt.Chart(airports_df)\n    .mark_circle(color=BRAND, opacity=0.95, stroke=INK, strokeWidth=0.8)\n    .encode(\n        longitude=\"longitude:Q\",\n        latitude=\"latitude:Q\",\n        size=alt.Size(\"connections:Q\", scale=alt.Scale(range=[50, 280]), legend=None),\n        tooltip=[\n            alt.Tooltip(\"city:N\", title=\"City\"),\n            alt.Tooltip(\"code:N\", title=\"Code\"),\n            alt.Tooltip(\"connections:Q\", title=\"Routes\"),\n        ],\n    )\n)\n\n# Direct labels for top 3 hubs (LHR=8, NRT=7, DXB=6 connections)\nhub_df = airports_df[airports_df[\"connections\"] >= 6]\nlabels_layer = (\n    alt.Chart(hub_df)\n    .mark_text(dy=-15, fontSize=9, color=INK, fontWeight=\"bold\", align=\"center\")\n    .encode(longitude=\"longitude:Q\", latitude=\"latitude:Q\", text=\"code:N\")\n)\n\n# Combine all layers with Natural Earth projection\nchart = (\n    alt.layer(base_map, arcs_layer, markers_layer, labels_layer)\n    .project(type=\"naturalEarth1\")\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.TitleParams(title_str, fontSize=title_fs, color=INK, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=620, continuousHeight=320)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n    .configure_title(color=INK, fontSize=title_fs)\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 canvas (vl-convert may land slightly under target)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG_TUPLE)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}