PageSourceSearch

https://hmohammedlab.com/js/viz-cells.js

js hmohammedlab.com collected 2026-10-08 09:54:07 UTC 9,570 bytes, 150 lines download raw bytes

1/* Mohammed Lab: spatial cell-polygon renderer (Concept 3, CosMx Patient D Bx3).
2   Whole-biopsy view drawn from real cell-segmentation polygons. Defaults to fit; scroll to
3   zoom and drag to pan into the cell shapes. A "colour by" selector switches between cell type,
4   spatial domain, and individual genes; hover a cell for its details. */
5(function () {
6  "use strict";
7  if (!window.LabViz) return;
8  var L = window.LabViz;
9
10  function hex2rgb(h) { h = h.replace("#", ""); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; }
11  function lerpStops(stops, t) {
12    t = Math.max(0, Math.min(1, t)); var s = t * (stops.length - 1), i = Math.floor(s), f = s - i;
13    if (i >= stops.length - 1) return stops[stops.length - 1];
14    var a = hex2rgb(stops[i]), b = hex2rgb(stops[i + 1]);
15    return "rgb(" + Math.round(a[0] + (b[0] - a[0]) * f) + "," + Math.round(a[1] + (b[1] - a[1]) * f) + "," + Math.round(a[2] + (b[2] - a[2]) * f) + ")";
16  }
17
18  L.register("cells", function (container, data) {
19    container.classList.add("viz-cells");
20    /* Cap at 720px wide; add 80px height (40px margin top+bottom) so edge cells breathe */
21    var W = Math.max(320, Math.min(container.clientWidth || 720, 720));
22    var H = W + 80;
23    var scale = W / data.width; /* uniform: data renders as a W×W square inside W×H canvas */
24    var sx = scale, sy = scale;
25    var yOff = (H - W) / 2; /* vertical offset to center the data */
26    var modes = data.modes, active = modes[0], activeClass = null, stops = data.geneColor;
27    var z = 1, tx = 0, ty = 0;
28
29    var cells = data.cells.map(function (c) {
30      var pts = [], cxs = 0, cys = 0, n = c.g.length / 2;
31      for (var i = 0; i < c.g.length; i += 2) { var X = c.g[i] * sx, Y = H - yOff - c.g[i + 1] * sy; pts.push(X, Y); cxs += X; cys += Y; }
32      return { pts: pts, cx: cxs / n, cy: cys / n, t: c.t, d: c.d, e: c.e };
33    });
34
35    /* mode selector */
36    var sel = document.createElement("div"); sel.className = "viz-select"; sel.setAttribute("role", "group"); sel.setAttribute("aria-label", "Colour cells by");
37    var clab = document.createElement("span"); clab.className = "viz-select-label"; clab.textContent = "colour by:"; sel.appendChild(clab);
38    var tabs = [];
39    modes.forEach(function (m) {
40      var b = document.createElement("button"); b.type = "button"; b.className = "viz-tab"; b.textContent = m.label; b.setAttribute("aria-pressed", m === active ? "true" : "false");
41      b.addEventListener("click", function () { active = m; activeClass = null;
41 tabs.forEach(function (t) { t.setAttribute("aria-pressed", "false"); }); b.setAttribute("aria-pressed", "true"); buildLegend(); draw(); });
42      sel.appendChild(b); tabs.push(b);
43    });
44    container.appendChild(sel);
45    var legend = document.createElement("div"); container.appendChild(legend);
46    var hint = document.createElement("p"); hint.className = "viz-hint"; hint.textContent = "scroll to zoom · drag to pan · double-click to reset"; container.appendChild(hint);
47
48    var stack = document.createElement("div"); stack.className = "cells-stack"; container.appendChild(stack);
49    if (data.caption) container.appendChild(captionEl(L, data));
50    var base = L.canvas(stack, W, H); base.canvas.className = "cells-base";
51    var over = L.canvas(stack, W, H); over.canvas.className = "overlay";
52    var ctx = base.ctx, octx = over.ctx, dpr = base.dpr;
53
54    function fillFor(c) {
55      if (active.type === "gene") return lerpStops(stops, c.e[active.gi]);
56      if (active.key === "domain") return c.d >= 0 ? active.classes[c.d].color : "#e7e4db";
57      return active.classes[c.t].color;
58    }
59    function classIdx(c) { return active.key === "domain" ? c.d : c.t; }
60    function pathPoly(g, c) { g.beginPath(); g.moveTo(c.pts[0], c.pts[1]); for (var i = 2; i < c.pts.length; i += 2) g.lineTo(c.pts[i], c.pts[i + 1]); g.closePath(); }
61
62    function draw() {
63      ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H);
64      ctx.setTransform(dpr * z, 0, 0, dpr * z, dpr * tx, dpr * ty);
65      var doStroke = z >= 2;
66      for (var i = 0; i < cells.length; i++) {
67        var c = cells[i], off = (activeClass != null && active.type === "cat" && classIdx(c) !== activeClass);
68        ctx.globalAlpha = off ? 0.1 : 1; pathPoly(ctx, c); ctx.fillStyle = fillFor(c); ctx.fill();
69        if (doStroke && !off) { ctx.lineWidth = 1.0 / z; ctx.strokeStyle = "rgba(0,0,0,0.85)"; ctx.stroke(); }
70      }
71      ctx.globalAlpha = 1;
72    }
73    function highlight(c) {
74      octx.setTransform(dpr, 0, 0, dpr, 0, 0); octx.clearRect(0, 0, W, H);
75      if (!c) return;
76      octx.setTransform(dpr * z, 0, 0, dpr * z, dpr * tx, dpr * ty);
77      pathPoly(octx, c); octx.lineWidth = 1.6 / z; octx.strokeStyle = "rgba(255,255,255,0.9)"; octx.stroke();
78    }
79
80    function buildLegend() {
81      legend.innerHTML = "";
82      if (active.type === "gene") {
83        var g = document.createElement("div"); g.className = "viz-gradient";
84        g.innerHTML = '<span class="gene">' + L.esc(active.label) + '</span><span class="lab">low</span><span class="bar" style="background:linear-gradient(90deg,' + stops.join(",") + ')"></span><span class="lab">high</span>';
85        legend.appendChild(g);
86      } else {
87        var lg = document.createElement("div"); lg.className = "viz-legend";
88        active.classes.forEach(function (cl, i) {
89          var b = document.createElement("button"); b.className = "viz-chip"; b.type = "button"; b.setAttribute("aria-pressed", "false");
90          b.innerHTML = '<span class="dot" style="background:' + cl.color + '"></span>' + L.esc(cl.label);
91          b.addEventListener("click", function () {
92            activeClass = (activeClass === i) ? null : i; lg.classList.toggle("has-active", activeClass != null);
93            [].forEach.call(lg.querySelectorAll(".viz-chip"), function (x, j) { x.setAttribute("aria-pressed", j === activeClass ? "true" : "false"); }); draw();
94          });
95          lg.appendChild(b);
96        });
97        legend.appendChild(lg);
98      }
99    }
100    buildLegend(); draw();
101
102    /* zoom + pan */
103    function clampPan() { var cw = W * z, ch = H * z; tx = Math.min(0, Math.max(W - cw, tx)); ty = Math.min(0, Math.max(H - ch, ty)); }
104    function zoomAt(mx, my, f) { var nz = Math.max(1, Math.min(18, z * f)); tx = mx - (mx - tx) * nz / z; ty = my - (my - ty) * nz / z; z = nz; clampPan(); highlight(null); draw(); }
105    base.canvas.addEventListener("wheel", function (ev) { ev.preventDefault(); var r = base.canvas.getBoundingClientRect(); zoomAt(ev.clientX - r.left, ev.clientY - r.top, ev.deltaY < 0 ? 1.18 : 1 / 1.18); }, { passive: false });
106    base.canvas.addEventListener("dblclick", function () { z = 1; tx = 0; ty = 0; highlight(null); draw(); });
107
108    var down = false, lx0 = 0, ly0 = 0, moved = false;
109    base.canvas.addEventListener("mousedown", function (ev) { down = true; moved = false; lx0 = ev.clientX; ly0 = ev.clientY; base.canvas.style.cursor = "grabbing"; });
110    window.addEventListener("mouseup", function () { down = false; base.canvas.style.cursor = ""; });
111
112    /* centroid grid (base coords) for hover */
113    var GS = 12, grid = {};
114    cells.forEach(function (c, i) { var k = ((c.cx / GS) | 0) + "," + ((c.cy / GS) | 0); (grid[k] = grid[k] || []).push(i); });
115    function typeLabel(c) { return modes[0].classes[c.t].label; }
116    function domLabel(c) { return c.d >= 0 ? modes[1].classes[c.d].label : null; }
117
118    var onMove = L.throttleRAF(function (clientX, clientY, lx, ly) {
119      var bx = (lx - tx) / z, by = (ly - ty) / z, thr = 10 / z, bd = thr * thr;
120      var gx = (bx / GS) | 0, gy = (by / GS) | 0, best = -1;
121      for (var ax = -1; ax <= 1; ax++) for (var ay = -1; ay <= 1; ay++) {
122        var arr = grid[(gx + ax) + "," + (gy + ay)]; if (!arr) continue;
123        for (var j = 0; j < arr.length; j++) { var c = cells[arr[j]], dx = c.cx - bx, dy = c.cy - by, d = dx * dx + dy * dy; if (d < bd) { bd = d; best = arr[j]; } }
124      }
125      if (best >= 0) {
126        var c = cells[best]; highlight(c);
127        var dl = domLabel(c), html = "<b>" + L.esc(typeLabel(c)) + "</b>";
128        if (active.type === "gene") html = "<b>" + L.esc(active.label) + " " + c.e[active.gi].toFixed(2) + "</b><span class=\"t-sub\">relative expression</span><span class=\"t-sub\">" + L.esc(typeLabel(c)) + "</span>";
129        if (dl) html += '<span class="t-sub">' + L.esc(dl) + "</span>";
130        L.tip.show(html, clientX, clientY);
131      } else { highlight(null); L.tip.hide(); }
132    });
133    base.canvas.addEventListener("mousemove", function (ev) {
134      var r = base.canvas.getBoundingClientRect();
135      if (down) {
136        moved = true; tx += ev.clientX - lx0; ty += ev.clientY - ly0; lx0 = ev.clientX; ly0 = ev.clientY; clampPan(); highlight(null); L.tip.hide(); draw();
137      } else { onMove(ev.clientX, ev.clientY, ev.clientX - r.left, ev.clientY - r.top); }
138    });
139    base.canvas.addEventListener("mouseleave", function () { highlight(null); L.tip.hide(); });
140  });
141
142  function captionEl(L, data) {
143    var p = document.createElement("p"); p.className = "viz-cap";
144    var src = data.source ? (data.source.url
145      ? ' <span class="viz-src">· <a href="' + L.esc(data.source.url) + '" target="_blank" rel="noopener">' + L.esc(data.source.label || data.source.url) + "</a></span>"
146      : ' <span class="viz-src">· ' + L.esc(data.source.label || data.source) + "</span>") : "";
147    p.innerHTML = L.esc(data.caption) + src + (data.updated ? '<span class="viz-stamp">Updated ' + L.esc(data.updated) + "</span>" : "");
148    return p;
149  }
150})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.