{"spec_id":"chord-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// chord-basic: Basic Chord Diagram\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n\n// Highcharts' dependency-wheel / sankey chord series lives in an add-on module\n// that is not vendored — only the core bundle (with its SVGRenderer) is loaded.\n// So we build a genuine chord diagram natively with `chart.renderer`: annular\n// arcs for each continent and quadratic-bezier ribbons (width ∝ flow) for the\n// migration links. No other charting library is used.\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: annual migration flows between 6 continents (thousands/year) ------\n// matrix[i][j] = people moving FROM continent i TO continent j. Deterministic,\n// in-memory. Diagonal is 0 (internal moves are not inter-continental flows).\nconst labels = [\n  \"Africa\",\n  \"Asia\",\n  \"Europe\",\n  \"N. America\",\n  \"S. America\",\n  \"Oceania\",\n];\n// prettier-ignore\nconst matrix = [\n  //  Af   As   Eu   NA   SA   Oc\n  [    0, 120, 380,  90,  20,  15 ], // Africa\n  [   60,   0, 450, 520,  40, 180 ], // Asia\n  [  110,  90,   0, 320,  70,  95 ], // Europe\n  [   40,  70, 150,   0, 110,  30 ], // N. America\n  [   15,  25,  90, 280,   0,  10 ], // S. America\n  [   10,  60,  70,  50,  15,   0 ], // Oceania\n];\nconst n = labels.length;\n\n// Each continent is an abstract category, so the Imprint palette is used in\n// canonical order — Africa = brand green (palette[0]).\nconst groupColor = (i) => t.palette[i % t.palette.length];\n\n// --- Angular layout (d3-chord convention) -----------------------------------\n// Group i's arc length is proportional to its total outgoing flow (row sum).\n// Within that arc, a sub-arc of width matrix[i][j] is reserved for the link to\n// j; the ribbon for pair (i,j) joins sub[i][j] on arc i with sub[j][i] on arc j.\nconst GAP = 0.045; // radians of padding between adjacent group arcs\nconst rowSum = matrix.map((row) => row.reduce((a, b) => a + b, 0));\nconst totalFlow = rowSum.reduce((a, b) => a + b, 0);\nconst scale = (2 * Math.PI - n * GAP) / totalFlow;\n\nconst groups = []; // [startAngle, endAngle] per continent\nconst sub = []; // sub[i][j] = [startAngle, endAngle] of the i→j slice on arc i\nlet angle = -Math.PI / 2; // start at the top, sweep clockwise\nfor (let i = 0; i < n; i++) {\n  sub[i] = [];\n  const gStart = angle;\n  for (let j = 0; j < n; j++) {\n    const w = matrix[i][j] * scale;\n    sub[i][j] = [angle, angle + w];\n    angle += w;\n  }\n  groups.push([gStart, angle]);\n  angle += GAP;\n}\n\n// --- Build the chart shell, then draw with the SVG renderer ------------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginTop: 96,\n    marginBottom: 60,\n    marginLeft: 60,\n    marginRight: 60,\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"chord-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Annual migration flows between continents — ribbon width ∝ migrants (thousands), coloured by origin\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\n// Geometry helpers in pixel space (independent of any axis).\nconst cx = chart.plotLeft + chart.plotWidth / 2;\nconst cy = chart.plotTop + chart.plotHeight / 2;\nconst radiusMax = Math.min(chart.plotWidth, chart.plotHeight) / 2;\nconst ro = radiusMax - 116; // outer edge of the arc band\nconst ri = ro - 30; // inner edge of the arc band == ribbon anchor radius\n\nconst pt = (a, r) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];\nconst f = (v) => v.toFixed(2);\nconst large = (a0, a1) => (Math.abs(a1 - a0) > Math.PI ? 1 : 0);\n\n// Annular sector (a group's arc band) as an SVG path string.\nfunction arcBand(a0, a1, rIn, rOut) {\n  const [x0o, y0o] = pt(a0, rOut);\n  const [x1o, y1o] = pt(a1, rOut);\n  const [x1i, y1i] = pt(a1, rIn);\n  const [x0i, y0i] = pt(a0, rIn);\n  const la = large(a0, a1);\n  return (\n    `M ${f(x0o)} ${f(y0o)} A ${f(rOut)} ${f(rOut)} 0 ${la} 1 ${f(x1o)} ${f(y1o)} ` +\n    `L ${f(x1i)} ${f(y1i)} A ${f(rIn)} ${f(rIn)} 0 ${la} 0 ${f(x0i)} ${f(y0i)} Z`\n  );\n}\n\n// Ribbon between two sub-arcs (each [a0,a1]) at radius r, curving through centre.\nfunction ribbon([si0, si1], [sj0, sj1], r) {\n  const [ax, ay] = pt(si0, r);\n  const [bx, by] = pt(si1, r);\n  const [cxp, cyp] = pt(sj0, r);\n  const [dx, dy] = pt(sj1, r);\n  return (\n    `M ${f(ax)} ${f(ay)} A ${f(r)} ${f(r)} 0 ${large(si0, si1)} 1 ${f(bx)} ${f(by)} ` +\n    `Q ${f(cx)} ${f(cy)} ${f(cxp)} ${f(cyp)} ` +\n    `A ${f(r)} ${f(r)} 0 ${large(sj0, sj1)} 1 ${f(dx)} ${f(dy)} ` +\n    `Q ${f(cx)} ${f(cy)} ${f(ax)} ${f(ay)} Z`\n  );\n}\n\nconst g = chart.renderer.g(\"chord\").add();\n\n// Ribbons first (one per unordered pair) so the solid arcs sit on top.\nfor (let i = 0; i < n; i++) {\n  for (let j = i + 1; j < n; j++) {\n    if (matrix[i][j] === 0 && matrix[j][i] === 0) continue;\n    // Colour the ribbon by the dominant direction's origin.\n    const src = matrix[i][j] >= matrix[j][i] ? i : j;\n    const color = groupColor(src);\n    const path = chart.renderer\n      .path()\n      .attr({\n        fill: color,\n        \"fill-opacity\": 0.6,\n        stroke: t.pageBg,\n        \"stroke-width\": 0.75,\n      })\n      .add(g);\n    path.element.setAttribute(\"d\", ribbon(sub[i][j], sub[j][i], ri));\n    // Honest hover detail for the interactive HTML view (absent from the PNG).\n    const title = document.createElementNS(\n      \"http://www.w3.org/2000/svg\",\n      \"title\",\n    );\n    title.textContent =\n      `${labels[i]} → ${labels[j]}: ${matrix[i][j]}k · ` +\n      `${labels[j]} → ${labels[i]}: ${matrix[j][i]}k`;\n    path.element.appendChild(title);\n  }\n}\n\n// Group arcs + radial labels on top.\nfor (let i = 0; i < n; i++) {\n  const [a0, a1] = groups[i];\n  const arc = chart.renderer\n    .path()\n    .attr({ fill: groupColor(i), stroke: t.pageBg, \"stroke-width\": 1.5 })\n    .add(g);\n  arc.element.setAttribute(\"d\", arcBand(a0, a1, ri, ro));\n\n  const mid = (a0 + a1) / 2;\n  const [lx, ly] = pt(mid, ro + 16);\n  const cosM = Math.cos(mid);\n  const align = Math.abs(cosM) < 0.25 ? \"center\" : cosM > 0 ? \"left\" : \"right\";\n  chart.renderer\n    .text(labels[i], lx, ly + 6)\n    .attr({ align })\n    .css({ color: t.ink, fontSize: \"16px\", fontWeight: \"600\" })\n    .add(g);\n}\n\n// Static-frame timing signal for the harness.\nwindow.__anyplotReady = true;\n"}