{"spec_id":"circos-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// circos-basic: Circos Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-04\n//# anyplot-orientation: square\n// anyplot.ai\n// circos-basic: Circos Plot\n// Library: Highcharts 12.6.0 | Node 22\n// License: Highcharts — commercial license, free for non-commercial use (highcharts.com/license)\n// Quality: pending | Created: 2026-09-04\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Microservice dependency graph: outer ring + ribbons encode call volume\n// between services, the inner ring encodes average response latency.\nconst modules = [\n  \"Gateway\",\n  \"Auth\",\n  \"Users\",\n  \"Orders\",\n  \"Payments\",\n  \"Inventory\",\n  \"Search\",\n  \"Notify\",\n];\n\nconst connections = [\n  [\"Gateway\", \"Auth\", 42],\n  [\"Gateway\", \"Users\", 38],\n  [\"Gateway\", \"Orders\", 34],\n  [\"Gateway\", \"Search\", 22],\n  [\"Gateway\", \"Notify\", 9],\n  [\"Auth\", \"Users\", 18],\n  [\"Auth\", \"Payments\", 7],\n  [\"Orders\", \"Payments\", 30],\n  [\"Orders\", \"Inventory\", 24],\n  [\"Orders\", \"Notify\", 13],\n  [\"Orders\", \"Users\", 16],\n  [\"Payments\", \"Notify\", 8],\n  [\"Users\", \"Notify\", 14],\n  [\"Users\", \"Search\", 11],\n  [\"Inventory\", \"Search\", 10],\n  [\"Inventory\", \"Notify\", 6],\n  [\"Search\", \"Notify\", 5],\n  [\"Payments\", \"Inventory\", 9],\n];\n\nconst avgLatencyMs = {\n  Gateway: 30,\n  Auth: 45,\n  Users: 60,\n  Orders: 85,\n  Payments: 120,\n  Inventory: 55,\n  Search: 70,\n  Notify: 25,\n};\n\n// --- Layout math (chord-diagram style, no add-on module required) ----------\nconst segmentTotal = {};\nmodules.forEach((m) => {\n  segmentTotal[m] = 0;\n});\nconnections.forEach(([source, target, value]) => {\n  segmentTotal[source] += value;\n  segmentTotal[target] += value;\n});\nconst grandTotal = modules.reduce((sum, m) => sum + segmentTotal[m], 0);\n\nconst gapRad = (2.2 * Math.PI) / 180;\nconst availableRad = 2 * Math.PI - gapRad * modules.length;\n\nconst segAngles = {};\nlet angleCursor = 0;\nmodules.forEach((m) => {\n  const span = (segmentTotal[m] / grandTotal) * availableRad;\n  segAngles[m] = { start: angleCursor, end: angleCursor + span };\n  angleCursor += span + gapRad;\n});\n\nconst ribbonCursor = {};\nmodules.forEach((m) => {\n  ribbonCursor[m] = segAngles[m].start;\n});\n\nconst latencyValues = modules.map((m) => avgLatencyMs[m]);\nconst minLatency = Math.min(...latencyValues);\nconst maxLatency = Math.max(...latencyValues);\n\n// --- Chart shell (title/subtitle only — the diagram is custom-drawn) -------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"circos-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    y: 28,\n  },\n  subtitle: {\n    text: \"Outer ring &amp; ribbons: call volume (K/day) · Inner ring: avg. latency (ms)\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n    y: 54,\n  },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: { series: { animation: false, enableMouseTracking: false } },\n  series: [],\n});\n\nconst W = chart.chartWidth;\nconst H = chart.chartHeight;\nconst cx = W / 2;\nconst cy = H / 2 + H * 0.035;\nconst outerR = Math.min(W, H) / 2 - H * 0.14;\nconst ringThickness = outerR * 0.06;\nconst ringInnerR = outerR - ringThickness;\nconst trackOuterR = ringInnerR - outerR * 0.025;\nconst trackBaseR = trackOuterR - outerR * 0.15;\nconst ribbonR = trackBaseR - outerR * 0.02;\n\n// --- Geometry helpers --------------------------------------------------------\nfunction polar(r, angle) {\n  return { x: cx + r * Math.sin(angle), y: cy - r * Math.cos(angle) };\n}\n\nfunction wedgePath(rOuter, rInner, a1, a2) {\n  const large = a2 - a1 > Math.PI ? 1 : 0;\n  const p1 = polar(rOuter, a1);\n  const p2 = polar(rOuter, a2);\n  const p3 = polar(rInner, a2);\n  const p4 = polar(rInner, a1);\n  return (\n    `M ${p1.x} ${p1.y} A ${rOuter} ${rOuter} 0 ${large} 1 ${p2.x} ${p2.y} ` +\n    `L ${p3.x} ${p3.y} A ${rInner} ${rInner} 0 ${large} 0 ${p4.x} ${p4.y} Z`\n  );\n}\n\nfunction ribbonPath(r, s1, s2, t1, t2) {\n  const largeS = s2 - s1 > Math.PI ? 1 : 0;\n  const largeT = t2 - t1 > Math.PI ? 1 : 0;\n  const pS1 = polar(r, s1);\n  const pS2 = polar(r, s2);\n  const pT1 = polar(r, t1);\n  const pT2 = polar(r, t2);\n  return (\n    `M ${pS1.x} ${pS1.y} A ${r} ${r} 0 ${largeS} 1 ${pS2.x} ${pS2.y} ` +\n    `C ${cx} ${cy} ${cx} ${cy} ${pT1.x} ${pT1.y} ` +\n    `A ${r} ${r} 0 ${largeT} 1 ${pT2.x} ${pT2.y} ` +\n    `C ${cx} ${cy} ${cx} ${cy} ${pS1.x} ${pS1.y} Z`\n  );\n}\n\nfunction lerpColor(hexA, hexB, ratio) {\n  const a = parseInt(hexA.slice(1), 16);\n  const b = parseInt(hexB.slice(1), 16);\n  const ar = (a >> 16) & 255,\n    ag = (a >> 8) & 255,\n    ab = a & 255;\n  const br = (b >> 16) & 255,\n    bg = (b >> 8) & 255,\n    bb = b & 255;\n  const r = Math.round(ar + (br - ar) * ratio);\n  const g = Math.round(ag + (bg - ag) * ratio);\n  const bl = Math.round(ab + (bb - ab) * ratio);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\n// --- Ribbons (drawn first, innermost layer) ---------------------------------\nconnections.forEach(([source, target, value]) => {\n  const spanS =\n    (value / segmentTotal[source]) *\n    (segAngles[source].end - segAngles[source].start);\n  const aS1 = ribbonCursor[source];\n  const aS2 = aS1 + spanS;\n  ribbonCursor[source] = aS2;\n\n  const spanT =\n    (value / segmentTotal[target]) *\n    (segAngles[target].end - segAngles[target].start);\n  const aT1 = ribbonCursor[target];\n  const aT2 = aT1 + spanT;\n  ribbonCursor[target] = aT2;\n\n  const color = t.palette[modules.indexOf(source)];\n  chart.renderer\n    .path()\n    .attr({\n      d: ribbonPath(ribbonR, aS1, aS2, aT1, aT2),\n      fill: Highcharts.color(color).setOpacity(0.55).get(),\n      stroke: Highcharts.color(color).setOpacity(0.8).get(),\n      \"stroke-width\": 0.75,\n    })\n    .add();\n});\n\n// --- Inner track: average latency per module --------------------------------\nmodules.forEach((m) => {\n  const { start, end } = segAngles[m];\n  const ratio = (avgLatencyMs[m] - minLatency) / (maxLatency - minLatency);\n  const barR = trackBaseR + ratio * (trackOuterR - trackBaseR);\n  chart.renderer\n    .path()\n    .attr({\n      d: wedgePath(barR, trackBaseR, start, end),\n      fill: lerpColor(t.seq[0], t.seq[1], ratio),\n      stroke: t.pageBg,\n      \"stroke-width\": 1.5,\n    })\n    .add();\n});\n\n// --- Outer ring: one wedge per module ----------------------------------------\nmodules.forEach((m, i) => {\n  const { start, end } = segAngles[m];\n  chart.renderer\n    .path()\n    .attr({\n      d: wedgePath(outerR, ringInnerR, start, end),\n      fill: t.palette[i],\n      stroke: t.pageBg,\n      \"stroke-width\": 2,\n    })\n    .add();\n});\n\n// --- Module labels ------------------------------------------------------------\nmodules.forEach((m) => {\n  const { start, end } = segAngles[m];\n  const mid = (start + end) / 2;\n  const pos = polar(outerR + 26, mid);\n  let rotation = (mid * 180) / Math.PI - 90;\n  let align = \"left\";\n  if (mid > Math.PI) {\n    rotation += 180;\n    align = \"right\";\n  }\n  chart.renderer\n    .text(m, pos.x, pos.y)\n    .attr({ rotation, align })\n    .css({ color: t.ink, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n});\n\n// --- Latency color-scale legend -----------------------------------------------\nconst legendW = W * 0.16;\nconst legendH = 12;\nconst legendX = cx - legendW / 2;\nconst legendY = H - 46;\n\nchart.renderer\n  .text(\"Avg. latency per module\", cx, legendY - 10)\n  .attr({ align: \"center\" })\n  .css({ color: t.inkSoft, fontSize: \"12px\" })\n  .add();\n\nchart.renderer\n  .rect(legendX, legendY, legendW, legendH, 2)\n  .attr({\n    fill: {\n      linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 },\n      stops: [\n        [0, t.seq[0]],\n        [1, t.seq[1]],\n      ],\n    },\n  })\n  .add();\n\nchart.renderer\n  .text(`${minLatency} ms`, legendX - 10, legendY + legendH)\n  .attr({ align: \"right\" })\n  .css({ color: t.inkSoft, fontSize: \"12px\" })\n  .add();\n\nchart.renderer\n  .text(`${maxLatency} ms`, legendX + legendW + 10, legendY + legendH)\n  .attr({ align: \"left\" })\n  .css({ color: t.inkSoft, fontSize: \"12px\" })\n  .add();\n"}