{"spec_id":"chord-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// chord-basic: Basic Chord Diagram\n// Library: muix 9.16.0 | JavaScript 22.23.3\n// Quality: 85/100 | Updated: 2026-10-24\n//# anyplot-orientation: square\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Constants\nconst HEADER_H = 98;\nconst FOOTER_H = 44;\nconst PAD_RAD = 0.05;\nconst R_OUT_SHARE = 0.39;\nconst R_IN_SHARE = 0.358;\nconst R_LABEL_SHARE = 0.422;\n\n// Columns: set these to your data's column names\nconst SOURCE = \"Origin region\"; // role: source\nconst TARGET = \"Destination region\"; // role: target\nconst VALUE = \"Migrants per five years (millions)\"; // role: value\n\n// Data: replace this block with your own records, one object per row\nconst data = [\n  { [SOURCE]: \"Asia\", [TARGET]: \"Africa\", [VALUE]: 1.2 },\n  { [SOURCE]: \"Asia\", [TARGET]: \"Europe\", [VALUE]: 6.5 },\n  { [SOURCE]: \"Asia\", [TARGET]: \"N. America\", [VALUE]: 7.8 },\n  { [SOURCE]: \"Asia\", [TARGET]: \"S. America\", [VALUE]: 0.6 },\n  { [SOURCE]: \"Asia\", [TARGET]: \"Oceania\", [VALUE]: 1.1 },\n  { [SOURCE]: \"Africa\", [TARGET]: \"Asia\", [VALUE]: 0.9 },\n  { [SOURCE]: \"Africa\", [TARGET]: \"Europe\", [VALUE]: 5.4 },\n  { [SOURCE]: \"Africa\", [TARGET]: \"N. America\", [VALUE]: 1.3 },\n  { [SOURCE]: \"Africa\", [TARGET]: \"S. America\", [VALUE]: 0.3 },\n  { [SOURCE]: \"Africa\", [TARGET]: \"Oceania\", [VALUE]: 0.2 },\n  { [SOURCE]: \"Europe\", [TARGET]: \"Asia\", [VALUE]: 2.1 },\n  { [SOURCE]: \"Europe\", [TARGET]: \"Africa\", [VALUE]: 1.7 },\n  { [SOURCE]: \"Europe\", [TARGET]: \"N. America\", [VALUE]: 4.2 },\n  { [SOURCE]: \"Europe\", [TARGET]: \"S. America\", [VALUE]: 0.7 },\n  { [SOURCE]: \"Europe\", [TARGET]: \"Oceania\", [VALUE]: 0.9 },\n  { [SOURCE]: \"N. America\", [TARGET]: \"Asia\", [VALUE]: 1.4 },\n  { [SOURCE]: \"N. America\", [TARGET]: \"Africa\", [VALUE]: 0.5 },\n  { [SOURCE]: \"N. America\", [TARGET]: \"Europe\", [VALUE]: 2.8 },\n  { [SOURCE]: \"N. America\", [TARGET]: \"S. America\", [VALUE]: 1.6 },\n  { [SOURCE]: \"N. America\", [TARGET]: \"Oceania\", [VALUE]: 0.4 },\n  { [SOURCE]: \"S. America\", [TARGET]: \"Asia\", [VALUE]: 0.4 },\n  { [SOURCE]: \"S. America\", [TARGET]: \"Africa\", [VALUE]: 0.2 },\n  { [SOURCE]: \"S. America\", [TARGET]: \"Europe\", [VALUE]: 2.3 },\n  { [SOURCE]: \"S. America\", [TARGET]: \"N. America\", [VALUE]: 3.1 },\n  { [SOURCE]: \"S. America\", [TARGET]: \"Oceania\", [VALUE]: 0.3 },\n  { [SOURCE]: \"Oceania\", [TARGET]: \"Asia\", [VALUE]: 0.7 },\n  { [SOURCE]: \"Oceania\", [TARGET]: \"Africa\", [VALUE]: 0.1 },\n  { [SOURCE]: \"Oceania\", [TARGET]: \"Europe\", [VALUE]: 0.6 },\n  { [SOURCE]: \"Oceania\", [TARGET]: \"N. America\", [VALUE]: 0.5 },\n  { [SOURCE]: \"Oceania\", [TARGET]: \"S. America\", [VALUE]: 0.2 },\n];\n\n// Plot: reads only data and the constants above\nconst entities = Array.from(\n  new Set(data.flatMap((d) => [String(d[SOURCE]), String(d[TARGET])])),\n);\nconst nEntities = entities.length;\nconst entityIndex = new Map(entities.map((name, idx) => [name, idx]));\nconst formatValue = (v: number) => v.toFixed(1);\n\nconst matrix = Array.from({ length: nEntities }, () =>\n  Array.from({ length: nEntities }, () => 0),\n);\nfor (const row of data) {\n  const sIdx = entityIndex.get(String(row[SOURCE]));\n  const tIdx = entityIndex.get(String(row[TARGET]));\n  // A flow from an entity to itself has no chord\n  if (sIdx !== tIdx) matrix[sIdx][tIdx] += Number(row[VALUE]);\n}\n\nconst groupTotal = matrix.map((row) => row.reduce((a, b) => a + b, 0));\nconst grandTotal = groupTotal.reduce((a, b) => a + b, 0);\nconst availRad = 2 * Math.PI - PAD_RAD * nEntities;\n\nlet curAngle = -Math.PI / 2 + PAD_RAD / 2;\nconst groups = entities.map((name, i) => {\n  const span = (groupTotal[i] / grandTotal) * availRad;\n  const gStart = curAngle;\n  const gEnd = curAngle + span;\n  let subCur = gStart;\n  // An entity with no outbound flow gets a zero-length arc\n  const subs = entities.map((_, j) => {\n    const subSpan =\n      groupTotal[i] > 0 ? (matrix[i][j] / groupTotal[i]) * span : 0;\n    const range: [number, number] = [subCur, subCur + subSpan];\n    subCur += subSpan;\n    return range;\n  });\n  curAngle = gEnd + PAD_RAD;\n  return {\n    i,\n    name,\n    color: t.palette[i],\n    total: groupTotal[i],\n    gStart,\n    gEnd,\n    mid: (gStart + gEnd) / 2,\n    subs,\n  };\n});\n\nconst ribbons: {\n  id: string;\n  i: number;\n  j: number;\n  a: number;\n  b: number;\n  color: string;\n  mag: number;\n  sI: [number, number];\n  sJ: [number, number];\n}[] = [];\n\nfor (let i = 0; i < nEntities; i += 1) {\n  for (let j = 0; j < i; j += 1) {\n    const a = matrix[i][j];\n    const b = matrix[j][i];\n    if (a + b <= 0) continue;\n    const dom = a >= b ? i : j;\n    ribbons.push({\n      id: `${entities[i]}-${entities[j]}`,\n      i,\n      j,\n      a,\n      b,\n      color: groups[dom].color,\n      mag: a + b,\n      sI: groups[i].subs[j],\n      sJ: groups[j].subs[i],\n    });\n  }\n}\nribbons.sort((x, y) => y.mag - x.mag);\nconst bothWays = ribbons.filter((r) => r.a > 0 && r.b > 0).length;\n\nconst TITLE = \"chord-basic · javascript · muix · anyplot.ai\";\nconst SUBTITLE = `${nEntities} entities · ${ribbons.length} connected pairs (${bothWays} with flows in both directions) · Total ${formatValue(grandTotal)}`;\n\nfunction ChordDiagramLayer() {\n  const { left, top, width, height } = useDrawingArea();\n  const cx = left + width / 2;\n  const cy = top + height / 2;\n  const size = Math.min(width, height);\n  const rOut = size * R_OUT_SHARE;\n  const rIn = size * R_IN_SHARE;\n  const rLabel = size * R_LABEL_SHARE;\n\n  return (\n    <g>\n      {ribbons.map((rb) => {\n        // Arc along the source end, curve through the centre, arc along the\n        // target end, curve back\n        const [a0, a1] = rb.sI;\n        const [b0, b1] = rb.sJ;\n        const d =\n          `M ${cx + rIn * Math.cos(a0)} ${cy + rIn * Math.sin(a0)}` +\n          ` A ${rIn} ${rIn} 0 ${a1 - a0 > Math.PI ? 1 : 0} 1 ${cx + rIn * Math.cos(a1)} ${cy + rIn * Math.sin(a1)}` +\n          ` Q ${cx} ${cy} ${cx + rIn * Math.cos(b0)} ${cy + rIn * Math.sin(b0)}` +\n          ` A ${rIn} ${rIn} 0 ${b1 - b0 > Math.PI ? 1 : 0} 1 ${cx + rIn * Math.cos(b1)} ${cy + rIn * Math.sin(b1)}` +\n          ` Q ${cx} ${cy} ${cx + rIn * Math.cos(a0)} ${cy + rIn * Math.sin(a0)} Z`;\n\n        return (\n          <path\n            key={rb.id}\n            d={d}\n            fill={rb.color}\n            fillOpacity={0.62}\n            stroke={t.pageBg}\n            strokeWidth={0.9}\n            strokeOpacity={0.55}\n          >\n            <title>\n              {`${VALUE}\\n${entities[rb.i]} → ${entities[rb.j]}: ${formatValue(rb.a)}\\n${entities[rb.j]} → ${entities[rb.i]}: ${formatValue(rb.b)}`}\n            </title>\n          </path>\n        );\n      })}\n\n      {groups.map((g) => {\n        const large = g.gEnd - g.gStart > Math.PI ? 1 : 0;\n        const d =\n          `M ${cx + rOut * Math.cos(g.gStart)} ${cy + rOut * Math.sin(g.gStart)}` +\n          ` A ${rOut} ${rOut} 0 ${large} 1 ${cx + rOut * Math.cos(g.gEnd)} ${cy + rOut * Math.sin(g.gEnd)}` +\n          ` L ${cx + rIn * Math.cos(g.gEnd)} ${cy + rIn * Math.sin(g.gEnd)}` +\n          ` A ${rIn} ${rIn} 0 ${large} 0 ${cx + rIn * Math.cos(g.gStart)} ${cy + rIn * Math.sin(g.gStart)} Z`;\n\n        const cosMid = Math.cos(g.mid);\n        const sinMid = Math.sin(g.mid);\n        const lx = cx + rLabel * cosMid + (cosMid > 0.33 ? 8 : cosMid < -0.33 ? -8 : 0);\n        const ly = cy + rLabel * sinMid;\n        const anchor =\n          cosMid > 0.33 ? \"start\" : cosMid < -0.33 ? \"end\" : \"middle\";\n\n        return (\n          <g key={g.name}>\n            <path\n              d={d}\n              fill={g.color}\n              stroke={t.pageBg}\n              strokeWidth={1.5}\n            >\n              <title>{`${g.name} · outbound ${formatValue(g.total)} (${VALUE})`}</title>\n            </path>\n            <text\n              x={lx}\n              y={ly - 8}\n              textAnchor={anchor}\n              dominantBaseline=\"middle\"\n              fontSize={19}\n              fontWeight={700}\n              fill={t.ink}\n            >\n              {g.name}\n            </text>\n            <text\n              x={lx}\n              y={ly + 14}\n              textAnchor={anchor}\n              dominantBaseline=\"middle\"\n              fontSize={14}\n              fill={t.inkSoft}\n            >\n              {`${formatValue(g.total)} out`}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - HEADER_H - FOOTER_H;\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: t.pageBg,\n        fontFamily: \"Inter, system-ui, sans-serif\",\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: HEADER_H,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: 6,\n          paddingTop: 6,\n        }}\n      >\n        <div style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          {TITLE}\n        </div>\n        <div style={{ fontSize: 15, color: t.inkSoft }}>{SUBTITLE}</div>\n      </div>\n\n      <ChartsContainer\n        width={width}\n        height={chartHeight}\n        margin={{ top: 10, right: 10, bottom: 10, left: 10 }}\n        series={[]}\n        skipAnimation\n      >\n        <ChordDiagramLayer />\n      </ChartsContainer>\n\n      <div\n        style={{\n          height: FOOTER_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 14,\n          color: t.inkSoft,\n        }}\n      >\n        Each chord carries both directions; arc length is proportional to total outbound {VALUE}\n      </div>\n    </div>\n  );\n}\n"}