Basic Chord Diagram in MUI X Charts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Basic Chord Diagram in Python, R, Julia and JavaScript.

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// chord-basic: Basic Chord Diagram
// Library: muix 9.16.0 | JavaScript 22.23.3
// Quality: 85/100 | Updated: 2026-10-24
//# anyplot-orientation: square
import { ChartsContainer } from "@mui/x-charts/ChartsContainer";
import { useDrawingArea } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// Constants
const HEADER_H = 98;
const FOOTER_H = 44;
const PAD_RAD = 0.05;
const R_OUT_SHARE = 0.39;
const R_IN_SHARE = 0.358;
const R_LABEL_SHARE = 0.422;

// Columns: set these to your data's column names
const SOURCE = "Origin region"; // role: source
const TARGET = "Destination region"; // role: target
const VALUE = "Migrants per five years (millions)"; // role: value

// Data: replace this block with your own records, one object per row
const data = [
  { [SOURCE]: "Asia", [TARGET]: "Africa", [VALUE]: 1.2 },
  { [SOURCE]: "Asia", [TARGET]: "Europe", [VALUE]: 6.5 },
  { [SOURCE]: "Asia", [TARGET]: "N. America", [VALUE]: 7.8 },
  { [SOURCE]: "Asia", [TARGET]: "S. America", [VALUE]: 0.6 },
  { [SOURCE]: "Asia", [TARGET]: "Oceania", [VALUE]: 1.1 },
  { [SOURCE]: "Africa", [TARGET]: "Asia", [VALUE]: 0.9 },
  { [SOURCE]: "Africa", [TARGET]: "Europe", [VALUE]: 5.4 },
  { [SOURCE]: "Africa", [TARGET]: "N. America", [VALUE]: 1.3 },
  { [SOURCE]: "Africa", [TARGET]: "S. America", [VALUE]: 0.3 },
  { [SOURCE]: "Africa", [TARGET]: "Oceania", [VALUE]: 0.2 },
  { [SOURCE]: "Europe", [TARGET]: "Asia", [VALUE]: 2.1 },
  { [SOURCE]: "Europe", [TARGET]: "Africa", [VALUE]: 1.7 },
  { [SOURCE]: "Europe", [TARGET]: "N. America", [VALUE]: 4.2 },
  { [SOURCE]: "Europe", [TARGET]: "S. America", [VALUE]: 0.7 },
  { [SOURCE]: "Europe", [TARGET]: "Oceania", [VALUE]: 0.9 },
  { [SOURCE]: "N. America", [TARGET]: "Asia", [VALUE]: 1.4 },
  { [SOURCE]: "N. America", [TARGET]: "Africa", [VALUE]: 0.5 },
  { [SOURCE]: "N. America", [TARGET]: "Europe", [VALUE]: 2.8 },
  { [SOURCE]: "N. America", [TARGET]: "S. America", [VALUE]: 1.6 },
  { [SOURCE]: "N. America", [TARGET]: "Oceania", [VALUE]: 0.4 },
  { [SOURCE]: "S. America", [TARGET]: "Asia", [VALUE]: 0.4 },
  { [SOURCE]: "S. America", [TARGET]: "Africa", [VALUE]: 0.2 },
  { [SOURCE]: "S. America", [TARGET]: "Europe", [VALUE]: 2.3 },
  { [SOURCE]: "S. America", [TARGET]: "N. America", [VALUE]: 3.1 },
  { [SOURCE]: "S. America", [TARGET]: "Oceania", [VALUE]: 0.3 },
  { [SOURCE]: "Oceania", [TARGET]: "Asia", [VALUE]: 0.7 },
  { [SOURCE]: "Oceania", [TARGET]: "Africa", [VALUE]: 0.1 },
  { [SOURCE]: "Oceania", [TARGET]: "Europe", [VALUE]: 0.6 },
  { [SOURCE]: "Oceania", [TARGET]: "N. America", [VALUE]: 0.5 },
  { [SOURCE]: "Oceania", [TARGET]: "S. America", [VALUE]: 0.2 },
];

