{"spec_id":"map-connection-lines","library":"makie","language":"julia","code":"# anyplot.ai\n# map-connection-lines: Connection Lines Map (Origin-Destination)\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-05-28\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\n# Land and coastline colors (theme-adaptive)\nconst LAND_FILL   = THEME == \"light\" ?\n    RGBAf(0.87f0, 0.85f0, 0.80f0, 1.0f0) :\n    RGBAf(0.24f0, 0.24f0, 0.22f0, 1.0f0)\nconst COAST_COLOR = THEME == \"light\" ?\n    RGBAf(0.58f0, 0.56f0, 0.52f0, 1.0f0) :\n    RGBAf(0.40f0, 0.40f0, 0.37f0, 1.0f0)\n\nconst GRAT_ALPHA  = THEME == \"light\" ? 0.08f0 : 0.13f0\nconst GRAT_COLOR  = THEME == \"light\" ?\n    RGBAf(26f0/255f0, 26f0/255f0, 23f0/255f0, GRAT_ALPHA) :\n    RGBAf(240f0/255f0, 239f0/255f0, 232f0/255f0, GRAT_ALPHA)\n\n# Sequential colormap: anyplot brand green → blue (single-polarity continuous)\nconst ANYPLOT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\nconst SEQ_R1, SEQ_G1, SEQ_B1 = 0f0/255f0,  158f0/255f0, 115f0/255f0   # #009E73\nconst SEQ_R2, SEQ_G2, SEQ_B2 = 68f0/255f0, 103f0/255f0, 163f0/255f0   # #4467A3\n\n# Airport data: (name, lat, lon)\nconst airport_names = [\n    \"London\", \"New York\", \"Dubai\", \"Singapore\",\n    \"Tokyo\", \"Sydney\", \"Paris\", \"Los Angeles\",\n    \"Hong Kong\", \"Frankfurt\",\n]\nconst airport_lats = Float64[\n     51.5,  40.7,  25.2,   1.4,\n     35.7, -33.9,  48.9,  34.1,\n     22.3,  50.0,\n]\nconst airport_lons = Float64[\n     -0.1, -74.0,  55.4, 103.8,\n    139.7, 151.2,   2.4, -118.2,\n    114.2,   8.6,\n]\n\n# Connections: (origin_idx, dest_idx, annual_passengers_millions)\nconst connections = [\n    (1, 2, 12.5), (1, 3,  8.3), (1, 9,  6.1), (1, 4,  5.2),\n    (2, 8,  9.8), (2, 7,  5.7), (3, 9,  7.2), (3, 4,  4.1),\n    (9, 4,  6.8), (4, 5,  4.8), (4, 6,  3.4), (5, 9,  5.3),\n    (5, 8,  4.2), (7, 2,  5.7), (10, 2, 3.9),\n]\n\nconst volumes = Float64[c[3] for c in connections]\nconst vmin = minimum(volumes)\nconst vmax = maximum(volumes)\n\n# Simplified continent polygon data as (lon, lat) tuple vectors.\n# These are approximate shapes for geographic context; internal seas may appear as land.\n# poly! auto-closes each polygon (last point connects back to first).\nconst _CONTINENTS_RAW = [\n    # North America (clockwise from NW Alaska)\n    [(-165,65),(-168,54),(-168,52),(-136,59),(-127,50),(-124,46),\n     (-120,34),(-116,32),(-105,22),(-90,16),(-83,9),(-77,8),\n     (-77,26),(-80,30),(-75,44),(-70,44),(-65,44),(-62,47),\n     (-55,47),(-53,47),(-56,50),(-60,60),(-65,64),(-80,63),\n     (-85,52),(-95,50),(-110,50),(-122,50),(-130,56),(-145,62),(-155,60)],\n    # South America\n    [(-82,9),(-77,0),(-50,-4),(-35,-8),(-35,-20),(-48,-28),\n     (-56,-38),(-68,-56),(-74,-50),(-76,-35),(-70,-18),(-70,-5),(-80,0)],\n    # Europe (Med coast → Atlantic → N Europe → back via Baltic states and Med)\n    [(-12,36),(-9,39),(-6,44),(-4,49),(0,52),(8,56),(14,54),\n     (22,53),(26,55),(30,60),(30,70),(20,70),(15,68),(10,63),\n     (14,58),(18,58),(24,57),(20,46),(14,46),(6,44),(0,38),(-5,36)],\n    # Asia: Turkey/Bosphorus → Middle East → India → SE Asia → China → Russia Arctic\n    [(26,42),(36,37),(43,14),(43,12),(60,22),(73,18),(80,10),\n     (80,26),(90,22),(100,20),(105,10),(115,4),(122,5),(125,10),\n     (125,20),(122,24),(122,30),(126,44),(130,42),(136,34),\n     (140,44),(140,50),(135,52),(130,60),(115,62),(110,68),\n     (100,72),(80,72),(60,72),(40,72),(30,70),(30,60)],\n    # Africa\n    [(-18,15),(-14,12),(-10,8),(-5,5),(4,5),(10,4),(16,3),\n     (22,-5),(30,-8),(40,-10),(36,-24),(28,-35),(18,-35),\n     (14,-30),(10,-17),(12,-10),(16,-5),(14,0),(14,8),(14,16),\n     (16,24),(24,22),(36,22),(43,14),(40,20),(16,30),(12,32),\n     (10,37),(7,37),(0,36),(-5,36),(-8,36),(-14,28),(-18,20)],\n    # Australia\n    [(114,-22),(116,-34),(124,-34),(130,-33),(138,-36),(146,-40),\n     (150,-37),(154,-28),(152,-24),(146,-18),(138,-15),(130,-12),(124,-16)],\n    # Japan (Honshu main island, simplified)\n    [(130,31),(132,33),(135,34),(136,36),(138,38),(140,40),\n     (141,42),(142,43),(141,45),(140,44),(138,38),(136,34),(132,33)],\n    # Greenland (partially visible above latitude crop)\n    [(-44,83),(-18,77),(-18,76),(-26,68),(-44,60),(-57,60),(-60,65),(-58,75)],\n]\nconst CONTINENTS = [[Point2f(p[1], p[2]) for p in c] for c in _CONTINENTS_RAW]\n\n# Figure: landscape 1600×900 → 3200×1800 at px_per_unit=2\nconst title_str = \"Global Air Routes · map-connection-lines · julia · makie · anyplot.ai\"\nconst title_sz  = round(Int, 20 * min(1.0, 67 / length(title_str)))\n\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 12,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title               = title_str,\n    titlesize           = title_sz,\n    titlecolor          = INK,\n    xlabel              = \"Longitude\",\n    ylabel              = \"Latitude\",\n    xlabelsize          = 13,\n    ylabelsize          = 13,\n    xlabelcolor         = INK,\n    ylabelcolor         = INK,\n    xticklabelsize      = 10,\n    yticklabelsize      = 10,\n    xticklabelcolor     = INK_SOFT,\n    yticklabelcolor     = INK_SOFT,\n    xtickcolor          = INK_SOFT,\n    ytickcolor          = INK_SOFT,\n    backgroundcolor     = PAGE_BG,\n    topspinevisible     = false,\n    rightspinevisible   = false,\n    leftspinecolor      = INK_SOFT,\n    bottomspinecolor    = INK_SOFT,\n    xgridvisible        = false,\n    ygridvisible        = false,\n    limits              = (-180, 180, -50, 75),\n    xticks              = -180:60:180,\n    yticks              = [-30, 0, 30, 60],\n)\n\n# Base map: simplified continent fills (drawn first, behind all other elements)\nfor pts in CONTINENTS\n    poly!(ax, pts; color = LAND_FILL, strokecolor = COAST_COLOR, strokewidth = 0.6)\nend\n\n# Graticule: reference grid lines for geographic context\nfor lon in -180:30:180\n    lines!(ax, [Float64(lon), Float64(lon)], [-50.0, 75.0];\n        color = GRAT_COLOR, linewidth = 0.5)\nend\nfor lat in -30:30:60\n    lines!(ax, [-180.0, 180.0], [Float64(lat), Float64(lat)];\n        color = GRAT_COLOR, linewidth = 0.5)\nend\n\n# Connection arcs: great-circle paths via SLERP, colored by passenger volume\nfor (oi, di, volume) in connections\n    φ1 = deg2rad(airport_lats[oi]);  λ1 = deg2rad(airport_lons[oi])\n    φ2 = deg2rad(airport_lats[di]);  λ2 = deg2rad(airport_lons[di])\n    d_ang = acos(clamp(sin(φ1) * sin(φ2) + cos(φ1) * cos(φ2) * cos(λ2 - λ1), -1.0, 1.0))\n\n    arc_lons = Float64[]\n    arc_lats = Float64[]\n    n_pts = 80\n    for i in 0:n_pts\n        t = i / n_pts\n        A = sin((1 - t) * d_ang) / sin(d_ang)\n        B = sin(t * d_ang) / sin(d_ang)\n        x = A * cos(φ1) * cos(λ1) + B * cos(φ2) * cos(λ2)\n        y = A * cos(φ1) * sin(λ1) + B * cos(φ2) * sin(λ2)\n        z = A * sin(φ1) + B * sin(φ2)\n        lon_pt = rad2deg(atan(y, x))\n        lat_pt = rad2deg(atan(z, sqrt(x^2 + y^2)))\n        if !isempty(arc_lons) && abs(lon_pt - arc_lons[end]) > 180\n            push!(arc_lons, NaN)\n            push!(arc_lats, NaN)\n        end\n        push!(arc_lons, lon_pt)\n        push!(arc_lats, lat_pt)\n    end\n\n    nv = Float32((volume - vmin) / (vmax - vmin))\n    arc_color = RGBAf(\n        SEQ_R1 + (SEQ_R2 - SEQ_R1) * nv,\n        SEQ_G1 + (SEQ_G2 - SEQ_G1) * nv,\n        SEQ_B1 + (SEQ_B2 - SEQ_B1) * nv,\n        0.50f0,   # within spec's recommended 0.3–0.6\n    )\n    lines!(ax, arc_lons, arc_lats;\n        color     = arc_color,\n        linewidth = 1.0 + 3.5 * nv,\n    )\nend\n\n# Airport endpoint markers\nscatter!(ax, airport_lons, airport_lats;\n    color       = colorant\"#009E73\",\n    markersize  = 10,\n    strokewidth = 1.5,\n    strokecolor = INK,\n)\n\n# Airport labels with manual offsets to minimise overlap in dense clusters\nconst label_offsets = [\n    (-5.0,  4.0),   # London\n    (-7.0, -5.5),   # New York\n    ( 4.0,  3.5),   # Dubai\n    ( 5.0, -5.5),   # Singapore\n    ( 5.0,  3.5),   # Tokyo\n    ( 5.0, -5.5),   # Sydney\n    ( 4.0,  3.5),   # Paris\n    (-7.0, -5.5),   # Los Angeles\n    ( 5.0,  3.5),   # Hong Kong\n    ( 4.0, -5.5),   # Frankfurt\n]\n\nfor (i, name) in enumerate(airport_names)\n    dx, dy = label_offsets[i]\n    text!(ax, airport_lons[i] + dx, airport_lats[i] + dy;\n        text     = name,\n        fontsize = 12,\n        color    = INK_SOFT,\n        align    = (:center, :center),\n    )\nend\n\n# Colorbar: maps passenger volume (M/year) to the sequential palette\nColorbar(fig[1, 2];\n    colormap       = ANYPLOT_SEQ,\n    limits         = (vmin, vmax),\n    label          = \"Passengers (M / year)\",\n    labelsize      = 12,\n    labelcolor     = INK,\n    ticklabelsize  = 10,\n    ticklabelcolor = INK_SOFT,\n    tickcolor      = INK_SOFT,\n    width          = 18,\n    tellheight     = false,\n)\n\ncolsize!(fig.layout, 2, Fixed(90))\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}