{"spec_id":"heatmap-risk-matrix","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 84/100 | Created: 2026-06-20\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Risk zone colors — Imprint semantic palette (green=safe, amber=caution, ochre=warning, red=critical)\nfunction riskColor(score) {\n  if (score <= 4)  return \"#009E73\"; // Low      — Imprint brand green\n  if (score <= 9)  return \"#DDCC77\"; // Medium   — Imprint amber (warning anchor)\n  if (score <= 16) return \"#BD8233\"; // High     — Imprint ochre\n  return \"#AE3030\";                  // Critical — Imprint matte red\n}\n\n// IT security risk register — 12 risks spanning all four risk zones\nconst risks = [\n  { name: \"Data Breach\",    likelihood: 3, impact: 5 },\n  { name: \"Cyber Attack\",   likelihood: 3, impact: 5 },\n  { name: \"Server Outage\",  likelihood: 4, impact: 4 },\n  { name: \"Compliance Gap\", likelihood: 2, impact: 5 },\n  { name: \"Key Staff Loss\", likelihood: 2, impact: 4 },\n  { name: \"Budget Overrun\", likelihood: 4, impact: 3 },\n  { name: \"Tech Debt\",      likelihood: 5, impact: 3 },\n  { name: \"Vendor Default\", likelihood: 2, impact: 3 },\n  { name: \"API Failure\",    likelihood: 3, impact: 3 },\n  { name: \"Scope Creep\",    likelihood: 5, impact: 2 },\n  { name: \"Supply Delay\",   likelihood: 4, impact: 2 },\n  { name: \"Talent Gap\",     likelihood: 3, impact: 2 },\n];\n\n// Deterministic jitter so overlapping risks in the same cell don't stack\nconst JX = [0,  0.18, -0.18,  0.09, -0.09];\nconst JY = [0,  0.20, -0.20,  0.18, -0.18];\nconst cellIdx = {};\nconst scatterData = risks.map(function (r) {\n  var key = r.likelihood + \",\" + r.impact;\n  var i   = cellIdx[key] = (cellIdx[key] !== undefined ? cellIdx[key] : 0);\n  cellIdx[key]++;\n  return { x: r.likelihood + (JX[i] || 0), y: r.impact + (JY[i] || 0), name: r.name };\n});\n\n// Draw colored background cells and score labels via SVG renderer\nfunction drawCells(chart) {\n  if (chart._riskGroup) chart._riskGroup.destroy();\n  var group = chart.renderer.g(\"risk-cells\").attr({ zIndex: 1 }).add();\n  chart._riskGroup = group;\n\n  var xa = chart.xAxis[0];\n  var ya = chart.yAxis[0];\n\n  for (var lk = 1; lk <= 5; lk++) {\n    for (var im = 1; im <= 5; im++) {\n      var score = lk * im;\n      var px1   = xa.toPixels(lk - 0.5);\n      var px2   = xa.toPixels(lk + 0.5);\n      var py1   = ya.toPixels(im - 0.5);\n      var py2   = ya.toPixels(im + 0.5);\n      var rx    = Math.min(px1, px2);\n      var ry    = Math.min(py1, py2);\n      var rw    = Math.abs(px2 - px1);\n      var rh    = Math.abs(py2 - py1);\n\n      chart.renderer.rect(rx, ry, rw, rh)\n        .attr({\n          fill: riskColor(score),\n          stroke: t.inkSoft,\n          \"stroke-width\": 1.5,\n          zIndex: 1,\n        })\n        .add(group);\n\n      // Score label in bottom-right corner of cell\n      chart.renderer.text(String(score), rx + rw - 12, ry + rh - 12)\n        .attr({ align: \"right\", zIndex: 2 })\n        .css({ color: t.ink, fontSize: \"12px\", opacity: \"0.5\" })\n        .add(group);\n    }\n  }\n}\n\nvar TITLE = \"heatmap-risk-matrix · javascript · highcharts · anyplot.ai\";\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginTop: 90,\n    marginBottom: 95,\n    marginLeft: 145,\n    marginRight: 50,\n    events: {\n      render: function () { drawCells(this); },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    useHTML: true,\n    text:\n      \"<span style=\\\"color:#009E73;font-weight:600\\\">■ Low (1–4)</span>\" +\n      \"&ensp;&ensp;\" +\n      \"<span style=\\\"color:#DDCC77;font-weight:600\\\">■ Medium (5–9)</span>\" +\n      \"&ensp;&ensp;\" +\n      \"<span style=\\\"color:#BD8233;font-weight:600\\\">■ High (10–16)</span>\" +\n      \"&ensp;&ensp;\" +\n      \"<span style=\\\"color:#AE3030;font-weight:600\\\">■ Critical (20–25)</span>\",\n    style: { fontSize: \"14px\" },\n  },\n\n  xAxis: {\n    min: 0.5,\n    max: 5.5,\n    tickInterval: 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: {\n      rotation: -30,\n      style: { color: t.inkSoft, fontSize: \"13px\" },\n      formatter: function () {\n        var map = { 1: \"Rare\", 2: \"Unlikely\", 3: \"Possible\", 4: \"Likely\", 5: \"Almost Certain\" };\n        return map[this.value] || \"\";\n      },\n    },\n    title: {\n      text: \"Likelihood\",\n      style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\" },\n      margin: 20,\n    },\n  },\n\n  yAxis: {\n    min: 0.5,\n    max: 5.5,\n    tickInterval: 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"13px\" },\n      formatter: function () {\n        var map = { 1: \"Negligible\", 2: \"Minor\", 3: \"Moderate\", 4: \"Major\", 5: \"Catastrophic\" };\n        return map[this.value] || \"\";\n      },\n    },\n    title: {\n      text: \"Impact\",\n      style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\" },\n      margin: 20,\n    },\n  },\n\n  legend: { enabled: false },\n\n  tooltip: {\n    formatter: function () {\n      var lk = Math.round(this.x);\n      var im = Math.round(this.y);\n      return \"<b>\" + this.point.name + \"</b><br/>Score: \" + (lk * im);\n    },\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink, fontSize: \"13px\" },\n  },\n\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      zIndex: 5,\n      marker: {\n        symbol: \"circle\",\n        radius: 10,\n        fillColor: t.ink,\n        lineColor: t.pageBg,\n        lineWidth: 2,\n      },\n      dataLabels: {\n        enabled: true,\n        allowOverlap: true,\n        formatter: function () { return this.point.name; },\n        style: {\n          color: t.ink,\n          fontSize: \"11px\",\n          fontWeight: \"500\",\n          textOutline: \"2px \" + t.pageBg,\n        },\n        verticalAlign: \"top\",\n        y: -18,\n        align: \"center\",\n      },\n    },\n  },\n\n  series: [{\n    name: \"Risks\",\n    data: scatterData,\n    zIndex: 5,\n  }],\n});\n"}