{"spec_id":"silhouette-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// silhouette-basic: Silhouette Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 230, bottom: 100, left: 70 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: silhouette scores for a k-means clustering of the iris dataset --\n// (fixed-seed LCG in place of a seeded RNG, which the browser lacks)\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\nfunction clip(v, lo, hi) {\n  return Math.max(lo, Math.min(hi, v));\n}\n\nconst clusterSpecs = [\n  { label: \"Cluster 0 · setosa\", count: 50, mean: 0.8, std: 0.07 },\n  { label: \"Cluster 1 · versicolor\", count: 47, mean: 0.42, std: 0.2 },\n  { label: \"Cluster 2 · virginica\", count: 53, mean: 0.33, std: 0.22 },\n];\n\nconst clusters = clusterSpecs.map((spec, i) => {\n  const values = Array.from({ length: spec.count }, () =>\n    clip(randNormal(spec.mean, spec.std), -1, 1),\n  );\n  values.sort((a, b) => b - a);\n  return { ...spec, index: i, values, avg: d3.mean(values) };\n});\n\nconst allValues = clusters.flatMap((c) => c.values);\nconst overallAvg = d3.mean(allValues);\n\n// --- Sample-space row layout (one thin bar per sample, gap between clusters)\nconst GAP = 10;\nlet cursor = 0;\nconst rows = [];\nfor (const cluster of clusters) {\n  const start = cursor;\n  cluster.values.forEach((val, i) =>\n    rows.push({ y0: cursor + i, y1: cursor + i + 1, val, cluster }),\n  );\n  cursor += cluster.values.length;\n  cluster.mid = (start + cursor) / 2;\n  cursor += GAP;\n}\nconst ySpaceMax = cursor - GAP;\n\n// --- Scales -------------------------------------------------------------\nconst xMin = Math.min(-0.1, d3.min(allValues) - 0.02);\nconst x = d3.scaleLinear().domain([xMin, 1]).nice().range([0, iw]);\nconst y = d3.scaleLinear().domain([0, ySpaceMax]).range([0, ih]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (value axis) -------------------------------------------------\ng.selectAll(\".grid-line\")\n  .data(x.ticks(6))\n  .join(\"line\")\n  .attr(\"class\", \"grid-line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid);\n\n// --- Zero-reference line -----------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(0))\n  .attr(\"x2\", x(0))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\n// --- Per-sample silhouette bars ---------------------------------------------\ng.selectAll(\"rect\")\n  .data(rows)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(Math.min(0, d.val)))\n  .attr(\"width\", (d) => Math.abs(x(d.val) - x(0)))\n  .attr(\"y\", (d) => y(d.y0))\n  .attr(\"height\", (d) => y(d.y1) - y(d.y0) + 0.5)\n  .attr(\"shape-rendering\", \"crispEdges\")\n  .attr(\"fill\", (d) => t.palette[d.cluster.index]);\n\n// --- Overall average reference line -----------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(overallAvg))\n  .attr(\"x2\", x(overallAvg))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\ng.append(\"text\")\n  .attr(\"x\", x(overallAvg) + 10)\n  .attr(\"y\", -18)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Overall avg = ${overallAvg.toFixed(2)}`);\n\n// --- X axis -------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 28)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Silhouette Coefficient\");\n\n// --- Cluster legend + per-cluster average annotation ------------------------\nconst clusterLabel = g\n  .selectAll(\".cluster-label\")\n  .data(clusters)\n  .join(\"g\")\n  .attr(\"class\", \"cluster-label\")\n  .attr(\"transform\", (d) => `translate(${iw + 24},${y(d.mid)})`);\n\nclusterLabel\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", -9)\n  .attr(\"width\", 16)\n  .attr(\"height\", 16)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => t.palette[d.index]);\n\nclusterLabel\n  .append(\"text\")\n  .attr(\"x\", 24)\n  .attr(\"y\", -1)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\nclusterLabel\n  .append(\"text\")\n  .attr(\"x\", 24)\n  .attr(\"y\", 20)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text((d) => `avg = ${d.avg.toFixed(2)}`);\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"silhouette-basic · javascript · d3 · anyplot.ai\");\n"}