{"spec_id":"heatmap-risk-matrix","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file (bokeh.py) from shadowing the installed bokeh package\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport time\nfrom collections import defaultdict\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, Label, LabelSet, LinearColorMapper, Range1d\nfrom bokeh.plotting import figure\nfrom bokeh.transform import transform\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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_WATERMARK = \"#1A1A1720\" if THEME == \"light\" else \"#F0EFE820\"\n\n# Imprint categorical palette — positions 1-4 for risk categories\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Imprint sequential colormap (green→blue) for continuous heatmap — colorblind-safe\n_c0 = np.array([0x00, 0x9E, 0x73])  # #009E73 brand green\n_c1 = np.array([0x44, 0x67, 0xA3])  # #4467A3 blue\nANYPLOT_SEQ256 = [\"#{:02X}{:02X}{:02X}\".format(*(_c0 + (_c1 - _c0) * t / 255).round().astype(int)) for t in range(256)]\n\n# Pre-computed zone swatch colors sampled from the gradient at representative t values\n# Low(t≈0.06), Medium(t≈0.25), High(t≈0.50), Critical(t≈0.88)\nzone_swatches = [\"#049B76\", \"#11907F\", \"#22838B\", \"#3C6E9D\"]\n\n# Data\nnp.random.seed(42)\n\n# Build 5×5 background grid with risk scores (likelihood × impact)\ngrid_x, grid_y, risk_scores, score_text = [], [], [], []\nfor i in range(5):\n    for j in range(5):\n        grid_x.append(j + 0.5)\n        grid_y.append(i + 0.5)\n        score = (i + 1) * (j + 1)\n        risk_scores.append(score)\n        score_text.append(str(score))\n\ngrid_source = ColumnDataSource(data={\"x\": grid_x, \"y\": grid_y, \"score\": risk_scores, \"text\": score_text})\nmapper = LinearColorMapper(palette=ANYPLOT_SEQ256, low=1, high=25)\n\n# Risk items: (name, likelihood 1-5, impact 1-5, category)\nrisks = [\n    (\"Server Outage\", 3, 4, \"Technical\"),\n    (\"Data Breach\", 2, 5, \"Technical\"),\n    (\"Budget Overrun\", 4, 3, \"Financial\"),\n    (\"Key Staff Loss\", 3, 3, \"Operational\"),\n    (\"Vendor Failure\", 2, 4, \"Operational\"),\n    (\"Scope Creep\", 4, 2, \"Financial\"),\n    (\"Regulatory Change\", 2, 3, \"Legal\"),\n    (\"Market Shift\", 3, 5, \"Financial\"),\n    (\"Power Failure\", 1, 4, \"Technical\"),\n    (\"Supply Delay\", 3, 2, \"Operational\"),\n    (\"Cyber Attack\", 2, 5, \"Technical\"),\n    (\"Contract Dispute\", 1, 3, \"Legal\"),\n    (\"Skill Gap\", 4, 2, \"Operational\"),\n    (\"Currency Risk\", 3, 3, \"Financial\"),\n    (\"System Migration\", 2, 4, \"Technical\"),\n]\n\ncat_order = [\"Technical\", \"Financial\", \"Operational\", \"Legal\"]\ncat_colors = {cat: IMPRINT_PALETTE[i] for i, cat in enumerate(cat_order)}\n\n# Group risks by cell to apply structured position offsets (avoids marker/label overlap)\ncell_groups = defaultdict(list)\nfor idx, (name, likelihood, impact, category) in enumerate(risks):\n    cell_groups[(impact, likelihood)].append((idx, name, category))\n\ncell_offsets = {\n    1: [(0, 0)],\n    2: [(-0.12, 0.2), (0.12, -0.2)],\n    3: [(-0.18, 0.22), (0.18, 0.22), (0, -0.22)],\n    4: [(-0.18, 0.22), (0.18, 0.22), (-0.18, -0.22), (0.18, -0.22)],\n}\n\nrisk_x = [0.0] * len(risks)\nrisk_y = [0.0] * len(risks)\nrisk_names = [\"\"] * len(risks)\nrisk_marker_colors = [\"\"] * len(risks)\nrisk_sizes = [0] * len(risks)\n\nfor (impact, likelihood), items in cell_groups.items():\n    offsets = cell_offsets.get(len(items), cell_offsets[4][: len(items)])\n    for pos, (idx, name, category) in enumerate(items):\n        ox, oy = offsets[pos]\n        risk_x[idx] = impact - 1 + 0.5 + ox\n        risk_y[idx] = likelihood - 1 + 0.5 + oy\n        risk_names[idx] = name\n        risk_marker_colors[idx] = cat_colors[category]\n        score = likelihood * impact\n        if score >= 20:\n            risk_sizes[idx] = 44\n        elif score >= 10:\n            risk_sizes[idx] = 36\n        elif score >= 5:\n            risk_sizes[idx] = 28\n        else:\n            risk_sizes[idx] = 22\n\nrisk_source = ColumnDataSource(\n    data={\"x\": risk_x, \"y\": risk_y, \"label\": risk_names, \"color\": risk_marker_colors, \"size\": risk_sizes}\n)\n\n# Plot — 2400×2400 square canvas; x_range extended to 6.5 to hold legend area\nW, H = 2400, 2400\ntitle_str = \"heatmap-risk-matrix · python · bokeh · anyplot.ai\"\n\np = figure(\n    width=W,\n    height=H,\n    x_range=Range1d(0, 6.5),\n    y_range=Range1d(0, 5),\n    title=title_str,\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Background heatmap cells\np.rect(\n    x=\"x\",\n    y=\"y\",\n    width=1,\n    height=1,\n    source=grid_source,\n    fill_color=transform(\"score\", mapper),\n    line_color=PAGE_BG,\n    line_width=3,\n)\n\n# Risk score watermarks in each cell (low alpha)\np.text(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    source=grid_source,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_size=\"24pt\",\n    text_color=INK_WATERMARK,\n    text_font_style=\"bold\",\n)\n\n# Risk item markers (size varies by risk score for visual hierarchy)\np.scatter(x=\"x\", y=\"y\", source=risk_source, size=\"size\", color=\"color\", line_color=INK, line_width=2, alpha=0.9)\n\n# Risk name labels below each marker\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"label\",\n    source=risk_source,\n    x_offset=0,\n    y_offset=-22,\n    text_align=\"center\",\n    text_baseline=\"top\",\n    text_font_size=\"18pt\",\n    text_color=INK,\n    text_font_style=\"bold\",\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.8,\n    border_line_color=None,\n)\np.add_layout(labels)\n\n# Axis tick overrides — descriptive categorical labels per spec\np.xaxis.ticker = [0.5, 1.5, 2.5, 3.5, 4.5]\np.yaxis.ticker = [0.5, 1.5, 2.5, 3.5, 4.5]\np.xaxis.major_label_overrides = {0.5: \"Negligible\", 1.5: \"Minor\", 2.5: \"Moderate\", 3.5: \"Major\", 4.5: \"Catastrophic\"}\np.yaxis.major_label_overrides = {0.5: \"Rare\", 1.5: \"Unlikely\", 2.5: \"Possible\", 3.5: \"Likely\", 4.5: \"Almost Certain\"}\n\n# Separator between grid and legend area\np.line([5.05, 5.05], [0.1, 4.9], line_color=INK_SOFT, line_width=1, line_alpha=0.4)\n\n# Zone legend — swatch colors sampled from the actual imprint_seq gradient\nzone_labels = [\"Low (1–4)\", \"Medium (5–9)\", \"High (10–16)\", \"Critical (20–25)\"]\np.add_layout(Label(x=5.15, y=4.72, text=\"Risk Zones\", text_font_size=\"22pt\", text_font_style=\"bold\", text_color=INK))\nfor idx, (zone_label, swatch_color) in enumerate(zip(zone_labels, zone_swatches, strict=True)):\n    py = 4.3 - idx * 0.42\n    p.rect(x=[5.35], y=[py], width=0.2, height=0.24, color=swatch_color, line_color=None)\n    p.add_layout(Label(x=5.52, y=py, text=zone_label, text_font_size=\"17pt\", text_color=INK_SOFT, y_offset=-9))\n\n# Category legend\np.add_layout(Label(x=5.15, y=2.5, text=\"Categories\", text_font_size=\"22pt\", text_font_style=\"bold\", text_color=INK))\nfor idx, cat_name in enumerate(cat_order):\n    py = 2.1 - idx * 0.38\n    p.scatter(x=[5.35], y=[py], size=20, color=cat_colors[cat_name], line_color=INK, line_width=1.5)\n    p.add_layout(Label(x=5.52, y=py, text=cat_name, text_font_size=\"17pt\", text_color=INK_SOFT, y_offset=-9))\n\n# Style — theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\np.xaxis.axis_label = \"Impact (Consequence Severity)\"\np.yaxis.axis_label = \"Likelihood (Probability of Occurrence)\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.axis.axis_line_color = None\np.axis.major_tick_line_color = None\np.grid.grid_line_color = None\n\n# Save HTML (catalog artifact) then screenshot with headless Chrome\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# CDP override forces an exact W×H viewport regardless of outer window chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}