294c36492d0ae4bac9ae450eade47fe33c4860d8
max
Fri May 16 09:22:07 2025 -0700
improving error message when gene is not found in matrix
diff --git src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
index c543433..fbea65c 100644
--- src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
+++ src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
@@ -1,9723 +1,9725 @@
// 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, showWatermark) {
/* update the watermark behind the image */
if (myRend===undefined)
myRend = renderer;
if (!myRend.isSplit() && !showWatermark) {
myRend.setWatermark("");
return;
}
let prefix = "";
if (db.conf.coords.length!==1)
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) {
/* 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');
//