{"spec_id":"heatmap-risk-matrix","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nheatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data\nnp.random.seed(42)\n\nlikelihood_labels = [\"Rare\", \"Unlikely\", \"Possible\", \"Likely\", \"Almost Certain\"]\nimpact_labels = [\"Negligible\", \"Minor\", \"Moderate\", \"Major\", \"Catastrophic\"]\n\nrisk_scores = np.array([[1, 2, 3, 4, 5], [2, 4, 6, 8, 10], [3, 6, 9, 12, 15], [4, 8, 12, 16, 20], [5, 10, 15, 20, 25]])\n\n# Semantic risk-zone colors: green → amber → ochre → red (Imprint palette members)\nzone_thresholds = [(4, \"Low\"), (9, \"Medium\"), (16, \"High\"), (25, \"Critical\")]\nzone_colors = {\n    \"Low\": \"#009E73\",  # Imprint brand green\n    \"Medium\": \"#DDCC77\",  # Imprint amber anchor\n    \"High\": \"#BD8233\",  # Imprint ochre — closest Imprint member to orange\n    \"Critical\": \"#AE3030\",  # Imprint matte red\n}\nzone_bg = {\n    \"Low\": \"rgba(0,158,115,0.22)\" if THEME == \"light\" else \"rgba(0,158,115,0.20)\",\n    \"Medium\": \"rgba(221,204,119,0.25)\" if THEME == \"light\" else \"rgba(221,204,119,0.22)\",\n    \"High\": \"rgba(189,130,51,0.22)\" if THEME == \"light\" else \"rgba(189,130,51,0.20)\",\n    \"Critical\": \"rgba(174,48,48,0.22)\" if THEME == \"light\" else \"rgba(174,48,48,0.20)\",\n}\n# Zone severity → marker border width (consistent marker size per change request)\nzone_border = {\"Critical\": 3.0, \"High\": 2.0, \"Medium\": 1.5, \"Low\": 1.0}\n\n# Imprint categorical palette for risk categories (canonical order)\ncategory_colors = {\n    \"Operational\": \"#009E73\",  # Imprint position 1 — brand green\n    \"Technical\": \"#C475FD\",  # Imprint position 2 — lavender\n    \"Financial\": \"#4467A3\",  # Imprint position 3 — blue\n}\ncategory_symbols = {\"Operational\": \"square\", \"Technical\": \"circle\", \"Financial\": \"diamond\"}\n\n# Risk items\nrisks = [\n    {\"name\": \"Supply Chain\", \"likelihood\": 3, \"impact\": 4, \"category\": \"Operational\"},\n    {\"name\": \"Data Breach\", \"likelihood\": 2, \"impact\": 5, \"category\": \"Technical\"},\n    {\"name\": \"Budget Overrun\", \"likelihood\": 4, \"impact\": 3, \"category\": \"Financial\"},\n    {\"name\": \"Staff Turnover\", \"likelihood\": 3, \"impact\": 3, \"category\": \"Operational\"},\n    {\"name\": \"Regulatory\", \"likelihood\": 2, \"impact\": 4, \"category\": \"Financial\"},\n    {\"name\": \"System Outage\", \"likelihood\": 3, \"impact\": 5, \"category\": \"Technical\"},\n    {\"name\": \"Scope Creep\", \"likelihood\": 5, \"impact\": 2, \"category\": \"Operational\"},\n    {\"name\": \"Vendor Failure\", \"likelihood\": 2, \"impact\": 3, \"category\": \"Financial\"},\n    {\"name\": \"Cyber Attack\", \"likelihood\": 4, \"impact\": 5, \"category\": \"Technical\"},\n    {\"name\": \"Market Shift\", \"likelihood\": 3, \"impact\": 2, \"category\": \"Financial\"},\n    {\"name\": \"Tech Debt\", \"likelihood\": 5, \"impact\": 3, \"category\": \"Technical\"},\n    {\"name\": \"Compliance Gap\", \"likelihood\": 2, \"impact\": 4, \"category\": \"Operational\"},\n]\n\n# Pre-compute cell occupancy for jitter\ncell_items = {}\nfor risk in risks:\n    key = (risk[\"likelihood\"], risk[\"impact\"])\n    cell_items.setdefault(key, []).append(risk)\n\njitter_offsets = {\n    1: [(0, 0)],\n    2: [(-0.20, 0.16), (0.20, -0.16)],\n    3: [(-0.22, 0.16), (0.22, 0.16), (0, -0.18)],\n    4: [(-0.20, 0.16), (0.20, 0.16), (-0.20, -0.16), (0.20, -0.16)],\n}\n\n# Plot\nfig = go.Figure()\n\n# Colored cell backgrounds with theme-adaptive borders\nfor i in range(5):\n    for j in range(5):\n        score = risk_scores[i][j]\n        zone = next(name for threshold, name in zone_thresholds if score <= threshold)\n        fig.add_shape(\n            type=\"rect\",\n            x0=j + 0.5,\n            x1=j + 1.5,\n            y0=i + 0.5,\n            y1=i + 1.5,\n            fillcolor=zone_bg[zone],\n            line={\"color\": PAGE_BG, \"width\": 3},\n            layer=\"below\",\n        )\n        # Score label at bottom-right of each cell\n        fig.add_annotation(\n            x=j + 1.40,\n            y=i + 0.62,\n            text=f\"<b>{score}</b>\",\n            showarrow=False,\n            font={\"size\": 10, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            xanchor=\"right\",\n            yanchor=\"bottom\",\n        )\n\n# Risk markers — consistent size (22px), border width encodes zone severity\nseen_categories = set()\nfor risk in risks:\n    key = (risk[\"likelihood\"], risk[\"impact\"])\n    items = cell_items[key]\n    idx = items.index(risk)\n    n = len(items)\n\n    jx, jy = jitter_offsets[min(n, 4)][idx % min(n, 4)]\n    cat = risk[\"category\"]\n    score = risk[\"likelihood\"] * risk[\"impact\"]\n    zone = next(name for threshold, name in zone_thresholds if score <= threshold)\n\n    # Text position: alternate top/bottom by impact parity for n=1 to break row-wide crowding;\n    # jitter-relative positioning for shared cells\n    if n == 1:\n        tpos = \"top center\" if risk[\"impact\"] % 2 == 1 else \"bottom center\"\n    elif n == 2:\n        tpos = \"top center\" if jy > 0 else \"bottom center\"\n    elif n == 3:\n        tpos = \"bottom center\" if idx == 2 else (\"top right\" if jx < 0 else \"top left\")\n    else:\n        tpos = \"top center\" if jy > 0 else \"bottom center\"\n\n    show_legend = cat not in seen_categories\n    seen_categories.add(cat)\n\n    fig.add_trace(\n        go.Scatter(\n            x=[risk[\"impact\"] + jx],\n            y=[risk[\"likelihood\"] + jy],\n            mode=\"markers+text\",\n            marker={\n                \"size\": 22,\n                \"color\": category_colors[cat],\n                \"line\": {\"color\": INK, \"width\": zone_border[zone]},\n                \"symbol\": category_symbols[cat],\n                \"opacity\": 0.88,\n            },\n            text=f\"<b>{risk['name']}</b>\",\n            textposition=tpos,\n            textfont={\"size\": 9, \"color\": category_colors[cat], \"family\": \"Arial\"},\n            name=cat,\n            legendgroup=cat,\n            showlegend=show_legend,\n            hovertemplate=(\n                f\"<b>{risk['name']}</b><br>\"\n                f\"Likelihood: {likelihood_labels[risk['likelihood'] - 1]}<br>\"\n                f\"Impact: {impact_labels[risk['impact'] - 1]}<br>\"\n                f\"Risk Score: {score} ({zone})<br>\"\n                f\"Category: {cat}<extra></extra>\"\n            ),\n        )\n    )\n\n# Zone legend entries\nzone_ranges = {\"Low\": \"1–4\", \"Medium\": \"5–9\", \"High\": \"10–16\", \"Critical\": \"20–25\"}\nfor zone_name, color in zone_colors.items():\n    fig.add_trace(\n        go.Scatter(\n            x=[None],\n            y=[None],\n            mode=\"markers\",\n            marker={\"size\": 16, \"color\": color, \"symbol\": \"square\", \"opacity\": 0.55},\n            name=f\"  {zone_name} ({zone_ranges[zone_name]})\",\n            legendgroup=\"zones\",\n            legendgrouptitle={\"text\": \"Risk Zones\", \"font\": {\"size\": 10, \"color\": INK_SOFT}},\n        )\n    )\n\n# Layout — square canvas (2400×2400): 600×600 logical × scale 4\ntitle_text = \"heatmap-risk-matrix · python · plotly · anyplot.ai\"\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": (\n            f\"{title_text}<br>\"\n            f\"<sup style='color:{INK_MUTED};font-weight:normal'>\"\n            f\"Enterprise Risk Assessment — Likelihood vs Impact Matrix</sup>\"\n        ),\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.98,\n    },\n    xaxis={\n        \"title\": {\"text\": \"Impact Severity →\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 12},\n        \"tickvals\": [1, 2, 3, 4, 5],\n        \"ticktext\": impact_labels,\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT, \"family\": \"Arial\"},\n        \"range\": [0.35, 5.65],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"fixedrange\": True,\n        \"linecolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": \"← Likelihood\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 12},\n        \"tickvals\": [1, 2, 3, 4, 5],\n        \"ticktext\": likelihood_labels,\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT, \"family\": \"Arial\"},\n        \"range\": [0.35, 5.65],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"fixedrange\": True,\n        \"linecolor\": INK_SOFT,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK, \"family\": \"Arial\"},\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 1.01,\n        \"y\": 1,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"tracegroupgap\": 8,\n        \"itemsizing\": \"constant\",\n    },\n    margin={\"l\": 105, \"r\": 185, \"t\": 72, \"b\": 88},\n    hoverlabel={\"bgcolor\": ELEVATED_BG, \"bordercolor\": INK_SOFT, \"font\": {\"size\": 11, \"family\": \"Arial\", \"color\": INK}},\n)\n\n# Save — square canvas: 2400×2400 (600×600 logical × scale 4)\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}