{"spec_id":"heatmap-risk-matrix","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-20\n//# anyplot-orientation: square\n// anyplot.ai\n// heatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\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-20\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\nconst TITLE = \"heatmap-risk-matrix · javascript · muix · anyplot.ai\";\n\nconst LIKELIHOOD_LABELS = [\"Rare\", \"Unlikely\", \"Possible\", \"Likely\", \"Almost Certain\"];\nconst IMPACT_LABELS = [\"Negligible\", \"Minor\", \"Moderate\", \"Major\", \"Catastrophic\"];\n\nconst ZONE_LEGEND = [\n  { label: \"Low (1–4)\",        color: \"#009E73\" },\n  { label: \"Medium (5–9)\",     color: \"#DDCC77\" },\n  { label: \"High (10–16)\",     color: \"#BD8233\" },\n  { label: \"Critical (20–25)\", color: \"#AE3030\" },\n];\n\n// IT security risk register — 12 items across the 5×5 matrix\nconst risks = [\n  { name: \"Data Breach\",    likelihood: 3, impact: 5 },\n  { name: \"Ransomware\",     likelihood: 4, impact: 5 },\n  { name: \"DDoS Attack\",    likelihood: 4, impact: 3 },\n  { name: \"Supply Chain\",   likelihood: 2, impact: 4 },\n  { name: \"Insider Threat\", likelihood: 2, impact: 5 },\n  { name: \"API Failure\",    likelihood: 3, impact: 3 },\n  { name: \"Key Staff Loss\", likelihood: 3, impact: 4 },\n  { name: \"Budget Cut\",     likelihood: 4, impact: 2 },\n  { name: \"Compliance\",     likelihood: 2, impact: 3 },\n  { name: \"Cloud Outage\",   likelihood: 2, impact: 4 },\n  { name: \"Phishing\",       likelihood: 5, impact: 2 },\n  { name: \"HW Failure\",     likelihood: 1, impact: 3 },\n];\n\nfunction zoneColor(lh, imp) {\n  const score = lh * imp;\n  if (score <= 4)  return \"#009E73\"; // Low  — Imprint brand green\n  if (score <= 9)  return \"#DDCC77\"; // Medium — Imprint amber\n  if (score <= 16) return \"#BD8233\"; // High — Imprint ochre\n  return \"#AE3030\";                  // Critical — Imprint matte red\n}\n\n// Jitter: count co-occupants per cell, assign circular offset\nconst _cnt = {};\nrisks.forEach(r => { const k = `${r.likelihood}-${r.impact}`; _cnt[k] = (_cnt[k] ?? 0) + 1; });\nconst _itr = {};\nconst risksPlaced = risks.map(r => {\n  const k = `${r.likelihood}-${r.impact}`;\n  const idx = _itr[k] ?? 0;\n  _itr[k] = idx + 1;\n  const siblings = _cnt[k];\n  if (siblings === 1) return { ...r, jx: r.impact, jy: r.likelihood };\n  const angle = (2 * Math.PI * idx) / siblings - Math.PI / 2;\n  return { ...r, jx: r.impact + 0.22 * Math.cos(angle), jy: r.likelihood + 0.22 * Math.sin(angle) };\n});\n\n// Scatter series data — x=impact, y=likelihood\nconst scatterData = risksPlaced.map((r, i) => ({ id: i, x: r.jx, y: r.jy }));\n\n// Custom scatter mark component used both as a slot and as a direct overlay\nfunction RiskMark({ x, y, markerSize }) {\n  return (\n    <circle cx={x} cy={y} r={markerSize ?? 10}\n      fill={t.elevatedBg} stroke={t.ink} strokeWidth={2} />\n  );\n}\n\n// Direct circle overlay via useDrawingArea — ensures correct positioning even when\n// ScatterPlot slots are not forwarded (MUI X v7 ScatterPlot may not support slots directly)\nfunction RiskCircles() {\n  const { left, top, width, height } = useDrawingArea();\n  const toSvg = (dx, dy) => ({\n    sx: left + (dx - 0.5) / 5 * width,\n    sy: top  + (5.5 - dy) / 5 * height,\n  });\n  return (\n    <g>\n      {risksPlaced.map((r, i) => {\n        const { sx, sy } = toSvg(r.jx, r.jy);\n        return (\n          <circle key={i} cx={sx} cy={sy} r={10}\n            fill={t.elevatedBg} stroke={t.ink} strokeWidth={2} />\n        );\n      })}\n    </g>\n  );\n}\n\n// Zone background + risk-score numbers — uses MUI X useDrawingArea hook for positioning\nfunction ZoneBackground() {\n  const { left, top, width, height } = useDrawingArea();\n  const cW = width / 5;\n  const cH = height / 5;\n  const items = [];\n  for (let lh = 1; lh <= 5; lh++) {\n    for (let imp = 1; imp <= 5; imp++) {\n      items.push(\n        <rect key={`z${lh}${imp}`}\n          x={left + (imp - 1) * cW} y={top + (5 - lh) * cH}\n          width={cW} height={cH}\n          fill={zoneColor(lh, imp)} fillOpacity={0.38}\n          stroke={t.ink} strokeOpacity={0.18} strokeWidth={1} />,\n        <text key={`n${lh}${imp}`}\n          x={left + (imp - 0.5) * cW} y={top + (5.5 - lh) * cH}\n          textAnchor=\"middle\" dominantBaseline=\"middle\"\n          fontSize={16} fill={t.ink} fillOpacity={0.42} fontFamily={FONT}>\n          {lh * imp}\n        </text>\n      );\n    }\n  }\n  return <g>{items}</g>;\n}\n\n// Risk name labels — uses useDrawingArea to map data coords → SVG coords\nfunction RiskLabels() {\n  const { left, top, width, height } = useDrawingArea();\n  const toSvg = (dx, dy) => ({\n    sx: left + (dx - 0.5) / 5 * width,\n    sy: top  + (5.5 - dy) / 5 * height,\n  });\n  return (\n    <g>\n      {risksPlaced.map((r, i) => {\n        const { sx, sy } = toSvg(r.jx, r.jy);\n        return (\n          <text key={i} x={sx} y={sy - 16}\n            textAnchor=\"middle\" fontSize={13} fontWeight=\"500\"\n            fill={t.ink} fontFamily={FONT}>\n            {r.name}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\n// Zone legend rendered as SVG elements below the chart area\nfunction ZoneLegend({ W, H, ML, MR }) {\n  const colW = (W - ML - MR) / 4;\n  const ly = H - 55;\n  return (\n    <g>\n      {ZONE_LEGEND.map((z, i) => {\n        const lx = ML + colW * i + colW / 2 - 50;\n        return (\n          <g key={i}>\n            <rect x={lx} y={ly - 9} width={18} height={18}\n              fill={z.color} fillOpacity={0.65} rx={2} />\n            <text x={lx + 22} y={ly} dominantBaseline=\"middle\"\n              fontSize={12} fill={t.ink} fontFamily={FONT}>\n              {z.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;   // 1200 CSS px (square)\n  const H = window.ANYPLOT_SIZE.height;  // 1200 CSS px (square)\n  const margin = { left: 150, top: 72, right: 44, bottom: 190 };\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      margin={margin}\n      series={[{\n        type: \"scatter\",\n        id: \"risks\",\n        data: scatterData,\n        color: t.ink,\n        markerSize: 10,\n      }]}\n      xAxis={[{\n        min: 0.5, max: 5.5,\n        tickInterval: [1, 2, 3, 4, 5],\n        valueFormatter: (v) => IMPACT_LABELS[Math.round(v) - 1] ?? \"\",\n      }]}\n      yAxis={[{\n        min: 0.5, max: 5.5,\n        tickInterval: [1, 2, 3, 4, 5],\n        valueFormatter: (v) => LIKELIHOOD_LABELS[Math.round(v) - 1] ?? \"\",\n      }]}\n    >\n      {/* Chart title */}\n      <text x={W / 2} y={46}\n        textAnchor=\"middle\" fontSize={20} fontWeight=\"500\"\n        fill={t.ink} fontFamily={FONT}>\n        {TITLE}\n      </text>\n\n      {/* Zone cells + score labels (behind markers) */}\n      <ZoneBackground />\n\n      {/* MUI X scatter plot (provides axis-aware series rendering) */}\n      <ScatterPlot slots={{ scatter: RiskMark }} />\n\n      {/* Direct circle overlay: styled markers with elevated-bg fill + ink stroke */}\n      <RiskCircles />\n\n      {/* Risk item name labels (above markers) */}\n      <RiskLabels />\n\n      {/* MUI X axis components */}\n      <ChartsXAxis\n        label=\"Impact\"\n        tickLabelStyle={{ fontSize: 13, fill: t.inkSoft, fontFamily: FONT }}\n        labelStyle={{ fontSize: 15, fontWeight: \"500\", fill: t.ink, fontFamily: FONT }}\n      />\n      <ChartsYAxis\n        label=\"Likelihood\"\n        tickLabelStyle={{ fontSize: 13, fill: t.inkSoft, fontFamily: FONT }}\n        labelStyle={{ fontSize: 15, fontWeight: \"500\", fill: t.ink, fontFamily: FONT }}\n      />\n\n      {/* Zone legend */}\n      <ZoneLegend W={W} H={H} ML={margin.left} MR={margin.right} />\n    </ChartContainer>\n  );\n}\n"}