{"spec_id":"chord-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// chord-basic: Basic Chord Diagram\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: migration flows between 6 continents (thousands/year) -----------\n// flow[i][j] = directed migration from entity i -> entity j. Diagonal is 0\n// (no self-flow). Bidirectional pairs differ, so each ribbon has two unequal\n// ends — exactly what a chord diagram reveals.\nconst labels = [\"Africa\", \"Asia\", \"Europe\", \"N. America\", \"S. America\", \"Oceania\"];\nconst flow = [\n  //  Af   As   Eu   NA   SA   Oc\n  [   0,  35,  92,  58,  11,   8],  // Africa\n  [  21,   0, 124, 138,  16,  46],  // Asia\n  [  27,  54,   0,  96,  31,  34],  // Europe\n  [  13,  41,  72,   0,  52,  11],  // N. America\n  [   9,  17,  66,  84,   0,   6],  // S. America\n  [   4,  29,  23,  15,   5,   0],  // Oceania\n];\n\n// Each entity's arc length is proportional to its total outgoing flow, so the\n// doughnut arcs and the ribbon slots tile the same angular budget.\nconst rowSum = flow.map((row) => row.reduce((a, b) => a + b, 0));\n\n// Each entity keeps a distinct Imprint colour (brand green leads at slot 0).\nconst entityColors = labels.map((_, i) => t.palette[i % t.palette.length]);\n\n// --- Theme-adaptive chrome -------------------------------------------------\nconst PAGE_BG = t.pageBg;\nconst INK = t.ink;\n\nconst hexToRgba = (hex, alpha) => {\n  const h = hex.replace(\"#\", \"\");\n  const r = parseInt(h.slice(0, 2), 16);\n  const g = parseInt(h.slice(2, 4), 16);\n  const b = parseInt(h.slice(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\n\n// --- Chord ribbon plugin ---------------------------------------------------\n// Chart.js has no native chord type, but its core plugin API exposes the live\n// canvas + the doughnut's computed arc geometry. We anchor each flow ribbon to\n// the inner edge of the ring and bow it through the centre — ribbon end-width\n// is proportional to the flow value, drawn largest-first so thin flows stay on\n// top. No external library, no community plugin: pure Chart.js extensibility.\nconst chordRibbons = {\n  id: \"chordRibbons\",\n  afterDatasetsDraw(chart) {\n    const arcs = chart.getDatasetMeta(0).data;\n    if (!arcs.length) return;\n\n    const { x: cx, y: cy } = arcs[0].getProps([\"x\", \"y\"], true);\n    const innerR = arcs[0].getProps([\"innerRadius\"], true).innerRadius;\n    const n = labels.length;\n\n    // Angular interval [a0, a1] of every ordered slot (i -> j) inside arc i.\n    const slot = arcs.map((arc, i) => {\n      const { startAngle, endAngle } = arc.getProps([\"startAngle\", \"endAngle\"], true);\n      const span = endAngle - startAngle;\n      let acc = 0;\n      return flow[i].map((w) => {\n        const a0 = startAngle + (acc / rowSum[i]) * span;\n        acc += w;\n        const a1 = startAngle + (acc / rowSum[i]) * span;\n        return [a0, a1];\n      });\n    });\n\n    const pointAt = (angle, r) => [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];\n\n    // One ribbon per unordered pair, ordered by combined flow (big first).\n    const pairs = [];\n    for (let i = 0; i < n; i++) {\n      for (let j = i + 1; j < n; j++) {\n        if (flow[i][j] + flow[j][i] === 0) continue;\n        pairs.push([i, j, flow[i][j] + flow[j][i]]);\n      }\n    }\n    pairs.sort((a, b) => b[2] - a[2]);\n\n    const ctx = chart.ctx;\n    ctx.save();\n    ctx.lineJoin = \"round\";\n    for (const [i, j] of pairs) {\n      const [si0, si1] = slot[i][j]; // end resting on entity i\n      const [sj0, sj1] = slot[j][i]; // end resting on entity j\n      const [xi, yi] = pointAt(si0, innerR);\n\n      ctx.beginPath();\n      ctx.moveTo(xi, yi);\n      ctx.arc(cx, cy, innerR, si0, si1);            // ride entity i's ring\n      const [xj, yj] = pointAt(sj0, innerR);\n      ctx.quadraticCurveTo(cx, cy, xj, yj);         // bow through the centre\n      ctx.arc(cx, cy, innerR, sj0, sj1);            // ride entity j's ring\n      ctx.quadraticCurveTo(cx, cy, xi, yi);         // bow back\n      ctx.closePath();\n\n      // Colour by the dominant direction's entity for a readable hierarchy.\n      const dominant = flow[i][j] >= flow[j][i] ? i : j;\n      ctx.fillStyle = hexToRgba(entityColors[dominant], 0.6);\n      ctx.fill();\n      ctx.lineWidth = 1.5;\n      ctx.strokeStyle = hexToRgba(entityColors[dominant], 0.9);\n      ctx.stroke();\n    }\n    ctx.restore();\n  },\n};\n\n// --- Mount -----------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart: doughnut ring of entities + chord ribbon plugin ----------------\nnew Chart(canvas, {\n  type: \"doughnut\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Total outgoing migration\",\n        data: rowSum,\n        backgroundColor: entityColors,\n        borderColor: PAGE_BG,\n        borderWidth: 3,\n        hoverOffset: 0,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    cutout: \"84%\",\n    layout: { padding: 28 },\n    plugins: {\n      title: {\n        display: true,\n        text: \"chord-basic · javascript · chartjs · anyplot.ai\",\n        color: INK,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 4, bottom: 18 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: INK,\n          font: { size: 16 },\n          padding: 18,\n          boxWidth: 16,\n          boxHeight: 16,\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: (item) => `${item.label}: ${item.parsed} k outgoing`,\n        },\n      },\n    },\n  },\n  plugins: [chordRibbons],\n});\n"}