// Plot: reads only data and the constants above
const entities = Array.from(
  new Set(data.flatMap((d) => [String(d[SOURCE]), String(d[TARGET])])),
);
const nEntities = entities.length;
const entityIndex = new Map(entities.map((name, idx) => [name, idx]));
const formatValue = (v: number) => v.toFixed(1);

const matrix = Array.from({ length: nEntities }, () =>
  Array.from({ length: nEntities }, () => 0),
);
for (const row of data) {
  const sIdx = entityIndex.get(String(row[SOURCE]));
  const tIdx = entityIndex.get(String(row[TARGET]));
  // A flow from an entity to itself has no chord
  if (sIdx !== tIdx) matrix[sIdx][tIdx] += Number(row[VALUE]);
}

const groupTotal = matrix.map((row) => row.reduce((a, b) => a + b, 0));
const grandTotal = groupTotal.reduce((a, b) => a + b, 0);
const availRad = 2 * Math.PI - PAD_RAD * nEntities;

let curAngle = -Math.PI / 2 + PAD_RAD / 2;
const groups = entities.map((name, i) => {
  const span = (groupTotal[i] / grandTotal) * availRad;
  const gStart = curAngle;
  const gEnd = curAngle + span;
  let subCur = gStart;
  // An entity with no outbound flow gets a zero-length arc
  const subs = entities.map((_, j) => {
    const subSpan =
      groupTotal[i] > 0 ? (matrix[i][j] / groupTotal[i]) * span : 0;
    const range: [number, number] = [subCur, subCur + subSpan];
    subCur += subSpan;
    return range;
  });
  curAngle = gEnd + PAD_RAD;
  return {
    i,
    name,
    color: t.palette[i],
    total: groupTotal[i],
    gStart,
    gEnd,
    mid: (gStart + gEnd) / 2,
    subs,
  };
});

const ribbons: {
  id: string;
  i: number;
  j: number;
  a: number;
  b: number;
  color: string;
  mag: number;
  sI: [number, number];
  sJ: [number, number];
}[] = [];

for (let i = 0; i < nEntities; i += 1) {
  for (let j = 0; j < i; j += 1) {
    const a = matrix[i][j];
    const b = matrix[j][i];
    if (a + b <= 0) continue;
    const dom = a >= b ? i : j;
    ribbons.push({
      id: `${entities[i]}-${entities[j]}`,
      i,
      j,
      a,
      b,
      color: groups[dom].color,
      mag: a + b,
      sI: groups[i].subs[j],
      sJ: groups[j].subs[i],
    });
  }
}
ribbons.sort((x, y) => y.mag - x.mag);
const bothWays = ribbons.filter((r) => r.a > 0 && r.b > 0).length;

const TITLE = "chord-basic · javascript · muix · anyplot.ai";
const SUBTITLE = `${nEntities} entities · ${ribbons.length} connected pairs (${bothWays} with flows in both directions) · Total ${formatValue(grandTotal)}`;

