{"spec_id":"confusion-matrix","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nconfusion-matrix: Confusion Matrix Heatmap\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\n\n\n_cwd = sys.path[0] if sys.path[0] else \".\"\nif _cwd in sys.path:\n    sys.path.remove(_cwd)\n\nfrom pygal.graph.graph import Graph\nfrom pygal.style import Style\n\n\nsys.path.insert(0, _cwd)\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\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\nclass ConfusionMatrixChart(Graph):\n    \"\"\"Custom Confusion Matrix Chart for pygal - displays classification results.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self.matrix_data = kwargs.pop(\"matrix_data\", [])\n        self.class_labels = kwargs.pop(\"class_labels\", [])\n        self.colormap = kwargs.pop(\"colormap\", [])\n        self.show_values = kwargs.pop(\"show_values\", True)\n        self.x_axis_title = kwargs.pop(\"x_axis_title\", \"Predicted Label\")\n        self.y_axis_title = kwargs.pop(\"y_axis_title\", \"True Label\")\n        super().__init__(*args, **kwargs)\n\n    def _interpolate_color(self, value, min_val, max_val):\n        \"\"\"Interpolate color for smooth gradient.\"\"\"\n        if max_val == min_val:\n            return self.colormap[-1]\n\n        normalized = (value - min_val) / (max_val - min_val)\n        normalized = max(0, min(1, normalized))\n\n        pos = normalized * (len(self.colormap) - 1)\n        idx1 = int(pos)\n        idx2 = min(idx1 + 1, len(self.colormap) - 1)\n        frac = pos - idx1\n\n        c1 = self.colormap[idx1]\n        c2 = self.colormap[idx2]\n\n        r1, g1, b1 = int(c1[1:3], 16), int(c1[3:5], 16), int(c1[5:7], 16)\n        r2, g2, b2 = int(c2[1:3], 16), int(c2[3:5], 16), int(c2[5:7], 16)\n\n        r = int(r1 + (r2 - r1) * frac)\n        g = int(g1 + (g2 - g1) * frac)\n        b = int(b1 + (b2 - b1) * frac)\n\n        return f\"#{r:02x}{g:02x}{b:02x}\"\n\n    def _get_text_color(self, bg_color):\n        \"\"\"Get contrasting text color based on background brightness.\"\"\"\n        r, g, b = int(bg_color[1:3], 16), int(bg_color[3:5], 16), int(bg_color[5:7], 16)\n        brightness = (r * 299 + g * 587 + b * 114) / 1000\n        return \"#FFFFFF\" if brightness < 140 else INK\n\n    def _plot(self):\n        \"\"\"Draw the confusion matrix.\"\"\"\n        if not self.matrix_data:\n            return\n\n        n_classes = len(self.matrix_data)\n\n        all_values = [v for row in self.matrix_data for v in row]\n        min_val = min(all_values)\n        max_val = max(all_values)\n\n        plot_width = self.view.width\n        plot_height = self.view.height\n\n        label_margin_left = 400\n        label_margin_bottom = 350\n        label_margin_top = 80\n        label_margin_right = 280\n\n        available_width = plot_width - label_margin_left - label_margin_right\n        available_height = plot_height - label_margin_bottom - label_margin_top\n\n        cell_size = min(available_width, available_height) / n_classes * 0.92\n        gap = cell_size * 0.03\n\n        grid_size = n_classes * (cell_size + gap) - gap\n\n        x_offset = self.view.x(0) + label_margin_left + (available_width - grid_size) / 2\n        y_offset = self.view.y(n_classes) + label_margin_top + (available_height - grid_size) / 2\n\n        plot_node = self.nodes[\"plot\"]\n        cm_group = self.svg.node(plot_node, class_=\"confusion-matrix\")\n\n        y_title_size = 48\n        y_title_x = x_offset - 320\n        y_title_y = y_offset + grid_size / 2\n        text_node = self.svg.node(cm_group, \"text\", x=y_title_x, y=y_title_y)\n        text_node.set(\"text-anchor\", \"middle\")\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{y_title_size}px;font-weight:bold;font-family:sans-serif\")\n        text_node.set(\"transform\", f\"rotate(-90, {y_title_x}, {y_title_y})\")\n        text_node.text = self.y_axis_title\n\n        x_title_size = 48\n        x_title_x = x_offset + grid_size / 2\n        x_title_y = y_offset + grid_size + 280\n        text_node = self.svg.node(cm_group, \"text\", x=x_title_x, y=x_title_y)\n        text_node.set(\"text-anchor\", \"middle\")\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{x_title_size}px;font-weight:bold;font-family:sans-serif\")\n        text_node.text = self.x_axis_title\n\n        row_font_size = min(44, int(cell_size * 0.45))\n        for i, label in enumerate(self.class_labels):\n            y = y_offset + i * (cell_size + gap) + cell_size / 2\n            text_node = self.svg.node(cm_group, \"text\", x=x_offset - 25, y=y + row_font_size * 0.35)\n            text_node.set(\"text-anchor\", \"end\")\n            text_node.set(\"fill\", INK)\n            text_node.set(\"style\", f\"font-size:{row_font_size}px;font-weight:600;font-family:sans-serif\")\n            text_node.text = label\n\n        col_font_size = min(44, int(cell_size * 0.45))\n        for j, label in enumerate(self.class_labels):\n            x = x_offset + j * (cell_size + gap) + cell_size / 2\n            y = y_offset + grid_size + 30\n            text_node = self.svg.node(cm_group, \"text\", x=x, y=y)\n            text_node.set(\"text-anchor\", \"start\")\n            text_node.set(\"fill\", INK)\n            text_node.set(\"style\", f\"font-size:{col_font_size}px;font-weight:600;font-family:sans-serif\")\n            text_node.set(\"transform\", f\"rotate(45, {x}, {y})\")\n            text_node.text = label\n\n        value_font_size = min(46, int(cell_size * 0.35))\n        for i in range(n_classes):\n            for j in range(n_classes):\n                value = self.matrix_data[i][j]\n                color = self._interpolate_color(value, min_val, max_val)\n                text_color = self._get_text_color(color)\n\n                x = x_offset + j * (cell_size + gap)\n                y = y_offset + i * (cell_size + gap)\n\n                stroke_color = \"#4467A3\" if i == j else INK_SOFT\n                stroke_width = \"4\" if i == j else \"2\"\n\n                rect = self.svg.node(cm_group, \"rect\", x=x, y=y, width=cell_size, height=cell_size, rx=6, ry=6)\n                rect.set(\"fill\", color)\n                rect.set(\"stroke\", stroke_color)\n                rect.set(\"stroke-width\", stroke_width)\n\n                if self.show_values:\n                    text_x = x + cell_size / 2\n                    text_y = y + cell_size / 2 + value_font_size * 0.35\n\n                    text_node = self.svg.node(cm_group, \"text\", x=text_x, y=text_y)\n                    text_node.set(\"text-anchor\", \"middle\")\n                    text_node.set(\"fill\", text_color)\n                    text_node.set(\"style\", f\"font-size:{value_font_size}px;font-weight:bold;font-family:sans-serif\")\n                    text_node.text = str(int(value))\n\n        colorbar_width = 55\n        colorbar_height = grid_size * 0.85\n        colorbar_x = x_offset + grid_size + 90\n        colorbar_y = y_offset + (grid_size - colorbar_height) / 2\n\n        n_segments = 50\n        segment_height = colorbar_height / n_segments\n        for seg_i in range(n_segments):\n            seg_value = min_val + (max_val - min_val) * (n_segments - 1 - seg_i) / (n_segments - 1)\n            seg_color = self._interpolate_color(seg_value, min_val, max_val)\n            seg_y = colorbar_y + seg_i * segment_height\n\n            self.svg.node(\n                cm_group, \"rect\", x=colorbar_x, y=seg_y, width=colorbar_width, height=segment_height + 1, fill=seg_color\n            )\n\n        self.svg.node(\n            cm_group,\n            \"rect\",\n            x=colorbar_x,\n            y=colorbar_y,\n            width=colorbar_width,\n            height=colorbar_height,\n            fill=\"none\",\n            stroke=INK,\n            stroke_width=\"3\",\n        )\n\n        cb_label_size = 38\n        text_node = self.svg.node(\n            cm_group, \"text\", x=colorbar_x + colorbar_width + 15, y=colorbar_y + cb_label_size * 0.35\n        )\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{cb_label_size}px;font-family:sans-serif\")\n        text_node.text = str(int(max_val))\n\n        mid_y = colorbar_y + colorbar_height / 2\n        text_node = self.svg.node(cm_group, \"text\", x=colorbar_x + colorbar_width + 15, y=mid_y + cb_label_size * 0.35)\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{cb_label_size}px;font-family:sans-serif\")\n        text_node.text = str(int((min_val + max_val) / 2))\n\n        text_node = self.svg.node(\n            cm_group, \"text\", x=colorbar_x + colorbar_width + 15, y=colorbar_y + colorbar_height + cb_label_size * 0.35\n        )\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{cb_label_size}px;font-family:sans-serif\")\n        text_node.text = str(int(min_val))\n\n        cb_title_size = 42\n        cb_title_x = colorbar_x + colorbar_width / 2\n        cb_title_y = colorbar_y - 35\n        text_node = self.svg.node(cm_group, \"text\", x=cb_title_x, y=cb_title_y)\n        text_node.set(\"text-anchor\", \"middle\")\n        text_node.set(\"fill\", INK)\n        text_node.set(\"style\", f\"font-size:{cb_title_size}px;font-weight:bold;font-family:sans-serif\")\n        text_node.text = \"Count\"\n\n    def _compute(self):\n        \"\"\"Compute the box for rendering.\"\"\"\n        n_classes = len(self.matrix_data) if self.matrix_data else 1\n        self._box.xmin = 0\n        self._box.xmax = n_classes\n        self._box.ymin = 0\n        self._box.ymax = n_classes\n\n\nnp.random.seed(42)\n\nclass_names = [\"Positive\", \"Neutral\", \"Negative\", \"Mixed\"]\nn_classes = len(class_names)\n\nconfusion_matrix = [[142, 12, 5, 8], [18, 98, 15, 22], [7, 9, 125, 11], [14, 28, 18, 89]]\n\nblue_colormap = [\"#f7fbff\", \"#deebf7\", \"#c6dbef\", \"#9ecae1\", \"#6baed6\", \"#4292c6\", \"#2171b5\", \"#08519c\", \"#08306b\"]\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\"#009E73\",),\n    title_font_size=72,\n    legend_font_size=48,\n    label_font_size=44,\n    value_font_size=38,\n    font_family=\"sans-serif\",\n)\n\nchart = ConfusionMatrixChart(\n    width=3600,\n    height=3600,\n    style=custom_style,\n    title=\"confusion-matrix · pygal · anyplot.ai\",\n    matrix_data=confusion_matrix,\n    class_labels=class_names,\n    colormap=blue_colormap,\n    show_values=True,\n    x_axis_title=\"Predicted Label\",\n    y_axis_title=\"True Label\",\n    show_legend=False,\n    margin=120,\n    margin_top=200,\n    margin_bottom=100,\n    show_x_labels=False,\n    show_y_labels=False,\n)\n\nchart.add(\"\", [0])\n\nchart.render_to_file(f\"plot-{THEME}.svg\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>confusion-matrix - pygal</title>\n    <style>\n        body {{ margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: {PAGE_BG}; }}\n        .chart {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    <figure class=\"chart\">\n        {chart.render(is_unicode=True)}\n    </figure>\n</body>\n</html>\n\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html_content)\n"}