{"spec_id":"map-connection-lines","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// map-connection-lines: Connection Lines Map (Origin-Destination)\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Layout constants --------------------------------------------------------\n// The mount is a fixed 1600×900 CSS box (harness renders at deviceScaleFactor 2\n// -> 3200×1800 PNG). Margins are explicit (not \"spacing\") so the plot-area pixel\n// size is known at codegen time, letting the latitude span be derived from the\n// longitude span + the plot's pixel aspect ratio (keeps the stylized coastlines\n// from looking stretched).\nconst MOUNT_WIDTH = 1600;\nconst MOUNT_HEIGHT = 900;\nconst MARGIN_LEFT = 70;\nconst MARGIN_RIGHT = 40;\nconst MARGIN_TOP = 110;\nconst MARGIN_BOTTOM = 195; // x-axis title/ticks + gap + hand-drawn route-width legend\nconst PLOT_WIDTH = MOUNT_WIDTH - MARGIN_LEFT - MARGIN_RIGHT;\nconst PLOT_HEIGHT = MOUNT_HEIGHT - MARGIN_TOP - MARGIN_BOTTOM;\n\n// Fictional trans-oceanic geography (the core Highcharts bundle has no maps\n// module — see prompts/library/highcharts.md — so the coastline is a hand-drawn\n// stylized curve, not surveyed data). Longitude/latitude are still genuine\n// coordinates so the axes read like a real map.\nconst LON_MIN = -100;\nconst LON_MAX = 40;\nconst LON_RANGE = LON_MAX - LON_MIN;\nconst LAT_MID = 30;\nconst LAT_RANGE = LON_RANGE * (PLOT_HEIGHT / PLOT_WIDTH);\nconst LAT_MIN = LAT_MID - LAT_RANGE / 2;\nconst LAT_MAX = LAT_MID + LAT_RANGE / 2;\n\n// --- Stylized coastlines (base-map context) ----------------------------------\n// North landmass sits above northCoastY(lon); south landmass sits below\n// southCoastY(lon). The band between them is open ocean, wide enough for the\n// route arcs to bow through without hugging the coastline.\nfunction northCoastY(x) {\n  const base = LAT_MID + 0.17 * LAT_RANGE;\n  return (\n    base +\n    0.09 * LAT_RANGE * Math.sin((x / LON_RANGE) * Math.PI * 2.3 + 0.4) +\n    0.04 * LAT_RANGE * Math.sin((x / LON_RANGE) * Math.PI * 5.4 + 1.6)\n  );\n}\nfunction southCoastY(x) {\n  const base = LAT_MID - 0.17 * LAT_RANGE;\n  return (\n    base -\n    0.09 * LAT_RANGE * Math.sin((x / LON_RANGE) * Math.PI * 1.9 + 1.1) -\n    0.04 * LAT_RANGE * Math.sin((x / LON_RANGE) * Math.PI * 4.6 + 0.2)\n  );\n}\nconst COAST_SAMPLES = 48;\nconst northCoastline = [];\nconst southCoastline = [];\nfor (let i = 0; i <= COAST_SAMPLES; i++) {\n  const x = LON_MIN + (i / COAST_SAMPLES) * LON_RANGE;\n  northCoastline.push([x, northCoastY(x)]);\n  southCoastline.push([x, southCoastY(x)]);\n}\n\n// --- Ports (origin/destination endpoints) ------------------------------------\n// Longitude picked freely; latitude is pinned to the local coastline value plus\n// a small inset so every port sits right at the shoreline.\nconst PORT_INSET = 0.03 * LAT_RANGE;\nfunction northPort(name, lon) {\n  return { name, lon, lat: northCoastY(lon) + PORT_INSET };\n}\nfunction southPort(name, lon) {\n  return { name, lon, lat: southCoastY(lon) - PORT_INSET };\n}\n\nconst NORTH_PORTS = [\n  northPort(\"Portavale\", -88),\n  northPort(\"Nordhaven\", -55),\n  northPort(\"Calderport\", -25),\n  northPort(\"Ashcliff\", 0),\n  northPort(\"Brightmoor\", 25),\n];\nconst SOUTH_PORTS = [\n  southPort(\"Suncastle\", -92),\n  southPort(\"Tidemark\", -58),\n  southPort(\"Coralbay\", -28),\n  southPort(\"Saltmere\", 2),\n  southPort(\"Palmcross\", 28),\n];\n\n// --- Routes: [originIdx, destIdx, annual cargo volume in thousand TEU] -------\n// Nordhaven and Tidemark are the two hub ports — busiest by connection count,\n// carrying the story of a hub-and-spoke shipping network.\nconst ROUTES = [\n  [0, 1, 180],\n  [0, 3, 90],\n  [0, 4, 55],\n  [1, 0, 260],\n  [1, 1, 310],\n  [1, 2, 140],\n  [1, 3, 95],\n  [1, 4, 120],\n  [2, 1, 150],\n  [2, 2, 80],\n  [2, 4, 45],\n  [3, 1, 175],\n  [3, 3, 100],\n  [3, 4, 60],\n  [4, 0, 70],\n  [4, 1, 130],\n];\nconst ROUTE_VALUES = ROUTES.map((r) => r[2]);\nconst MIN_VALUE = Math.min(...ROUTE_VALUES);\nconst MAX_VALUE = Math.max(...ROUTE_VALUES);\nconst MIN_WIDTH = 1.5;\nconst MAX_WIDTH = 6.5;\nfunction widthForValue(value) {\n  const frac = (value - MIN_VALUE) / (MAX_VALUE - MIN_VALUE);\n  return MIN_WIDTH + frac * (MAX_WIDTH - MIN_WIDTH);\n}\n\n// --- Arc geometry -------------------------------------------------------------\n// Quadratic Bezier with the control point's longitude fixed at the midpoint\n// collapses x(t) to a plain linear interpolation between the endpoints, so the\n// sampled longitude is always monotonic — each arc is safe as its own\n// Highcharts line series (core `requireSorting` would otherwise reorder a\n// non-monotonic point list and scramble the curve, exactly like the coastline\n// pitfall documented in prompts/library/highcharts.md-adjacent implementations).\n// The latitude control point bows toward the ocean's mid-line, approximating\n// the curvature of a great-circle path.\nconst ARC_SAMPLES = 28;\nfunction arcPoints(origin, dest) {\n  const avgLat = (origin.lat + dest.lat) / 2;\n  // Capped so the bulge stays inside the open-ocean band regardless of route\n  // length — an uncapped bow on the longest routes would arc into the\n  // landmasses instead of curving through water.\n  const bow = Math.min(8, Math.max(5, 0.12 * Math.abs(dest.lon - origin.lon)));\n  // Bow toward the ocean's mid-latitude so the arc bulges through open water\n  // instead of cutting the straight chord closer to either coastline.\n  const controlLat = avgLat + Math.sign(LAT_MID - avgLat || 1) * bow;\n  const points = [];\n  for (let i = 0; i <= ARC_SAMPLES; i++) {\n    const tt = i / ARC_SAMPLES;\n    const lon = origin.lon + tt * (dest.lon - origin.lon);\n    const lat =\n      (1 - tt) * (1 - tt) * origin.lat +\n      2 * (1 - tt) * tt * controlLat +\n      tt * tt * dest.lat;\n    points.push([lon, lat]);\n  }\n  return points;\n}\n\n// --- Colors --------------------------------------------------------------\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nconst brandRgb = hexToRgb(t.palette[0]);\nconst routeColor = `rgba(${brandRgb[0]},${brandRgb[1]},${brandRgb[2]},0.45)`;\nconst landRgb = hexToRgb(t.inkSoft);\nconst landColor = `rgba(${landRgb[0]},${landRgb[1]},${landRgb[2]},0.14)`; // land -> neutral chrome, recessive\nconst waterRgb = hexToRgb(t.palette[2]); // #4467A3 -> semantic water/sky exception\nconst waterColor = `rgba(${waterRgb[0]},${waterRgb[1]},${waterRgb[2]},0.07)`;\n\n// --- Port markers, sized by connection count ----------------------------------\nfunction portDegree(list, idx) {\n  return ROUTES.filter((r) =>\n    list === NORTH_PORTS ? r[0] === idx : r[1] === idx,\n  ).length;\n}\nfunction portData(list) {\n  return list.map((p, idx) => {\n    const degree = portDegree(list, idx);\n    return {\n      x: p.lon,\n      y: p.lat,\n      name: p.name,\n      degree,\n      marker: { radius: 5 + degree * 1.1 },\n    };\n  });\n}\nconst portSeriesData = [...portData(NORTH_PORTS), ...portData(SOUTH_PORTS)];\n\n// --- Route (arc) series --------------------------------------------------\nconst routeSeries = ROUTES.map(([oIdx, dIdx, value]) => {\n  const origin = NORTH_PORTS[oIdx];\n  const dest = SOUTH_PORTS[dIdx];\n  return {\n    type: \"line\",\n    name: `${origin.name} → ${dest.name}`,\n    data: arcPoints(origin, dest),\n    color: routeColor,\n    lineWidth: widthForValue(value),\n    marker: { enabled: false },\n    showInLegend: false,\n    zIndex: 2,\n    custom: { value },\n    tooltip: {\n      pointFormatter() {\n        return `<b>${this.series.name}</b><br/>Cargo: ${this.series.userOptions.custom.value}k TEU / yr`;\n      },\n    },\n  };\n});\n\n// --- Chart -----------------------------------------------------------------\nconst title = \"map-connection-lines · javascript · highcharts · anyplot.ai\";\n\nfunction drawWidthLegend(chart) {\n  const r = chart.renderer;\n  const x0 = chart.plotLeft;\n  const y0 = chart.plotTop + chart.plotHeight + 90;\n  const gap = 150;\n  const samples = [\n    { label: \"Lower volume\", w: MIN_WIDTH },\n    { label: \"Higher volume\", w: MAX_WIDTH },\n  ];\n  r.text(\"Route width ∝ annual cargo volume\", x0, y0 - 14)\n    .css({ color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n  samples.forEach((s, i) => {\n    const lx = x0 + i * gap;\n    r.path([\"M\", lx, y0 + 10, \"L\", lx + 60, y0 + 10])\n      .attr({\n        \"stroke-width\": s.w,\n        stroke: routeColor,\n        \"stroke-linecap\": \"round\",\n      })\n      .add();\n    r.text(s.label, lx, y0 + 32)\n      .css({ color: t.inkSoft, fontSize: \"12px\" })\n      .add();\n  });\n}\n\nfunction drawHubLabels(chart) {\n  const hubs = [NORTH_PORTS[1], SOUTH_PORTS[1]]; // Nordhaven, Tidemark\n  hubs.forEach((h) => {\n    const px = chart.xAxis[0].toPixels(h.lon, false);\n    const py =\n      chart.yAxis[0].toPixels(h.lat, false) + (h === NORTH_PORTS[1] ? -20 : 30);\n    chart.renderer\n      .text(h.name, px, py)\n      .attr({ align: \"center\" })\n      .css({ color: t.ink, fontSize: \"13px\", fontWeight: \"700\" })\n      .add();\n  });\n}\n\nfunction drawOceanLabel(chart) {\n  const px = chart.xAxis[0].toPixels(LON_MIN + 0.05 * LON_RANGE, false);\n  const py = chart.yAxis[0].toPixels(LAT_MID, false);\n  chart.renderer\n    .text(\"Open ocean\", px, py)\n    .css({ color: t.inkSoft, fontSize: \"13px\", fontStyle: \"italic\" })\n    .add();\n}\n\nHighcharts.chart(\n  \"container\",\n  {\n    chart: {\n      type: \"line\",\n      backgroundColor: \"transparent\",\n      plotBackgroundColor: waterColor, // whole plot area reads as ocean; land fills sit on top\n      animation: false,\n      style: { fontFamily: \"inherit\" },\n      marginLeft: MARGIN_LEFT,\n      marginRight: MARGIN_RIGHT,\n      marginTop: MARGIN_TOP,\n      marginBottom: MARGIN_BOTTOM,\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n      text: title,\n      align: \"left\",\n      style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    subtitle: {\n      text: \"Fictional trans-oceanic shipping network — 16 routes between 10 ports, curved to avoid overlap and echo geodesic paths\",\n      align: \"left\",\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    xAxis: {\n      min: LON_MIN,\n      max: LON_MAX,\n      startOnTick: false,\n      endOnTick: false,\n      title: {\n        text: \"Longitude (°)\",\n        style: { color: t.inkSoft, fontSize: \"16px\" },\n      },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      gridLineColor: t.grid,\n      gridLineWidth: 1,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    yAxis: {\n      min: LAT_MIN,\n      max: LAT_MAX,\n      startOnTick: false,\n      endOnTick: false,\n      title: {\n        text: \"Latitude (°)\",\n        style: { color: t.inkSoft, fontSize: \"16px\" },\n      },\n      lineColor: t.inkSoft,\n      tickColor: t.inkSoft,\n      gridLineColor: t.grid,\n      gridLineWidth: 1,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    legend: { enabled: false },\n    tooltip: {\n      backgroundColor: t.elevatedBg,\n      borderColor: t.inkSoft,\n      style: { color: t.ink, fontSize: \"13px\" },\n      useHTML: false,\n    },\n    plotOptions: {\n      series: { animation: false },\n    },\n    series: [\n      {\n        type: \"area\",\n        name: \"North landmass\",\n        data: northCoastline,\n        threshold: LAT_MAX,\n        color: landColor,\n        lineWidth: 1.5,\n        lineColor: landColor,\n        marker: { enabled: false },\n        enableMouseTracking: false,\n        showInLegend: false,\n        zIndex: 0,\n      },\n      {\n        type: \"area\",\n        name: \"South landmass\",\n        data: southCoastline,\n        threshold: LAT_MIN,\n        color: landColor,\n        lineWidth: 1.5,\n        lineColor: landColor,\n        marker: { enabled: false },\n        enableMouseTracking: false,\n        showInLegend: false,\n        zIndex: 0,\n      },\n      ...routeSeries,\n      {\n        type: \"scatter\",\n        name: \"Ports\",\n        data: portSeriesData,\n        color: t.palette[0],\n        marker: { symbol: \"circle\", lineWidth: 1.5, lineColor: t.pageBg },\n        showInLegend: false,\n        zIndex: 3,\n        tooltip: {\n          pointFormatter() {\n            return `<b>${this.name}</b><br/>${this.degree} route${this.degree === 1 ? \"\" : \"s\"}`;\n          },\n        },\n      },\n    ],\n  },\n  function (chart) {\n    drawWidthLegend(chart);\n    drawHubLabels(chart);\n    drawOceanLabel(chart);\n  },\n);\n"}