{"spec_id":"ma-differential-expression","library":"echarts","language":"javascript","code":"// anyplot.ai\n// ma-differential-expression: MA Plot for Differential Expression\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 83/100 | Created: 2026-06-21\n\nconst t = window.ANYPLOT_TOKENS;\n\n// LCG RNG (seed 42) — deterministic, no Math.random seeding in browser\nlet _s = 42;\nfunction rand() { _s = (_s * 1664525 + 1013904223) >>> 0; return _s / 4294967296; }\nfunction randn() { return Math.sqrt(-2 * Math.log(rand() || 1e-15)) * Math.cos(2 * Math.PI * rand()); }\n\n// Data — RNA-seq drug treatment vs. control, cancer cell line (n=2000 genes)\nconst N = 2000;\nconst GENE_NAMES = ['TP53', 'EGFR', 'KRAS', 'MYC', 'BRCA1', 'MDM2', 'CDK4', 'PTEN', 'RB1', 'VEGFA'];\nconst meanArr = [], lfcArr = [], sigArr = [];\n\nfor (let i = 0; i < N; i++) {\n  // A: mean average expression (log2), bimodal — some lowly-expressed genes\n  const a = rand() < 0.25 ? 1 + rand() * 4 : 4 + rand() * 12;\n  // M: LFC with expression-dependent variance + subtle low-expression bias\n  const sigma = 2.0 / (1 + a * 0.08);\n  const bias  = 0.35 * Math.exp(-a * 0.25);\n  const isDE  = rand() < 0.10;\n  const dir   = rand() < 0.5 ? 1 : -1;\n  const m     = isDE\n    ? dir * (1.8 + rand() * 2.5) + randn() * 0.25 + bias\n    : randn() * sigma + bias;\n  // Significance: DE genes at adequate expression with |LFC| > 1\n  const sig = isDE && a > 4 && Math.abs(m) > 1.0 && rand() < 0.80;\n  meanArr.push(a); lfcArr.push(m); sigArr.push(sig);\n}\n\n// Select 1 top gene per spatial quadrant to eliminate label collision\nconst sigIdx = sigArr.map((s, i) => s ? i : -1).filter(i => i >= 0);\nconst sigAVals = sigIdx.map(i => meanArr[i]).sort((a, b) => a - b);\nconst medA = sigAVals[Math.floor(sigAVals.length / 2)];\n\nconst quads = [\n  i => lfcArr[i] > 0 && meanArr[i] < medA,   // upper-left\n  i => lfcArr[i] > 0 && meanArr[i] >= medA,  // upper-right\n  i => lfcArr[i] < 0 && meanArr[i] < medA,   // lower-left\n  i => lfcArr[i] < 0 && meanArr[i] >= medA,  // lower-right\n];\n\nconst labelMap = new Map();\nquads.forEach((pred, qi) => {\n  const best = sigIdx.filter(pred).sort((a, b) => Math.abs(lfcArr[b]) - Math.abs(lfcArr[a]))[0];\n  if (best !== undefined) labelMap.set(best, GENE_NAMES[qi]);\n});\n\n// Label position: away from the data cloud center to avoid obstructing points\nfunction labelPos(i) {\n  if (meanArr[i] >= medA * 1.5) return 'left';  // far-right genes label leftward\n  return lfcArr[i] > 0 ? 'top' : 'bottom';\n}\n\n// Partition into scatter series\nconst nsData = [], upData = [], dnData = [];\nfor (let i = 0; i < N; i++) {\n  const name = labelMap.get(i) || '';\n  const pt = { value: [+meanArr[i].toFixed(3), +lfcArr[i].toFixed(4)], name };\n  if (name) pt.label = { show: true, position: labelPos(i) };\n  if (!sigArr[i]) nsData.push(pt);\n  else if (lfcArr[i] >= 0) upData.push(pt);\n  else dnData.push(pt);\n}\n\n// LOESS trend — 25% bandwidth, local linear regression (tricube kernel), 60 query points\nconst sorted = meanArr.map((x, i) => [x, lfcArr[i]]).sort((a, b) => a[0] - b[0]);\nconst span   = Math.floor(N * 0.25);\n\nfunction bsearch(x) {\n  let lo = 0, hi = sorted.length - 1;\n  while (lo < hi) { const m = (lo + hi) >> 1; if (sorted[m][0] < x) lo = m + 1; else hi = m; }\n  return lo;\n}\n\nfunction loessAt(qx) {\n  const n = sorted.length;\n  let lo = bsearch(qx), hi = lo;\n  while (hi - lo + 1 < span && (lo > 0 || hi < n - 1)) {\n    const dL = lo > 0     ? Math.abs(sorted[lo - 1][0] - qx) : Infinity;\n    const dR = hi < n - 1 ? Math.abs(sorted[hi + 1][0] - qx) : Infinity;\n    if (dL <= dR) lo--; else hi++;\n  }\n  const maxD = Math.max(Math.abs(sorted[lo][0] - qx), Math.abs(sorted[hi][0] - qx)) + 1e-10;\n  let sw = 0, swy = 0, swx = 0, swxy = 0, swx2 = 0;\n  for (let j = lo; j <= hi; j++) {\n    const u = Math.abs(sorted[j][0] - qx) / maxD;\n    const w = (1 - u * u * u) ** 3; // tricube kernel\n    sw += w; swy += w * sorted[j][1]; swx += w * sorted[j][0];\n    swxy += w * sorted[j][0] * sorted[j][1]; swx2 += w * sorted[j][0] * sorted[j][0];\n  }\n  const det = sw * swx2 - swx * swx;\n  if (Math.abs(det) < 1e-10) return swy / sw;\n  return ((swx2 * swy - swx * swxy) + (sw * swxy - swx * swy) * qx) / det;\n}\n\nconst xMin = sorted[0][0], xMax = sorted[sorted.length - 1][0];\nconst loessData = Array.from({ length: 60 }, (_, q) => {\n  const qx = xMin + (xMax - xMin) * q / 59;\n  return [+qx.toFixed(3), +loessAt(qx).toFixed(4)];\n});\n\n// Imprint \"muted\" semantic anchor — non-significant genes sit behind the data\nconst MUTED = window.ANYPLOT_THEME === 'light' ? '#6B6A63' : '#A8A79F';\n\n// Chart\nconst chart = echarts.init(document.getElementById('container'));\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: 'transparent',\n  title: {\n    text: 'ma-differential-expression · javascript · echarts · anyplot.ai',\n    left: 'center',\n    top: 14,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: '500' }\n  },\n  tooltip: {\n    trigger: 'item',\n    formatter: params => {\n      if (params.seriesName === 'LOESS Trend') return '';\n      const { value: [x, y], name } = params.data;\n      return `<strong>${params.seriesName}</strong>${name ? ': ' + name : ''}<br/>` +\n             `Mean Expr: ${x.toFixed(2)}<br/>log₂FC: ${y.toFixed(3)}`;\n    }\n  },\n  legend: {\n    bottom: 12,\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    data: ['Not Significant', 'Upregulated', 'Downregulated', 'LOESS Trend']\n  },\n  grid: { left: 100, right: 60, top: 80, bottom: 100 },\n  xAxis: {\n    type: 'value',\n    name: 'Mean Average Expression (log₂ A)',\n    nameLocation: 'center',\n    nameGap: 46,\n    nameTextStyle: { color: t.ink, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false }\n  },\n  yAxis: {\n    type: 'value',\n    name: 'Log₂ Fold Change (M)',\n    nameLocation: 'center',\n    nameGap: 58,\n    nameTextStyle: { color: t.ink, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } }\n  },\n  series: [\n    {\n      name: 'Not Significant',\n      type: 'scatter',\n      data: nsData,\n      symbolSize: 4,\n      itemStyle: { color: MUTED, opacity: 0.4 },\n      emphasis: { disabled: true },\n      label: { show: false },\n      markLine: {\n        silent: true,\n        symbol: ['none', 'none'],\n        label: { formatter: '{b}', color: t.inkSoft, fontSize: 11 },\n        data: [\n          { yAxis:  0, name: 'M = 0',  lineStyle: { color: t.inkSoft, width: 1.5, type: 'solid'  } },\n          { yAxis:  1, name: 'M = +1', lineStyle: { color: t.inkSoft, width: 1.0, type: 'dashed' } },\n          { yAxis: -1, name: 'M = −1', lineStyle: { color: t.inkSoft, width: 1.0, type: 'dashed' } }\n        ]\n      }\n    },\n    {\n      name: 'Upregulated',\n      type: 'scatter',\n      data: upData,\n      symbolSize: 8,\n      itemStyle: { color: t.palette[0], opacity: 0.75 }, // #009E73 — semantic: gain/up\n      label: {\n        show: false,\n        formatter: p => p.data.name,\n        color: t.ink,\n        fontSize: 13,\n        distance: 6\n      }\n    },\n    {\n      name: 'Downregulated',\n      type: 'scatter',\n      data: dnData,\n      symbolSize: 8,\n      itemStyle: { color: t.palette[4], opacity: 0.75 }, // #AE3030 — semantic: loss/down\n      label: {\n        show: false,\n        formatter: p => p.data.name,\n        color: t.ink,\n        fontSize: 13,\n        distance: 6\n      }\n    },\n    {\n      name: 'LOESS Trend',\n      type: 'line',\n      data: loessData,\n      smooth: false,\n      symbol: 'none',\n      lineStyle: { color: t.palette[3], width: 2.5 }, // #BD8233 ochre — trend reference\n      itemStyle: { color: t.palette[3] }\n    }\n  ]\n});\n"}