Basic Chord Diagram — Chart.js

A chord diagram displays relationships or flows between entities arranged around a circle's perimeter. Arcs (chords) connect related entities, with chord width proportional to the flow magnitude. This visualization excels at revealing the overall structure of connections and identifying the strongest relationships within a system.

Basic Chord Diagram rendered with Chart.js

JavaScript source (Chart.js)

// anyplot.ai
// chord-basic: Basic Chord Diagram
// Library: chartjs 4.4.7 | JavaScript 22.22.3
// Quality: 90/100 | Created: 2026-06-17
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;

// --- Data: migration flows between 6 continents (thousands/year) -----------
// flow[i][j] = directed migration from entity i -> entity j. Diagonal is 0
// (no self-flow). Bidirectional pairs differ, so each ribbon has two unequal
// ends — exactly what a chord diagram reveals.
const labels = ["Africa", "Asia", "Europe", "N. America", "S. America", "Oceania"];
const flow = [
  //  Af   As   Eu   NA   SA   Oc
  [   0,  35,  92,  58,  11,   8],  // Africa
  [  21,   0, 124, 138,  16,  46],  // Asia
  [  27,  54,   0,  96,  31,  34],  // Europe
  [  13,  41,  72,   0,  52,  11],  // N. America
  [   9,  17,  66,  84,   0,   6],  // S. America
  [   4,  29,  23,  15,   5,   0],  // Oceania
];

// Each entity's arc length is proportional to its total outgoing flow, so the
// doughnut arcs and the ribbon slots tile the same angular budget.
const rowSum = flow.map((row) => row.reduce((a, b) => a + b, 0));

// Each entity keeps a distinct Imprint colour (brand green leads at slot 0).
const entityColors = labels.map((_, i) => t.palette[i % t.palette.length]);

// --- Theme-adaptive chrome -------------------------------------------------
const PAGE_BG = t.pageBg;
const INK = t.ink;

const hexToRgba = (hex, alpha) => {
  const h = hex.replace("#", "");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};

// --- Chord ribbon plugin ---------------------------------------------------
// Chart.js has no native chord type, but its core plugin API exposes the live
// canvas + the doughnut's computed arc geometry. We anchor each flow ribbon to
// the inner edge of the ring and bow it through the centre — ribbon end-width
// is proportional to the flow value, drawn largest-first so thin flows stay on
// top. No external library, no community plugin: pure Chart.js extensibility.
const chordRibbons = {
  id: "chordRibbons",
  afterDatasetsDraw(chart) {
    const arcs = chart.getDatasetMeta(0).data;
    if (!arcs.length) return;

    const { x: cx, y: cy } = arcs[0].getProps(["x", "y"], true);
    const innerR = arcs[0].getProps(["innerRadius"], true).innerRadius;
    const n = labels.length;

    // Angular interval [a0, a1] of every ordered slot (i -> j) inside arc i.
    const slot = arcs.map((arc, i) => {
      const { startAngle, endAngle } = arc.getProps(["startAngle", "endAngle"], true);
      const span = endAngle - startAngle;
      let acc = 0;
      return flow[i].map((w) => {
        const a0 = startAngle + (acc / rowSum[i]) * span;
        acc += w;
        const a1 = startAngle + (acc / rowSum[i]) * span;
        return [a0, a1];
      });
    });

    const pointAt = (angle, r) => [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];

    // One ribbon per unordered pair, ordered by combined flow (big first).
    const pairs = [];
    for (let i = 0; i < n; i++) {
      for (let j = i + 1; j < n; j++) {
        if (flow[i][j] + flow[j][i] === 0) continue;
        pairs.push([i, j, flow[i][j] + flow[j][i]]);
      }
    }
    pairs.sort((a, b) => b[2] - a[2]);

    const ctx = chart.ctx;
    ctx.save();
    ctx.lineJoin = "round";
    for (const [i, j] of pairs) {
      const [si0, si1] = slot[i][j]; // end resting on entity i
      const [sj0, sj1] = slot[j][i]; // end resting on entity j
      const [xi, yi] = pointAt(si0, innerR);

      ctx.beginPath();
      ctx.moveTo(xi, yi);
      ctx.arc(cx, cy, innerR, si0, si1);            // ride entity i's ring
      const [xj, yj] = pointAt(sj0, innerR);
      ctx.quadraticCurveTo(cx, cy, xj, yj);         // bow through the centre
      ctx.arc(cx, cy, innerR, sj0, sj1);            // ride entity j's ring
      ctx.quadraticCurveTo(cx, cy, xi, yi);         // bow back
      ctx.closePath();

      // Colour by the dominant direction's entity for a readable hierarchy.
      const dominant = flow[i][j] >= flow[j][i] ? i : j;
      ctx.fillStyle = hexToRgba(entityColors[dominant], 0.6);
      ctx.fill();
      ctx.lineWidth = 1.5;
      ctx.strokeStyle = hexToRgba(entityColors[dominant], 0.9);
      ctx.stroke();
    }
    ctx.restore();
  },
};

// --- Mount -----------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Chart: doughnut ring of entities + chord ribbon plugin ----------------
new Chart(canvas, {
  type: "doughnut",
  data: {
    labels,
    datasets: [
      {
        label: "Total outgoing migration",
        data: rowSum,
        backgroundColor: entityColors,
        borderColor: PAGE_BG,
        borderWidth: 3,
        hoverOffset: 0,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    cutout: "84%",
    layout: { padding: 28 },
    plugins: {
      title: {
        display: true,
        text: "chord-basic · javascript · chartjs · anyplot.ai",
        color: INK,
        font: { size: 22, weight: "600" },
        padding: { top: 4, bottom: 18 },
      },
      legend: {
        position: "bottom",
        labels: {
          color: INK,
          font: { size: 16 },
          padding: 18,
          boxWidth: 16,
          boxHeight: 16,
        },
      },
      tooltip: {
        callbacks: {
          label: (item) => `${item.label}: ${item.parsed} k outgoing`,
        },
      },
    },
  },
  plugins: [chordRibbons],
});

Part of Basic Chord Diagram on anyplot.ai.

Other implementations