{"spec_id":"map-connection-lines","library":"muix","language":"javascript","code":"// anyplot.ai\n// map-connection-lines: Connection Lines Map (Origin-Destination)\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n//# anyplot-orientation: landscape\n// anyplot.ai\n// map-connection-lines: Connection Lines Map (Origin-Destination)\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-08-26\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"map-connection-lines · javascript · muix · anyplot.ai\";\n\n// --- Data: major airports (in-memory, deterministic) ------------------------\nconst airports = {\n  JFK: { name: \"New York\", lat: 40.64, lon: -73.78 },\n  LHR: { name: \"London\", lat: 51.47, lon: -0.45 },\n  CDG: { name: \"Paris\", lat: 49.01, lon: 2.55 },\n  LAX: { name: \"Los Angeles\", lat: 33.94, lon: -118.41 },\n  NRT: { name: \"Tokyo\", lat: 35.76, lon: 140.39 },\n  SYD: { name: \"Sydney\", lat: -33.95, lon: 151.18 },\n  SFO: { name: \"San Francisco\", lat: 37.62, lon: -122.38 },\n  HKG: { name: \"Hong Kong\", lat: 22.31, lon: 113.91 },\n  ORD: { name: \"Chicago\", lat: 41.98, lon: -87.9 },\n  FRA: { name: \"Frankfurt\", lat: 50.03, lon: 8.57 },\n  DXB: { name: \"Dubai\", lat: 25.25, lon: 55.36 },\n  SIN: { name: \"Singapore\", lat: 1.35, lon: 103.99 },\n  GRU: { name: \"São Paulo\", lat: -23.43, lon: -46.47 },\n  JNB: { name: \"Johannesburg\", lat: -26.13, lon: 28.24 },\n  DEL: { name: \"New Delhi\", lat: 28.56, lon: 77.1 },\n  PEK: { name: \"Beijing\", lat: 40.08, lon: 116.58 },\n};\n\n// Flight routes: passenger volume in thousands per year (line weight)\nconst routes = [\n  { from: \"JFK\", to: \"LHR\", passengers: 820 },\n  { from: \"JFK\", to: \"CDG\", passengers: 610 },\n  { from: \"LAX\", to: \"NRT\", passengers: 540 },\n  { from: \"LAX\", to: \"SYD\", passengers: 310 },\n  { from: \"SFO\", to: \"HKG\", passengers: 460 },\n  { from: \"ORD\", to: \"FRA\", passengers: 390 },\n  { from: \"DXB\", to: \"LHR\", passengers: 705 },\n  { from: \"DXB\", to: \"JFK\", passengers: 480 },\n  { from: \"SIN\", to: \"SYD\", passengers: 520 },\n  { from: \"SIN\", to: \"NRT\", passengers: 445 },\n  { from: \"GRU\", to: \"JFK\", passengers: 385 },\n  { from: \"GRU\", to: \"CDG\", passengers: 260 },\n  { from: \"JNB\", to: \"DXB\", passengers: 300 },\n  { from: \"DEL\", to: \"LHR\", passengers: 560 },\n  { from: \"PEK\", to: \"LAX\", passengers: 415 },\n  { from: \"HKG\", to: \"SYD\", passengers: 330 },\n];\n\nconst passengerValues = routes.map((r) => r.passengers);\nconst minPassengers = Math.min(...passengerValues);\nconst maxPassengers = Math.max(...passengerValues);\n\n// Hub size = number of routes touching an airport\nconst degreeByAirport = {};\nroutes.forEach((r) => {\n  degreeByAirport[r.from] = (degreeByAirport[r.from] || 0) + 1;\n  degreeByAirport[r.to] = (degreeByAirport[r.to] || 0) + 1;\n});\n\n// Manual label nudges for the closely clustered European hubs (LHR/CDG/FRA)\n// so their code labels don't collide — markers stay at true coordinates.\nconst labelNudge = {\n  LHR: { dx: -18, dy: -8 },\n  CDG: { dx: -6, dy: 22 },\n  FRA: { dx: 22, dy: -2 },\n};\n\nconst lons = Object.values(airports).map((a) => a.lon);\nconst lats = Object.values(airports).map((a) => a.lat);\nconst LON_MIN = Math.min(...lons) - 14;\nconst LON_MAX = Math.max(...lons) + 14;\nconst LAT_MIN = Math.min(...lats) - 8;\nconst LAT_MAX = Math.max(...lats) + 8;\n\n// Simplified world coastlines (lon/lat vertices, hand-traced low-poly\n// continent outlines) so the chart reads as a map rather than a bare\n// lon/lat grid. Rendered via the same xScale/yScale hooks as the route\n// arcs and clipped to the drawing area, so out-of-range vertices (e.g. the\n// Canadian arctic) simply fall outside the visible plot.\nconst CONTINENTS = [\n  [\n    [-165, 65], [-140, 60], [-125, 49], [-124, 40], [-117, 32], [-105, 22], [-97, 16],\n    [-90, 14], [-88, 21], [-81, 25], [-80, 32], [-75, 40], [-70, 44], [-65, 47],\n    [-75, 55], [-95, 62], [-110, 68], [-140, 68], [-165, 65],\n  ], // North America\n  [\n    [-77, 8], [-80, 0], [-81, -6], [-71, -18], [-70, -33], [-68, -45], [-68, -54],\n    [-65, -52], [-58, -38], [-48, -25], [-40, -13], [-35, -6], [-50, 0], [-60, 8],\n    [-72, 10], [-77, 8],\n  ], // South America\n  [\n    [-9, 43], [-9, 37], [3, 36], [10, 44], [15, 38], [20, 40], [28, 41], [30, 46],\n    [38, 55], [30, 60], [10, 58], [5, 51], [-1, 51], [-5, 55], [-9, 43],\n  ], // Europe\n  [\n    [-17, 21], [-17, 14], [-10, 6], [3, 6], [9, 4], [13, -6], [18, -34], [26, -33],\n    [35, -20], [40, -3], [45, 11], [43, 12], [38, 15], [32, 31], [10, 37], [-6, 35],\n    [-17, 21],\n  ], // Africa\n  [\n    [27, 41], [35, 30], [42, 13], [56, 25], [61, 25], [72, 19], [77, 8], [80, 13],\n    [88, 22], [96, 16], [100, 13], [104, 10], [109, 21], [121, 25], [122, 31],\n    [130, 38], [140, 36], [142, 43], [135, 50], [100, 55], [60, 55], [48, 42],\n    [35, 45], [27, 41],\n  ], // Asia\n  [\n    [113, -22], [115, -32], [131, -12], [142, -11], [145, -17], [153, -28],\n    [151, -34], [140, -38], [135, -35], [113, -22],\n  ], // Australia\n];\n\n// --- Overlay: title drawn in the reserved top margin -------------------------\n// Plain SVG <text>, not ChartsText: ChartsText applies its own measured-width\n// anchor offset on top of the native SVG text-anchor, which for a long\n// middle-anchored string like this double-shifts it off-center.\nfunction MapTitle() {\n  const { width } = window.ANYPLOT_SIZE;\n  return (\n    <text x={width / 2} y={40} textAnchor=\"middle\" dominantBaseline=\"hanging\" fontSize={22} fontWeight={500} fill={t.ink}>\n      {TITLE}\n    </text>\n  );\n}\n\n// --- Overlay: simplified land masses behind the connection arcs -------------\nfunction WorldOutline() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const drawingArea = useDrawingArea();\n  return (\n    <g>\n      <defs>\n        <clipPath id=\"worldClip\">\n          <rect x={drawingArea.left} y={drawingArea.top} width={drawingArea.width} height={drawingArea.height} />\n        </clipPath>\n      </defs>\n      <g clipPath=\"url(#worldClip)\">\n        {CONTINENTS.map((points, i) => {\n          const d = points.map(([lon, lat], j) => `${j === 0 ? \"M\" : \"L\"} ${xScale(lon)},${yScale(lat)}`).join(\" \") + \" Z\";\n          return <path key={i} d={d} fill={t.grid} fillOpacity={0.6} stroke={t.inkSoft} strokeWidth={1} strokeOpacity={0.5} />;\n        })}\n      </g>\n    </g>\n  );\n}\n\n// --- Overlay: geodesic-style connection arcs + endpoint markers -------------\nfunction ConnectionOverlay() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g>\n      {routes.map((route, i) => {\n        const origin = airports[route.from];\n        const dest = airports[route.to];\n        const x1 = xScale(origin.lon);\n        const y1 = yScale(origin.lat);\n        const x2 = xScale(dest.lon);\n        const y2 = yScale(dest.lat);\n\n        const dx = x2 - x1;\n        const dy = y2 - y1;\n        const dist = Math.sqrt(dx * dx + dy * dy);\n        const midX = (x1 + x2) / 2;\n        const midY = (y1 + y2) / 2;\n        // Perpendicular offset bows the line into a great-circle-like arc.\n        const ux = dist === 0 ? 0 : -dy / dist;\n        const uy = dist === 0 ? 0 : dx / dist;\n        const bow = dist * 0.16;\n        const controlX = midX + ux * bow;\n        const controlY = midY + uy * bow;\n\n        const ratio =\n          (route.passengers - minPassengers) / (maxPassengers - minPassengers || 1);\n        const strokeWidth = 2.5 + ratio * 6;\n        const strokeOpacity = 0.3 + ratio * 0.3;\n\n        return (\n          <path\n            key={`${route.from}-${route.to}-${i}`}\n            d={`M ${x1},${y1} Q ${controlX},${controlY} ${x2},${y2}`}\n            fill=\"none\"\n            stroke={t.palette[0]}\n            strokeWidth={strokeWidth}\n            strokeOpacity={strokeOpacity}\n            strokeLinecap=\"round\"\n          />\n        );\n      })}\n      {Object.entries(airports).map(([code, airport]) => {\n        const cx = xScale(airport.lon);\n        const cy = yScale(airport.lat);\n        const radius = 5 + (degreeByAirport[code] || 1) * 1.8;\n        const nudge = labelNudge[code] || { dx: 0, dy: -(radius + 8) };\n        return (\n          <g key={code}>\n            <circle\n              cx={cx}\n              cy={cy}\n              r={radius}\n              fill={t.palette[0]}\n              stroke={t.pageBg}\n              strokeWidth={2}\n            />\n            <text x={cx + nudge.dx} y={cy + nudge.dy} textAnchor=\"middle\" fontSize={14} fontWeight={500} fill={t.ink}>\n              {code}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Overlay: caption explaining the encoding --------------------------------\nfunction Caption() {\n  const drawingArea = useDrawingArea();\n  return (\n    <text\n      x={drawingArea.left}\n      y={drawingArea.top + drawingArea.height + 74}\n      textAnchor=\"start\"\n      dominantBaseline=\"hanging\"\n      fontSize={14}\n      fontWeight={400}\n      fill={t.inkSoft}\n    >\n      Line thickness and opacity encode annual passenger volume (thousands).\n    </text>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={window.ANYPLOT_SIZE.width}\n      height={window.ANYPLOT_SIZE.height}\n      margin={{ top: 90, right: 60, bottom: 100, left: 70 }}\n      series={[]}\n      skipAnimation\n      disableAxisListener\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: LON_MIN,\n          max: LON_MAX,\n          label: \"Longitude (°)\",\n          valueFormatter: (v) => `${v}°`,\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: LAT_MIN,\n          max: LAT_MAX,\n          label: \"Latitude (°)\",\n          valueFormatter: (v) => `${v}°`,\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n    >\n      <ChartsGrid horizontal vertical />\n      <WorldOutline />\n      <ChartsReferenceLine\n        y={0}\n        label=\"Equator\"\n        labelAlign=\"end\"\n        lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 6\", strokeOpacity: 0.6 }}\n        labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n      />\n      <ChartsReferenceLine\n        x={0}\n        label=\"Prime Meridian\"\n        labelAlign=\"start\"\n        lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 6\", strokeOpacity: 0.6 }}\n        labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n      />\n      <ConnectionOverlay />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <MapTitle />\n      <Caption />\n    </ChartContainer>\n  );\n}\n"}