{"spec_id":"manhattan-gwas","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// manhattan-gwas: Manhattan Plot for GWAS\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG + Box-Muller) ----------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\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}\nfunction hexToRgba(hex, alpha) {\n  const n = parseInt(hex.replace(\"#\", \"\"), 16);\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n}\n\n// --- Data: simulated GWAS summary statistics --------------------------------\n// Approximate human chromosome lengths (Mb), 1-22 + X. Points-per-chromosome is\n// a representative subsample (not the full 100k-1M variants) so the scatter\n// stays legible and renders quickly.\nconst CHROM_LENGTHS_MB = [\n  248, 242, 198, 190, 181, 170, 159, 145, 138, 133, 135, 133, 114, 107, 101,\n  90, 83, 80, 58, 64, 46, 50, 155,\n];\nconst CHROM_LABELS = [...Array(22).keys()].map((i) => String(i + 1)).concat(\"X\");\nconst POINTS_PER_CHROM = 200;\nconst GENOME_WIDE = -Math.log10(5e-8); // ~7.301\nconst SUGGESTIVE = -Math.log10(1e-5); // 5\n\nconst chromRanges = [];\nconst points = [];\nlet cumOffset = 0;\nCHROM_LABELS.forEach((label, i) => {\n  const lengthMb = CHROM_LENGTHS_MB[i];\n  const start = cumOffset;\n  for (let j = 0; j < POINTS_PER_CHROM; j++) {\n    const cumPos = start + rand() * lengthMb;\n    const negLog10p = -Math.log10(Math.max(rand(), 1e-12));\n    points.push({ x: cumPos, y: negLog10p, chromIndex: i });\n  }\n  chromRanges.push({ label, start, end: start + lengthMb, mid: start + lengthMb / 2 });\n  cumOffset += lengthMb;\n});\n\n// Inject a handful of significant association peaks on selected chromosomes.\nconst peakChromLabels = [\"2\", \"6\", \"11\", \"17\"];\npeakChromLabels.forEach((label) => {\n  const range = chromRanges.find((r) => r.label === label);\n  const chromIndex = CHROM_LABELS.indexOf(label);\n  const peakCenter = range.start + rand() * (range.end - range.start);\n  for (let k = 0; k < 14; k++) {\n    const cumPos = Math.min(Math.max(peakCenter + randNormal() * 1.4, range.start), range.end);\n    const negLog10p = GENOME_WIDE + Math.abs(randNormal()) * 3 + (k === 0 ? 2.5 : 0);\n    points.push({ x: cumPos, y: negLog10p, chromIndex });\n  }\n});\n\n// Split by chromosome parity (alternating color bands) and significance.\nconst evenChromPoints = [];\nconst oddChromPoints = [];\nconst significantPoints = [];\npoints.forEach((p) => {\n  if (p.y >= GENOME_WIDE) {\n    significantPoints.push({ x: p.x, y: p.y, chromIndex: p.chromIndex });\n  } else if (p.chromIndex % 2 === 0) {\n    evenChromPoints.push({ x: p.x, y: p.y });\n  } else {\n    oddChromPoints.push({ x: p.x, y: p.y });\n  }\n});\n\n// Single strongest association becomes a labeled focal point.\nconst topHit = significantPoints.reduce((best, p) => (p.y > best.y ? p : best));\nconst topHitLabel = chromRanges[topHit.chromIndex].label;\nconst restSignificant = significantPoints.filter((p) => p !== topHit);\n\nconst genomeLength = cumOffset;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Draws a text callout next to the top-hit marker, clamped inside the plot area.\nconst topHitLabelPlugin = {\n  id: \"topHitLabel\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const text = `Top hit — Chr ${topHitLabel}`;\n    ctx.save();\n    ctx.font = \"bold 13px sans-serif\";\n    ctx.fillStyle = t.ink;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    const halfWidth = ctx.measureText(text).width / 2 + 4;\n    const rawX = scales.x.getPixelForValue(topHit.x);\n    const px = Math.min(Math.max(rawX, chartArea.left + halfWidth), chartArea.right - halfWidth);\n    const py = scales.y.getPixelForValue(topHit.y);\n    const labelY = py - 18 >= chartArea.top + 10 ? py - 18 : py + 22;\n    ctx.fillText(text, px, labelY);\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Chr (even)\",\n        data: evenChromPoints,\n        backgroundColor: hexToRgba(t.palette[0], 0.65),\n        pointRadius: 2.5,\n        pointHoverRadius: 2.5,\n      },\n      {\n        label: \"Chr (odd)\",\n        data: oddChromPoints,\n        backgroundColor: hexToRgba(t.palette[1], 0.65),\n        pointRadius: 2.5,\n        pointHoverRadius: 2.5,\n      },\n      {\n        label: \"Genome-wide significant\",\n        data: restSignificant,\n        backgroundColor: t.palette[4],\n        pointRadius: 3.5,\n        pointHoverRadius: 3.5,\n      },\n      {\n        type: \"line\",\n        label: \"Genome-wide (p < 5e-8)\",\n        data: [\n          { x: 0, y: GENOME_WIDE },\n          { x: genomeLength, y: GENOME_WIDE },\n        ],\n        borderColor: t.palette[4],\n        borderWidth: 2,\n        borderDash: [8, 6],\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        type: \"line\",\n        label: \"Suggestive (p < 1e-5)\",\n        data: [\n          { x: 0, y: SUGGESTIVE },\n          { x: genomeLength, y: SUGGESTIVE },\n        ],\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        borderDash: [4, 4],\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: `Top hit (Chr ${topHitLabel})`,\n        data: [{ x: topHit.x, y: topHit.y }],\n        backgroundColor: t.amber,\n        borderColor: t.ink,\n        borderWidth: 1.5,\n        pointRadius: 7,\n        pointHoverRadius: 7,\n        pointStyle: \"star\",\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"manhattan-gwas · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: { color: t.inkSoft, font: { size: 13 }, boxWidth: 16, padding: 16 },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: genomeLength,\n        afterBuildTicks: (axis) => {\n          axis.ticks = chromRanges.map((r) => ({ value: r.mid }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: (value) => {\n            const range = chromRanges.find((r) => r.mid === value);\n            return range ? range.label : \"\";\n          },\n        },\n        grid: { display: false },\n        title: { display: true, text: \"Chromosome\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        min: 0,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"-log10(p-value)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [topHitLabelPlugin],\n});\n"}