{"spec_id":"confusion-matrix","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nconfusion-matrix: Confusion Matrix Heatmap\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, LabelSet, LinearColorMapper\nfrom bokeh.plotting import figure\nfrom bokeh.transform import transform\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\"\n\n# Data - Multi-class classification results for a sentiment analysis model\nnp.random.seed(42)\n\nclass_names = [\"Negative\", \"Neutral\", \"Positive\", \"Very Positive\"]\n\n# Simulated confusion matrix with realistic patterns:\n# - Good diagonal (correct predictions)\n# - Adjacent classes more likely to be confused\n# - Some class imbalance\nconfusion = np.array(\n    [\n        [142, 23, 8, 2],  # Negative: mostly correct, some confused with Neutral\n        [18, 98, 31, 5],  # Neutral: often confused with adjacent classes\n        [5, 28, 156, 24],  # Positive: good accuracy, some confusion with Neutral/Very Positive\n        [1, 4, 19, 86],  # Very Positive: smaller class, good precision\n    ]\n)\n\n# Prepare data for Bokeh heatmap using rect glyphs\nx_coords = []\ny_coords = []\nvalues = []\ntext_labels = []\n\nfor i, true_class in enumerate(class_names):\n    for j, pred_class in enumerate(class_names):\n        x_coords.append(pred_class)\n        y_coords.append(true_class)\n        val = confusion[i, j]\n        values.append(val)\n        text_labels.append(str(val))\n\nsource = ColumnDataSource(data={\"x\": x_coords, \"y\": y_coords, \"value\": values, \"text\": text_labels})\n\n# Color mapping - Blues sequential palette for counts\ncolors = [\"#f7fbff\", \"#deebf7\", \"#c6dbef\", \"#9ecae1\", \"#6baed6\", \"#4292c6\", \"#2171b5\", \"#08519c\", \"#08306b\"]\nmapper = LinearColorMapper(palette=colors, low=0, high=max(values))\n\n# Create figure - Square format works better for confusion matrices\np = figure(\n    width=3600,\n    height=3600,\n    title=\"confusion-matrix · bokeh · anyplot.ai\",\n    x_range=class_names,\n    y_range=list(reversed(class_names)),  # Reverse to have first class at top\n    x_axis_label=\"Predicted Label\",\n    y_axis_label=\"True Label\",\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Draw heatmap cells using rect\np.rect(\n    x=\"x\",\n    y=\"y\",\n    width=1,\n    height=1,\n    source=source,\n    fill_color=transform(\"value\", mapper),\n    line_color=PAGE_BG,\n    line_width=3,\n)\n\n# Add text annotations for cell values\n# Calculate contrasting colors for text (white on dark, dark on light)\ntext_colors = []\nfor val in values:\n    # Use white text on darker cells (higher values)\n    if val > max(values) * 0.5:\n        text_colors.append(\"#FFFFFF\")\n    else:\n        text_colors.append(\"#08306b\")\n\nsource.data[\"text_color\"] = text_colors\n\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    text_color=\"text_color\",\n    text_font_size=\"32pt\",\n    text_font_style=\"bold\",\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    source=source,\n)\np.add_layout(labels)\n\n# Style the figure for large canvas and theme\np.title.text_font_size = \"36pt\"\np.title.text_font_style = \"bold\"\np.title.align = \"center\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"28pt\"\np.yaxis.axis_label_text_font_size = \"28pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"24pt\"\np.yaxis.major_label_text_font_size = \"24pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Axis styling\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.axis_line_width = 2\np.yaxis.axis_line_width = 2\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.major_tick_line_width = 2\np.yaxis.major_tick_line_width = 2\n\n# Background colors\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Remove grid for cleaner heatmap look\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\n# Add colorbar\ncolor_bar = ColorBar(\n    color_mapper=mapper,\n    location=(0, 0),\n    title=\"Count\",\n    title_text_font_size=\"22pt\",\n    label_standoff=12,\n    major_label_text_font_size=\"18pt\",\n    bar_line_color=INK_SOFT,\n    bar_line_width=2,\n    width=30,\n    padding=40,\n    background_fill_color=ELEVATED_BG,\n)\np.add_layout(color_bar, \"right\")\n\n# Adjust overall padding\np.min_border_left = 150\np.min_border_right = 150\np.min_border_top = 100\np.min_border_bottom = 150\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome using Selenium\nW, H = 3600, 3600\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)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}