function ChordDiagramLayer() {
  const { left, top, width, height } = useDrawingArea();
  const cx = left + width / 2;
  const cy = top + height / 2;
  const size = Math.min(width, height);
  const rOut = size * R_OUT_SHARE;
  const rIn = size * R_IN_SHARE;
  const rLabel = size * R_LABEL_SHARE;

  return (
    <g>
      {ribbons.map((rb) => {
        // Arc along the source end, curve through the centre, arc along the
        // target end, curve back
        const [a0, a1] = rb.sI;
        const [b0, b1] = rb.sJ;
        const d =
          `M ${cx + rIn * Math.cos(a0)} ${cy + rIn * Math.sin(a0)}` +
          ` A ${rIn} ${rIn} 0 ${a1 - a0 > Math.PI ? 1 : 0} 1 ${cx + rIn * Math.cos(a1)} ${cy + rIn * Math.sin(a1)}` +
          ` Q ${cx} ${cy} ${cx + rIn * Math.cos(b0)} ${cy + rIn * Math.sin(b0)}` +
          ` A ${rIn} ${rIn} 0 ${b1 - b0 > Math.PI ? 1 : 0} 1 ${cx + rIn * Math.cos(b1)} ${cy + rIn * Math.sin(b1)}` +
          ` Q ${cx} ${cy} ${cx + rIn * Math.cos(a0)} ${cy + rIn * Math.sin(a0)} Z`;

        return (
          <path
            key={rb.id}
            d={d}
            fill={rb.color}
            fillOpacity={0.62}
            stroke={t.pageBg}
            strokeWidth={0.9}
            strokeOpacity={0.55}
          >
            <title>
              {`${VALUE}\n${entities[rb.i]} → ${entities[rb.j]}: ${formatValue(rb.a)}\n${entities[rb.j]} → ${entities[rb.i]}: ${formatValue(rb.b)}`}
            </title>
          </path>
        );
      })}

      {groups.map((g) => {
        const large = g.gEnd - g.gStart > Math.PI ? 1 : 0;
        const d =
          `M ${cx + rOut * Math.cos(g.gStart)} ${cy + rOut * Math.sin(g.gStart)}` +
          ` A ${rOut} ${rOut} 0 ${large} 1 ${cx + rOut * Math.cos(g.gEnd)} ${cy + rOut * Math.sin(g.gEnd)}` +
          ` L ${cx + rIn * Math.cos(g.gEnd)} ${cy + rIn * Math.sin(g.gEnd)}` +
          ` A ${rIn} ${rIn} 0 ${large} 0 ${cx + rIn * Math.cos(g.gStart)} ${cy + rIn * Math.sin(g.gStart)} Z`;

        const cosMid = Math.cos(g.mid);
        const sinMid = Math.sin(g.mid);
        const lx = cx + rLabel * cosMid + (cosMid > 0.33 ? 8 : cosMid < -0.33 ? -8 : 0);
        const ly = cy + rLabel * sinMid;
        const anchor =
          cosMid > 0.33 ? "start" : cosMid < -0.33 ? "end" : "middle";

        return (
          <g key={g.name}>
            <path
              d={d}
              fill={g.color}
              stroke={t.pageBg}
              strokeWidth={1.5}
            >
              <title>{`${g.name} · outbound ${formatValue(g.total)} (${VALUE})`}</title>
            </path>
            <text
              x={lx}
              y={ly - 8}
              textAnchor={anchor}
              dominantBaseline="middle"
              fontSize={19}
              fontWeight={700}
              fill={t.ink}
            >
              {g.name}
            </text>
            <text
              x={lx}
              y={ly + 14}
              textAnchor={anchor}
              dominantBaseline="middle"
              fontSize={14}
              fill={t.inkSoft}
            >
              {`${formatValue(g.total)} out`}
            </text>
          </g>
        );
      })}
    </g>
  );
}

export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const chartHeight = height - HEADER_H - FOOTER_H;

  return (
    <div
      style={{
        width,
        height,
        background: t.pageBg,
        fontFamily: "Inter, system-ui, sans-serif",
        display: "flex",
        flexDirection: "column",
      }}
    >
      <div
        style={{
          height: HEADER_H,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          gap: 6,
          paddingTop: 6,
        }}
      >
        <div style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>
          {TITLE}
        </div>
        <div style={{ fontSize: 15, color: t.inkSoft }}>{SUBTITLE}</div>
      </div>

      <ChartsContainer
        width={width}
        height={chartHeight}
        margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
        series={[]}
        skipAnimation
      >
        <ChordDiagramLayer />
      </ChartsContainer>

      <div
        style={{
          height: FOOTER_H,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: 14,
          color: t.inkSoft,
        }}
      >
        Each chord carries both directions; arc length is proportional to total outbound {VALUE}
      </div>
    </div>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/chord-basic/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "chord-basic",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/chord-basic/javascript/muix",
  "hub": "https://anyplot.ai/chord-basic",
  "code_json": "https://api.anyplot.ai/specs/chord-basic/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/chord-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/chord-basic/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/chord-basic/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/chord-basic/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/chord-basic/javascript/muix/plot-dark.html",
  "quality_score": 85.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Chord Diagram on anyplot.ai.

Other implementations