7df99a27a400ec5627663c3a0692e9ab7d04796c
max
Fri May 9 09:25:54 2025 -0700
fix: null on recent genes, split screen titles, scroll bar coming up without reason, zooming on split screen, checkbox on both sides updating on change event, hide checkbox when not in split screen mode
diff --git src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
index 6276394..c543433 100644
--- src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
+++ src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
@@ -1,9709 +1,9723 @@
// and expression data (string -> float, one mapping per circle)
/* jshint -W097 */
/* jshint -W117 */ // don't complain about unknown classes, like Set()
/* jshint -W104 */ // allow 'const'
/* jshint -W069 */ // object access with ["xx"]
/* TODO:
* - status bug - reset last expression array when coloring by meta */
"use strict";
var cellbrowser = function() {
var db = null; // the cbData object from cbData.js. Loads coords,
// annotations and gene expression vectors
var gVersion = "$VERSION$"; // cellbrowser.py:copyStatic will replace this with the pip version or git release
var gCurrentCoordName = null; // currently shown coordinates
// object with all information needed to map to the legend colors:
// all info about the current legend. gLegend.rows is an object with keys:
// color, defColor, label, count, intKey, strKey
// intKey can be int or str, depending on current coloring mode.
// E.g. in meta coloring, it's the metadata string value.
// When doing expression coloring, it's the expression bin index.
// strKey is used to save manually defined colors to localStorage, a string
var gLegend = null;
// object with info about the current meta left side bar
// keys are:
// .rows = array of objects with .field and .value
var gMeta = {rows : [], mode:null};
// optional second legend, for split screen mode
var gOtherLegend = null;
var renderer = null;
var background = null;
// last 10 genes
var gRecentGenes = [];
// -- CONSTANTS
var gTitle = "UCSC Cell Browser";
var COL_PREFIX = "col_";
var gOpenDataset = null; // while navigating the open dataset dialog, this contains the current name
// it's a global variable as the dialog is not a class (yet?) and it's the only piece of data
// it is a subset of dataset.json , e.g. name, description, cell count, etc.
// depending on the type of data, single cell or bulk RNA-seq, we call a circle a
// "sample" or a "cell". This will adapt help menus, menus, etc.
var gSampleDesc = "cell";
// width of left meta bar in pixels
var metaBarWidth = 250;
// margin between left meta bar and drawing canvas
var metaBarMargin = 0;
// width of legend, pixels
var legendBarWidth = 200;
var legendBarMargin = 0;
// width of the metaBar tooltip (histogram)
var metaTipWidth = 400;
// height of pull-down menu bar at the top, in pixels
var menuBarHeight = null; // defined at runtime by div.height
// height of the toolbar, in pixels
var toolBarHeight = 28;
// position of first combobox in toolbar from left, in pixels
var toolBarComboLeft = metaBarWidth;
var toolBarComboTop = 2;
// width of the collection combobox
var collectionComboWidth = 200;
var layoutComboWidth = 200;
// width of a single gene cell in the meta gene bar tables
//var gGeneCellWidth = 66;
// height of the trace viewer at the bottom of the screen
var traceHeight = 100;
// height of bottom gene bar
var geneBarHeight = 100;
var geneBarMargin = 5;
// color for missing value when coloring by expression value
//var cNullColor = "CCCCCC";
//const cNullColor = "DDDDDD";
//const cNullColor = "95DFFF"; //= light blue, also tried e1f6ff
//const cNullColor = "e1f6ff"; //= light blue
const cNullColor = "AFEFFF"; //= light blue
const cDefGradPalette = "magma"; // default legend gradient palette for gene expression
// this is a special palette, tol-sq with the first entry being a light blue, so 0 stands out a bit more
const cDefGradPaletteHeat = "magma"; // default legend gradient palette for the heatmap
const cDefQualPalette = "rainbow"; // default legend palette for categorical values
var datasetGradPalette = cDefGradPalette;
var datasetQualPalette = cDefQualPalette;
const exprBinCount = 10; //number of expression bins for genes
// has to match cbData.js.exprBinCount - TODO - share the constant between these two files
var HIDELABELSNAME = "Hide labels";
var SHOWLABELSNAME = "Show labels";
var METABOXTITLE = "By Annotation";
// maximum number of distinct values that one can color on
const MAXCOLORCOUNT = 1500;
const MAXLABELCOUNT = 500;
// histograms show only the top X values and summarize the rest into "other"
var HISTOCOUNT = 12;
// the sparkline is a bit shorter
var SPARKHISTOCOUNT = 12;
// links to various external databases
var dbLinks = {
"HPO" : "https://hpo.jax.org/app/browse/gene/", // entrez ID
"OMIM" : "https://omim.org/entry/", // OMIM ID
"COSMIC" : "http://cancer.sanger.ac.uk/cosmic/gene/analysis?ln=", // gene symbol
"SFARI" : "https://gene.sfari.org/database/human-gene/", // gene symbol
"BrainSpLMD" : "http://www.brainspan.org/lcm/search?exact_match=true&search_type=gene&search_term=", // entrez
"BrainSpMouseDev" : "http://developingmouse.brain-map.org/gene/show/", // internal Brainspan ID
"Eurexp" : "http://www.eurexpress.org/ee/databases/assay.jsp?assayID=", // internal ID
"LMD" : "http://www.brainspan.org/lcm/search?exact_match=true&search_type=gene&search_term=" // entrez
};
var DEBUG = true;
function _dump(o) {
/* for debugging */
console.log(JSON.stringify(o));
}
function formatString (str) {
/* Stackoverflow code https://stackoverflow.com/a/18234317/233871 */
/* "a{0}bcd{1}ef".formatUnicorn("foo", "bar"); // yields "aFOObcdBARef" */
if (arguments.length) {
var t = typeof arguments[0];
var key;
var args = ("string" === t || "number" === t) ?
Array.prototype.slice.call(arguments)
: arguments[0];
for (key in args) {
str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]);
}
}
return str;
}
// Median of medians: https://en.wikipedia.org/wiki/Median_of_medians
// find median in an unsorted array, worst-case complexity O(n).
// from https://gist.github.com/wlchn/ee15de1da59b8d6981a400eee4376ea4
const selectMedian = (arr, compare) => {
return _selectK(arr, Math.floor(arr.length / 2), compare);
};
const _selectK = (arr, k, compare) => {
if (!Array.isArray(arr) || arr.length === 0 || arr.length - 1 < k) {
return;
}
if (arr.length === 1) {
return arr[0];
}
let idx = _selectIdx(arr, 0, arr.length - 1, k, compare || _defaultCompare);
return arr[idx];
};
const _partition = (arr, left, right, pivot, compare) => {
let temp = arr[pivot];
arr[pivot] = arr[right];
arr[right] = temp;
let track = left;
for (let i = left; i < right; i++) {
// if (arr[i] < arr[right]) {
if (compare(arr[i], arr[right]) === -1) {
let t = arr[i];
arr[i] = arr[track];
arr[track] = t;
track++;
}
}
temp = arr[track];
arr[track] = arr[right];
arr[right] = temp;
return track;
};
const _selectIdx = (arr, left, right, k, compare) => {
if (left === right) {
return left;
}
let dest = left + k;
while (true) {
let pivotIndex =
right - left + 1 <= 5
? Math.floor(Math.random() * (right - left + 1)) + left
: _medianOfMedians(arr, left, right, compare);
pivotIndex = _partition(arr, left, right, pivotIndex, compare);
if (pivotIndex === dest) {
return pivotIndex;
} else if (pivotIndex < dest) {
left = pivotIndex + 1;
} else {
right = pivotIndex - 1;
}
}
};
const _medianOfMedians = (arr, left, right, compare) => {
let numMedians = Math.ceil((right - left) / 5);
for (let i = 0; i < numMedians; i++) {
let subLeft = left + i * 5;
let subRight = subLeft + 4;
if (subRight > right) {
subRight = right;
}
let medianIdx = _selectIdx(arr, subLeft, subRight, Math.floor((subRight - subLeft) / 2), compare);
let temp = arr[medianIdx];
arr[medianIdx] = arr[left + i];
arr[left + i] = temp;
}
return _selectIdx(arr, left, left + numMedians - 1, Math.floor(numMedians / 2), compare);
};
const _defaultCompare = (a, b) => {
return a < b ? -1 : a > b ? 1 : 0;
};
// End median of medians
function debug(msg, args) {
if (DEBUG) {
console.log(formatString(msg, args));
}
}
const getRandomIndexes = (length, size) =>
/* get 'size' random indexes from an array of length 'length' */
{
const indexes = [];
const created = {};
while (indexes.length < size) {
const random = Math.floor(Math.random() * length);
if (!created[random]) {
indexes.push(random);
created[random] = true;
}
}
return indexes;
};
function arrSample(arr, size) {
var arrLen = arr.length;
let rndIndexes = getRandomIndexes(arrLen, size);
let sampleArr = [];
for (let i = 0; i < rndIndexes.length; i++) {
let rndIdx = rndIndexes[i];
sampleArr.push(arr[rndIdx]);
}
return sampleArr;
}
function warn(msg) {
alert(msg);
}
function getById(query) {
return document.getElementById(query);
}
function cloneObj(d) {
/* returns a copy of an object, wasteful */
// see http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript
return JSON.parse(JSON.stringify(d));
}
function cloneArray(a) {
/* returns a copy of an array */
return a.slice();
}
function copyNonNull(srcArr, trgArr) {
/* copy non-null values to trgArr */
if (srcArr.length!==trgArr.length)
alert("warning - copyNonNull - target and source array have different sizes.");
for (var i = 0; i < srcArr.length; i++) {
if (srcArr[i]!==null)
trgArr[i] = srcArr[i];
}
return trgArr;
}
function isEmpty(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}
function allEmpty(arr) {
/* return true if all members of array are white space only strings */
var newArr = arr.filter(function(str) { return /\S/.test(str); });
return (newArr.length===0);
}
function copyNonEmpty(srcArr, trgArr) {
/* copy from src to target array if value is not "". Just return trgArr is srcArr is null or lengths don't match. */
if (!srcArr || (srcArr.length!==trgArr.length))
return trgArr;
for (var i = 0; i < srcArr.length; i++) {
if (srcArr[i]!=="")
trgArr[i] = srcArr[i];
}
return trgArr;
}
function keys(o) {
/* return all keys of object as an array */
var allKeys = [];
for(var k in o) allKeys.push(k);
return allKeys;
}
function trackEvent(eventName, eventLabel) {
/* send an event to google analytics */
if (typeof gtag !== 'function')
return;
gtag('event', eventName, eventLabel);
}
function trackEventObj(eventName, obj) {
/* send an event obj to google analytics */
if (typeof gtag !== 'function')
return;
gtag('event', obj);
}
function classAddListener(className, type, listener) {
/* add an event listener for all elements of a class */
var els = document.getElementsByClassName(className);
for (let el of els) {
el.addEventListener(type, listener);
}
}
function capitalize(s) {
return s[0].toUpperCase() + s.slice(1);
}
function cleanString(s) {
/* make sure that string only contains normal characters. Good when printing something that may contain
* dangerous ones */
if (s===undefined)
return undefined;
return s.replace(/[^0-9a-zA-Z _-]/g, '');
}
function cleanStrings(inArr) {
/* cleanString on arrays */
var outArr = [];
for (var i = 0; i < inArr.length; i++) {
var s = inArr[i];
outArr.push(cleanString(s));
}
return outArr;
}
function findMetaValIndex(metaInfo, value) {
/* return the index of the value of an enum meta field */
var valCounts = metaInfo.valCounts;
for (var valIdx = 0; valIdx < valCounts.length; valIdx++) {
if (valCounts[valIdx][0]===value)
return valIdx;
}
}
function intersectArrays(arrList) {
/* return the intersection of all arrays as an array. Non-IE11? */
var smallSet = new Set(arrList[0]);
for (var i=1; i < arrList.length; i++) {
var otherSet = new Set(arrList[i]);
smallSet = new Set([...smallSet].filter(x => otherSet.has(x)));
}
var newArr = Array.from(smallSet);
// alternative without spread:
//function intersection(setA, setB) {
// var _intersection = new Set();
// for (var elem of setB) {
// if (setA.has(elem)) {
// _intersection.add(elem);
// }
// }
// return _intersection;
//}
return newArr;
}
function saveToUrl(key, value, defaultValue) {
/* save a value in both localStorage and the URL. If the value is defaultValue or null, remove it */
if (value===defaultValue || value===null) {
localStorage.removeItem(key);
delState(key);
}
else {
localStorage.setItem(key, value);
addStateVar(key, value);
}
}
function getFromUrl(key, defaultValue) {
/* get a value from localStorage or the current URL or return the default if not defined in either place.
* The URL overrides localStorage. */
var val = getVar(key);
if (val!==undefined)
return val;
val = localStorage.getItem(key);
if (val===null)
return defaultValue
else
return val;
}
function getBaseUrl() {
/* return URL of current page, without args or query part */
var myUrl = window.location.href;
myUrl = myUrl.replace("#", "");
var urlParts = myUrl.split("?");
var baseUrl = urlParts[0];
return baseUrl;
}
function copyToClipboard(element) {
/* https://stackoverflow.com/questions/22581345/click-button-copy-to-clipboard-using-jquery */
var $temp = $("");
$("body").append($temp);
$temp.val($(element).text()).select();
document.execCommand("copy");
$temp.remove();
}
function iWantHue(n) {
/* a palette as downloaded from iwanthue.com - not sure if this is better. Ellen likes it */
if (n>30)
return null;
var colList = ["7e4401", "244acd", "afc300", "a144cb", "00a13e",
"f064e5", "478700", "727eff", "9ed671", "b6006c", "5fdd90", "f8384b",
"00b199", "bb000f", "0052a3", "fcba56", "005989", "c57000", "7a3a78",
"ccca76", "ff6591", "265e1c", "ff726c", "7b8550", "923223", "9a7e00",
"ffa9ad", "5f5300", "ff9d76", "b3885f"];
var colList2 = ["cd6a00", "843dc3", "c9cd31", "eda3ff", "854350"];
if (n<=5)
colList = colList2;
return colList.slice(0, n);
}
function activateTooltip(selector) {
// see noconflict line in html source code, I had to rename BS's tooltip to avoid overwrite by jquery, argh: both are called .tooltip() !
var ttOpt = {
"html": true,
"animation": false,
"delay": {"show":350, "hide":100},
"trigger" : "hover",
container:"body"
};
$(selector).bsTooltip(ttOpt);
}
function menuBarHide(idStr) {
/* hide a menu bar selector */
$(idStr).parent().addClass("disabled").css("pointer-events", "none");
}
function menuBarShow(idStr) {
/* show a menu bar entry given its selector */
$(idStr).parent().removeClass("disabled").css("pointer-events", '');
}
function updateMenu() {
/* deactivate menu options based on current variables */
// the "hide selected" etc menu options are only shown if some cells are selected
if (renderer.selCells.length===0) {
menuBarHide("#tpOnlySelectedButton");
menuBarHide("#tpFilterButton");
}
else {
menuBarShow("#tpOnlySelectedButton");
menuBarShow("#tpFilterButton");
}
// the "show all" menu entry is only shown if some dots are actually hidden
//if ((pixelCoords!==null && shownCoords!==null) && pixelCoords.length===shownCoords.length)
//menuBarHide("#tpShowAllButton");
//else
//menuBarShow("#tpShowAllButton");
//$("#tpTrans"+(transparency*100)).addClass("active");
//$("#tpSize"+circleSize).addClass("active");
// the "hide labels" menu entry is only shown if there are labels
//if (gCurrentDataset.labelField === null)
//menuBarHide("#tpHideLabels");
//if (gCurrentDataset.showLabels===true)
//$("#tpHideLabels").text(HIDELABELSNAME);
//else
//$("#tpHideLabels").text(SHOWLABELSNAME);
}
function prettySeqDist(count, addSign) {
/* create human-readable string from chrom distance */
var f = count;
if (f==="0")
return "+0bp";
var sign = "";
if (addSign && count > 0)
sign = "+";
if (Math.abs(count)>=1000000) {
f = (count / 1000000);
return sign+f.toFixed(3)+"Mbp";
}
if (Math.abs(count)>=10000) {
f = (count / 1000);
return sign+f.toFixed(2)+"kbp";
}
if (Math.abs(count)>=1000) {
f = (count / 1000);
return sign+f.toFixed(2)+"kbp";
}
return sign+f+"bp";
}
function prettyNumber(count, isBp) /*str*/ {
/* convert a number to a shorter string, e.g. 1200 -> 1.2k, 1200000 -> 1.2M, etc */
var f = count;
if (count>1000000) {
f = (count / 1000000);
return f.toFixed(1)+"M";
}
if (count>10000) {
f = (count / 1000);
return f.toFixed(0)+"k";
}
if (count>1000) {
f = (count / 1000);
return f.toFixed(1)+"k";
}
return f;
}
function addMd5(url, md5s, key) {
/* lookup key in md5s and add value to url separate by ? */
if (md5s && md5s[key])
url += "?"+md5s[key];
return url;
}
function preloadImage(url) {
let img= new Image();
img.src = url;
}
function openDatasetLoadPane(datasetInfo, openTab) {
/* open dataset dialog: load html into the three panes */
//var datasetName = datasetInfo.name;
//var md5 = datasetInfo.md5;
// the UCSC apache serves latin1, so we force it back to utf8
gOpenDataset = datasetInfo; // for click handlers in the right panel
$.ajaxSetup({
'beforeSend' : function(xhr) {
if (xhr && xhr.overrideMimeType)
xhr.overrideMimeType('text/html; charset=utf8');
},
});
let datasetName = datasetInfo.name;
let md5 = datasetInfo.md5;
if (datasetInfo.hasFiles && datasetInfo.hasFiles.indexOf("datasetDesc")!==-1) {
// description is not through html files but a json file
var jsonUrl = cbUtil.joinPaths([datasetName, "desc.json"]) +"?"+md5;
fetch(jsonUrl)
.then(function(response) {
if(!response.ok) {
throw new Error('Could not find desc.json file');
}
return response.json();
})
.catch(function(err) {
var msg = "File "+jsonUrl+" was not found but datasetDesc.json has 'datasetDesc' in hasFiles. Internal error. Please contact the site admin or cells@ucsc.edu";
$( "#pane1" ).html(msg);
$( "#pane2" ).html(msg);
$( "#pane3" ).html(msg);
$( "#pane3" ).show();
})
.then(function(desc) {
datasetDescToHtml(datasetInfo, desc);
if (openTab==="images")
$("#tabLinkImg").click();
});
}
else {
var message = "This dataset does not seem to have a desc.conf file. Please "+
"read https://cellbrowser.readthedocs.io/en/master/dataDesc.html or run 'cbBuild --init' to create one";
if (datasetInfo.abstract)
// the top-level non-hierarchy dataset.conf has a message in it. Use it here, as a fallback.
message = datasetInfo.abstract;
$( "#pane1" ).html(message);
$( "#pane2" ).hide();
$( "#tabLink2" ).hide();
$( "#pane3" ).hide();
$( "#tabLink3" ).hide();
}
var tabIdx = 0;
if (openTab==="images")
tabIdx=3;
$("#tpOpenDialogTabs").tabs("refresh").tabs("option", "active", tabIdx);
if (openTab==="images")
$("#tabLinkImg").click();
}
let descLabels = {
"paper_url":"Publication",
"other_url" : "Website",
"geo_series" : "NCBI GEO Series", // = CIRM tagsV5
"sra" : "NCBI Short Read Archive",
"pmid" : "PubMed Abstract",
"pmcid" : "PubMed Fulltext",
"sra_study" : "NCBI Short-Read Archive",
"ega_study" : "European Genotype-Phenot. Archive Study",
"ega_dataset" : "European Genotype-Phenot. Archive Dataset",
"bioproject" : "NCBI Bioproject",
"dbgap" : "NCBI DbGaP",
"biorxiv_url" : "BioRxiv preprint",
"doi" : "Publication Fulltext",
"cbDoi" : "Data Citation DOI",
"arrayexpress" : "ArrayExpress",
"ena_project" : "European Nucleotide Archive",
"hca_dcp" : "Human Cell Atlas Data Portal",
"cirm_dataset" : "California Institute of Regenerative Medicine Dataset",
};
let descUrls = {
"geo_series" : "https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=",
"sra_study" : "https://trace.ncbi.nlm.nih.gov/Traces/sra/?study=",
"bioproject" : "https://www.ncbi.nlm.nih.gov/bioproject/",
"ega_study" : "https://ega-archive.org/studies/",
"ega_dataset" : "https://ega-archive.org/datasets/",
"pmid" : "https://www.ncbi.nlm.nih.gov/pubmed/",
"pmcid" : "https://www.ncbi.nlm.nih.gov/pmc/articles/",
"dbgap" : "https://www.ncbi.nlm.nih.gov/projects/gap/cgi-bin/study.cgi?study_id=",
"doi" : "http://dx.doi.org/",
"cbDoi" : "http://dx.doi.org/",
"ena_project" : "https://www.ebi.ac.uk/ena/data/view/",
"cirm_dataset" : "https://cirm.ucsc.edu/d/",
"arrayexpress" : "https://www.ebi.ac.uk/arrayexpress/experiments/",
"hca_dcp" : "https://data.humancellatlas.org/explore/projects/",
}
function htmlAddLink(htmls, desc, key, linkLabel) {
/* add a link to html on a new line. if desc[key] includes a space, the part after it is the link label. */
if (!desc[key])
return;
let label = "Link";
if (linkLabel)
label = linkLabel;
else
label = descLabels[key];
htmls.push("");
htmls.push(label);
htmls.push(": ");
// for cases where more than one ID is needed, this function also accepts a list in the object
// for 99% of the cases, it'll be a string though
let urls = desc[key];
if (!(urls instanceof Array))
urls = [urls];
let frags = []; // html fragments, one per identifier
for (let url of urls) {
url = url.toString(); // in case it's an integer or float
let urlLabel = url;
let spcPos = url.indexOf(" ");
if (spcPos!==-1) {
urlLabel = url.slice(spcPos+1);
url = url.slice(0,spcPos);
}
if (!url.startsWith("http"))
url = descUrls[key]+url;
let parts = []
parts.push("");
parts.push(urlLabel);
parts.push("");
let htmlLine = parts.join("");
frags.push(htmlLine);
}
htmls.push(frags.join(", "));
htmls.push("
");
}
function buildLinkToMatrix(htmls, dsName, matFname, label) {
/* link to a matrix file for download, handles mtx.gz */
if (label)
htmls.push(" Matrix for "+label+": ");
htmls.push(""+matFname+"");
if (matFname.endsWith(".mtx.gz")) {
var prefix = "";
if (matFname.indexOf("_")!==-1)
prefix = matFname.split("_")[0]+"_";
var ftName = prefix+"features.tsv.gz";
htmls.push(", "+ftName+"");
var barName = prefix+"barcodes.tsv.gz";
htmls.push(", "+barName+"");
}
htmls.push("
");
}
function buildSupplFiles(desc, dsName, htmls) {
/* create html with links to supplementary files */
if (desc.supplFiles) {
let supplFiles = desc.supplFiles;
for (let suppFile of supplFiles) {
let label = suppFile.label;
let fname = suppFile.file;
htmls.push(""+label+": "+fname+"");
htmls.push("
");
}
}
}
function buildDownloadsPane(datasetInfo, desc) {
var htmls = [];
if (datasetInfo.name==="") { // the top-level desc page has no methods/downloads, it has only informative text
$( "#pane3" ).hide();
$( "#tabLink3" ).hide();
} else {
if (desc.coordFiles===undefined) {
htmls.push("To download the data for datasets in this collection: open the collection, ");
htmls.push("select a dataset in the list to the left, and navigate to the 'Data Download' tab. ");
htmls.push("This information can also be accessed while viewing a dataset by clicking the 'Info & Downloads' button.");
} else if (desc.hideDownload===true || desc.hideDownload=="True" || desc.hideDownload=="true") {
htmls.push("The downloads section has been deactivated by the authors.");
htmls.push("Please contact the dataset authors to get access.");
} else {
if (desc.matrices) {
htmls.push("
"); for (var key in desc.matrices) { var mat = desc.matrices[key]; buildLinkToMatrix(htmls, datasetInfo.name, mat.fileName, mat.label); } htmls.push("
"); } else if (desc.matrixFile!==undefined && desc.matrixFile.endsWith(".mtx.gz")) { htmls.push(""); var matBaseName = desc.matrixFile.split("/").pop(); buildLinkToMatrix(htmls, datasetInfo.name, matBaseName, "dataset"); htmls.push("
"); } else { htmls.push("Matrix: exprMatrix.tsv.gz");
}
if (desc.unitDesc)
htmls.push("
Values in matrix are: "+desc.unitDesc);
htmls.push("
Raw count matrix: "+desc.rawMatrixFile+"");
if (desc.rawMatrixNote)
htmls.push("
"+desc.rawMatrixNote);
htmls.push("
Help: Load matrix/meta into Seurat or Scanpy
"); htmls.push("Cell meta annotations: meta.tsv");
if (desc.metaNote)
htmls.push("
"+desc.metaNote);
htmls.push("
Dimensionality reduction coordinates:
");
for (let fname of desc.coordFiles)
htmls.push(""+fname+"
");
htmls.push("
Dataset description: "); htmls.push("desc.json
"); htmls.push("Cell Browser configuration: "); htmls.push("dataset.json
"); $( "#pane3" ).html(htmls.join("")); $( "#pane3" ).show(); $( "#tabLink3" ).show(); } } } function buildImagesPane(datasetInfo, desc) { if (!desc.imageSets) { $( "#tabLinkImg" ).hide(); $( "#paneImg" ).hide(); return; } let htmls = []; htmls.push(""+desc.imageSetNote+"
"); // actual HTML catIdx = 0; for (let catInfo of desc.imageSets) { htmls.push("
"); htmls.push(desc.methods); htmls.push("
"); } if (desc.algParams) { htmls.push("Algorithm parameters: ");
let algParams = desc.algParams;
if (algParams instanceof Object)
algParams = Object.entries(algParams);
for (let i=0; i ");
htmls.push(desc.abstract);
htmls.push(" ");
htmls.push(datasetInfo.abstract);
htmls.push(" If you use the Cell Browser of this dataset, please cite the " +
"original publication and " +
"" +
"Speir et al. 2021. Feedback? Email us at cells@ucsc.edu."+
" Cell Browser dataset ID: "+datasetInfo.name+
" The collection '"+openDsInfo.shortLabel+"' contains "+dsCount+" datasets. " +
"Double-click or click 'Open' below. Loading abstract... Loading methods... Loading download instructions... Loading image data... ');
}
function chosenSetValue(elId, value) {
//var el = getById(elId);
//el.value = ("tpMetaVal_"+fieldIdx);
//el.trigger('chosen:updated'); // update the meta dropdown
// looks like this needs jquery
$('#'+elId).val(value).trigger('chosen:updated'); // somehow chosen needs this?
}
function findCellsUpdateMetaCombo(rowIdx, fieldIdx) {
/* given the row and the ID name of the field, setup the combo box row */
var metaInfo = db.getMetaFields()[fieldIdx];
var valCounts = metaInfo.valCounts;
var shortLabels = metaInfo.ui.shortLabels;
//$('#tpSelectMetaCombo_'+rowIdx).val("tpMetaVal_"+fieldIdx).trigger('chosen:updated'); // update the meta dropdown
chosenSetValue('tpSelectMetaCombo_'+rowIdx, "tpMetaVal_"+fieldIdx);
if (valCounts===undefined) {
// this is a numeric field
$('#tpSelectValue_'+rowIdx).val("");
$('#tpSelectValue_'+rowIdx).show();
$('#tpSelectMetaValueEnum_'+rowIdx).hide();
} else {
// it's an enum field
$('#tpSelectValue_'+rowIdx).hide();
$('#tpSelectMetaValueEnum_'+rowIdx).empty();
for (var i = 0; i < valCounts.length; i++) {
//var valName = valCounts[i][0];
var valLabel = shortLabels[i];
$('#tpSelectMetaValueEnum_'+rowIdx).append("");
}
$('#tpSelectMetaValueEnum_'+rowIdx).show();
}
}
function findCellsUpdateRowType(rowIdx, rowType) {
if (rowType === "meta") {
$("#tpSelectType_"+rowIdx).val("meta");
$("#tpSelectGeneCombo_"+rowIdx).next().hide();
$("#tpSelectValue_"+rowIdx).hide();
$("#tpSelectMetaComboBox_"+rowIdx).show();
$("#tpSelectMetaValueEnum_"+rowIdx).show();
} else {
$("#tpSelectType_"+rowIdx).val("expr");
$("#tpSelectGeneCombo_"+rowIdx).next().show();
$("#tpSelectValue_"+rowIdx).show();
$("#tpSelectMetaComboBox_"+rowIdx).hide();
$("#tpSelectMetaValueEnum_"+rowIdx).hide();
}
}
function connectOneComboboxRow(comboWidth, rowIdx, query) {
/* Filter dialog. Call the jquery inits and setup the change listeners for a combobox row */
/* Yes, a UI framework, like react or angular, would be very helpful here */
// first of all: check if the meta name actually exists in this dataset still
let metaInfo = null;
var metaName = query["m"];
if (metaName!==undefined) {
metaInfo = db.findMetaInfo(metaName);
if (metaInfo===null)
return;
}
// auto-suggest for gene searches
$('#tpSelectGeneCombo_'+rowIdx).selectize({
"labelField" : 'text',
"valueField" : 'id',
"searchField" : 'text',
"load" : comboLoadGene,
});
activateCombobox("tpSelectMetaCombo_"+rowIdx, comboWidth);
$('#tpSelectRemove_'+rowIdx).click( function(ev) {
//console.log(ev);
var rowToDel = (this.id).split("_")[1];
$("#tpSelectRow_"+rowToDel).remove();
});
//$("#tpSelectGeneCombo_"+rowIdx).next().hide();
//$("#tpSelectValue_"+rowIdx).hide();
var rowType = "gene";
var op = getQueryOp(query);
if (metaName===undefined) {
// this is a gene query
findCellsUpdateRowType(rowIdx, rowType);
selectizeSetValue("tpSelectGeneCombo_"+rowIdx, query["g"]);
$("#tpSelectValue_"+rowIdx).val(query[op]);
} else {
// it's a meta query
rowType = "meta";
findCellsUpdateRowType(rowIdx, rowType);
findCellsUpdateMetaCombo(rowIdx, metaInfo.index);
var enumIdx = findMetaValIndex(metaInfo, query[op]);
$("#tpSelectMetaValueEnum_"+rowIdx).val(enumIdx);
}
$("#tpSelectOperator_"+rowIdx).val(op);
$('#tpSelectMetaCombo_'+rowIdx).change(function(ev) {
// when the user changes the meta field, update the list of meta field values in the dropdown
var selVal = this.value;
var fieldIdx = parseInt(selVal.split("_")[1]);
findCellsUpdateMetaCombo(rowIdx, fieldIdx);
});
$('#tpSelectType_'+rowIdx).change(function(ev) {
// when the user changes the gene expression / meta dropdown, hide/show the
// respective other dropdowns
var rowType = this.value;
findCellsUpdateRowType(rowIdx, rowType);
});
}
function readSelectForm() {
/* convert the current state of the dialog box to a short string and return it */
// example: [{"g":"PITX2", "gt":0.05}, {"m":"Cluster", "eq":"cluster 2"}]
// XX TODO: non-enum meta data fields ???
var queries = [];
var rowCount = $(".tpSelectRow").length;
for (var rowIdx=0; rowIdx There are '+renderer.getSelection().length+' '+gSampleDesc+' in the current selection. Name of annotation field: Annotate selected cells as: Remove annotations later by clicking Tools > Remove all annotations. Change labels below. To keep the old name, leave the 'New Name' cell empty. You cannot modify the 'Orig. Name' column. To rename a single cluster without this dialog: click onto it in the legend, then click its label. ");
var idListEnc = encodeURIComponent(idList.join("\n"));
htmls.push("");
var buttons =
[
{
text:"Download as file",
click : function() {
var blob = new Blob([idList.join("\n")], {type: "text/plain;charset=utf-8"});
saveAs(blob, "identifiers.txt");
},
},
{
text:"Copy to clipboard",
click :function() {
$("textarea").select();
document.execCommand('copy');
$( this ).dialog( "close" );
}
}
];
let title = "List of "+idList.length+" selected IDs";
if (selCells.length===0)
title = "No cells selected";
showDialogBox(htmls, title,
{showClose:true, height:dlgHeight, width:500, buttons:buttons}
);
}
db.loadCellIds(selCells, buildExportDialog);
}
function onAboutClick() {
/* user clicked on help > about */
var dlgHeight = 500;
var htmls = [];
var title = "UCSC Cell Browser";
htmls.push(" Version: "+gVersion+" Written by: Maximilian Haeussler, Nikolay Markov (U Northwestern), Brian Raney, Lucas Seninge Testing / User interface / Documentation / Data import / User support: Matt Speir, Brittney Wick Code contributions by: Pablo Moreno (EBI, UK) Documentation: Readthedocs Github Repo: cellBrowser Paper: Speir et al, Bioinformatics 2021, DOI:10.1093/bioinformatics/btab503/6318386");
htmls.push(desc.title);
htmls.push("
");
}
if (desc.image) {
htmls.push("");
}
if (desc.imageMap) {
htmls.push('');
}
if (desc.abstract) {
htmls.push("
");
}
if (desc.authors) {
htmls.push("Authors: "+desc.authors);
htmls.push("
");
}
if (desc.lab) {
htmls.push("Lab: "+desc.lab);
htmls.push("
");
}
if (desc.institution) {
htmls.push("Institution: "+desc.institution);
htmls.push("
");
}
htmlAddLink(htmls, desc, "cbDoi");
htmlAddLink(htmls, desc, "biorxiv_url");
htmlAddLink(htmls, desc, "paper_url");
htmlAddLink(htmls, desc, "other_url");
htmlAddLink(htmls, desc, "geo_series");
htmlAddLink(htmls, desc, "pmid");
htmlAddLink(htmls, desc, "dbgap");
htmlAddLink(htmls, desc, "sra_study");
htmlAddLink(htmls, desc, "bioproject");
htmlAddLink(htmls, desc, "sra");
htmlAddLink(htmls, desc, "doi");
htmlAddLink(htmls, desc, "arrayexpress");
htmlAddLink(htmls, desc, "cirm_dataset");
htmlAddLink(htmls, desc, "ega_study");
htmlAddLink(htmls, desc, "ega_dataset");
htmlAddLink(htmls, desc, "ena_project");
htmlAddLink(htmls, desc, "hca_dcp");
if (desc.urls) {
for (let key in desc.urls)
htmlAddLink(htmls, desc.urls, key, key);
}
if (desc.custom) {
for (let key in desc.custom) {
htmls.push(""+key+": "+desc.custom[key]);
htmls.push("
");
}
}
if (desc.submitter) {
htmls.push("Submitted by: "+desc.submitter);
if (desc.submission_date) {
htmls.push(" ("+desc.submission_date);
htmls.push(")");
}
if (desc.version)
htmls.push(", Version "+desc.version);
htmls.push("
");
}
if (desc.shepherd) {
htmls.push("UCSC Data Shepherd: "+desc.shepherd);
htmls.push("
");
}
if (desc.wrangler) {
htmls.push("UCSC Data Wrangler: "+desc.wrangler);
htmls.push("
");
}
// collections have no downloads tab, but multiomic-gbm wants supplementary files there, so make them appear
buildSupplFiles(desc, datasetInfo.name, htmls);
let topName = datasetInfo.name.split("/")[0];
if (pageAtUcsc()) {
if (datasetInfo.name!=="") {
// Only do this if this is not the root dataset
if ((datasetInfo.parents) && (datasetInfo.parents.length > 1)) {
// if the dataset is a collection
htmls.push("Direct link to this collection for manuscripts: https://"+topName+".cells.ucsc.edu");
htmls.push("
");
}
else {
htmls.push("Direct link to this plot for manuscripts: https://"+topName+".cells.ucsc.edu");
htmls.push("
");
}
console.log(datasetInfo);
if ( datasetInfo.atacSearch) {
htmls.push("ATAC-seq search gene models: " + datasetInfo.atacSearch);
htmls.push("
");
}
htmls.push("Dataset classification: ");
buildClassification(htmls, datasetInfo, "body_parts", "Organs", true);
buildClassification(htmls, datasetInfo, "diseases", "Diseases", true);
buildClassification(htmls, datasetInfo, "organisms", "Organism", true);
buildClassification(htmls, datasetInfo, "life_stages", "Life Stage", true);
buildClassification(htmls, datasetInfo, "domains", "Scientific Domain", true);
buildClassification(htmls, datasetInfo, "sources", "Source Database", false);
htmls.push("
To move between datasets later in the cell browser, " +
"use the 'Collection' dropdown.
');
htmls.push('
');
htmls.push('
start with 'TH' or '-1$' to find all IDs that end with '-1'");
var title = "Paste a list of IDs (one per line) to select "+gSampleDesc+"s";
showDialogBox(htmls, title, {showClose:true, height:dlgHeight, width:dlgWidth, buttons:buttons});
}
function onExportIdsClick() {
/* Edit - Export cell IDs */
var selCells = renderer.getSelection();
function buildExportDialog(idList) {
/* callback when cellIds have arrived */
var dlgHeight = 500;
var htmls = [];
if (selCells.length===0)
htmls.push("Shown below are the identifiers of all "+idList.length+" cells in the dataset. domEl, not sure why no event
url = url.split("?")[0]; // strip off the md5 checksum
if (url.search("exprMatrix.bin")!==-1) // never show progress bar for single gene vector requests
return;
var progressRowIdx = progressUrls[url]; // there can be multiple progress bars
if (progressRowIdx===undefined) {
// if there is none yet, find the first free index
progressRowIdx = 0;
for (var oldUrl in progressUrls) {
progressRowIdx = Math.max(progressRowIdx, progressUrls[oldUrl]);
}
progressRowIdx++;
progressUrls[url] = progressRowIdx;
}
var label = url;
if (url.endsWith("coords.bin"))
label = "Loading Coordinates";
else if (url.endsWith(".bin"))
label = "Loading cell annotations";
var labelId = "#mpProgressLabel"+progressRowIdx;
$(labelId).html(label);
var percent = Math.round(100 * (ev.loaded / ev.total));
if (percent>=99) {
$("#mpProgress"+progressRowIdx).css("width", percent+"%");
$("#mpProgress"+progressRowIdx).show(0);
//progressUrls.splice(index, 1);
delete progressUrls[url];
$("#mpProgressDiv"+progressRowIdx).css("display", "none");
}
else {
$("#mpProgress"+progressRowIdx).css("width", percent+"%");
$("#mpProgressDiv"+progressRowIdx).css("display", "inherit");
}
}
function getActiveColorField() {
/* return the current field that is used for coloring the UMAP */
// XX Probably should use db.conf.activeColorField here! - a recent addition
let fieldName = getVar("meta");
if (fieldName===undefined)
fieldName = db.getDefaultColorField();
return fieldName;
}
function getActiveLabelField() {
/* return default label field or from URL */
let fieldName = getVar("label");
if (fieldName===undefined)
fieldName = renderer.getLabelField();
if (fieldName===undefined)
fieldName = db.conf.labelField;
return fieldName;
}
function colorByMetaField(fieldName, doneLoad) {
/* load the meta data for a field, setup the colors, send it all to the renderer and call doneLoad. if doneLoad is undefined, redraw everything */
function onMetaArrLoaded(metaArr, metaInfo) {
gLegend = buildLegendForMeta(metaInfo);
buildLegendBar();
var renderColors = legendGetColors(gLegend.rows);
renderer.setColors(renderColors);
renderer.setColorArr(metaArr);
buildWatermark(); // if we're in split mode
metaInfo.arr = metaArr;
doneLoad();
}
if (doneLoad===undefined)
doneLoad = function() { renderer.drawDots(); };
if (fieldName===null || fieldName===undefined) {
// obscure hacky option: you can set the default color field to "None"
// so there is no coloring at all on startup
colorByNothing();
doneLoad();
return;
}
var metaInfo = db.findMetaInfo(fieldName);
console.log("Color by meta field "+fieldName);
// cbData always keeps the most recent expression array. Reset it now.
if (db.lastExprArr)
delete db.lastExprArr;
var defaultMetaField = db.getDefaultColorField();
// internal field names cannot contain non-alpha chars, so tolerate user errors here
// otherwise throw an error
if (metaInfo === null && fieldName!==undefined) {
metaInfo = db.findMetaInfo(fieldName.replace(/[^0-9a-z]/gi, ''));
if (metaInfo === null) {
alert("The field "+fieldName+" does not exist in the sample/cell annotations. Cannot color on it.");
metaInfo = db.findMetaInfo(defaultMetaField);
}
}
if (metaInfo.type==="uniqueString") {
warn("This field contains a unique identifier. You cannot color on such a field. However, you can search for values in this field using 'Edit > Find by ID'.");
return null;
}
if (metaInfo.diffValCount > MAXCOLORCOUNT && metaInfo.type==="enum") {
warn("This field has "+metaInfo.diffValCount+" different values. Coloring on a field that has more than "+MAXCOLORCOUNT+" different values is not supported.");
return null;
}
if (fieldName===defaultMetaField)
changeUrl({"meta":null, "gene":null});
else
changeUrl({"meta":fieldName, "gene":null});
db.conf.activeColorField = fieldName;
if (metaInfo.arr) // eg custom fields
onMetaArrLoaded(metaInfo.arr, metaInfo);
else
db.loadMetaVec(metaInfo, onMetaArrLoaded, onProgress, {}, db.conf.binStrategy);
changeUrl({"pal":null});
// clear the gene search box
var select = $('#tpGeneCombo')[0].selectize.clear();
}
function activateTab(name) {
/* activate a tab on the left side */
var idx = 0;
if (name==="gene")
idx = 1;
$( "#tpLeftTabs" ).tabs( "option", "active", idx );
}
function doLog2(arr) {
/* take log2(x+1) for all values in array and return the result */
var arr2 = new Float64Array(arr.length);
for (var i = 0; i < arr.length; i++) {
arr2.push(Math.log2(arr[i]+1));
}
return arr2;
}
function splitExprByMeta(metaArr, metaCountSize, exprArr) {
/* split expression values by meta annotation, return array metaIdx -> array of expression values */
var metaValToExprArr = [];
// initialize result array
for (var i=0; i < metaCountSize; i++) {
metaValToExprArr.push([]);
}
let exprMax = exprArr[0];
let exprMin = exprArr[0];
for (var i=0; i < exprArr.length; i++) {
var exprVal = exprArr[i];
var metaVal = metaArr[i];
metaValToExprArr[metaVal].push(exprVal);
exprMax = Math.max(exprMax, exprVal);
//if (exprMax > 50)
//exprMax = Math.round(exprMax);
exprMin = Math.min(exprMin, exprVal);
}
return [metaValToExprArr, exprMin, exprMax];
}
function splitExprByMetaSelected(exprVec, splitArr, selCells) {
/* split the expression vector into two vectors. splitArr is an array with 0/1, indicates where values go.
* if selCells is not null, restrict the splitting to just indices in selCells.
* Returns array of the two arrays.
* */
console.time("findCellsWithMeta");
if (exprVec.length!==splitArr.length) {
warn("internal error - splitExprByMetaSelected: exprVec has diff length from splitArr");
}
var arr1 = [];
var arr2 = [];
// code duplication, not very elegant, but avoids creating an array just for the indices
if (selCells.length===0)
// the version if no cells are selected
for (var cellIdx = 0; cellIdx < exprVec.length; cellIdx++) {
var val = exprVec[cellIdx];
if (splitArr[cellIdx]===0)
arr1.push(val);
else
arr2.push(val);
}
else
// the version with a cell selection
for (var i = 0; i < selCells.length; i++) {
let cellIdx = selCells[i];
let val = exprVec[cellIdx];
if (splitArr[cellIdx]===0)
arr1.push(val);
else
arr2.push(val);
}
if (db.conf.violinDoLog2) {
console.time("log2");
arr1 = doLog2(arr1);
arr2 = doLog2(arr2);
console.timeEnd("log2");
}
console.timeEnd("findCellsWithMeta");
return [arr1, arr2];
}
function splitExpr(exprVec, selCells) {
/* split the expression vector into two vectors, one for selected and one for unselected cells */
console.time("splitExpr");
var selMap = {};
for (var i = 0; i < selCells.length; i++) {
selMap[selCells[i]] = null;
}
var sel = [];
var unsel = [];
for (i = 0; i < exprVec.length; i++) {
if (i in selMap)
sel.push(exprVec[i]);
else
unsel.push(exprVec[i]);
}
console.timeEnd("splitExpr");
return [sel, unsel];
}
function log2All(arr) {
/* take log2(x+1 of all subarrays */
return arr.map(subArr =>
subArr.map(num => Math.log2(num + 1))
);
}
function buildViolinFromValues(labelList, dataList) {
/* make a violin plot given the labels and the values for them */
if ("violinChart" in window)
window.violinChart.destroy();
let log2Done = false;
if (db.conf.matrixArrType.includes("int")) {
dataList = log2All(dataList);
log2Done = true;
}
var labelLines = [];
labelLines[0] = labelList[0].split("\n");
labelLines[0].push(dataList[0].length);
if (dataList.length > 1) {
labelLines[1] = labelList[1].split("\n");
labelLines[1].push(dataList[1].length);
}
const ctx = getById("tpViolinCanvas").getContext("2d");
var violinData = {
labels : labelLines,
datasets: [{
data : dataList,
label: 'Mean',
backgroundColor: 'rgba(255,0,0,0.5)',
borderColor: 'red',
borderWidth: 1,
outlierColor: '#999999',
padding: 7,
itemRadius: 0
}]
};
var optDict = {
maintainAspectRatio: false,
legend: { display: false },
title: { display: false }
};
var yLabel = null;
if (db.conf.unit===undefined && db.conf.matrixArrType==="Uint32") {
yLabel = "read/UMI count";
}
if (db.conf.unit!==undefined)
yLabel = db.conf.unit;
if (log2Done)
yLabel = "log2("+yLabel+" + 1)";
if (yLabel!==null)
optDict.scales = {
yAxes: [{
scaleLabel: {
display: true,
labelString: yLabel
}
}]
};
window.setTimeout(function() {
console.time("violinDraw");
window.violinChart = new Chart(ctx, {
type: 'violin',
data: violinData,
options: optDict
});
console.timeEnd("violinDraw");
}, 10);
}
function buildViolinFromMeta(exprVec, metaName, selCells) {
/* load a binary meta vector, split the exprVector by it and make two violin plots, one meta value vs the other. */
var metaInfo = db.findMetaInfo(metaName);
if (metaInfo.valCounts.length!==2) {
alert("Config error: meta field in 'violinField', '"+db.conf.violinField+"' does not have two distinct values.");
return;
}
var labelList = [metaInfo.valCounts[0][0], metaInfo.valCounts[1][0]];
db.loadMetaVec( metaInfo,
function(metaArr) {
var dataList = splitExprByMetaSelected(exprVec, metaArr, selCells);
buildViolinFromValues(labelList, dataList);
},
null, {}, db.conf.binStrategy);
}
//function removeViolinPlot() {
/* destroy the violin plot */
//if ("violinChart" in window)
//window.violinChart.destroy();
//$('#tpViolinCanvas').remove();
//}
function buildViolinPlot() {
/* create the violin plot at the bottom right, depending on the current selection and the violinField config */
var exprVec = gLegend.exprVec;
if (exprVec===undefined)
return;
var dataList = [];
var labelList = [];
var selCells = renderer.getSelection();
// filter exprVec by background
if (background !== null) {
var ourSelCells = {};
for (var i = 0; i < selCells.length; i++) {
ourSelCells[selCells[i]] = true;
}
var ourCells = {};
for (i = 0; i < background.length; i++) {
ourCells[background[i]] = true;
}
var result = [];
var renamedSelCells = [];
for (i = 0; i < exprVec.length; i++) {
if (i in ourSelCells) {
renamedSelCells.push(result.length);
result.push(exprVec[i]);
} else if (i in ourCells) {
result.push(exprVec[i]);
}
}
exprVec = result;
selCells = renamedSelCells;
}
// if we have a violin meta field to split on, make two violin plots, metavalue vs rest
// restrict the plot to the selected cells, if any
if (db.conf.violinField!==undefined) {
buildViolinFromMeta(exprVec, db.conf.violinField, selCells);
} else {
// there is no violin field
if (selCells.length===0) {
// no selection, no violinField: default to a single violin plot
dataList = [Array.prototype.slice.call(exprVec)];
if (background === null) {
labelList = ['All cells'];
} else {
labelList = ['Background\ncells'];
}
buildViolinFromValues(labelList, dataList);
} else {
// cells are selected and no violin field: make two violin plots, selected against other cells
dataList = splitExpr(exprVec, selCells);
if (background === null) {
labelList = ['Selected', 'Others'];
} else {
labelList = ['Selected', 'Background'];
}
if (dataList[1].length===0) {
dataList = [dataList[0]];
labelList = ['All Selected'];
}
buildViolinFromValues(labelList, dataList);
}
}
}
function selectizeSetValue(elId, name) {
/* little convenience method to set a selective dropdown to a given
* value. does not trigger the change event. */
if (name===undefined)
return;
var sel = getById(elId).selectize;
sel.addOption({id: name, text: name});
sel.setValue(name, 1); // 1 = do not fire change
}
function selectizeClear(elId) {
/* clear a selectize Dropdown */
if (name===undefined)
return;
var sel = getById(elId).selectize;
sel.clear();
}
function colorByNothing() {
/* color by nothing, rarely needed */
renderer.setColors([cNullColor]);
var cellCount = db.conf.sampleCount;
renderer.setColorArr(new Uint8Array(cellCount));
gLegend.rows = [];
gLegend.title = "Nothing selected";
gLegend.subTitle = "";
gLegend.rows.push( {
color:cNullColor, defColor:null, label:"No Value",
count:cellCount, intKey:0, strKey:null
} );
buildLegendBar();
}
- function buildWatermark(myRend) {
+ function buildWatermark(myRend, showWatermark) {
/* update the watermark behind the image */
if (myRend===undefined)
myRend = renderer;
- if (!myRend.isSplit()) {
+ if (!myRend.isSplit() && !showWatermark) {
myRend.setWatermark("");
return;
}
let prefix = "";
if (db.conf.coords.length!==1)
- prefix = renderer.coords.coordInfo.shortLabel+": ";
+ prefix = myRend.coords.coordInfo.shortLabel+": ";
let labelStr;
if (gLegend.type==="expr")
labelStr = prefix+gLegend.geneSym;
else
labelStr = prefix+gLegend.metaInfo.label;
let waterLabel;
if (db.isAtacMode())
waterLabel= labelStr.split("|").length + " peak(s)";
else
waterLabel = labelStr;
myRend.setWatermark(waterLabel);
}
function colorByLocus(locusStr, onDone, locusLabel) {
- /* color by a gene or peak, load the array into the renderer and call onDone or just redraw
+ /* colorByGene: color by a gene or peak, load the array into the renderer and call onDone or just redraw
* peak can be in format: +chr1:1-1000
* gene can be in format: geneSym or geneSym=geneId
* */
if (onDone===undefined || onDone===null)
onDone = function() { renderer.drawDots(); };
function gotGeneVec(exprArr, decArr, locusStr, geneDesc, binInfo) {
/* called when the expression vector has been loaded and binning is done */
if (decArr===null)
return;
console.log("Received expression vector, for "+locusStr+", desc: "+geneDesc);
// update the URL and possibly the gene combo box
if (locusStr.indexOf("|") > -1) {
if (locusStr.length < 600)
// this is rare, so just completely skip this URL change now
changeUrl({"locus":locusStr, "meta":null});
} else
changeUrl({"gene":locusStr, "meta":null});
makeLegendExpr(locusStr, geneDesc, binInfo, exprArr, decArr);
renderer.setColors(legendGetColors(gLegend.rows));
renderer.setColorArr(decArr);
+ if (renderer.childPlot && document.getElementById("splitJoinBox").checked) {
+ renderer.childPlot.setColors(legendGetColors(gLegend.rows));
+ renderer.childPlot.setColorArr(decArr);
+ buildWatermark(renderer.childPlot);
+ }
buildWatermark(renderer);
buildLegendBar();
onDone();
// update the "recent genes" div
for (var i = 0; i < gRecentGenes.length; i++) {
// remove previous gene entry with the same symbol
if (gRecentGenes[i][0]===locusStr || gRecentGenes[i][1]===locusStr) { // match symbol or ID
gRecentGenes.splice(i, 1);
break;
}
}
// make sure that recent genes table has symbol and Id
var locusWithSym = locusStr;
if (db.isAtacMode()) {
locusWithSym = shortenRange(locusStr);
} else {
if (locusStr.indexOf("+")===-1) {
let geneInfo = db.getGeneInfo(locusStr);
if ((geneInfo.sym!==geneInfo.geneId))
locusWithSym = geneInfo.id+"|"+geneInfo.sym;
} else {
let geneCount = locusStr.split("+").length;
locusWithSym = locusStr+"|Sum of "+geneCount+" genes";
}
}
gRecentGenes.unshift([locusWithSym, geneDesc]); // insert at position 0
gRecentGenes = gRecentGenes.slice(0, 9); // keep only nine last
buildGeneTable(null, "tpRecentGenes", null, null, gRecentGenes);
$('#tpRecentGenes .tpGeneBarCell').click( onGeneClick );
resizeGeneTableDivs("tpRecentGenes");
}
// clear the meta combo
$('#tpMetaCombo').val(0).trigger('chosen:updated');
console.log("Loading gene expression vector for "+locusStr);
db.loadExprAndDiscretize(locusStr, gotGeneVec, onProgress, db.conf.binStrategy);
}
function colorByMultiGenes(geneIds, syms) {
var locusLabel = syms.join("+");
colorByLocus(geneIds.join("+"), null, locusLabel)
}
function gotCoords(coords, info, clusterInfo, newRadius) {
/* called when the coordinates have been loaded */
if (coords.length===0)
alert("cellBrowser.js/gotCoords: coords.bin seems to be empty");
var opts = {};
if (newRadius)
opts["radius"] = newRadius;
// label text can be overriden by the user cart
var labelField = db.conf.labelField;
if (clusterInfo) {
var origLabels = [];
var clusterMids = clusterInfo.labels;
// old-style files contain just coordinates, no order
if (clusterMids === undefined) {
clusterMids = clusterInfo;
}
for (var i = 0; i < clusterMids.length; i++) {
origLabels.push(clusterMids[i][2]);
}
renderer.origLabels = origLabels;
}
if (clusterInfo && clusterInfo.lines) {
opts["lines"] = clusterInfo.lines;
opts["lineWidth"] = db.conf.lineWidth;
opts["lineColor"] = db.conf.lineColor;
opts["lineAlpha"] = db.conf.lineAlpha;
}
renderer.setCoords(coords, clusterMids, info, opts);
+ buildWatermark(renderer);
}
function computeAndSetLabels(values, metaInfo) {
/* recompute the label positions and redraw everything. Updates the dropdown. */
var labelCoords;
var coords = renderer.coords.orig;
var names = null;
if (metaInfo.type !== "float" && metaInfo.type !== "int") {
var names = metaInfo.ui.shortLabels;
}
console.time("cluster centers");
var calc = renderer.calcMedian(coords, values, names, metaInfo.origVals);
labelCoords = [];
for (var label in calc) {
var labelInfo = calc[label];
var midX = selectMedian(labelInfo[0]);
var midY = selectMedian(labelInfo[1]);
labelCoords.push([midX, midY, label]);
}
console.timeEnd("cluster centers");
renderer.setLabelCoords(labelCoords);
renderer.setLabelField(metaInfo.name);
setLabelDropdown(metaInfo.name);
}
function setLabelField(labelField) {
/* updates the UI: change the field that is used for drawing the labels. 'null' means hide labels. Do not redraw. */
if (labelField===null) {
renderer.setLabelField(null);
setLabelDropdown(null);
}
else {
var metaInfo = db.findMetaInfo(labelField);
if (metaInfo.valCounts.length > MAXLABELCOUNT) {
let valCount = metaInfo.valCounts.length;
alert("Error: This field contains "+valCount+" different values. "+
"The limit is "+MAXLABELCOUNT+". Too many labels overload the screen.");
renderer.setLabelField(null);
setLabelDropdown(null);
return;
}
if (metaInfo.arr) // preloaded
computeAndSetLabels(metaInfo.arr, metaInfo);
else
db.loadMetaVec(metaInfo, computeAndSetLabels);
}
}
function setColorByDropdown(fieldName) {
/* set the meta 'color by' dropdown to a given value. The value is the meta field name, or its label, or its index */
var fieldIdx = db.fieldNameToIndex(fieldName);
chosenSetValue("tpMetaCombo", "tpMetaVal_"+fieldIdx);
}
function setLabelDropdown(fieldName) {
/* set the meta 'label by' dropdown to a given value. The value is the meta field name, or its short label, or its index
The special value null means "No Label" */
var fieldIdx = "none";
if (fieldName!==null)
fieldIdx = db.fieldNameToIndex(fieldName);
chosenSetValue("tpLabelCombo", "tpMetaVal_"+fieldIdx);
}
function colorByDefaultField(onDone, ignoreUrl) {
/* get the default color field from the config or the URL and start coloring by it.
* Call onDone() when done. */
var colorType = "meta";
var colorBy = db.getDefaultColorField();
if (ignoreUrl!==true) {
// allow to override coloring by URL args
if (getVar("gene")!==undefined) {
colorType = "gene";
colorBy = getVar("gene");
activateTab("gene");
}
else if (getVar("meta")!==undefined) {
colorType = "meta";
colorBy = getVar("meta");
activateTab("meta");
} else if (getVar("locus")!==undefined) {
colorType = "locus";
colorBy = getVar("locus");
activateTab("gene");
if (getVar("locusGene")!==undefined) {
let geneId = getVar("locusGene");
updatePeakListWithGene(geneId);
}
}
}
gLegend = {};
if (colorType==="meta") {
colorByMetaField(colorBy, onDone);
// update the meta field combo box
var fieldIdx = db.fieldNameToIndex(colorBy);
if (fieldIdx===null) {
alert("Default coloring is configured to be on field "+colorBy+
" but cannot find a field with this name, using field 1 instead.");
fieldIdx = 1;
}
setColorByDropdown(colorBy);
$('#tpMetaBox_'+fieldIdx).addClass('tpMetaSelect');
}
else {
if (colorType==="locus") {
colorByLocus(colorBy, onDone);
peakListSetStatus(colorBy);
} else {
// must be gene then
var geneId = db.mustFindOneGeneExact(colorBy);
colorByLocus(geneId, onDone);
}
}
}
function makeFullLabel(db) {
/* return full name of current dataset, including parent names */
var nameParts = [];
var parents = db.conf.parents;
if (parents)
for (var i=0; i < parents.length; i++)
if (parents[i][0]!="") // "" is the root dataset = no need to add
nameParts.push( parents[i][1] );
nameParts.push( db.conf.shortLabel );
var datasetLabel = nameParts.join(" - ");
return datasetLabel;
}
function gotSpatial(img) {
/* called when the spatial image has been loaded */
renderer.setBackground(img);
if (renderer.readyToDraw())
renderer.drawDots();
else
console.log("got spatial, but cannot draw yet");
}
function plotTrace(cellId) {
/* plot a trace of a cell */
let lineEl = document.createElement('line');
//