{"spec_id":"confusion-matrix","library":"d3","language":"javascript","code":"// anyplot.ai\n// confusion-matrix: Confusion Matrix Heatmap\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-04\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (quality-control defect classifier, in-memory & deterministic) ---\nconst classNames = [\"OK\", \"Scratch\", \"Dent\", \"Crack\", \"Stain\"];\nconst confusionMatrix = [\n  [180, 5, 3, 1, 2],\n  [8, 92, 4, 2, 1],\n  [6, 3, 78, 5, 2],\n  [2, 1, 4, 65, 1],\n  [3, 2, 1, 2, 70],\n];\nconst n = classNames.length;\nconst rowSums = confusionMatrix.map((row) => d3.sum(row));\nconst colSums = classNames.map((_, col) => d3.sum(confusionMatrix.map((row) => row[col])));\nconst totalSum = d3.sum(confusionMatrix.flat());\n\n// --- Normalization modes (spec asks for none / by-row / by-column / by-total) ----\nconst modes = {\n  raw: {\n    label: \"Counts\",\n    axisLabel: \"Sample Count\",\n    value: (r, c) => confusionMatrix[r][c],\n    format: (v) => `${v}`,\n    tickFormat: d3.format(\"d\"),\n  },\n  row: {\n    label: \"Row %\",\n    axisLabel: \"Recall (% of true class)\",\n    value: (r, c) => (confusionMatrix[r][c] / rowSums[r]) * 100,\n    format: (v) => `${v.toFixed(1)}%`,\n    tickFormat: (v) => `${v}%`,\n  },\n  col: {\n    label: \"Column %\",\n    axisLabel: \"Precision (% of predicted class)\",\n    value: (r, c) => (confusionMatrix[r][c] / colSums[c]) * 100,\n    format: (v) => `${v.toFixed(1)}%`,\n    tickFormat: (v) => `${v}%`,\n  },\n  total: {\n    label: \"Total %\",\n    axisLabel: \"Share of Total (%)\",\n    value: (r, c) => (confusionMatrix[r][c] / totalSum) * 100,\n    format: (v) => `${v.toFixed(1)}%`,\n    tickFormat: (v) => `${v}%`,\n  },\n};\nconst modeOrder = [\"raw\", \"row\", \"col\", \"total\"];\nlet activeMode = \"raw\";\n\nfunction flatCellsFor(modeKey) {\n  const cellValue = modes[modeKey].value;\n  const cells = [];\n  for (let row = 0; row < n; row++) {\n    for (let col = 0; col < n; col++) {\n      cells.push({ row, col, isDiagonal: row === col, value: cellValue(row, col) });\n    }\n  }\n  return cells;\n}\n\n// --- Layout ------------------------------------------------------------\nconst margin = { top: 150, right: 190, bottom: 150, left: 190 };\nconst availW = width - margin.left - margin.right;\nconst availH = height - margin.top - margin.bottom;\nconst gridSize = Math.min(availW, availH);\nconst gridLeft = margin.left + (availW - gridSize) / 2;\nconst gridTop = margin.top + (availH - gridSize) / 2;\nconst cell = gridSize / n;\n\n// --- SVG mount -------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst defs = svg.append(\"defs\");\n\n// Subtle glow on the diagonal (correct-prediction) cells for a stronger focal point.\nconst glow = defs\n  .append(\"filter\")\n  .attr(\"id\", \"diagonal-glow\")\n  .attr(\"x\", \"-50%\")\n  .attr(\"y\", \"-50%\")\n  .attr(\"width\", \"200%\")\n  .attr(\"height\", \"200%\");\nglow\n  .append(\"feDropShadow\")\n  .attr(\"dx\", 0)\n  .attr(\"dy\", 0)\n  .attr(\"stdDeviation\", 5)\n  .attr(\"flood-color\", t.ink)\n  .attr(\"flood-opacity\", 0.35);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${gridLeft},${gridTop})`);\nconst cellsG = g.append(\"g\").attr(\"class\", \"cells\");\nconst labelsG = g.append(\"g\").attr(\"class\", \"labels\");\n\n// --- Colorbar shell (updated per mode by renderMatrix) ----------------------\nconst barWidth = 34;\nconst barX = gridLeft + gridSize + 60;\nconst barY = gridTop;\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", \"confusion-seq-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"y2\", \"0%\");\nsvg\n  .append(\"rect\")\n  .attr(\"x\", barX)\n  .attr(\"y\", barY)\n  .attr(\"width\", barWidth)\n  .attr(\"height\", gridSize)\n  .attr(\"fill\", \"url(#confusion-seq-gradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\nconst barAxisG = svg.append(\"g\").attr(\"transform\", `translate(${barX + barWidth},0)`);\nconst barAxisLabel = svg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${barX + barWidth + 62}, ${barY + gridSize / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\");\n\n// --- Render (data-join driven; re-run on every mode switch) -----------------\nfunction renderMatrix(modeKey) {\n  activeMode = modeKey;\n  const mode = modes[modeKey];\n  const flatCells = flatCellsFor(modeKey);\n  const maxValue = d3.max(flatCells, (d) => d.value);\n  const color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, maxValue]);\n\n  cellsG\n    .selectAll(\"rect.cell\")\n    .data(flatCells, (d) => `${d.row}-${d.col}`)\n    .join(\"rect\")\n    .attr(\"class\", \"cell\")\n    .attr(\"x\", (d) => d.col * cell)\n    .attr(\"y\", (d) => d.row * cell)\n    .attr(\"width\", cell)\n    .attr(\"height\", cell)\n    .attr(\"rx\", 6)\n    .attr(\"ry\", 6)\n    .attr(\"fill\", (d) => color(d.value))\n    .attr(\"stroke\", (d) => (d.isDiagonal ? t.ink : t.pageBg))\n    .attr(\"stroke-width\", (d) => (d.isDiagonal ? 4 : 2))\n    .attr(\"filter\", (d) => (d.isDiagonal ? \"url(#diagonal-glow)\" : null));\n\n  labelsG\n    .selectAll(\"text.cell-value\")\n    .data(flatCells, (d) => `${d.row}-${d.col}`)\n    .join(\"text\")\n    .attr(\"class\", \"cell-value\")\n    .attr(\"x\", (d) => d.col * cell + cell / 2)\n    .attr(\"y\", (d) => d.row * cell + cell / 2)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"dominant-baseline\", \"central\")\n    .attr(\"fill\", (d) => (d.value / maxValue > 0.45 ? t.pageBg : t.ink))\n    .style(\"font-size\", \"24px\")\n    .style(\"font-weight\", (d) => (d.isDiagonal ? \"700\" : \"500\"))\n    .text((d) => mode.format(d.value));\n\n  // Colorbar: gradient stops, axis scale, and axis label follow the active mode.\n  const stops = d3.range(0, 1.0001, 0.1);\n  gradient\n    .selectAll(\"stop\")\n    .data(stops)\n    .join(\"stop\")\n    .attr(\"offset\", (stop) => `${stop * 100}%`)\n    .attr(\"stop-color\", (stop) => color(stop * maxValue));\n\n  const barScale = d3.scaleLinear().domain([0, maxValue]).range([barY + gridSize, barY]);\n  barAxisG.call(d3.axisRight(barScale).ticks(5).tickFormat(mode.tickFormat));\n  barAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  barAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\n  barAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n  barAxisLabel.text(mode.axisLabel);\n\n  toggleG\n    .select(\"rect\")\n    .attr(\"fill\", (d) => (d === activeMode ? t.palette[0] : \"none\"))\n    .attr(\"stroke\", (d) => (d === activeMode ? t.palette[0] : t.inkSoft));\n  toggleG\n    .select(\"text\")\n    .attr(\"fill\", (d) => (d === activeMode ? t.pageBg : t.inkSoft));\n}\n\n// --- Axis ticks (categorical class names, data-join, static across modes) ---\ng.selectAll(\".col-tick\")\n  .data(classNames)\n  .join(\"text\")\n  .attr(\"class\", \"col-tick\")\n  .attr(\"x\", (d, i) => i * cell + cell / 2)\n  .attr(\"y\", gridSize + 34)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text((d) => d);\n\ng.selectAll(\".row-tick\")\n  .data(classNames)\n  .join(\"text\")\n  .attr(\"class\", \"row-tick\")\n  .attr(\"x\", -16)\n  .attr(\"y\", (d, i) => i * cell + cell / 2)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dominant-baseline\", \"central\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text((d) => d);\n\n// --- Axis titles -------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", gridLeft + gridSize / 2)\n  .attr(\"y\", gridTop + gridSize + 100)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Predicted Label\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${gridLeft - 130}, ${gridTop + gridSize / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"True Label\");\n\n// --- Normalization toggle (real click handlers; none/row/column/total) ------\nconst btnWidth = 140;\nconst btnHeight = 34;\nconst btnGap = 12;\nconst toggleTotalWidth = modeOrder.length * btnWidth + (modeOrder.length - 1) * btnGap;\nconst toggleX = (width - toggleTotalWidth) / 2;\nconst toggleY = 95;\n\nconst toggleG = svg\n  .selectAll(\".toggle-btn\")\n  .data(modeOrder)\n  .join(\"g\")\n  .attr(\"class\", \"toggle-btn\")\n  .attr(\"transform\", (d, i) => `translate(${toggleX + i * (btnWidth + btnGap)},${toggleY})`)\n  .style(\"cursor\", \"pointer\")\n  .on(\"click\", (event, d) => renderMatrix(d));\n\ntoggleG\n  .append(\"rect\")\n  .attr(\"width\", btnWidth)\n  .attr(\"height\", btnHeight)\n  .attr(\"rx\", 17)\n  .attr(\"ry\", 17)\n  .attr(\"stroke-width\", 1.5);\n\ntoggleG\n  .append(\"text\")\n  .attr(\"x\", btnWidth / 2)\n  .attr(\"y\", btnHeight / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => modes[d].label);\n\n// --- Title -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"confusion-matrix · javascript · d3 · anyplot.ai\");\n\n// Default view: raw counts (matches the spec's \"none\" normalization mode).\nrenderMatrix(\"raw\");\n"}