{"spec_id":"volcano-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// volcano-basic: Volcano Plot for Statistical Significance\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic RNA-seq differential expression) -------\n// Small 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 randn() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_GENES = 2200;\nconst FC_SIG = 1.6;\nconst P_THRESHOLD = 1.3; // -log10(0.05)\nconst FC_THRESHOLD = 1;\n\nconst genes = [];\nfor (let i = 0; i < N_GENES; i += 1) {\n  const logFc = randn() * FC_SIG;\n  // stronger fold changes tend to carry lower p-values (more significant)\n  const base = Math.abs(logFc) * 1.55 + Math.abs(randn()) * 0.9;\n  const negLogP = Math.max(0.02, base + (rand() - 0.5) * 1.3);\n  genes.push({ logFc, negLogP });\n}\n\nconst topHits = [\n  { label: \"TP53\", logFc: 3.4, negLogP: 6.3 },\n  { label: \"MYC\", logFc: -3.1, negLogP: 5.6 },\n  { label: \"BRCA1\", logFc: 2.6, negLogP: 4.8 },\n  { label: \"EGFR\", logFc: -2.4, negLogP: 4.2 },\n];\nfor (const hit of topHits) genes.push({ ...hit });\n\nfunction status(d) {\n  if (d.negLogP < P_THRESHOLD || Math.abs(d.logFc) < FC_THRESHOLD) return \"ns\";\n  return d.logFc > 0 ? \"up\" : \"down\";\n}\n\nconst colorFor = { ns: t.inkSoft, up: t.palette[4], down: t.palette[2] };\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// --- Scales -------------------------------------------------------------------\nconst fcExtent = d3.max(genes, (d) => Math.abs(d.logFc)) * 1.15;\nconst x = d3.scaleLinear().domain([-fcExtent, fcExtent]).nice().range([0, iw]);\nconst yMax = d3.max(genes, (d) => d.negLogP) * 1.1;\nconst y = d3.scaleLinear().domain([0, yMax]).nice().range([ih, 0]);\n\n// --- Gridlines (both axes — scatter plot) --------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid);\n\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(x.ticks(8))\n  .join(\"line\")\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"stroke\", t.grid);\n\n// --- Threshold lines ------------------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(P_THRESHOLD))\n  .attr(\"y2\", y(P_THRESHOLD))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\nfor (const fc of [-FC_THRESHOLD, FC_THRESHOLD]) {\n  g.append(\"line\")\n    .attr(\"x1\", x(fc))\n    .attr(\"x2\", x(fc))\n    .attr(\"y1\", 0)\n    .attr(\"y2\", ih)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-dasharray\", \"6,5\");\n}\n\n// --- Points -----------------------------------------------------------------\ng.selectAll(\"circle\")\n  .data(genes)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.logFc))\n  .attr(\"cy\", (d) => y(d.negLogP))\n  .attr(\"r\", 6)\n  .attr(\"fill\", (d) => colorFor[status(d)])\n  .attr(\"fill-opacity\", (d) => (status(d) === \"ns\" ? 0.35 : 0.75))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.6);\n\n// --- Top-hit emphasis rings ---------------------------------------------------\ng.selectAll(\".hit-ring\")\n  .data(topHits)\n  .join(\"circle\")\n  .attr(\"class\", \"hit-ring\")\n  .attr(\"cx\", (d) => x(d.logFc))\n  .attr(\"cy\", (d) => y(d.negLogP))\n  .attr(\"r\", 10)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Top-hit labels ----------------------------------------------------------\ng.selectAll(\".hit-label\")\n  .data(topHits)\n  .join(\"text\")\n  .attr(\"class\", \"hit-label\")\n  .attr(\"x\", (d) => x(d.logFc) + (d.logFc > 0 ? 20 : -20))\n  .attr(\"y\", (d) => y(d.negLogP) - 18)\n  .attr(\"text-anchor\", (d) => (d.logFc > 0 ? \"start\" : \"end\"))\n  .attr(\"fill\", t.ink)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-linejoin\", \"round\")\n  .style(\"paint-order\", \"stroke\")\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\n// --- Axes ----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -----------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"log2 Fold Change\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"-log10(p-value)\");\n\n// --- Legend -----------------------------------------------------------------\nconst legendItems = [\n  { key: \"up\", label: \"Up-regulated\" },\n  { key: \"down\", label: \"Down-regulated\" },\n  { key: \"ns\", label: \"Not significant\" },\n];\nconst legend = svg\n  .append(\"g\")\n  .attr(\n    \"transform\",\n    `translate(${width - margin.right - 220},${margin.top + 10})`,\n  );\nlegend\n  .append(\"rect\")\n  .attr(\"x\", -16)\n  .attr(\"y\", -22)\n  .attr(\"width\", 210)\n  .attr(\"height\", legendItems.length * 30 + 12)\n  .attr(\"fill\", t.elevatedBg);\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 30})`);\n  row\n    .append(\"circle\")\n    .attr(\"r\", 7)\n    .attr(\"cx\", 7)\n    .attr(\"cy\", 0)\n    .attr(\"fill\", colorFor[item.key]);\n  row\n    .append(\"text\")\n    .attr(\"x\", 22)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(item.label);\n});\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"volcano-basic · javascript · d3 · anyplot.ai\");\n"}