{"spec_id":"silhouette-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// silhouette-basic: Silhouette Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-09\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny fixed-seed LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gaussian(mean, std) {\n  const u1 = rand() || 1e-9;\n  const u2 = rand();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Simulated silhouette coefficients for a 3-cluster k-means result on a\n// flower-measurement dataset (analogous to clustering the iris species).\nconst clusterSpecs = [\n  { name: \"Cluster 0 (setosa-like)\", n: 50, mean: 0.78, std: 0.08 },\n  { name: \"Cluster 1 (versicolor-like)\", n: 62, mean: 0.42, std: 0.22 },\n  { name: \"Cluster 2 (virginica-like)\", n: 58, mean: 0.5, std: 0.25 },\n];\n\nconst clusters = clusterSpecs.map((spec) => {\n  const values = [];\n  for (let i = 0; i < spec.n; i++) {\n    let v = gaussian(spec.mean, spec.std);\n    v = Math.max(-0.35, Math.min(0.98, v));\n    values.push(v);\n  }\n  values.sort((a, b) => a - b);\n  return { name: spec.name, values };\n});\n\nlet allValues = [];\nclusters.forEach((c) => (allValues = allValues.concat(c.values)));\nconst avgSilhouette = allValues.reduce((a, b) => a + b, 0) / allValues.length;\n\n// One horizontal bar per sample, samples stacked cluster-by-cluster with a\n// thin gap between clusters (mirrors sklearn's silhouette_plot convention).\nconst categories = [];\nconst barValues = [];\nconst barColors = [];\nconst clusterAverages = [];\nconst gap = 3;\nlet cursor = 0;\n\nclusters.forEach((cluster, ci) => {\n  const clusterStart = cursor;\n  cluster.values.forEach((v) => {\n    categories.push(\"\");\n    barValues.push(v);\n    barColors.push(t.palette[ci]);\n    cursor++;\n  });\n  const clusterEnd = cursor;\n  const clusterAvg = cluster.values.reduce((a, b) => a + b, 0) / cluster.values.length;\n  clusterAverages.push({ name: cluster.name, mid: (clusterStart + clusterEnd - 1) / 2, avg: clusterAvg });\n  for (let g = 0; g < gap; g++) {\n    categories.push(\"\");\n    barValues.push(0);\n    barColors.push(\"transparent\");\n    cursor++;\n  }\n});\n\nconst barData = barValues.map((v, i) => ({ value: v, itemStyle: { color: barColors[i], borderRadius: 3 } }));\n\n// --- Layout (shared so the graphic overlays never drift from the grid) ------\nconst size = window.ANYPLOT_SIZE;\nconst margin = { left: 190, right: 60, top: 90, bottom: 70 };\nconst plotWidth = size.width - margin.left - margin.right;\nconst plotHeight = size.height - margin.top - margin.bottom;\nconst xMin = -0.4;\nconst xMax = 1;\nconst xToPixel = (x) => margin.left + ((x - xMin) / (xMax - xMin)) * plotWidth;\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"silhouette-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  grid: { left: margin.left, right: margin.right, top: margin.top, bottom: margin.bottom },\n  xAxis: {\n    type: \"value\",\n    name: \"Silhouette coefficient\",\n    nameLocation: \"middle\",\n    nameGap: 36,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: xMin,\n    max: xMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    data: categories,\n    inverse: true,\n    axisLabel: { show: false },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      type: \"bar\",\n      data: barData,\n      barCategoryGap: \"0%\",\n      markLine: {\n        symbol: \"none\",\n        silent: true,\n        label: { show: false },\n        lineStyle: { color: t.ink, type: \"dashed\", width: 2.5 },\n        data: [{ xAxis: avgSilhouette }],\n      },\n    },\n  ],\n  graphic: clusterAverages\n    .map((c) => ({\n      type: \"text\",\n      left: 20,\n      top: margin.top + (c.mid / cursor) * plotHeight - 12,\n      style: {\n        text: `{name|${c.name}}\\n{avg|avg ${c.avg.toFixed(2)}}`,\n        rich: {\n          name: { fill: t.inkSoft, fontSize: 14, fontWeight: 600, lineHeight: 18 },\n          avg: { fill: t.inkSoft, fontSize: 13, lineHeight: 17 },\n        },\n      },\n    }))\n    .concat([\n      {\n        type: \"text\",\n        left: xToPixel(avgSilhouette) + 8,\n        top: margin.top - 22,\n        style: {\n          text: `avg = ${avgSilhouette.toFixed(2)}`,\n          fill: t.ink,\n          fontSize: 14,\n          fontWeight: 600,\n        },\n      },\n    ]),\n});\n"}