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.