{"spec_id":"circos-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// circos-basic: Circos Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-04\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width: W, height: H } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Software-module call graph: which service talks to which, and how often\n// (calls/sec, thousands). The inner track shows each module's test-coverage %.\nconst modules = [\n  \"API Gateway\",\n  \"Auth Service\",\n  \"Database\",\n  \"Cache Layer\",\n  \"Job Queue\",\n  \"Web UI\",\n  \"Analytics\",\n  \"Notifications\",\n];\n\nconst connections = [\n  [\"API Gateway\", \"Auth Service\", 45],\n  [\"API Gateway\", \"Database\", 60],\n  [\"API Gateway\", \"Cache Layer\", 35],\n  [\"API Gateway\", \"Job Queue\", 20],\n  [\"Auth Service\", \"Database\", 30],\n  [\"Auth Service\", \"Cache Layer\", 15],\n  [\"Web UI\", \"API Gateway\", 55],\n  [\"Web UI\", \"Analytics\", 25],\n  [\"Job Queue\", \"Database\", 40],\n  [\"Job Queue\", \"Notifications\", 22],\n  [\"Cache Layer\", \"Database\", 18],\n  [\"Analytics\", \"Database\", 28],\n  [\"Analytics\", \"Cache Layer\", 12],\n  [\"Notifications\", \"Web UI\", 10],\n  [\"Notifications\", \"Database\", 14],\n  [\"Web UI\", \"Cache Layer\", 9],\n];\n\nconst coverage = {\n  \"API Gateway\": 82,\n  \"Auth Service\": 90,\n  Database: 65,\n  \"Cache Layer\": 78,\n  \"Job Queue\": 55,\n  \"Web UI\": 88,\n  Analytics: 70,\n  Notifications: 60,\n};\n\n// --- Layout geometry ---------------------------------------------------------\nconst cx = W / 2;\nconst cy = H / 2 + 30; // nudge down to leave room for the title\nconst outerLimit = Math.min(W, H) / 2 - 60;\nconst labelBand = 80;\nconst segmentOuter = outerLimit - labelBand;\nconst ringWidth = segmentOuter * 0.09;\nconst segmentInner = segmentOuter - ringWidth;\nconst trackOuter = segmentInner - 8;\nconst trackBandHeight = ringWidth;\nconst trackBase = trackOuter - trackBandHeight;\nconst ribbonRadius = trackBase - 10;\n\nconst GAP_DEG = 4;\nconst moduleColor = Object.fromEntries(\n  modules.map((m, i) => [m, t.palette[i]]),\n);\n\n// --- Angle allocation (chord-diagram convention: arc span ∝ total connection\n// value touching each module) --------------------------------------------\nconst segmentTotal = Object.fromEntries(modules.map((m) => [m, 0]));\nconnections.forEach(([source, target, value]) => {\n  segmentTotal[source] += value;\n  segmentTotal[target] += value;\n});\n\nconst totalGap = GAP_DEG * modules.length;\nconst totalValue = modules.reduce((sum, m) => sum + segmentTotal[m], 0);\nconst degPerValue = (360 - totalGap) / totalValue;\n\nconst segmentAngle = {};\nlet cursor = 0;\nmodules.forEach((m) => {\n  const span = segmentTotal[m] * degPerValue;\n  segmentAngle[m] = { start: cursor, end: cursor + span };\n  cursor += span + GAP_DEG;\n});\n\nconst segmentCursor = Object.fromEntries(\n  modules.map((m) => [m, segmentAngle[m].start]),\n);\nconst ribbons = connections.map(([source, target, value]) => {\n  const width = value * degPerValue;\n  const sStart = segmentCursor[source];\n  const sEnd = sStart + width;\n  segmentCursor[source] = sEnd;\n  const tStart = segmentCursor[target];\n  const tEnd = tStart + width;\n  segmentCursor[target] = tEnd;\n  return { source, target, value, sStart, sEnd, tStart, tEnd };\n});\n\n// --- Geometry helpers --------------------------------------------------------\nfunction polar(r, deg) {\n  const rad = (deg * Math.PI) / 180;\n  return [cx + r * Math.sin(rad), cy - r * Math.cos(rad)];\n}\n\n// The declarative `graphic` component only ships a handful of registered shape\n// types (circle/sector/ring/polygon/polyline/rect/line/bezierCurve/arc/text) —\n// no generic SVG-path type. Donut wedges and chord ribbons are built instead as\n// sampled-point polygons, dense enough to read as smooth curves.\nfunction arcPoints(r, startDeg, endDeg, steps) {\n  const pts = [];\n  for (let i = 0; i <= steps; i++) {\n    pts.push(polar(r, startDeg + (endDeg - startDeg) * (i / steps)));\n  }\n  return pts;\n}\n\nfunction quadBezierPoints(p0, pControl, p1, steps) {\n  const pts = [];\n  for (let i = 1; i <= steps; i++) {\n    const u = i / steps;\n    const x =\n      (1 - u) * (1 - u) * p0[0] + 2 * (1 - u) * u * pControl[0] + u * u * p1[0];\n    const y =\n      (1 - u) * (1 - u) * p0[1] + 2 * (1 - u) * u * pControl[1] + u * u * p1[1];\n    pts.push([x, y]);\n  }\n  return pts;\n}\n\nfunction sectorPoints(rOuter, rInner, startDeg, endDeg) {\n  const steps = Math.max(4, Math.round((endDeg - startDeg) / 3));\n  return arcPoints(rOuter, startDeg, endDeg, steps).concat(\n    arcPoints(rInner, endDeg, startDeg, steps),\n  );\n}\n\nfunction ribbonPoints(r, sStart, sEnd, tStart, tEnd) {\n  const curveSteps = 20;\n  const arcS = arcPoints(\n    r,\n    sStart,\n    sEnd,\n    Math.max(4, Math.round((sEnd - sStart) / 3)),\n  );\n  const arcT = arcPoints(\n    r,\n    tStart,\n    tEnd,\n    Math.max(4, Math.round((tEnd - tStart) / 3)),\n  );\n  const curve1 = quadBezierPoints(\n    arcS[arcS.length - 1],\n    [cx, cy],\n    arcT[0],\n    curveSteps,\n  );\n  const curve2 = quadBezierPoints(\n    arcT[arcT.length - 1],\n    [cx, cy],\n    arcS[0],\n    curveSteps,\n  );\n  return arcS.concat(curve1, arcT, curve2);\n}\n\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction lerpColor(a, b, frac) {\n  const [r1, g1, b1] = hexToRgb(a);\n  const [r2, g2, b2] = hexToRgb(b);\n  const r = Math.round(r1 + (r2 - r1) * frac);\n  const g = Math.round(g1 + (g2 - g1) * frac);\n  const bl = Math.round(b1 + (b2 - b1) * frac);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\n// --- Graphic elements: ribbons (bottom) → track → segment ring → labels -----\nconst covValues = Object.values(coverage);\nconst covMin = Math.min(...covValues);\nconst covMax = Math.max(...covValues);\n\n// Visual hierarchy: weaker connections fade back, the 3 strongest are drawn\n// last (so they layer on top) and get a brighter fill + colored outline —\n// giving the reader a focal point instead of a uniform hairball.\nconst connectionValues = ribbons.map((rb) => rb.value);\nconst valueMin = Math.min(...connectionValues);\nconst valueMax = Math.max(...connectionValues);\nconst emphasisCutoff = [...connectionValues].sort((a, b) => b - a)[2];\n\nconst ribbonElements = [...ribbons]\n  .sort((a, b) => a.value - b.value)\n  .map((rb) => {\n    const frac = (rb.value - valueMin) / (valueMax - valueMin);\n    const emphasized = rb.value >= emphasisCutoff;\n    return {\n      type: \"polygon\",\n      shape: {\n        points: ribbonPoints(\n          ribbonRadius,\n          rb.sStart,\n          rb.sEnd,\n          rb.tStart,\n          rb.tEnd,\n        ),\n      },\n      style: {\n        fill: moduleColor[rb.source],\n        opacity: 0.22 + frac * 0.45 + (emphasized ? 0.1 : 0),\n        stroke: emphasized ? moduleColor[rb.source] : t.pageBg,\n        lineWidth: emphasized ? 2 : 1,\n      },\n      silent: true,\n    };\n  });\n\nconst trackBaseline = {\n  type: \"ring\",\n  shape: { cx, cy, r: trackBase + 1.5, r0: trackBase - 1.5 },\n  style: { fill: t.grid },\n  silent: true,\n};\n\nconst trackElements = modules.map((m) => {\n  const { start, end } = segmentAngle[m];\n  const frac = (coverage[m] - covMin) / (covMax - covMin);\n  const barOuter = trackBase + trackBandHeight * Math.max(frac, 0.3);\n  return {\n    type: \"polygon\",\n    shape: { points: sectorPoints(barOuter, trackBase, start, end) },\n    style: {\n      fill: lerpColor(t.seq[0], t.seq[1], frac),\n      stroke: t.pageBg,\n      lineWidth: 1,\n    },\n    silent: true,\n  };\n});\n\nconst segmentElements = modules.map((m) => {\n  const { start, end } = segmentAngle[m];\n  return {\n    type: \"polygon\",\n    shape: { points: sectorPoints(segmentOuter, segmentInner, start, end) },\n    style: { fill: moduleColor[m], stroke: t.pageBg, lineWidth: 2 },\n    silent: true,\n  };\n});\n\nconst labelElements = modules.map((m) => {\n  const { start, end } = segmentAngle[m];\n  const mid = (start + end) / 2;\n  const [lx, ly] = polar(segmentOuter + 18, mid);\n  const rightHalf = mid < 180;\n  return {\n    type: \"text\",\n    style: {\n      text: m,\n      x: lx,\n      y: ly,\n      fill: t.ink,\n      fontSize: 18,\n      fontWeight: 500,\n      align: rightHalf ? \"left\" : \"right\",\n      verticalAlign: \"middle\",\n    },\n    silent: true,\n  };\n});\n\n// A small swatch + caption identifying the inner ring, so its meaning reads\n// from the static image alone (no need to inspect the source for what the\n// green→blue gradient track encodes).\nconst circleTop = cy - segmentOuter;\nconst legendSwatchY = Math.max(70, circleTop / 2 - 7);\nconst legendSwatchWidth = 120;\nconst legendSwatchHeight = 14;\nconst coverageGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [\n  { offset: 0, color: t.seq[0] },\n  { offset: 1, color: t.seq[1] },\n]);\n\nconst legendElements = [\n  {\n    type: \"rect\",\n    shape: {\n      x: cx - legendSwatchWidth / 2,\n      y: legendSwatchY,\n      width: legendSwatchWidth,\n      height: legendSwatchHeight,\n    },\n    style: { fill: coverageGradient, stroke: t.grid, lineWidth: 1 },\n    silent: true,\n  },\n  {\n    type: \"text\",\n    style: {\n      text: \"Inner track: test coverage % (low → high)\",\n      x: cx,\n      y: legendSwatchY + legendSwatchHeight + 16,\n      fill: t.inkSoft,\n      fontSize: 14,\n      align: \"center\",\n      verticalAlign: \"middle\",\n    },\n    silent: true,\n  },\n];\n\n// --- Init & option ------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"circos-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 30,\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  graphic: [\n    ...ribbonElements,\n    trackBaseline,\n    ...trackElements,\n    ...segmentElements,\n    ...labelElements,\n    ...legendElements,\n  ],\n});\n\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}