{"spec_id":"heatmap-risk-matrix","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nheatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.colors import LinearSegmentedColormap\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\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\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    {\"name\": \"Srv Outage\", \"likelihood\": 2, \"impact\": 4, \"category\": \"Technical\"},\n    {\"name\": \"Data Breach\", \"likelihood\": 2, \"impact\": 5, \"category\": \"Technical\"},\n    {\"name\": \"Over Budget\", \"likelihood\": 4, \"impact\": 3, \"category\": \"Financial\"},\n    {\"name\": \"Staff Loss\", \"likelihood\": 3, \"impact\": 3, \"category\": \"Operational\"},\n    {\"name\": \"Vendor Fail\", \"likelihood\": 2, \"impact\": 3, \"category\": \"Operational\"},\n    {\"name\": \"Scope Creep\", \"likelihood\": 5, \"impact\": 2, \"category\": \"Project\"},\n    {\"name\": \"Reg. Change\", \"likelihood\": 3, \"impact\": 4, \"category\": \"Financial\"},\n    {\"name\": \"Cyber Attack\", \"likelihood\": 3, \"impact\": 5, \"category\": \"Technical\"},\n    {\"name\": \"Supply Delay\", \"likelihood\": 4, \"impact\": 4, \"category\": \"Operational\"},\n    {\"name\": \"Mkt Shift\", \"likelihood\": 3, \"impact\": 2, \"category\": \"Financial\"},\n    {\"name\": \"Power Fail\", \"likelihood\": 1, \"impact\": 3, \"category\": \"Technical\"},\n    {\"name\": \"Disputes\", \"likelihood\": 2, \"impact\": 2, \"category\": \"Financial\"},\n    {\"name\": \"Defects\", \"likelihood\": 3, \"impact\": 3, \"category\": \"Project\"},\n    {\"name\": \"Deadline\", \"likelihood\": 4, \"impact\": 2, \"category\": \"Project\"},\n    {\"name\": \"IP Theft\", \"likelihood\": 1, \"impact\": 5, \"category\": \"Technical\"},\n]\n\n# Imprint-derived risk colormap: green (Low) → amber (Medium) → ochre (High) → red (Critical)\ncmap = LinearSegmentedColormap.from_list(\"risk_imprint\", [\"#009E73\", ANYPLOT_AMBER, \"#BD8233\", \"#AE3030\"], N=256)\n\n# Plot — square canvas for symmetric 5×5 grid\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400)\nfig.patch.set_facecolor(PAGE_BG)\nfig.subplots_adjust(left=0.15, right=0.70, top=0.88, bottom=0.13)\n\nsns.heatmap(\n    risk_scores,\n    annot=False,\n    cmap=cmap,\n    vmin=1,\n    vmax=25,\n    linewidths=1.8,\n    linecolor=PAGE_BG,\n    cbar_kws={\"shrink\": 0.72, \"pad\": 0.10, \"aspect\": 22},\n    square=True,\n    ax=ax,\n)\n\n# Score labels in bottom-right corner of each cell\nfor i in range(5):\n    for j in range(5):\n        ax.text(\n            j + 0.88,\n            i + 0.88,\n            str(risk_scores[i, j]),\n            ha=\"right\",\n            va=\"bottom\",\n            fontsize=9,\n            fontweight=\"bold\",\n            color=INK,\n            alpha=0.45,\n            zorder=2,\n        )\n\n# Category markers from Imprint palette positions 1-4\ncategories = [\"Technical\", \"Financial\", \"Operational\", \"Project\"]\ncat_colors = {cat: IMPRINT_PALETTE[i] for i, cat in enumerate(categories)}\ncat_markers = {\"Technical\": \"o\", \"Financial\": \"s\", \"Operational\": \"D\", \"Project\": \"^\"}\n\n# Build per-cell item lists for offset positioning\ncell_items = {}\nfor risk in risks:\n    key = (risk[\"likelihood\"] - 1, risk[\"impact\"] - 1)\n    cell_items.setdefault(key, []).append(risk)\n\noffsets_map = {\n    1: [(0.0, 0.0)],\n    2: [(-0.24, 0.0), (0.24, 0.0)],\n    3: [(-0.24, -0.16), (0.24, -0.16), (0.0, 0.20)],\n    4: [(-0.22, -0.16), (0.22, -0.16), (-0.22, 0.20), (0.22, 0.20)],\n}\n\nplot_data = []\nfor _key, items in cell_items.items():\n    n = len(items)\n    offsets = offsets_map.get(n, offsets_map[3])\n    for i, risk in enumerate(items):\n        ox, oy = offsets[i] if i < len(offsets) else (0, 0)\n        score = risk[\"likelihood\"] * risk[\"impact\"]\n        # Alternate labels above/below for multi-item cells to prevent overlap\n        label_dy = 0.26 if i % 2 == 0 else -0.30\n        plot_data.append(\n            {\n                \"x\": risk[\"impact\"] - 1 + 0.5 + ox,\n                \"y\": risk[\"likelihood\"] - 1 + 0.40 + oy,\n                \"name\": risk[\"name\"],\n                \"category\": risk[\"category\"],\n                \"score\": score,\n                \"size\": 120 + score * 12,\n                \"is_critical\": score >= 16,\n                \"label_dy\": label_dy,\n            }\n        )\n\ndf_risks = pd.DataFrame(plot_data)\nsize_min = df_risks[\"size\"].min()\nsize_max = df_risks[\"size\"].max()\n\n# Scatter markers per category with consistent size scale\nfor cat, marker in cat_markers.items():\n    cat_df = df_risks[df_risks[\"category\"] == cat]\n    if cat_df.empty:\n        continue\n    sns.scatterplot(\n        data=cat_df,\n        x=\"x\",\n        y=\"y\",\n        size=\"size\",\n        sizes=(size_min, size_max),\n        color=cat_colors[cat],\n        marker=marker,\n        edgecolor=PAGE_BG,\n        linewidth=1.8,\n        legend=False,\n        ax=ax,\n        zorder=5,\n    )\n\n# Critical risk emphasis rings\ncritical_df = df_risks[df_risks[\"is_critical\"]]\nfor _, row in critical_df.iterrows():\n    ax.scatter(row[\"x\"], row[\"y\"], s=row[\"size\"] + 160, facecolors=\"none\", edgecolors=INK, linewidths=2, zorder=4)\n\n# Risk item labels with elevated background boxes\nfor _, row in df_risks.iterrows():\n    ax.text(\n        row[\"x\"],\n        row[\"y\"] + row[\"label_dy\"],\n        row[\"name\"],\n        ha=\"center\",\n        va=\"top\",\n        fontsize=6,\n        fontweight=\"bold\",\n        color=INK,\n        zorder=6,\n        clip_on=False,\n        bbox={\n            \"boxstyle\": \"round,pad=0.10\",\n            \"facecolor\": ELEVATED_BG,\n            \"edgecolor\": INK if row[\"is_critical\"] else \"none\",\n            \"linewidth\": 1.0 if row[\"is_critical\"] else 0,\n            \"alpha\": 0.88,\n        },\n    )\n\n# Axis styling\nax.set_xticklabels(impact_labels, fontsize=8, fontweight=\"medium\", color=INK_SOFT)\nax.set_yticklabels(likelihood_labels, fontsize=8, rotation=0, fontweight=\"medium\", color=INK_SOFT)\nax.set_xlabel(\"Impact\", fontsize=10, fontweight=\"medium\", labelpad=10, color=INK)\nax.set_ylabel(\"Likelihood\", fontsize=10, fontweight=\"medium\", labelpad=10, color=INK)\n\ntitle = \"heatmap-risk-matrix · python · seaborn · anyplot.ai\"\nn_chars = len(title)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_fontsize = max(8, round(12 * ratio))\nfig.suptitle(title, fontsize=title_fontsize, fontweight=\"bold\", color=INK, y=0.95, x=0.44)\n\n# Colorbar theming\ncbar = ax.collections[0].colorbar\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT)\ncbar.ax.set_ylabel(\"Risk Score\", fontsize=9, color=INK)\ncbar.ax.yaxis.label.set_color(INK)\ncbar.outline.set_linewidth(0.5)\ncbar.outline.set_edgecolor(INK_SOFT)\n\n# Legend outside the axes: category markers + zone patches + critical indicator\nlegend_handles = [\n    plt.Line2D(\n        [0],\n        [0],\n        marker=cat_markers[cat],\n        color=\"w\",\n        markerfacecolor=cat_colors[cat],\n        markersize=10,\n        markeredgecolor=PAGE_BG,\n        markeredgewidth=1.2,\n        label=cat,\n    )\n    for cat in categories\n]\nlegend_handles.append(plt.Line2D([0], [0], color=\"none\", linewidth=0, label=\"\"))\n\nzone_levels = [\n    (\"Low (1–4)\", \"#009E73\"),\n    (\"Medium (5–9)\", ANYPLOT_AMBER),\n    (\"High (10–16)\", \"#BD8233\"),\n    (\"Critical (20–25)\", \"#AE3030\"),\n]\nfor label, color in zone_levels:\n    legend_handles.append(mpatches.Patch(facecolor=color, edgecolor=INK_MUTED, linewidth=0.7, label=label))\n\nlegend_handles.append(plt.Line2D([0], [0], color=\"none\", linewidth=0, label=\"\"))\nlegend_handles.append(\n    plt.Line2D(\n        [0],\n        [0],\n        marker=\"o\",\n        color=\"w\",\n        markerfacecolor=\"none\",\n        markersize=12,\n        markeredgecolor=INK,\n        markeredgewidth=2,\n        label=\"Critical risk\",\n    )\n)\n\nleg = ax.legend(\n    handles=legend_handles,\n    loc=\"center left\",\n    bbox_to_anchor=(1.22, 0.5),\n    fontsize=8,\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    fancybox=False,\n    shadow=False,\n    title=\"Categories & Zones\",\n    title_fontsize=9,\n    ncol=1,\n    borderpad=0.9,\n    labelspacing=0.85,\n)\nleg.get_title().set_fontweight(\"bold\")\nleg.get_title().set_color(INK)\nfor text in leg.get_texts():\n    text.set_color(INK_SOFT)\n\nsns.despine(ax=ax, left=True, bottom=True)\n\n# Save — no bbox_inches to preserve exact 2400×2400 canvas\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}