Basic Chord Diagram — Highcharts

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 Highcharts

JavaScript source (Highcharts)

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

// Highcharts' dependency-wheel / sankey chord series lives in an add-on module
// that is not vendored — only the core bundle (with its SVGRenderer) is loaded.
// So we build a genuine chord diagram natively with `chart.renderer`: annular
// arcs for each continent and quadratic-bezier ribbons (width ∝ flow) for the
// migration links. No other charting library is used.

const t = window.ANYPLOT_TOKENS;

// --- Data: annual migration flows between 6 continents (thousands/year) ------
// matrix[i][j] = people moving FROM continent i TO continent j. Deterministic,
// in-memory. Diagonal is 0 (internal moves are not inter-continental flows).
const labels = [
  "Africa",
  "Asia",
  "Europe",
  "N. America",
  "S. America",
  "Oceania",
];
// prettier-ignore
const matrix = [
  //  Af   As   Eu   NA   SA   Oc
  [    0, 120, 380,  90,  20,  15 ], // Africa
  [   60,   0, 450, 520,  40, 180 ], // Asia
  [  110,  90,   0, 320,  70,  95 ], // Europe
  [   40,  70, 150,   0, 110,  30 ], // N. America
  [   15,  25,  90, 280,   0,  10 ], // S. America
  [   10,  60,  70,  50,  15,   0 ], // Oceania
];
const n = labels.length;

// Each continent is an abstract category, so the Imprint palette is used in
// canonical order — Africa = brand green (palette[0]).
const groupColor = (i) => t.palette[i % t.palette.length];

// --- Angular layout (d3-chord convention) -----------------------------------
// Group i's arc length is proportional to its total outgoing flow (row sum).
// Within that arc, a sub-arc of width matrix[i][j] is reserved for the link to
// j; the ribbon for pair (i,j) joins sub[i][j] on arc i with sub[j][i] on arc j.
const GAP = 0.045; // radians of padding between adjacent group arcs
const rowSum = matrix.map((row) => row.reduce((a, b) => a + b, 0));
const totalFlow = rowSum.reduce((a, b) => a + b, 0);
const scale = (2 * Math.PI - n * GAP) / totalFlow;

const groups = []; // [startAngle, endAngle] per continent
const sub = []; // sub[i][j] = [startAngle, endAngle] of the i→j slice on arc i
let angle = -Math.PI / 2; // start at the top, sweep clockwise
for (let i = 0; i < n; i++) {
  sub[i] = [];
  const gStart = angle;
  for (let j = 0; j < n; j++) {
    const w = matrix[i][j] * scale;
    sub[i][j] = [angle, angle + w];
    angle += w;
  }
  groups.push([gStart, angle]);
  angle += GAP;
}

// --- Build the chart shell, then draw with the SVG renderer ------------------
const chart = Highcharts.chart("container", {
  chart: {
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
    marginTop: 96,
    marginBottom: 60,
    marginLeft: 60,
    marginRight: 60,
  },
  credits: { enabled: false },
  colors: t.palette,
  title: {
    text: "chord-basic · javascript · highcharts · anyplot.ai",
    style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
  },
  subtitle: {
    text: "Annual migration flows between continents — ribbon width ∝ migrants (thousands), coloured by origin",
    style: { color: t.inkSoft, fontSize: "14px" },
  },
  xAxis: { visible: false },
  yAxis: { visible: false },
  legend: { enabled: false },
  plotOptions: { series: { animation: false } },
  series: [],
});

// Geometry helpers in pixel space (independent of any axis).
const cx = chart.plotLeft + chart.plotWidth / 2;
const cy = chart.plotTop + chart.plotHeight / 2;
const radiusMax = Math.min(chart.plotWidth, chart.plotHeight) / 2;
const ro = radiusMax - 116; // outer edge of the arc band
const ri = ro - 30; // inner edge of the arc band == ribbon anchor radius

