{"spec_id":"chord-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// chord-basic: Basic Chord Diagram\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 94/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n// anyplot.ai\n// chord-basic: Basic Chord Diagram\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-17\n//\n// MUI X community has no chord/Sankey primitive, so the diagram is drawn with\n// the supported escape hatch: a <ChartContainer> establishes a square linear\n// coordinate space, and the chord ring + ribbons are SVG overlays positioned\n// through the chart's own useXScale/useYScale hooks. Everything is community\n// @mui/x-charts — no Pro, no second charting library.\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\nconst TITLE = \"chord-basic · javascript · muix · anyplot.ai\";\n\nconst FONT =\n  '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n// Square drawing domain. The chord ring lives inside radius ~1; the band beyond\n// it is reserved for region labels, the title, and the footnote.\nconst R = 1.42;\n\n// --- Data: annual migration flows between six world regions (millions) -------\n// Deterministic, in-memory. MATRIX[i][j] = migrants moving FROM region i TO\n// region j (diagonal = 0, no internal moves). Both directions are kept, so each\n// chord shows two magnitudes — one per ribbon end.\nconst ENTITIES = [\n  { name: \"Asia\", color: t.palette[0] }, // first categorical series → brand green\n  { name: \"Africa\", color: t.palette[1] },\n  { name: \"Europe\", color: t.palette[2] },\n  { name: \"N. America\", color: t.palette[3] },\n  { name: \"S. America\", color: t.palette[4] },\n  { name: \"Oceania\", color: t.palette[5] },\n];\n\n// rows = source, cols = target (Asia, Africa, Europe, N.Am, S.Am, Oceania)\nconst MATRIX = [\n  [0.0, 1.2, 6.5, 7.8, 0.6, 1.1], // Asia →\n  [0.9, 0.0, 5.4, 1.3, 0.3, 0.2], // Africa →\n  [2.1, 1.7, 0.0, 4.2, 0.7, 0.9], // Europe →\n  [1.4, 0.5, 2.8, 0.0, 1.6, 0.4], // N. America →\n  [0.4, 0.2, 2.3, 3.1, 0.0, 0.3], // S. America →\n  [0.7, 0.1, 0.6, 0.5, 0.2, 0.0], // Oceania →\n];\n\nconst N = ENTITIES.length;\n\n// --- Chord layout (manual; standard d3-chord arithmetic) ---------------------\n// Each region subtends an arc proportional to its total outbound flow. Inside a\n// region's arc, sub-segments (one per partner) carry the per-pair magnitude, so\n// a ribbon's two ends can differ in width — that is the bidirectional flow.\nconst groupTotal = MATRIX.map((row) => row.reduce((a, b) => a + b, 0));\nconst grand = groupTotal.reduce((a, b) => a + b, 0);\n\nconst PAD = 0.05; // angular gap between region arcs (radians)\nconst avail = 2 * Math.PI - PAD * N;\n\nconst groups = [];\nlet cur = -Math.PI / 2 + PAD / 2; // start at top, sweep clockwise\nfor (let i = 0; i < N; i++) {\n  const span = (groupTotal[i] / grand) * avail;\n  const gStart = cur;\n  const gEnd = cur + span;\n  const subs = [];\n  let sc = gStart;\n  for (let j = 0; j < N; j++) {\n    const sspan = groupTotal[i] > 0 ? (MATRIX[i][j] / groupTotal[i]) * span : 0;\n    subs.push([sc, sc + sspan]);\n    sc += sspan;\n  }\n  groups.push({ i, gStart, gEnd, mid: (gStart + gEnd) / 2, subs });\n  cur = gEnd + PAD;\n}\n\n// One ribbon per unordered region pair, connecting subgroup(i,j) ↔ subgroup(j,i).\nconst ribbons = [];\nfor (let i = 0; i < N; i++) {\n  for (let j = 0; j < i; j++) {\n    const a = MATRIX[i][j]; // i → j\n    const b = MATRIX[j][i]; // j → i\n    if (a + b <= 0) continue;\n    const dom = a >= b ? i : j; // colour by the dominant origin region\n    ribbons.push({\n      i,\n      j,\n      a,\n      b,\n      color: ENTITIES[dom].color,\n      mag: a + b,\n      sI: groups[i].subs[j],\n      sJ: groups[j].subs[i],\n    });\n  }\n}\n// Draw fat ribbons first so thin ones stay legible on top.\nribbons.sort((x, y) => y.mag - x.mag);\n\n// Ring geometry (data-space radii).\nconst R_OUT = 0.9; // outer edge of region band\nconst R_IN = 0.83; // inner edge of region band = ribbon attach radius\nconst R_RIB = 0.83;\nconst R_LABEL = 1.0;\n\n// --- Geometry helpers (data coordinates → SVG path via the chart scales) -----\nfunction arcPts(a0, a1, r) {\n  const steps = Math.max(2, Math.ceil(Math.abs(a1 - a0) / 0.04));\n  const pts = [];\n  for (let k = 0; k <= steps; k++) {\n    const a = a0 + (a1 - a0) * (k / steps);\n    pts.push([r * Math.cos(a), r * Math.sin(a)]);\n  }\n  return pts;\n}\n\nfunction ribbonPath(xs, ys, sI, sJ, r) {\n  const cx = xs(0).toFixed(1);\n  const cy = ys(0).toFixed(1);\n  const A = arcPts(sI[0], sI[1], r);\n  const B = arcPts(sJ[0], sJ[1], r);\n  const p = (pt) => `${xs(pt[0]).toFixed(1)} ${ys(pt[1]).toFixed(1)}`;\n  let d = `M ${p(A[0])}`;\n  for (let k = 1; k < A.length; k++) d += ` L ${p(A[k])}`;\n  d += ` Q ${cx} ${cy} ${p(B[0])}`;\n  for (let k = 1; k < B.length; k++) d += ` L ${p(B[k])}`;\n  d += ` Q ${cx} ${cy} ${p(A[0])} Z`;\n  return d;\n}\n\nfunction bandPath(xs, ys, a0, a1, rIn, rOut) {\n  const outer = arcPts(a0, a1, rOut);\n  const inner = arcPts(a1, a0, rIn);\n  const p = (pt) => `${xs(pt[0]).toFixed(1)} ${ys(pt[1]).toFixed(1)}`;\n  let d = `M ${p(outer[0])}`;\n  for (let k = 1; k < outer.length; k++) d += ` L ${p(outer[k])}`;\n  for (let k = 0; k < inner.length; k++) d += ` L ${p(inner[k])}`;\n  return d + \" Z\";\n}\n\nconst fmt = (v) => `${v.toFixed(1)}M`;\n\n// --- Overlay layers ----------------------------------------------------------\nfunction Ribbons() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g>\n      {ribbons.map((rb, k) => (\n        <path\n          key={k}\n          d={ribbonPath(xs, ys, rb.sI, rb.sJ, R_RIB)}\n          fill={rb.color}\n          fillOpacity={0.6}\n          stroke={t.pageBg}\n          strokeWidth={0.8}\n          strokeOpacity={0.5}\n        >\n          <title>\n            {`${ENTITIES[rb.i].name} → ${ENTITIES[rb.j].name}: ${fmt(rb.a)}\\n` +\n              `${ENTITIES[rb.j].name} → ${ENTITIES[rb.i].name}: ${fmt(rb.b)}`}\n          </title>\n        </path>\n      ))}\n    </g>\n  );\n}\n\nfunction Ring() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g>\n      {groups.map((g) => (\n        <path\n          key={g.i}\n          d={bandPath(xs, ys, g.gStart, g.gEnd, R_IN, R_OUT)}\n          fill={ENTITIES[g.i].color}\n          stroke={t.pageBg}\n          strokeWidth={1.5}\n        >\n          <title>{`${ENTITIES[g.i].name} · total outbound ${fmt(groupTotal[g.i])}`}</title>\n        </path>\n      ))}\n    </g>\n  );\n}\n\nfunction Labels() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g fontFamily={FONT}>\n      {groups.map((g) => {\n        const mid = g.mid;\n        const c = Math.cos(mid);\n        const lx = R_LABEL * c;\n        const ly = R_LABEL * Math.sin(mid);\n        const anchor = c > 0.33 ? \"start\" : c < -0.33 ? \"end\" : \"middle\";\n        const dx = c > 0.33 ? 8 : c < -0.33 ? -8 : 0;\n        return (\n          <g key={g.i}>\n            <text\n              x={xs(lx) + dx}\n              y={ys(ly)}\n              textAnchor={anchor}\n              dominantBaseline=\"middle\"\n              fontSize={21}\n              fontWeight={700}\n              fill={ENTITIES[g.i].color}\n            >\n              {ENTITIES[g.i].name}\n            </text>\n            <text\n              x={xs(lx) + dx}\n              y={ys(ly) + 24}\n              textAnchor={anchor}\n              dominantBaseline=\"middle\"\n              fontSize={14}\n              fill={t.inkSoft}\n            >\n              {fmt(groupTotal[g.i])} out\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction Frame() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g fontFamily={FONT}>\n      <text\n        x={xs(0)}\n        y={ys(1.32)}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={27}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {TITLE}\n      </text>\n      <text\n        x={xs(0)}\n        y={ys(1.19)}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={16}\n        fill={t.inkSoft}\n      >\n        Annual migration flows between six world regions · ribbon width ∝ migrants\n      </text>\n      <text\n        x={xs(0)}\n        y={ys(-1.3)}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={14}\n        fill={t.inkSoft}\n      >\n        Each chord carries both directions; arc length ∝ a region's total outbound flow (millions / yr)\n      </text>\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) --------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={SIZE.width}\n      height={SIZE.height}\n      margin={{ top: 12, right: 12, bottom: 12, left: 12 }}\n      series={[]}\n      xAxis={[{ id: \"x\", scaleType: \"linear\", min: -R, max: R }]}\n      yAxis={[{ id: \"y\", scaleType: \"linear\", min: -R, max: R }]}\n      skipAnimation\n    >\n      <Ribbons />\n      <Ring />\n      <Labels />\n      <Frame />\n    </ChartContainer>\n  );\n}\n"}