{"spec_id":"dot-matrix-proportional","library":"d3","language":"javascript","code":"// anyplot.ai\n// dot-matrix-proportional: Dot Matrix Chart for Proportional Counts\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// 12-week clinical trial follow-up outcomes for 400 enrolled patients.\nconst t = window.ANYPLOT_TOKENS;\nconst categories = [\n  { label: \"Improved\", count: 220 },\n  { label: \"No change\", count: 110 },\n  { label: \"Worsened\", count: 45 },\n  { label: \"Discontinued\", count: 25 },\n];\nconst total = d3.sum(categories, (c) => c.count);\n\n// Non-semantic categories fill the canonical palette in order; \"Worsened\"\n// is the deferred semantic exception, reassigned to the matte-red anchor\n// (palette[4]) to reinforce negative outcome polarity.\nconst color = d3\n  .scaleOrdinal([t.palette[0], t.palette[1], t.palette[4], t.palette[3]])\n  .domain(categories.map((c) => c.label));\n\n// Flatten into one dot per patient, grouped by category and filled\n// left-to-right/top-to-bottom within each group; a half-cell vertical gap\n// separates each category block from the next to reinforce grouping.\nconst nCols = 20;\nlet rowCursor = 0;\nconst dots = [];\ncategories.forEach((c, ci) => {\n  d3.range(c.count).forEach((i) => {\n    dots.push({\n      col: i % nCols,\n      row: rowCursor + Math.floor(i / nCols),\n      color: color(c.label),\n    });\n  });\n  rowCursor += Math.ceil(c.count / nCols);\n  if (ci < categories.length - 1) rowCursor += 0.5;\n});\nconst totalRowUnits = rowCursor;\n\n// --- SVG mount ---------------------------------------------------------------\nconst { width, height } = window.ANYPLOT_SIZE;\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst margin = { top: 170, right: 90, bottom: 210, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst cell = Math.min(iw / nCols, ih / totalRowUnits);\nconst gridW = nCols * cell;\nconst gridH = totalRowUnits * cell;\nconst offsetX = (iw - gridW) / 2;\nconst offsetY = (ih - gridH) / 2;\n\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + offsetX},${margin.top + offsetY})`);\n\n// --- Dots ----------------------------------------------------------------\nconst dotRadius = cell * 0.36;\ng.selectAll(\"circle\")\n  .data(dots)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => d.col * cell + cell / 2)\n  .attr(\"cy\", (d) => d.row * cell + cell / 2)\n  .attr(\"r\", dotRadius)\n  .attr(\"fill\", (d) => d.color);\n\n// --- Title ---------------------------------------------------------------\nconst title = \"Clinical Trial Outcomes at 12 Weeks · dot-matrix-proportional · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(14, Math.round(22 * Math.min(1, 67 / title.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 64 + titleFontSize + 20)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(`${total} patients, one dot per patient`);\n\n// --- Legend ----------------------------------------------------------------\nconst legendY = margin.top + offsetY + gridH + 60;\nconst slotWidth = width / categories.length;\nconst pct = d3.format(\".0%\");\n\nconst legend = svg\n  .selectAll(\"g.legend-item\")\n  .data(categories)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(${slotWidth * (i + 0.5)},${legendY})`);\n\nlegend\n  .append(\"circle\")\n  .attr(\"r\", 11)\n  .attr(\"cy\", -34)\n  .attr(\"fill\", (d) => color(d.label));\n\nlegend\n  .append(\"text\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"y\", -6)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\nlegend\n  .append(\"text\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"y\", 18)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\")\n  .text((d) => `${d.count} (${pct(d.count / total)})`);\n"}