{"spec_id":"heatmap-risk-matrix","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nheatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 83/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file (matplotlib.py) from shadowing the matplotlib package\n_d = os.path.dirname(os.path.abspath(__file__))\nwhile _d in sys.path:\n    sys.path.remove(_d)\n\nimport matplotlib.patches as mpatches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom matplotlib.lines import Line2D\n\n\n# Theme\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# Imprint palette — 8 hues, theme-independent, hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / caution semantic anchor\n\n# Data\nnp.random.seed(42)\n\nlikelihood_labels = [\"Rare\", \"Unlikely\", \"Possible\", \"Likely\", \"Almost\\nCertain\"]\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\nrisks = [\n    (\"Supply Chain\\nDisruption\", 4, 4, \"Operational\"),\n    (\"Data Breach\", 3, 5, \"Technical\"),\n    (\"Budget\\nOverrun\", 4, 3, \"Financial\"),\n    (\"Key Staff\\nTurnover\", 3, 3, \"Operational\"),\n    (\"Regulatory\\nChange\", 2, 4, \"Financial\"),\n    (\"Server\\nOutage\", 3, 4, \"Technical\"),\n    (\"Scope\\nCreep\", 4, 2, \"Operational\"),\n    (\"Vendor\\nFailure\", 2, 3, \"Financial\"),\n    (\"Cyber\\nAttack\", 2, 5, \"Technical\"),\n    (\"Market\\nShift\", 3, 2, \"Financial\"),\n    (\"Power\\nFailure\", 1, 4, \"Technical\"),\n    (\"Deadline\\nSlip\", 5, 2, \"Operational\"),\n    (\"Equipment\\nWear\", 1, 1, \"Operational\"),\n    (\"Minor\\nDelay\", 2, 1, \"Financial\"),\n]\n\n# Risk colormap: Imprint green (low) → amber → ochre → matte red (critical)\ncmap = LinearSegmentedColormap.from_list(\"risk_matrix\", [\"#009E73\", ANYPLOT_AMBER, \"#BD8233\", \"#AE3030\"], N=256)\n\n# Category colors — Imprint palette positions 1→3 in first-appearance order\ncategory_order = [\"Operational\", \"Technical\", \"Financial\"]\ncategory_colors = {\n    \"Operational\": IMPRINT_PALETTE[0],  # #009E73 green\n    \"Technical\": IMPRINT_PALETTE[1],  # #C475FD lavender\n    \"Financial\": IMPRINT_PALETTE[2],  # #4467A3 blue\n}\n\n# Plot — square canvas (2400×2400 px)\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nfig.subplots_adjust(left=0.17, right=0.83, top=0.92, bottom=0.24)\n\n# Draw cells with rounded rectangles; score in bottom-left corner to reduce crowding\nfor i in range(5):\n    for j in range(5):\n        score = risk_scores[i, j]\n        color = cmap(score / 25.0)\n        rect = mpatches.FancyBboxPatch(\n            (j, i), 1, 1, boxstyle=\"round,pad=0.02\", facecolor=color, edgecolor=PAGE_BG, linewidth=2.5\n        )\n        ax.add_patch(rect)\n        score_color = INK if score < 10 else \"white\"\n        ax.text(\n            j + 0.13,\n            i + 0.12,\n            str(score),\n            ha=\"left\",\n            va=\"bottom\",\n            fontsize=10,\n            fontweight=\"bold\",\n            color=score_color,\n            alpha=0.58,\n            zorder=3,\n        )\n\n# Zone boundary dashed lines\nzone_boundaries = [\n    ([0, 1, 2, 3, 4, 5], [4, 3, 2, 1, 1, 1]),\n    ([0, 1, 2, 3, 4, 5], [5, 5, 4, 3, 2, 2]),\n    ([2, 3, 4, 5], [5, 5, 5, 4]),\n]\nfor xs, ys in zone_boundaries:\n    ax.plot(xs, ys, color=\"white\", linewidth=2.0, linestyle=\"--\", alpha=0.65, zorder=3)\n\n# Risk markers with jitter\njitter_offsets = np.random.uniform(-0.15, 0.15, (len(risks), 2))\n\nlabel_offsets = {\n    # Likely row × Minor col: above (no one else above here)\n    \"Scope\\nCreep\": (0.0, 0.28, \"bottom\"),\n    # Likely row × Moderate col: below (avoids Supply Chain above at right)\n    \"Budget\\nOverrun\": (0.0, -0.28, \"top\"),\n    # Likely row × Major col: above-left, stays within Major column\n    \"Supply Chain\\nDisruption\": (-0.25, 0.28, \"bottom\"),\n    # Possible row × Minor col: below (avoids Scope Creep above in same column)\n    \"Market\\nShift\": (0.0, -0.28, \"top\"),\n    # Possible row × Moderate col: below-left (away from Market Shift at x=1.5)\n    \"Key Staff\\nTurnover\": (-0.18, -0.28, \"top\"),\n    # Possible row × Major col: below-right\n    \"Server\\nOutage\": (0.16, -0.28, \"top\"),\n    # Unlikely row × Major col: below-left\n    \"Regulatory\\nChange\": (-0.10, -0.28, \"top\"),\n    \"Equipment\\nWear\": (0.0, 0.26, \"bottom\"),\n    \"Minor\\nDelay\": (0.0, 0.26, \"bottom\"),\n}\n\nfor idx, (name, lik, imp, cat) in enumerate(risks):\n    x = (imp - 1) + 0.5 + jitter_offsets[idx, 0]\n    y = (lik - 1) + 0.5 + jitter_offsets[idx, 1]\n    score = lik * imp\n    msize = 18 if score >= 20 else (15 if score >= 10 else (12 if score >= 5 else 10))\n\n    ax.plot(\n        x,\n        y,\n        \"o\",\n        markersize=msize,\n        color=category_colors[cat],\n        markeredgecolor=\"white\",\n        markeredgewidth=2.0,\n        zorder=5,\n        alpha=0.92,\n    )\n\n    if name in label_offsets:\n        dx, dy, va = label_offsets[name]\n        lx, ly = x + dx, y + dy\n    else:\n        lx, ly, va = x, y + 0.28, \"bottom\"\n\n    label = ax.text(\n        lx, ly, name, ha=\"center\", va=va, fontsize=7, fontweight=\"bold\", color=INK, zorder=6, linespacing=0.85\n    )\n    label.set_path_effects([pe.withStroke(linewidth=4.0, foreground=PAGE_BG)])\n\n# Style\nax.set_xlim(0, 5)\nax.set_ylim(0, 5)\nax.set_xticks([0.5, 1.5, 2.5, 3.5, 4.5])\nax.set_xticklabels(impact_labels, fontsize=8, color=INK_SOFT)\nax.set_yticks([0.5, 1.5, 2.5, 3.5, 4.5])\nax.set_yticklabels(likelihood_labels, fontsize=8, color=INK_SOFT)\nax.set_xlabel(\"Impact\", fontsize=10, labelpad=10, color=INK)\nax.set_ylabel(\"Likelihood\", fontsize=10, labelpad=10, color=INK)\nax.tick_params(axis=\"both\", length=0, colors=INK_SOFT)\n\ntitle = \"heatmap-risk-matrix · python · matplotlib · anyplot.ai\"\ntitle_n = len(title)\ntitle_fontsize = max(8, round(12 * 67 / title_n)) if title_n > 67 else 12\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=14)\n\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\n# Legends — placed horizontally below the matrix\ncat_handles = [\n    Line2D(\n        [0],\n        [0],\n        marker=\"o\",\n        color=\"w\",\n        markerfacecolor=category_colors[cat],\n        markersize=9,\n        markeredgecolor=\"white\",\n        markeredgewidth=1.5,\n        label=cat,\n    )\n    for cat in category_order\n]\nzone_info = [\n    (\"Low (1–4)\", cmap(2 / 25.0)),\n    (\"Medium (5–9)\", cmap(7 / 25.0)),\n    (\"High (10–16)\", cmap(13 / 25.0)),\n    (\"Critical (20–25)\", cmap(22 / 25.0)),\n]\nzone_handles = [\n    mpatches.Patch(facecolor=color, edgecolor=INK_MUTED, linewidth=0.5, label=label) for label, color in zone_info\n]\n\nleg1 = ax.legend(\n    handles=cat_handles,\n    title=\"Category\",\n    fontsize=8,\n    title_fontsize=9,\n    loc=\"upper left\",\n    bbox_to_anchor=(0.0, -0.08),\n    ncols=3,\n    frameon=True,\n    handletextpad=0.5,\n    columnspacing=1.0,\n)\nleg1.get_frame().set_facecolor(ELEVATED_BG)\nleg1.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg1.get_texts(), color=INK_SOFT)\nleg1.get_title().set_color(INK)\nax.add_artist(leg1)\n\nleg2 = ax.legend(\n    handles=zone_handles,\n    title=\"Risk Level\",\n    fontsize=8,\n    title_fontsize=9,\n    loc=\"upper left\",\n    bbox_to_anchor=(0.0, -0.18),\n    ncols=4,\n    frameon=True,\n    handletextpad=0.5,\n    columnspacing=1.0,\n)\nleg2.get_frame().set_facecolor(ELEVATED_BG)\nleg2.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg2.get_texts(), color=INK_SOFT)\nleg2.get_title().set_color(INK)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}