{"spec_id":"ma-differential-expression","library":"d3","language":"javascript","code":"// anyplot.ai\n// ma-differential-expression: MA Plot for Differential Expression\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-21\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Theme-adaptive muted color for non-significant genes\nconst INK_MUTED = THEME === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\nconst margin = { top: 80, right: 210, bottom: 80, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic LCG RNG ---\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967295;\n}\nfunction randn() {\n  const u1 = Math.max(rand(), 1e-10);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Gene expression data (RNA-seq differential expression scenario) ---\n// Treatment vs Control: 12000 genes, simulating DESeq2-style results\nconst N = 12000;\nconst genes = [];\nfor (let i = 0; i < N; i++) {\n  const meanExpr = rand() * 13.5 + 1.5;        // baseMean: 1.5–15 (log2 CPM)\n  const spread = 3.2 / (1 + meanExpr * 0.2);   // heteroscedasticity: low expr → high variance\n  const lfc = randn() * spread;\n  // Significance proxy: large fold-change with sufficient expression\n  const zStat = Math.abs(lfc) / spread * Math.sqrt(meanExpr * 0.45);\n  const significant = zStat > 2.5;\n  genes.push({ meanExpr, lfc, significant, up: lfc > 0 });\n}\n\n// --- Binned smoothing (LOESS approximation via bin medians) ---\nconst nBins = 35;\nconst xBinMin = 1, xBinMax = 15.5;\nconst bw = (xBinMax - xBinMin) / nBins;\nconst bins = Array.from({ length: nBins }, (_, i) => ({\n  x: xBinMin + (i + 0.5) * bw,\n  ys: [],\n}));\nfor (const gene of genes) {\n  const bi = Math.min(nBins - 1, Math.floor((gene.meanExpr - xBinMin) / bw));\n  if (bi >= 0) bins[bi].ys.push(gene.lfc);\n}\nconst smoothCurve = bins\n  .filter((b) => b.ys.length >= 5)\n  .map((b) => {\n    const sorted = [...b.ys].sort((a, c) => a - c);\n    return { x: b.x, y: sorted[Math.floor(sorted.length / 2)] };\n  });\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\n// Clip path to contain scatter points within the plot area\nsvg.append(\"defs\").append(\"clipPath\").attr(\"id\", \"ma-clip\")\n  .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst x = d3.scaleLinear().domain([0, 15]).range([0, iw]);\nconst y = d3.scaleLinear().domain([-6, 6]).range([ih, 0]);\n\n// --- Grid lines (both axes for scatter plots per style guide) ---\ng.append(\"g\").selectAll(\".grid-y\")\n  .data(y.ticks(8))\n  .join(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d)).attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\ng.append(\"g\").selectAll(\".grid-x\")\n  .data(x.ticks(8))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d)).attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\n// --- Reference lines ---\n// M = 0: no change line\ng.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", y(0)).attr(\"y2\", y(0))\n  .attr(\"stroke\", t.ink).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.45);\n\n// M = ±1: 2-fold change thresholds (dashed)\nfor (const m of [1, -1]) {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y(m)).attr(\"y2\", y(m))\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"7 4\").attr(\"opacity\", 0.55);\n}\n\n// --- Scatter: draw in layers (non-sig background, then sig foreground) ---\nconst gClip = g.append(\"g\").attr(\"clip-path\", \"url(#ma-clip)\");\n\nconst nonsig = genes.filter((d) => !d.significant);\nconst upGenes = genes.filter((d) => d.significant && d.up);\nconst downGenes = genes.filter((d) => d.significant && !d.up);\n\n// Non-significant genes (muted, semi-transparent background layer)\ngClip.selectAll(\".ns\").data(nonsig).join(\"circle\")\n  .attr(\"cx\", (d) => x(d.meanExpr)).attr(\"cy\", (d) => y(d.lfc))\n  .attr(\"r\", 2).attr(\"fill\", INK_MUTED).attr(\"opacity\", 0.2);\n\n// Down-regulated DEGs (matte red — semantic: loss/negative/down)\ngClip.selectAll(\".down\").data(downGenes).join(\"circle\")\n  .attr(\"cx\", (d) => x(d.meanExpr)).attr(\"cy\", (d) => y(d.lfc))\n  .attr(\"r\", 3).attr(\"fill\", t.palette[4]).attr(\"opacity\", 0.7);\n\n// Up-regulated DEGs (brand green — semantic: gain/positive/up)\ngClip.selectAll(\".up\").data(upGenes).join(\"circle\")\n  .attr(\"cx\", (d) => x(d.meanExpr)).attr(\"cy\", (d) => y(d.lfc))\n  .attr(\"r\", 3).attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.7);\n\n// --- LOESS trend curve (bin median, smoothed with Catmull-Rom spline) ---\nconst smooth = d3.line()\n  .x((d) => x(d.x)).y((d) => y(d.y))\n  .curve(d3.curveCatmullRom);\n\ngClip.append(\"path\")\n  .datum(smoothCurve)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[2])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", smooth);\n\n// --- Axes ---\nconst xAx = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8));\nconst yAx = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(8));\n\nfor (const ax of [xAx, yAx]) {\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 ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 16)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Mean Average Expression (A)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2)).attr(\"y\", 22)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Log₂ Fold Change (M)\");\n\n// --- Legend ---\nconst legX = margin.left + iw + 24;\nconst legY = margin.top + 50;\nconst legG = svg.append(\"g\").attr(\"transform\", `translate(${legX},${legY})`);\n\nconst nSig = upGenes.length + downGenes.length;\nconst legItems = [\n  { label: `Not significant (${nonsig.length})`, type: \"dot\", color: INK_MUTED, opacity: 0.45 },\n  { label: `Up-regulated (${upGenes.length})`, type: \"dot\", color: t.palette[0], opacity: 0.9 },\n  { label: `Down-regulated (${downGenes.length})`, type: \"dot\", color: t.palette[4], opacity: 0.9 },\n  { label: \"LOESS trend\", type: \"line\", color: t.palette[2], opacity: 1 },\n  { label: \"M = 0 (no change)\", type: \"solid\", color: t.ink, opacity: 0.6 },\n  { label: \"M = ±1 (2-fold)\", type: \"dashed\", color: t.inkSoft, opacity: 0.7 },\n];\n\nlegItems.forEach((item, i) => {\n  const row = legG.append(\"g\").attr(\"transform\", `translate(0,${i * 28})`);\n  if (item.type === \"dot\") {\n    row.append(\"circle\")\n      .attr(\"cx\", 8).attr(\"cy\", 9).attr(\"r\", 5)\n      .attr(\"fill\", item.color).attr(\"opacity\", item.opacity);\n  } else {\n    row.append(\"line\")\n      .attr(\"x1\", 0).attr(\"x2\", 16).attr(\"y1\", 9).attr(\"y2\", 9)\n      .attr(\"stroke\", item.color)\n      .attr(\"stroke-width\", item.type === \"line\" ? 2.5 : 1.5)\n      .attr(\"stroke-dasharray\", item.type === \"dashed\" ? \"6 4\" : null)\n      .attr(\"opacity\", item.opacity);\n  }\n  row.append(\"text\")\n    .attr(\"x\", 22).attr(\"y\", 14)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Gene count annotation ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + 8).attr(\"y\", margin.top + 22)\n  .attr(\"fill\", INK_MUTED).style(\"font-size\", \"14px\")\n  .text(`n = ${N.toLocaleString()} genes | ${nSig} DEGs (|M| > 1, p_adj < 0.05)`);\n\n// --- Title ---\nconst title = \"ma-differential-expression · javascript · d3 · anyplot.ai\";\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(title);\n"}