{"spec_id":"line-pca-variance-cumulative","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-pca-variance-cumulative: Cumulative Explained Variance for PCA Component Selection\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// 13 standardized chemical-composition features (alcohol, acidity, phenols,\n// color intensity, etc.) from a wine-cultivar dataset — a classic PCA example.\n// Individual variance ratios decay geometrically with a small deterministic\n// wobble (fixed-seed LCG), then are normalized so they sum to 1.0.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) & 0x7fffffff;\n    return state / 0x7fffffff;\n  };\n}\nconst rand = lcg(42);\n\nconst nComponents = 13;\nconst rawRatios = Array.from({ length: nComponents }, (_, i) => {\n  const decay = Math.exp(-0.34 * i);\n  const wobble = 1 + (rand() - 0.5) * 0.12;\n  return decay * wobble;\n});\nconst rawTotal = rawRatios.reduce((a, b) => a + b, 0);\nconst individualRatio = rawRatios.map((v) => v / rawTotal);\n\nconst componentLabels = individualRatio.map((_, i) => String(i + 1));\nconst cumulativePct = [];\nindividualRatio.reduce((acc, v, i) => {\n  const next = acc + v;\n  cumulativePct[i] = next * 100;\n  return next;\n}, 0);\nconst individualPct = individualRatio.map((v) => v * 100);\n\n// Elbow detection (kneedle-style): the point on the cumulative curve with the\n// largest perpendicular distance from the line connecting the first and last\n// points — the classic scree-plot \"knee\".\nfunction findElbowIndex(xs, ys) {\n  const x1 = xs[0], y1 = ys[0];\n  const xN = xs[xs.length - 1], yN = ys[ys.length - 1];\n  const denom = Math.hypot(xN - x1, yN - y1);\n  let bestIdx = 0;\n  let bestDist = -1;\n  xs.forEach((x, i) => {\n    const dist = Math.abs((yN - y1) * x - (xN - x1) * ys[i] + xN * y1 - yN * x1) / denom;\n    if (dist > bestDist) {\n      bestDist = dist;\n      bestIdx = i;\n    }\n  });\n  return bestIdx;\n}\nconst elbowIdx = findElbowIndex(\n  componentLabels.map(Number),\n  cumulativePct\n);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Elbow callout (custom Chart.js plugin, no external deps) ----------------\n// Labels the elbow marker with the component count + cumulative variance so\n// the recommendation is self-explanatory without cross-referencing the legend.\nconst elbowCalloutPlugin = {\n  id: \"elbowCallout\",\n  afterDatasetsDraw(chart) {\n    const meta = chart.getDatasetMeta(4);\n    const point = meta.data[elbowIdx];\n    if (!point) return;\n\n    const { ctx } = chart;\n    const { x, y } = point.getProps([\"x\", \"y\"], true);\n    const label = `${elbowIdx + 1} components · ${cumulativePct[elbowIdx].toFixed(1)}%`;\n\n    ctx.save();\n    ctx.font = \"600 15px sans-serif\";\n    const textWidth = ctx.measureText(label).width;\n    const boxWidth = textWidth + 22;\n    const boxHeight = 30;\n    const boxX = x - boxWidth / 2;\n    const boxY = y - boxHeight - 20;\n\n    ctx.fillStyle = t.ink;\n    ctx.beginPath();\n    ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 6);\n    ctx.moveTo(x - 7, boxY + boxHeight);\n    ctx.lineTo(x + 7, boxY + boxHeight);\n    ctx.lineTo(x, boxY + boxHeight + 9);\n    ctx.closePath();\n    ctx.fill();\n\n    ctx.fillStyle = t.pageBg;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(label, x, boxY + boxHeight / 2);\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: componentLabels,\n    datasets: [\n      {\n        type: \"bar\",\n        label: \"Individual variance\",\n        data: individualPct,\n        backgroundColor: `${t.palette[1]}8c`,\n        borderWidth: 0,\n        yAxisID: \"y1\",\n        order: 3,\n      },\n      {\n        type: \"line\",\n        label: \"Cumulative variance\",\n        data: cumulativePct,\n        borderColor: t.palette[0],\n        backgroundColor: t.palette[0],\n        pointBackgroundColor: t.palette[0],\n        pointBorderColor: t.pageBg,\n        pointRadius: 7,\n        pointBorderWidth: 2,\n        borderWidth: 4,\n        tension: 0,\n        yAxisID: \"y\",\n        order: 1,\n      },\n      {\n        type: \"line\",\n        label: \"90% threshold\",\n        data: componentLabels.map(() => 90),\n        borderColor: t.ink,\n        borderDash: [10, 6],\n        borderWidth: 2,\n        pointRadius: 0,\n        yAxisID: \"y\",\n        order: 2,\n      },\n      {\n        type: \"line\",\n        label: \"95% threshold\",\n        data: componentLabels.map(() => 95),\n        borderColor: t.ink,\n        borderDash: [3, 5],\n        borderWidth: 2,\n        pointRadius: 0,\n        yAxisID: \"y\",\n        order: 2,\n      },\n      {\n        type: \"line\",\n        label: \"Elbow point\",\n        data: componentLabels.map((_, i) => (i === elbowIdx ? cumulativePct[i] : null)),\n        showLine: false,\n        backgroundColor: t.ink,\n        borderColor: t.ink,\n        pointRadius: 11,\n        pointBackgroundColor: t.ink,\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 3,\n        yAxisID: \"y\",\n        order: 0,\n      },\n    ],\n  },\n  plugins: [elbowCalloutPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 46, right: 10, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"line-pca-variance-cumulative · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 14 }, boxWidth: 22, padding: 18 },\n      },\n    },\n    scales: {\n      x: {\n        title: { display: true, text: \"Number of Principal Components\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { display: false },\n      },\n      y: {\n        position: \"left\",\n        min: 0,\n        max: 105,\n        title: { display: true, text: \"Cumulative Explained Variance (%)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 13 }, stepSize: 20 },\n        grid: { color: t.grid },\n      },\n      y1: {\n        position: \"right\",\n        min: 0,\n        max: 45,\n        title: { display: true, text: \"Individual Component Variance (%)\", color: t.inkSoft, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { display: false },\n      },\n    },\n  },\n});\n"}