{"spec_id":"circos-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// circos-basic: Circos Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-04\n//# anyplot-orientation: square\n// anyplot.ai\n// circos-basic: Circos Plot\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-09-04\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { PiePlot } from \"@mui/x-charts/PieChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: microservice call graph -----------------------------------------\n// Segments = services (outer ring, arc length ∝ code size). Ribbons = call\n// volume between services (width ∝ value). Inner track = test coverage.\nconst MODULES = [\n  \"Gateway\",\n  \"Auth\",\n  \"Catalog\",\n  \"Orders\",\n  \"Payments\",\n  \"Inventory\",\n  \"Notify\",\n  \"Analytics\",\n];\nconst CODE_SIZE_KLOC = [22, 14, 35, 40, 18, 28, 9, 16]; // segment_size\nconst TEST_COVERAGE = [73, 88, 65, 79, 95, 58, 82, 69]; // track_data (%)\n\nconst CONNECTIONS = [\n  { source: \"Gateway\", target: \"Auth\", value: 120 },\n  { source: \"Gateway\", target: \"Catalog\", value: 95 },\n  { source: \"Gateway\", target: \"Orders\", value: 80 },\n  { source: \"Gateway\", target: \"Payments\", value: 60 },\n  { source: \"Gateway\", target: \"Inventory\", value: 55 },\n  { source: \"Gateway\", target: \"Notify\", value: 20 },\n  { source: \"Gateway\", target: \"Analytics\", value: 8 },\n  { source: \"Orders\", target: \"Payments\", value: 70 },\n  { source: \"Orders\", target: \"Inventory\", value: 65 },\n  { source: \"Orders\", target: \"Notify\", value: 30 },\n  { source: \"Orders\", target: \"Catalog\", value: 25 },\n  { source: \"Orders\", target: \"Auth\", value: 14 },\n  { source: \"Catalog\", target: \"Inventory\", value: 40 },\n  { source: \"Catalog\", target: \"Notify\", value: 10 },\n  { source: \"Payments\", target: \"Notify\", value: 35 },\n  { source: \"Payments\", target: \"Auth\", value: 18 },\n  { source: \"Inventory\", target: \"Notify\", value: 12 },\n  { source: \"Analytics\", target: \"Orders\", value: 45 },\n  { source: \"Analytics\", target: \"Catalog\", value: 38 },\n  { source: \"Analytics\", target: \"Payments\", value: 28 },\n  { source: \"Analytics\", target: \"Inventory\", value: 22 },\n  { source: \"Auth\", target: \"Notify\", value: 15 },\n];\n\nconst GAP_DEG = 3;\n\n// Segments arranged clockwise from 12 o'clock, span ∝ code size, fixed gaps\n// between neighbors for visual separation. This is the single source of\n// truth for the ribbon/label geometry below, AND it exactly matches what MUI\n// X's <PiePlot> renders for the two pie series further down (see\n// PIE_START_ANGLE/PIE_END_ANGLE) — both use the real @mui/x-charts-vendored\n// d3 `pie()` layout: cumulative value share + trailing `paddingAngle`.\nconst buildSegments = () => {\n  const totalSize = CODE_SIZE_KLOC.reduce((a, b) => a + b, 0);\n  const availableDeg = 360 - GAP_DEG * MODULES.length;\n  let cursor = 0;\n  return MODULES.map((name, i) => {\n    const span = (CODE_SIZE_KLOC[i] / totalSize) * availableDeg;\n    const startAngle = cursor;\n    const endAngle = cursor + span;\n    cursor = endAngle + GAP_DEG;\n    return {\n      name,\n      startAngle,\n      endAngle,\n      color: t.palette[i % t.palette.length],\n      codeSize: CODE_SIZE_KLOC[i],\n      coverage: TEST_COVERAGE[i],\n    };\n  });\n};\n\nconst SEGMENTS = buildSegments();\nconst SEGMENT_BY_NAME = Object.fromEntries(SEGMENTS.map((s) => [s.name, s]));\n\n// Subdivide each segment's angular span among the connections that touch it\n// (as source or target), proportional to connection value — the classic\n// chord-diagram allocation. Ribbons are colored by their source segment.\nconst buildRibbons = () => {\n  const touchTotal = {};\n  SEGMENTS.forEach((s) => {\n    touchTotal[s.name] = 0;\n  });\n  CONNECTIONS.forEach((c) => {\n    touchTotal[c.source] += c.value;\n    touchTotal[c.target] += c.value;\n  });\n\n  const cursor = {};\n  SEGMENTS.forEach((s) => {\n    cursor[s.name] = s.startAngle;\n  });\n\n  return CONNECTIONS.map((c) => {\n    const segS = SEGMENT_BY_NAME[c.source];\n    const segT = SEGMENT_BY_NAME[c.target];\n    const spanS =\n      (c.value / touchTotal[c.source]) * (segS.endAngle - segS.startAngle);\n    const spanT =\n      (c.value / touchTotal[c.target]) * (segT.endAngle - segT.startAngle);\n    const sStart = cursor[c.source];\n    const sEnd = sStart + spanS;\n    cursor[c.source] = sEnd;\n    const tStart = cursor[c.target];\n    const tEnd = tStart + spanT;\n    cursor[c.target] = tEnd;\n    return { ...c, sStart, sEnd, tStart, tEnd, color: segS.color };\n  });\n};\n\nconst RIBBONS = buildRibbons();\n\n// --- Geometry helpers for the ribbons (chords) and radial labels -----------\n// MUI X's community chart types have no ribbon/chord mark, so these two marks\n// stay hand-drawn (angle 0 = 12 o'clock, increasing = clockwise, same\n// convention d3/MUI X uses for its own pie arcs).\nconst toRad = (deg) => (deg * Math.PI) / 180;\nconst polarPoint = (cx, cy, angleDeg, radius) => ({\n  x: cx + radius * Math.sin(toRad(angleDeg)),\n  y: cy - radius * Math.cos(toRad(angleDeg)),\n});\n\nconst ribbonPath = (cx, cy, sStart, sEnd, tStart, tEnd, radius) => {\n  const largeS = sEnd - sStart > 180 ? 1 : 0;\n  const largeT = tEnd - tStart > 180 ? 1 : 0;\n  const p1 = polarPoint(cx, cy, sStart, radius);\n  const p2 = polarPoint(cx, cy, sEnd, radius);\n  const p3 = polarPoint(cx, cy, tStart, radius);\n  const p4 = polarPoint(cx, cy, tEnd, radius);\n  return [\n    `M ${p1.x} ${p1.y}`,\n    `A ${radius} ${radius} 0 ${largeS} 1 ${p2.x} ${p2.y}`,\n    `Q ${cx} ${cy} ${p3.x} ${p3.y}`,\n    `A ${radius} ${radius} 0 ${largeT} 1 ${p4.x} ${p4.y}`,\n    `Q ${cx} ${cy} ${p1.x} ${p1.y}`,\n    \"Z\",\n  ].join(\" \");\n};\n\nconst hexToRgb = (hex) => {\n  const clean = hex.replace(\"#\", \"\");\n  const value = parseInt(clean, 16);\n  return [(value >> 16) & 255, (value >> 8) & 255, value & 255];\n};\n\nconst lerpColor = (hexA, hexB, ratio) => {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const c = a.map((v, i) => Math.round(v + (b[i] - v) * ratio));\n  return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;\n};\n\nconst COVERAGE_MIN = Math.min(...TEST_COVERAGE);\nconst COVERAGE_MAX = Math.max(...TEST_COVERAGE);\n\n// --- The two rings as genuine @mui/x-charts pie series ----------------------\n// A circos ring (segments with gaps, arc length ∝ value) is exactly what a\n// MUI X pie series already computes via d3's pie() layout: cumulative value\n// share + a trailing `paddingAngle`. `PIE_START_ANGLE`/`PIE_END_ANGLE` cancel\n// out d3's half-`paddingAngle` offset (its first slice starts at\n// `startAngle + paddingAngle / 2`) so the rendered ring lands on the exact\n// same angles as SEGMENTS above — that's what lets the hand-drawn ribbons\n// attach cleanly to the MUI-rendered outer ring.\nconst PIE_START_ANGLE = -GAP_DEG / 2;\nconst PIE_END_ANGLE = 360 - GAP_DEG / 2;\n\nconst segmentSeriesData = SEGMENTS.map((s) => ({\n  id: s.name,\n  value: s.codeSize,\n  label: `${s.name} · ${s.codeSize} kLOC`,\n  color: s.color,\n}));\n\nconst trackSeriesData = SEGMENTS.map((s) => {\n  const ratio =\n    (s.coverage - COVERAGE_MIN) / (COVERAGE_MAX - COVERAGE_MIN || 1);\n  return {\n    id: `${s.name}-coverage`,\n    value: s.codeSize, // same values as the segments series => identical angular spans\n    label: `${s.name} · ${s.coverage}% test coverage`,\n    color: lerpColor(t.seq[0], t.seq[1], ratio),\n  };\n});\n\nconst RADIUS_RATIO = {\n  segOuter: 0.85,\n  segThickness: 0.07,\n  trackGap: 0.02,\n  trackThickness: 0.12,\n  ribbonGap: 0.015,\n  labelGap: 0.05,\n};\n\nconst computeRadii = (r) => {\n  const rSegOuter = r * RADIUS_RATIO.segOuter;\n  const rSegInner = rSegOuter - r * RADIUS_RATIO.segThickness;\n  const rTrackOuter = rSegInner - r * RADIUS_RATIO.trackGap;\n  const rTrackBase = rTrackOuter - r * RADIUS_RATIO.trackThickness;\n  const rRibbon = rTrackBase - r * RADIUS_RATIO.ribbonGap;\n  const rLabel = rSegOuter + r * RADIUS_RATIO.labelGap;\n  return { rSegOuter, rSegInner, rTrackOuter, rTrackBase, rRibbon, rLabel };\n};\n\n// --- Chrome ------------------------------------------------------------------\nconst TITLE = \"circos-basic · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_DEFAULT = 30;\nconst titleFontSize =\n  TITLE.length > 67\n    ? Math.round(TITLE_FONT_DEFAULT * (67 / TITLE.length))\n    : TITLE_FONT_DEFAULT;\nconst SUBTITLE =\n  \"Ribbon width ∝ API call volume · outer ring ∝ code size · inner track ∝ test coverage\";\nconst TITLE_H = 58;\nconst SUBTITLE_H = 28;\nconst LEGEND_H = 26;\n\n// Ribbons (chords) sit below both pie rings; MUI X has no chord/ribbon mark,\n// so they're hand-drawn — see the geometry helpers above.\nfunction RibbonsLayer({ cx, cy, radius }) {\n  return (\n    <g>\n      {RIBBONS.map((rb, i) => (\n        <path\n          key={`ribbon-${i}`}\n          d={ribbonPath(cx, cy, rb.sStart, rb.sEnd, rb.tStart, rb.tEnd, radius)}\n          fill={rb.color}\n          fillOpacity={0.45}\n          stroke={rb.color}\n          strokeOpacity={0.55}\n          strokeWidth={1}\n        />\n      ))}\n    </g>\n  );\n}\n\n// Segment name labels, radially rotated around the outer ring.\nfunction SegmentLabelsLayer({ cx, cy, radius }) {\n  return (\n    <g>\n      {SEGMENTS.map((s) => {\n        const mid = (s.startAngle + s.endAngle) / 2;\n        const p = polarPoint(cx, cy, mid, radius);\n        const rotate = mid < 180 ? mid - 90 : mid + 90;\n        const anchor = mid < 180 ? \"start\" : \"end\";\n        return (\n          <text\n            key={`label-${s.name}`}\n            x={p.x}\n            y={p.y}\n            textAnchor={anchor}\n            dominantBaseline=\"middle\"\n            fontSize={15}\n            fill={t.ink}\n            transform={`rotate(${rotate}, ${p.x}, ${p.y})`}\n          >\n            {s.name}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction CircosDiagram({ size }) {\n  const cx = size / 2;\n  const cy = size / 2;\n  const { rSegOuter, rSegInner, rTrackOuter, rTrackBase, rRibbon, rLabel } =\n    computeRadii(size / 2);\n\n  return (\n    <ChartContainer\n      width={size}\n      height={size}\n      margin={{ top: 0, bottom: 0, left: 0, right: 0 }}\n      colors={t.palette}\n      sx={{ overflow: \"visible\" }}\n      skipAnimation\n      series={[\n        {\n          type: \"pie\",\n          id: \"segments\",\n          data: segmentSeriesData,\n          innerRadius: rSegInner,\n          outerRadius: rSegOuter,\n          startAngle: PIE_START_ANGLE,\n          endAngle: PIE_END_ANGLE,\n          paddingAngle: GAP_DEG,\n          sortingValues: \"none\",\n        },\n        {\n          type: \"pie\",\n          id: \"track\",\n          data: trackSeriesData,\n          innerRadius: rTrackBase,\n          outerRadius: rTrackOuter,\n          startAngle: PIE_START_ANGLE,\n          endAngle: PIE_END_ANGLE,\n          paddingAngle: GAP_DEG,\n          sortingValues: \"none\",\n        },\n      ]}\n    >\n      <RibbonsLayer cx={cx} cy={cy} radius={rRibbon} />\n      <PiePlot skipAnimation />\n      <SegmentLabelsLayer cx={cx} cy={cy} radius={rLabel} />\n      <ChartsTooltip trigger=\"item\" />\n    </ChartContainer>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chromeH = TITLE_H + SUBTITLE_H + LEGEND_H;\n  // Margin so rotated segment labels never reach the viewport edge (the\n  // harness clips at the exact ANYPLOT_SIZE bounds).\n  const svgSize = Math.min(width, height - chromeH) * 0.78;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div style={{ paddingLeft: \"20px\" }}>\n        <div\n          style={{\n            height: `${TITLE_H}px`,\n            lineHeight: `${TITLE_H}px`,\n            fontSize: `${titleFontSize}px`,\n            fontWeight: 500,\n            color: t.ink,\n          }}\n        >\n          {TITLE}\n        </div>\n        <div\n          style={{\n            height: `${SUBTITLE_H}px`,\n            lineHeight: `${SUBTITLE_H}px`,\n            fontSize: \"14px\",\n            fontStyle: \"italic\",\n            color: t.inkSoft,\n          }}\n        >\n          {SUBTITLE}\n        </div>\n        <div\n          style={{\n            height: `${LEGEND_H}px`,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: \"8px\",\n            fontSize: \"12px\",\n            color: t.inkSoft,\n          }}\n        >\n          <span>Test coverage track:</span>\n          <span>{COVERAGE_MIN}%</span>\n          <div\n            style={{\n              width: \"70px\",\n              height: \"8px\",\n              borderRadius: \"4px\",\n              background: `linear-gradient(to right, ${t.seq[0]}, ${t.seq[1]})`,\n            }}\n          />\n          <span>{COVERAGE_MAX}%</span>\n        </div>\n      </div>\n      <div\n        style={{\n          flex: 1,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <CircosDiagram size={svgSize} />\n      </div>\n    </div>\n  );\n}\n"}