const pt = (a, r) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];
const f = (v) => v.toFixed(2);
const large = (a0, a1) => (Math.abs(a1 - a0) > Math.PI ? 1 : 0);

// Annular sector (a group's arc band) as an SVG path string.
function arcBand(a0, a1, rIn, rOut) {
  const [x0o, y0o] = pt(a0, rOut);
  const [x1o, y1o] = pt(a1, rOut);
  const [x1i, y1i] = pt(a1, rIn);
  const [x0i, y0i] = pt(a0, rIn);
  const la = large(a0, a1);
  return (
    `M ${f(x0o)} ${f(y0o)} A ${f(rOut)} ${f(rOut)} 0 ${la} 1 ${f(x1o)} ${f(y1o)} ` +
    `L ${f(x1i)} ${f(y1i)} A ${f(rIn)} ${f(rIn)} 0 ${la} 0 ${f(x0i)} ${f(y0i)} Z`
  );
}

// Ribbon between two sub-arcs (each [a0,a1]) at radius r, curving through centre.
function ribbon([si0, si1], [sj0, sj1], r) {
  const [ax, ay] = pt(si0, r);
  const [bx, by] = pt(si1, r);
  const [cxp, cyp] = pt(sj0, r);
  const [dx, dy] = pt(sj1, r);
  return (
    `M ${f(ax)} ${f(ay)} A ${f(r)} ${f(r)} 0 ${large(si0, si1)} 1 ${f(bx)} ${f(by)} ` +
    `Q ${f(cx)} ${f(cy)} ${f(cxp)} ${f(cyp)} ` +
    `A ${f(r)} ${f(r)} 0 ${large(sj0, sj1)} 1 ${f(dx)} ${f(dy)} ` +
    `Q ${f(cx)} ${f(cy)} ${f(ax)} ${f(ay)} Z`
  );
}

const g = chart.renderer.g("chord").add();

// Ribbons first (one per unordered pair) so the solid arcs sit on top.
for (let i = 0; i < n; i++) {
  for (let j = i + 1; j < n; j++) {
    if (matrix[i][j] === 0 && matrix[j][i] === 0) continue;
    // Colour the ribbon by the dominant direction's origin.
    const src = matrix[i][j] >= matrix[j][i] ? i : j;
    const color = groupColor(src);
    const path = chart.renderer
      .path()
      .attr({
        fill: color,
        "fill-opacity": 0.6,
        stroke: t.pageBg,
        "stroke-width": 0.75,
      })
      .add(g);
    path.element.setAttribute("d", ribbon(sub[i][j], sub[j][i], ri));
    // Honest hover detail for the interactive HTML view (absent from the PNG).
    const title = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "title",
    );
    title.textContent =
      `${labels[i]} → ${labels[j]}: ${matrix[i][j]}k · ` +
      `${labels[j]} → ${labels[i]}: ${matrix[j][i]}k`;
    path.element.appendChild(title);
  }
}

// Group arcs + radial labels on top.
for (let i = 0; i < n; i++) {
  const [a0, a1] = groups[i];
  const arc = chart.renderer
    .path()
    .attr({ fill: groupColor(i), stroke: t.pageBg, "stroke-width": 1.5 })
    .add(g);
  arc.element.setAttribute("d", arcBand(a0, a1, ri, ro));

  const mid = (a0 + a1) / 2;
  const [lx, ly] = pt(mid, ro + 16);
  const cosM = Math.cos(mid);
  const align = Math.abs(cosM) < 0.25 ? "center" : cosM > 0 ? "left" : "right";
  chart.renderer
    .text(labels[i], lx, ly + 6)
    .attr({ align })
    .css({ color: t.ink, fontSize: "16px", fontWeight: "600" })
    .add(g);
}

// Static-frame timing signal for the harness.
window.__anyplotReady = true;

Part of Basic Chord Diagram on anyplot.ai.

Other implementations