{"spec_id":"silhouette-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// silhouette-basic: Silhouette Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- PRNG (deterministic, no seeded Math.random in the browser) ------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nfunction gaussian(rng, mean, std) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// --- Data: synthetic petal measurements clustered into 3 species-like groups\nconst rng = makeLcg(42);\nconst clusterSpecs = [\n  { center: [1.5, 0.3], spread: [0.18, 0.1], count: 50 },\n  { center: [4.3, 1.3], spread: [0.55, 0.24], count: 50 },\n  { center: [5.4, 1.9], spread: [0.6, 0.3], count: 50 },\n];\nconst petalLength = [];\nconst petalWidth = [];\nconst clusterLabels = [];\nclusterSpecs.forEach((spec, clusterIndex) => {\n  for (let i = 0; i < spec.count; i++) {\n    petalLength.push(gaussian(rng, spec.center[0], spec.spread[0]));\n    petalWidth.push(gaussian(rng, spec.center[1], spec.spread[1]));\n    clusterLabels.push(clusterIndex);\n  }\n});\nconst sampleCount = petalLength.length;\n\n// --- Silhouette coefficient per sample (standard formula, Euclidean space) -\nconst silhouette = new Array(sampleCount).fill(0);\nfor (let i = 0; i < sampleCount; i++) {\n  const ownCluster = clusterLabels[i];\n  let cohesionSum = 0;\n  let cohesionCount = 0;\n  const separationSums = {};\n  const separationCounts = {};\n  for (let j = 0; j < sampleCount; j++) {\n    if (i === j) continue;\n    const dx = petalLength[i] - petalLength[j];\n    const dy = petalWidth[i] - petalWidth[j];\n    const d = Math.sqrt(dx * dx + dy * dy);\n    if (clusterLabels[j] === ownCluster) {\n      cohesionSum += d;\n      cohesionCount++;\n    } else {\n      separationSums[clusterLabels[j]] = (separationSums[clusterLabels[j]] || 0) + d;\n      separationCounts[clusterLabels[j]] = (separationCounts[clusterLabels[j]] || 0) + 1;\n    }\n  }\n  const a = cohesionCount > 0 ? cohesionSum / cohesionCount : 0;\n  const b = Math.min(\n    ...Object.keys(separationSums).map((k) => separationSums[k] / separationCounts[k])\n  );\n  silhouette[i] = cohesionCount > 0 ? (b - a) / Math.max(a, b) : 0;\n}\n\n// --- Sort samples within each cluster (descending) and insert spacer gaps --\nconst GAP_ROWS = 4;\nconst barValues = [];\nconst barColors = [];\nconst clusterBounds = [];\nclusterSpecs.forEach((_, clusterIndex) => {\n  const indices = [];\n  for (let i = 0; i < sampleCount; i++) {\n    if (clusterLabels[i] === clusterIndex) indices.push(i);\n  }\n  indices.sort((a, b) => silhouette[b] - silhouette[a]);\n  const startIndex = barValues.length;\n  indices.forEach((i) => {\n    barValues.push(silhouette[i]);\n    barColors.push(t.palette[clusterIndex % t.palette.length]);\n  });\n  const avg = indices.reduce((sum, i) => sum + silhouette[i], 0) / indices.length;\n  clusterBounds.push({ clusterIndex, startIndex, endIndex: barValues.length - 1, avg });\n  if (clusterIndex < clusterSpecs.length - 1) {\n    for (let g = 0; g < GAP_ROWS; g++) {\n      barValues.push(0);\n      barColors.push(\"transparent\");\n    }\n  }\n});\nconst overallAvg = silhouette.reduce((sum, v) => sum + v, 0) / sampleCount;\nconst minValue = Math.min(-0.1, Math.min(...silhouette) - 0.05);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: overall-average reference line + per-cluster averages --\nconst silhouetteAnnotations = {\n  id: \"silhouetteAnnotations\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    ctx.save();\n\n    const avgX = scales.x.getPixelForValue(overallAvg);\n    ctx.strokeStyle = t.amber;\n    ctx.lineWidth = 2;\n    ctx.setLineDash([8, 5]);\n    ctx.beginPath();\n    ctx.moveTo(avgX, chartArea.top);\n    ctx.lineTo(avgX, chartArea.bottom);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    const avgLabel = `avg ${overallAvg.toFixed(2)}`;\n    ctx.font = \"600 14px sans-serif\";\n    ctx.fillStyle = t.amber;\n    ctx.textBaseline = \"alphabetic\";\n    const nearRightEdge = avgX > chartArea.right - 100;\n    ctx.textAlign = nearRightEdge ? \"right\" : \"left\";\n    ctx.fillText(avgLabel, nearRightEdge ? avgX - 8 : avgX + 8, chartArea.top + 16);\n\n    ctx.font = \"600 15px sans-serif\";\n    ctx.textBaseline = \"middle\";\n    ctx.textAlign = \"left\";\n    clusterBounds.forEach(({ clusterIndex, startIndex, endIndex, avg }) => {\n      const yTop = scales.y.getPixelForValue(startIndex);\n      const yBottom = scales.y.getPixelForValue(endIndex);\n      ctx.fillStyle = t.palette[clusterIndex % t.palette.length];\n      ctx.fillText(`Cluster ${clusterIndex} · avg ${avg.toFixed(2)}`, chartArea.left + 14, (yTop + yBottom) / 2);\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: barValues.map((_, i) => i),\n    datasets: [\n      {\n        data: barValues,\n        backgroundColor: barColors,\n        borderWidth: 0,\n        barPercentage: 1.0,\n        categoryPercentage: 1.0,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"silhouette-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 25 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        min: minValue,\n        max: 1,\n        title: { display: true, text: \"Silhouette Coefficient\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        display: false,\n        grid: { display: false },\n      },\n    },\n  },\n  plugins: [silhouetteAnnotations],\n});\n"}