{"spec_id":"genome-track-multi","library":"d3","language":"javascript","code":"// anyplot.ai\n// genome-track-multi: Genome Track Viewer\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-25\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: a single-locus genome browser view (in-memory, deterministic) ---\n\nconst CHROM = \"chr7\";\nconst REGION_START = 140500000;\nconst REGION_END = 140521000;\n\n// Gene model: one transcript, 6 exons / 5 introns, plus strand.\nconst GENE_LABEL = \"GENEA-201\";\nconst exons = [\n  [140500200, 140500460],\n  [140503780, 140503940],\n  [140508160, 140508510],\n  [140513390, 140513710],\n  [140517880, 140518310],\n  [140519380, 140519800],\n];\nconst introns = exons.slice(0, -1).map((exon, i) => [exon[1], exons[i + 1][0]]);\n\n// Read-depth coverage: one bin per 350 bp, higher over exons than introns.\nconst BIN = 350;\nconst coverage = [];\nfor (let pos = REGION_START; pos < REGION_END; pos += BIN) {\n  const mid = pos + BIN / 2;\n  let depth = 9;\n  for (const [exonStart, exonEnd] of exons) {\n    const center = (exonStart + exonEnd) / 2;\n    const spread = 900;\n    depth += 26 * Math.exp(-((mid - center) ** 2) / (2 * spread ** 2));\n  }\n  coverage.push({ pos: mid, depth });\n}\n\n// Variant calls: SNPs and indels, clustered mostly in coding exons.\nconst variants = [\n  { pos: 140500320, type: \"SNP\", quality: 58 },\n  { pos: 140503860, type: \"SNP\", quality: 41 },\n  { pos: 140503905, type: \"indel\", quality: 33 },\n  { pos: 140505900, type: \"SNP\", quality: 22 },\n  { pos: 140508260, type: \"SNP\", quality: 64 },\n  { pos: 140508430, type: \"indel\", quality: 47 },\n  { pos: 140513500, type: \"SNP\", quality: 52 },\n  { pos: 140516200, type: \"SNP\", quality: 18 },\n  { pos: 140518050, type: \"SNP\", quality: 38 },\n  { pos: 140519500, type: \"indel\", quality: 29 },\n];\n\n// Regulatory elements: a promoter upstream of the gene, two enhancers.\nconst regulatory = [\n  { start: 140499850, end: 140500180, kind: \"Promoter\" },\n  { start: 140514600, end: 140515000, kind: \"Enhancer\" },\n  { start: 140520100, end: 140520500, kind: \"Enhancer\" },\n];\n\n// --- Layout ------------------------------------------------------------------\n\nconst margin = { top: 130, right: 70, bottom: 90, left: 190 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst x = d3.scaleLinear().domain([REGION_START, REGION_END]).range([0, iw]);\n\nconst tracks = [\n  { key: \"genes\", label: \"Genes\", frac: 0.3, note: \"solid = exon · line = intron · chevrons = strand\" },\n  { key: \"coverage\", label: \"Coverage\", frac: 0.26, note: \"shaded area = read depth\" },\n  { key: \"variants\", label: \"Variants\", frac: 0.24, note: \"circle = SNP · diamond = indel · height ∝ quality\" },\n  { key: \"regulatory\", label: \"Regulatory\", frac: 0.2, note: \"colored block = regulatory element\" },\n];\nconst GAP = 22;\nconst usableHeight = ih - GAP * (tracks.length - 1);\nlet cursor = 0;\nfor (const track of tracks) {\n  track.height = usableHeight * track.frac;\n  track.y0 = cursor;\n  cursor += track.height + GAP;\n}\nconst [genesTrack, coverageTrack, variantsTrack, regulatoryTrack] = tracks;\n\n// --- SVG mount -----------------------------------------------------------------\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Alternating track-lane shading.\ntracks.forEach((track, i) => {\n  if (i % 2 === 1) {\n    g.append(\"rect\")\n      .attr(\"x\", 0)\n      .attr(\"y\", track.y0)\n      .attr(\"width\", iw)\n      .attr(\"height\", track.height)\n      .attr(\"fill\", t.elevatedBg);\n  }\n});\n\n// Shared vertical guides so a position lines up across every track.\nconst xTicks = x.ticks(6);\ng.selectAll(\".vgrid\")\n  .data(xTicks)\n  .join(\"line\")\n  .attr(\"class\", \"vgrid\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Track labels + encoding notes (left margin / in-track caption).\ntracks.forEach((track) => {\n  g.append(\"text\")\n    .attr(\"x\", -20)\n    .attr(\"y\", track.y0 + track.height / 2)\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"16px\")\n    .style(\"font-weight\", \"600\")\n    .text(track.label);\n\n  g.append(\"text\")\n    .attr(\"x\", 6)\n    .attr(\"y\", track.y0 + 15)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"12px\")\n    .text(track.note);\n});\n\n// --- Genes track -----------------------------------------------------------\n\nconst geneY = genesTrack.y0 + genesTrack.height / 2 + 8;\nconst exonHeight = genesTrack.height * 0.5;\n\ng.selectAll(\".intron\")\n  .data(introns)\n  .join(\"line\")\n  .attr(\"class\", \"intron\")\n  .attr(\"x1\", (d) => x(d[0]))\n  .attr(\"x2\", (d) => x(d[1]))\n  .attr(\"y1\", geneY)\n  .attr(\"y2\", geneY)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5);\n\nconst chevron = 7;\nintrons.forEach(([intronStart, intronEnd]) => {\n  const steps = 3;\n  for (let i = 1; i <= steps; i++) {\n    const cx = x(intronStart + ((intronEnd - intronStart) * i) / (steps + 1));\n    g.append(\"path\")\n      .attr(\n        \"d\",\n        `M ${cx - chevron},${geneY - chevron} L ${cx + chevron},${geneY} L ${cx - chevron},${geneY + chevron}`\n      )\n      .attr(\"fill\", \"none\")\n      .attr(\"stroke\", t.inkSoft)\n      .attr(\"stroke-width\", 2)\n      .attr(\"stroke-linecap\", \"round\")\n      .attr(\"stroke-linejoin\", \"round\");\n  }\n});\n\ng.selectAll(\".exon\")\n  .data(exons)\n  .join(\"rect\")\n  .attr(\"class\", \"exon\")\n  .attr(\"x\", (d) => x(d[0]))\n  .attr(\"y\", geneY - exonHeight / 2)\n  .attr(\"width\", (d) => Math.max(2, x(d[1]) - x(d[0])))\n  .attr(\"height\", exonHeight)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\ng.append(\"text\")\n  .attr(\"x\", x(exons[0][0]))\n  .attr(\"y\", geneY - exonHeight / 2 - 14)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-style\", \"italic\")\n  .text(`${GENE_LABEL} (+ strand)`);\n\n// --- Coverage track ----------------------------------------------------------\n\nconst coverageBaseline = coverageTrack.y0 + coverageTrack.height;\nconst maxDepth = d3.max(coverage, (d) => d.depth);\nconst yCoverage = d3\n  .scaleLinear()\n  .domain([0, maxDepth])\n  .nice()\n  .range([coverageBaseline, coverageTrack.y0 + 10]);\n\nconst areaGenerator = d3\n  .area()\n  .x((d) => x(d.pos))\n  .y0(coverageBaseline)\n  .y1((d) => yCoverage(d.depth))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(coverage)\n  .attr(\"d\", areaGenerator)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"fill-opacity\", 0.55)\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 2);\n\nconst coverageAxis = g\n  .append(\"g\")\n  .attr(\"transform\", \"translate(0,0)\")\n  .call(d3.axisLeft(yCoverage).ticks(3).tickSize(-4));\ncoverageAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\");\ncoverageAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\ncoverageAxis.select(\".domain\").remove();\n\n// --- Variants track ------------------------------------------------------------\n\nconst variantBaseline = variantsTrack.y0 + variantsTrack.height;\nconst maxQuality = d3.max(variants, (d) => d.quality);\nconst yVariant = d3\n  .scaleLinear()\n  .domain([0, maxQuality])\n  .nice()\n  .range([variantBaseline, variantsTrack.y0 + 26]);\n\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", variantBaseline)\n  .attr(\"y2\", variantBaseline)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ng.selectAll(\".variant-stem\")\n  .data(variants)\n  .join(\"line\")\n  .attr(\"class\", \"variant-stem\")\n  .attr(\"x1\", (d) => x(d.pos))\n  .attr(\"x2\", (d) => x(d.pos))\n  .attr(\"y1\", variantBaseline)\n  .attr(\"y2\", (d) => yVariant(d.quality))\n  .attr(\"stroke\", t.palette[2])\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\".variant-snp\")\n  .data(variants.filter((d) => d.type === \"SNP\"))\n  .join(\"circle\")\n  .attr(\"class\", \"variant-snp\")\n  .attr(\"cx\", (d) => x(d.pos))\n  .attr(\"cy\", (d) => yVariant(d.quality))\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.palette[2])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\ng.selectAll(\".variant-indel\")\n  .data(variants.filter((d) => d.type === \"indel\"))\n  .join(\"path\")\n  .attr(\"class\", \"variant-indel\")\n  .attr(\"transform\", (d) => `translate(${x(d.pos)},${yVariant(d.quality)}) rotate(45)`)\n  .attr(\"d\", d3.symbol().type(d3.symbolSquare).size(110))\n  .attr(\"fill\", t.palette[2])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Regulatory track ----------------------------------------------------------\n\nconst regulatoryY0 = regulatoryTrack.y0 + regulatoryTrack.height * 0.25;\nconst regulatoryHeight = regulatoryTrack.height * 0.5;\n\ng.selectAll(\".regulatory\")\n  .data(regulatory)\n  .join(\"rect\")\n  .attr(\"class\", \"regulatory\")\n  .attr(\"x\", (d) => x(d.start))\n  .attr(\"y\", regulatoryY0)\n  .attr(\"width\", (d) => Math.max(2, x(d.end) - x(d.start)))\n  .attr(\"height\", regulatoryHeight)\n  .attr(\"fill\", t.palette[3])\n  .attr(\"opacity\", 0.85)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\ng.selectAll(\".regulatory-label\")\n  .data(regulatory)\n  .join(\"text\")\n  .attr(\"class\", \"regulatory-label\")\n  .attr(\"x\", (d) => (x(d.start) + x(d.end)) / 2)\n  .attr(\"y\", regulatoryY0 + regulatoryHeight + 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text((d) => d.kind);\n\n// --- Shared x-axis (genomic position) -------------------------------------------\n\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickFormat(d3.format(\",\")));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(`Genomic position — ${CHROM} (bp)`);\n\n// --- Title -----------------------------------------------------------------------\n\nconst title = \"GENEA-201 Locus · genome-track-multi · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}