{"spec_id":"genome-track-multi","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// genome-track-multi: Genome Track Viewer\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\nconst NEUTRAL = t.ink; // structural/reference elements (Imprint \"neutral\" anchor == INK)\nconst BAND_FILL = THEME === \"light\" ? \"rgba(26,26,23,0.035)\" : \"rgba(240,239,232,0.035)\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny fixed-seed LCG — the browser has no seeded RNG.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeLcg(42);\n\nconst REGION_START = 140720000;\nconst REGION_END = 140880000;\nconst REGION_SPAN = REGION_END - REGION_START;\n\n// Genes track: exon/intron structure with strand direction.\nconst genes = [\n  {\n    label: \"MYCT2\",\n    strand: \"+\",\n    exons: [\n      [140720000, 140722500],\n      [140730000, 140731200],\n      [140740000, 140741800],\n      [140758000, 140760000],\n    ],\n  },\n  {\n    label: \"PALB3\",\n    strand: \"-\",\n    exons: [\n      [140800000, 140802000],\n      [140815000, 140816500],\n      [140830000, 140832000],\n      [140848000, 140850000],\n    ],\n  },\n];\n\n// Regulatory track: promoters and enhancers.\nconst regulatory = [\n  { start: 140717200, end: 140719700, type: \"promoter\", label: \"MYCT2 promoter\" },\n  { start: 140762200, end: 140764600, type: \"enhancer\", label: \"Enh-1\" },\n  { start: 140795800, end: 140799200, type: \"promoter\", label: \"PALB3 promoter\" },\n  { start: 140835800, end: 140838900, type: \"enhancer\", label: \"Enh-2\" },\n  { start: 140860200, end: 140863500, type: \"enhancer\", label: \"Enh-3\" },\n];\n\n// Coverage track: sequencing read depth, enriched over exons.\nconst COVERAGE_POINTS = 90;\nconst coverage = [];\nfor (let i = 0; i <= COVERAGE_POINTS; i++) {\n  const pos = REGION_START + (i / COVERAGE_POINTS) * REGION_SPAN;\n  let depth = 26 + 13 * Math.sin(i / 5.5) + rng() * 9;\n  const inExon = genes.some((g) => g.exons.some(([s, e]) => pos >= s && pos <= e));\n  if (inExon) depth += 34;\n  coverage.push({ x: pos, depth: Math.max(5, depth) });\n}\nconst maxDepth = Math.max(...coverage.map((c) => c.depth));\n\n// Variants track: SNPs and indels with a quality score.\nconst VARIANT_TYPES = [\n  \"SNP\", \"SNP\", \"indel\", \"SNP\", \"SNP\", \"indel\", \"SNP\", \"SNP\",\n  \"indel\", \"SNP\", \"SNP\", \"indel\", \"SNP\", \"indel\", \"SNP\", \"SNP\",\n];\nconst variants = VARIANT_TYPES.map((type, i) => ({\n  x: REGION_START + 6000 + rng() * (REGION_SPAN - 12000),\n  type,\n  quality: 22 + rng() * 78,\n  label: `${type === \"SNP\" ? \"rs\" : \"in\"}${1000 + i}`,\n}));\n\n// --- Row layout (bottom to top) ---------------------------------------------\nconst ROW = { VARIANTS: 0, COVERAGE: 1, REGULATORY: 2, GENES: 3 };\nconst ROW_LABELS = { 3.5: \"Genes\", 2.5: \"Regulatory\", 1.5: \"Coverage\", 0.5: \"Variants\" };\nconst GENES_Y = ROW.GENES + 0.5;\nconst REG_Y = ROW.REGULATORY + 0.5;\nconst COV_BASE = ROW.COVERAGE + 0.08;\nconst COV_TOP = ROW.COVERAGE + 0.92;\nconst VAR_BASE = ROW.VARIANTS + 0.12;\nconst VAR_TOP = ROW.VARIANTS + 0.88;\n\nconst REG_COLOR = { promoter: t.palette[1], enhancer: t.palette[2] };\nconst VARIANT_COLOR = { SNP: t.palette[5], indel: t.palette[6] };\nconst EXON_COLOR = t.palette[0]; // Imprint palette position 1 — always first series\nconst COVERAGE_COLOR = t.palette[3];\n\n// --- Custom drawing plugin ---------------------------------------------------\n// Chart.js ships no genome-track geometry (exon boxes, strand chevrons,\n// lollipop markers), so this plugin paints the tracks directly onto the\n// scale's coordinate system while Chart.js owns axes, ticks and legend.\nconst genomeTracksPlugin = {\n  id: \"genomeTracks\",\n  beforeDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const y = scales.y;\n    ctx.save();\n    // Alternating subtle band shading to separate adjacent tracks.\n    [ROW.GENES, ROW.COVERAGE].forEach((row) => {\n      const top = y.getPixelForValue(row + 1);\n      const bottom = y.getPixelForValue(row);\n      ctx.fillStyle = BAND_FILL;\n      ctx.fillRect(chartArea.left, top, chartArea.right - chartArea.left, bottom - top);\n    });\n    // Thin separator rules between tracks.\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1;\n    [1, 2, 3].forEach((row) => {\n      const py = y.getPixelForValue(row);\n      ctx.beginPath();\n      ctx.moveTo(chartArea.left, py);\n      ctx.lineTo(chartArea.right, py);\n      ctx.stroke();\n    });\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const x = scales.x;\n    const y = scales.y;\n    const px = (pos) => x.getPixelForValue(pos);\n    const py = (row) => y.getPixelForValue(row);\n\n    // --- Genes: backbone line, exon boxes, strand chevrons -----------------\n    ctx.save();\n    genes.forEach((gene) => {\n      const spanStart = gene.exons[0][0];\n      const spanEnd = gene.exons[gene.exons.length - 1][1];\n      const rowPx = py(GENES_Y);\n\n      ctx.strokeStyle = NEUTRAL;\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(px(spanStart), rowPx);\n      ctx.lineTo(px(spanEnd), rowPx);\n      ctx.stroke();\n\n      // Strand chevrons at each intron midpoint.\n      for (let i = 0; i < gene.exons.length - 1; i++) {\n        const midPos = (gene.exons[i][1] + gene.exons[i + 1][0]) / 2;\n        const cx = px(midPos);\n        const dir = gene.strand === \"+\" ? 1 : -1;\n        ctx.fillStyle = NEUTRAL;\n        ctx.beginPath();\n        ctx.moveTo(cx - dir * 6, rowPx - 7);\n        ctx.lineTo(cx + dir * 6, rowPx);\n        ctx.lineTo(cx - dir * 6, rowPx + 7);\n        ctx.closePath();\n        ctx.fill();\n      }\n\n      // Exon boxes.\n      ctx.fillStyle = EXON_COLOR;\n      gene.exons.forEach(([s, e]) => {\n        const width = Math.max(px(e) - px(s), 2);\n        ctx.fillRect(px(s), rowPx - 18, width, 36);\n      });\n\n      // Gene label with strand arrow.\n      ctx.fillStyle = t.ink;\n      ctx.font = \"600 15px sans-serif\";\n      ctx.textAlign = \"left\";\n      ctx.textBaseline = \"bottom\";\n      ctx.fillText(`${gene.label} (${gene.strand})`, px(spanStart), rowPx - 24);\n    });\n    ctx.restore();\n\n    // --- Regulatory: colored boxes ------------------------------------------\n    ctx.save();\n    regulatory.forEach((r) => {\n      const rowPx = py(REG_Y);\n      const width = Math.max(px(r.end) - px(r.start), 2);\n      ctx.fillStyle = REG_COLOR[r.type];\n      ctx.fillRect(px(r.start), rowPx - 15, width, 30);\n    });\n    ctx.restore();\n\n    // --- Coverage: filled area plot -----------------------------------------\n    ctx.save();\n    ctx.beginPath();\n    ctx.moveTo(px(coverage[0].x), py(COV_BASE));\n    coverage.forEach((c) => {\n      const yy = COV_BASE + (c.depth / maxDepth) * (COV_TOP - COV_BASE);\n      ctx.lineTo(px(c.x), py(yy));\n    });\n    ctx.lineTo(px(coverage[coverage.length - 1].x), py(COV_BASE));\n    ctx.closePath();\n    ctx.fillStyle = COVERAGE_COLOR + \"8c\"; // ~55% alpha fill\n    ctx.fill();\n\n    ctx.beginPath();\n    coverage.forEach((c, i) => {\n      const yy = COV_BASE + (c.depth / maxDepth) * (COV_TOP - COV_BASE);\n      const fn = i === 0 ? \"moveTo\" : \"lineTo\";\n      ctx[fn](px(c.x), py(yy));\n    });\n    ctx.strokeStyle = COVERAGE_COLOR;\n    ctx.lineWidth = 2.5;\n    ctx.stroke();\n    ctx.restore();\n\n    // --- Variants: lollipop markers -----------------------------------------\n    ctx.save();\n    variants.forEach((v) => {\n      const cx = px(v.x);\n      const topY = py(VAR_BASE + (v.quality / 100) * (VAR_TOP - VAR_BASE));\n      const baseY = py(VAR_BASE);\n      const color = VARIANT_COLOR[v.type];\n\n      ctx.strokeStyle = color;\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(cx, baseY);\n      ctx.lineTo(cx, topY);\n      ctx.stroke();\n\n      ctx.beginPath();\n      ctx.arc(cx, topY, 7.5, 0, Math.PI * 2);\n      ctx.fillStyle = color;\n      ctx.fill();\n      ctx.lineWidth = 1.5;\n      ctx.strokeStyle = t.pageBg;\n      ctx.stroke();\n    });\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\n// Chart.js supplies axes, legend and title; the plugin above paints the actual\n// genome-track geometry. Legend entries are color-only proxy datasets (empty\n// `data`) since Chart.js has no native exon/lollipop/area-band chart type.\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    datasets: [\n      { label: \"Exon\", data: [], backgroundColor: EXON_COLOR },\n      { label: \"Promoter\", data: [], backgroundColor: REG_COLOR.promoter },\n      { label: \"Enhancer\", data: [], backgroundColor: REG_COLOR.enhancer },\n      { label: \"Coverage depth\", data: [], backgroundColor: COVERAGE_COLOR },\n      { label: \"SNP\", data: [], backgroundColor: VARIANT_COLOR.SNP },\n      { label: \"Indel\", data: [], backgroundColor: VARIANT_COLOR.indel },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 24, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"genome-track-multi · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"600\" },\n        padding: { bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Marker height and lollipop stem length encode variant quality score\",\n        color: t.inkSoft,\n        font: { size: 13, style: \"italic\" },\n        padding: { bottom: 14 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.inkSoft, font: { size: 14 }, boxWidth: 18, boxHeight: 14 },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: REGION_START - REGION_SPAN * 0.01,\n        max: REGION_END + REGION_SPAN * 0.01,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => (value / 1e6).toFixed(2) + \" Mb\",\n          maxTicksLimit: 8,\n        },\n        grid: { color: t.grid },\n        border: { color: t.grid },\n        title: { display: true, text: \"Genomic Position — chr7 (Mb)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: \"linear\",\n        min: 0,\n        max: 4,\n        afterBuildTicks: (axis) => {\n          axis.ticks = [0.5, 1.5, 2.5, 3.5].map((value) => ({ value }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 15, weight: \"600\" },\n          callback: (value) => ROW_LABELS[value] ?? \"\",\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n  plugins: [genomeTracksPlugin],\n});\n"}