{"spec_id":"volcano-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// volcano-basic: Volcano Plot for Statistical Significance\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst INK_MUTED = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data (in-memory, deterministic LCG — proteomics case study) -----------\n// Differential protein abundance, tumor vs. healthy tissue, mass-spec proteomics.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst PVAL_THRESHOLD = 1.3; // -log10(0.05)\nconst FC_THRESHOLD = 1; // log2(2)\n\nconst nonSigPoints = [];\nconst downPoints = [];\nconst upPoints = [];\n\nconst nProteins = 1400;\nfor (let i = 0; i < nProteins; i++) {\n  const log2FoldChange = randNormal() * 1.6;\n  const signalBoost = Math.abs(log2FoldChange) / 2.4;\n  const rawPValue = Math.exp(-rand() * 7 - signalBoost * 6);\n  const negLog10Pvalue = Math.min(-Math.log10(Math.max(rawPValue, 1e-30)), 26);\n\n  const point = { x: log2FoldChange, y: negLog10Pvalue };\n  const isSignificant =\n    negLog10Pvalue > PVAL_THRESHOLD && Math.abs(log2FoldChange) > FC_THRESHOLD;\n  if (!isSignificant) {\n    nonSigPoints.push(point);\n  } else if (log2FoldChange > 0) {\n    upPoints.push(point);\n  } else {\n    downPoints.push(point);\n  }\n}\n\nconst allY = [...nonSigPoints, ...downPoints, ...upPoints].map((p) => p.y);\nconst allX = [...nonSigPoints, ...downPoints, ...upPoints].map((p) => p.x);\nconst xLimit = Math.ceil(Math.max(...allX.map(Math.abs)) * 1.08 * 2) / 2;\nconst yLimit = Math.ceil(Math.max(...allY) * 1.08);\n\nconst horizontalThreshold = [\n  { x: -xLimit, y: PVAL_THRESHOLD },\n  { x: xLimit, y: PVAL_THRESHOLD },\n];\nconst verticalThresholdDown = [\n  { x: -FC_THRESHOLD, y: 0 },\n  { x: -FC_THRESHOLD, y: yLimit },\n];\nconst verticalThresholdUp = [\n  { x: FC_THRESHOLD, y: 0 },\n  { x: FC_THRESHOLD, y: yLimit },\n];\n\n// --- Top-hit labels (spec's \"consider labeling top significant features\") --\nconst GENE_POOL = [\"TP53\", \"EGFR\", \"KRAS\", \"BRCA1\", \"PTEN\"];\nconst topUp = [...upPoints].sort((a, b) => b.y - a.y).slice(0, 3);\nconst topDown = [...downPoints].sort((a, b) => b.y - a.y).slice(0, 2);\nconst topFeatures = [...topUp, ...topDown].map((p, i) => ({\n  ...p,\n  name: GENE_POOL[i % GENE_POOL.length],\n}));\n\nconst topFeatureLabels = {\n  id: \"topFeatureLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.font = \"600 13px sans-serif\";\n    ctx.fillStyle = t.ink;\n    ctx.textBaseline = \"bottom\";\n    for (const feature of topFeatures) {\n      const px = scales.x.getPixelForValue(feature.x);\n      const py = scales.y.getPixelForValue(feature.y);\n      ctx.fillText(feature.name, px + 7, py - 3);\n    }\n    ctx.restore();\n  },\n};\n\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [topFeatureLabels],\n  data: {\n    datasets: [\n      {\n        type: \"line\",\n        label: \"p = 0.05 cutoff\",\n        data: horizontalThreshold,\n        borderColor: t.ink,\n        borderDash: [8, 5],\n        borderWidth: 1.5,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        type: \"line\",\n        label: \"±2-fold cutoff\",\n        data: verticalThresholdDown,\n        borderColor: t.ink,\n        borderDash: [8, 5],\n        borderWidth: 1.5,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        type: \"line\",\n        label: \"±2-fold cutoff\",\n        data: verticalThresholdUp,\n        borderColor: t.ink,\n        borderDash: [8, 5],\n        borderWidth: 1.5,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: \"Not significant\",\n        data: nonSigPoints,\n        backgroundColor: withAlpha(INK_MUTED, 0.4),\n        pointRadius: 2.5,\n        pointHoverRadius: 2.5,\n      },\n      {\n        label: \"Down-regulated\",\n        data: downPoints,\n        backgroundColor: withAlpha(t.palette[2], 0.65),\n        pointRadius: 3.5,\n        pointHoverRadius: 3.5,\n      },\n      {\n        label: \"Up-regulated\",\n        data: upPoints,\n        backgroundColor: withAlpha(t.palette[4], 0.65),\n        pointRadius: 3.5,\n        pointHoverRadius: 3.5,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"volcano-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          filter: (item) => item.datasetIndex >= 3,\n        },\n      },\n    },\n    scales: {\n      x: {\n        min: -xLimit,\n        max: xLimit,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"log2(Fold Change)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n      },\n      y: {\n        min: 0,\n        max: yLimit,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"-log10(p-value)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n      },\n    },\n  },\n});\n"}