{"spec_id":"map-connection-lines","library":"echarts","language":"javascript","code":"// anyplot.ai\n// map-connection-lines: Connection Lines Map (Origin-Destination)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic): a global hub-airport flight network --\nconst airports = [\n  { code: \"JFK\", lon: -73.7781, lat: 40.6413 },\n  { code: \"LAX\", lon: -118.4085, lat: 33.9416 },\n  { code: \"GRU\", lon: -46.4731, lat: -23.4356 },\n  { code: \"LHR\", lon: -0.4614, lat: 51.47 },\n  { code: \"FRA\", lon: 8.5706, lat: 50.0379 },\n  { code: \"JNB\", lon: 28.246, lat: -26.1392 },\n  { code: \"DXB\", lon: 55.3644, lat: 25.2532 },\n  { code: \"DEL\", lon: 77.1025, lat: 28.5562 },\n  { code: \"SIN\", lon: 103.9915, lat: 1.3644 },\n  { code: \"HKG\", lon: 113.9185, lat: 22.308 },\n  { code: \"HND\", lon: 139.7798, lat: 35.5494 },\n  { code: \"SYD\", lon: 151.1772, lat: -33.9399 },\n];\n\n// [origin, destination, annual passengers in thousands]\nconst routes = [\n  [\"JFK\", \"LHR\", 2800],\n  [\"JFK\", \"FRA\", 1200],\n  [\"JFK\", \"GRU\", 1100],\n  [\"LAX\", \"JFK\", 1000],\n  [\"LAX\", \"LHR\", 1300],\n  [\"GRU\", \"LHR\", 750],\n  [\"LHR\", \"DXB\", 2200],\n  [\"LHR\", \"JNB\", 950],\n  [\"FRA\", \"DXB\", 1800],\n  [\"FRA\", \"DEL\", 800],\n  [\"DXB\", \"JNB\", 1300],\n  [\"DXB\", \"SIN\", 2600],\n  [\"DXB\", \"HKG\", 1700],\n  [\"DEL\", \"SIN\", 1100],\n  [\"SIN\", \"HKG\", 2400],\n  [\"SIN\", \"SYD\", 1600],\n  [\"HKG\", \"HND\", 1900],\n  [\"HND\", \"SYD\", 1000],\n];\n\nconst byCode = Object.fromEntries(airports.map((a) => [a.code, a]));\nconst maxPassengers = Math.max(...routes.map((r) => r[2]));\n\nconst traffic = {};\nfor (const [origin, dest, passengers] of routes) {\n  traffic[origin] = (traffic[origin] || 0) + passengers;\n  traffic[dest] = (traffic[dest] || 0) + passengers;\n}\n\n// Hand-simplified continent silhouettes (lon/lat vertex lists) so the plot\n// reads as a base map — no bundled world GeoJSON or network fetch is\n// available to this offline browser runtime, so the coastlines are\n// hard-coded low-vertex approximations rather than authoritative data.\nconst landmasses = [\n  [[-170, 70], [-150, 72], [-130, 70], [-125, 60], [-95, 68], [-80, 62], [-65, 50], [-55, 48], [-60, 45], [-75, 35], [-80, 25], [-97, 26], [-105, 20], [-115, 30], [-124, 40], [-124, 48], [-130, 55], [-140, 60], [-165, 60], [-170, 70]],\n  [[-80, 10], [-77, 5], [-79, -5], [-70, -18], [-70, -30], [-73, -45], [-68, -55], [-65, -55], [-58, -38], [-48, -25], [-35, -8], [-50, 2], [-60, 8], [-70, 12], [-80, 10]],\n  [[-10, 43], [-9, 53], [0, 51], [5, 58], [10, 58], [20, 60], [30, 60], [40, 66], [35, 55], [28, 45], [22, 40], [15, 38], [10, 44], [0, 44], [-5, 43], [-10, 43]],\n  [[-17, 15], [-16, 27], [-6, 35], [10, 37], [20, 33], [32, 31], [35, 20], [43, 12], [51, 12], [42, -1], [40, -15], [35, -25], [27, -33], [18, -34], [12, -18], [8, -5], [-5, 5], [-17, 15]],\n  [[40, 66], [60, 68], [80, 73], [105, 75], [130, 72], [140, 60], [135, 50], [142, 45], [130, 32], [122, 25], [110, 20], [100, 10], [95, 5], [80, 8], [70, 20], [60, 25], [50, 30], [45, 40], [40, 50], [35, 55], [40, 66]],\n  [[113, -22], [122, -18], [130, -12], [136, -12], [142, -11], [145, -16], [153, -28], [150, -37], [140, -38], [130, -32], [115, -35], [113, -22]],\n];\n\n// The DXB/DEL/HKG/SIN hub cluster sits close together, so its arcs converge\n// and overlap; bow those routes out further and thin them slightly so\n// individual connections stay distinguishable.\nconst denseCluster = new Set([\"DXB\", \"DEL\", \"HKG\", \"SIN\"]);\n// LHR and FRA sit ~9° of longitude apart, close enough that two top-anchored\n// labels crowd each other; drop FRA's label below its marker instead.\nconst labelPositionOverrides = { FRA: \"bottom\" };\n\nconst lineData = routes.map(([origin, dest, passengers]) => {\n  const share = passengers / maxPassengers;\n  const inDenseCluster = denseCluster.has(origin) && denseCluster.has(dest);\n  return {\n    coords: [\n      [byCode[origin].lon, byCode[origin].lat],\n      [byCode[dest].lon, byCode[dest].lat],\n    ],\n    value: passengers,\n    lineStyle: {\n      width: (1.2 + share * 5) * (inDenseCluster ? 0.8 : 1),\n      opacity: 0.28 + share * 0.32,\n      curveness: inDenseCluster ? 0.32 : 0.2,\n    },\n  };\n});\n\nconst nodeData = airports.map((a) => ({\n  name: a.code,\n  value: [a.lon, a.lat, traffic[a.code]],\n  label: labelPositionOverrides[a.code] ? { position: labelPositionOverrides[a.code] } : undefined,\n}));\n\n// --- Init --------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------\nconst title = \"Global Flight Routes · map-connection-lines · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: [t.palette[0]],\n  title: {\n    text: title,\n    subtext: \"Line width & opacity scale with annual passenger volume (thousands) · marker size scales with hub traffic\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) =>\n      p.seriesType === \"lines\" ? `${p.data.value}k passengers/yr` : `${p.name}: ${p.value[2]}k passengers`,\n  },\n  grid: { left: 110, right: 90, top: 190, bottom: 110, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    min: -180,\n    max: 180,\n    interval: 30,\n    name: \"Longitude\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      formatter: (v) => `${Math.abs(v)}°${v > 0 ? \"E\" : v < 0 ? \"W\" : \"\"}`,\n    },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    min: -60,\n    max: 80,\n    interval: 30,\n    name: \"Latitude\",\n    nameLocation: \"middle\",\n    nameGap: 55,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      formatter: (v) => `${Math.abs(v)}°${v > 0 ? \"N\" : v < 0 ? \"S\" : \"\"}`,\n    },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Landmasses\",\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      silent: true,\n      z: 1,\n      data: landmasses.map((_, i) => i),\n      renderItem: (params, api) => ({\n        type: \"polygon\",\n        shape: { points: landmasses[params.dataIndex].map((p) => api.coord(p)) },\n        style: { fill: t.grid, opacity: 0.6, stroke: t.inkSoft, lineWidth: 1, strokeOpacity: 0.4 },\n      }),\n    },\n    {\n      name: \"Routes\",\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      symbol: [\"none\", \"arrow\"],\n      symbolSize: [0, 8],\n      data: lineData,\n      lineStyle: { color: t.palette[0] },\n      z: 2,\n    },\n    {\n      name: \"Airports\",\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: nodeData,\n      symbolSize: (val) => 12 + Math.sqrt(val[2]) * 0.55,\n      itemStyle: { color: t.palette[0], opacity: 0.9, borderColor: t.pageBg, borderWidth: 1.5 },\n      label: { show: true, formatter: \"{b}\", position: \"top\", color: t.ink, fontSize: 13, fontWeight: 500 },\n      z: 3,\n    },\n  ],\n});\n"}