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("

"); if (desc.rawMatrixFile) { htmls.push("

Raw count matrix: "+desc.rawMatrixFile+""); if (desc.rawMatrixNote) htmls.push("
"+desc.rawMatrixNote); htmls.push("

"); } 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("

"); htmls.push("

Dimensionality reduction coordinates:
"); for (let fname of desc.coordFiles) htmls.push(""+fname+"
"); htmls.push("

"); buildSupplFiles(desc, datasetInfo.name, htmls); 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("

Supplemental high-resolution images

"); // TOC let catIdx = 0; htmls.push("
Jump to: "); for (let catInfo of desc.imageSets) { htmls.push(""+catInfo.categoryLabel+""); catIdx++; } htmls.push("
"); if (desc.imageSetNote) htmls.push("

"+desc.imageSetNote+"

"); // actual HTML catIdx = 0; for (let catInfo of desc.imageSets) { htmls.push("

"); htmls.push(""); catIdx++; htmls.push(""+catInfo.categoryLabel+":
"); let imgDir = datasetInfo.name+"/images/"; htmls.push("
"); for (let imgSet of catInfo.categoryImageSets) { if (imgSet.setLabel) htmls.push(""+imgSet.setLabel+"
"); htmls.push(""); // tpImgSetLinks } htmls.push("
"); // tpImgSets htmls.push("
"); // tpImgCategory } //htmls.push(""); $( "#paneImg" ).html(htmls.join("")); $( "#paneImg" ).show(); $( "#tabLinkImg" ).show(); } function buildMethodsPane(datasetInfo, desc) { // methods panel // var htmls = []; if (desc.methods) { 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"); } if (htmls.length!==0) { $( "#pane2" ).html(htmls.join("")); $( "#pane2" ).show(); $( "#tabLink2" ).show(); } else { $( "#pane2" ).hide(); $( "#tabLink2" ).hide(); } } function pageAtUcsc() { // return true if current page is at ucsc.edu return (window.location.hostname.endsWith("ucsc.edu")); } function buildClassification(htmls, datasetInfo, attrName, label, addSep) { if (datasetInfo[attrName]===undefined && (datasetInfo.facets===undefined || datasetInfo.facets[attrName]===undefined)) return; var values; // in Nov 2022, the facets moved into their own object, old dataasets have them in the dataset itself as attributes if (datasetInfo.facets) values = datasetInfo.facets[attrName]; else values = datasetInfo[attrName]; if (values===undefined) values = []; htmls.push(label+"=" + values.join(",")); if (addSep) htmls.push("; "); } function datasetDescToHtml(datasetInfo, desc) { /* given an object with keys title, abstract, pmid, etc, fill the dataset description tabs with html */ if (!desc) // http errors call this with undefined return; let htmls = []; if (datasetInfo.name==="") // the root dataset $('#tabLink1').text("Overview"); else $('#tabLink1').text("Abstract"); if (desc.title) { htmls.push("

"); htmls.push(desc.title); htmls.push("

"); } if (desc.image) { htmls.push(""); } if (desc.imageMap) { htmls.push(''); htmls.push(desc.imageMap); htmls.push(''); } if (desc.abstract) { htmls.push("

"); htmls.push(desc.abstract); htmls.push("

"); } else { // the top-level hardcoded dataset for non-hierarchy mode has the abstract in the // dataset config. It's a lot easier this way, so just pull it in here. htmls.push("

"); htmls.push(datasetInfo.abstract); htmls.push("

"); } if (desc.author) { htmls.push("Author: "+desc.author); 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("

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."+ "

"); htmls.push("

Cell Browser dataset ID: "+datasetInfo.name+ "

"); } } $( "#pane1" ).html(htmls.join("")); buildMethodsPane(datasetInfo, desc); buildDownloadsPane(datasetInfo, desc); buildImagesPane(datasetInfo, desc); $("#tpOpenDialogTabs").tabs("refresh"); //.tabs("option", "active", 0) does not do the color change of the tab so doing this instead $("#tabLink1").click(); $("area").click( function(ev) { var dsName = ev.target.href.split("/").pop(); loadDataset(gOpenDataset.name+"/"+dsName, true); $(".ui-dialog-content").dialog("close"); ev.preventDefault(); }); } function getFacetString(ds, facetName) { /* search for an attribute under ds.facets or directly under ds, for backwards compatibility, and return as a |-sep string */ let facets = []; if (ds.facets!==undefined && ds.facets[facetName]!==undefined) facets = ds.facets[facetName]; if (ds[facetName]!==undefined) facets = ds[facetName]; facets = cleanStrings(facets); let facetStr = facets.join("|"); return facetStr; } function buildListPanel(datasetList, listGroupHeight, leftPaneWidth, htmls, selName) { /* make a dataset list and append its html lines to htmls */ htmls.push("
"); if (!datasetList || datasetList.length===0) { alert("No datasets are available. Please make sure that at least one dataset does not set visibility=hide "+ " or that at least one collection is defined. Problems? -> cells@ucsc.edu"); return; } var selIdx = 0; for (var i = 0; i < datasetList.length; i++) { var dataset = datasetList[i]; var clickClass = "tpDatasetButton"; if (dataset.isCollection) clickClass = "tpCollectionButton"; if (dataset.name===selName || (selName===undefined && i===0)) { clickClass += " active"; selIdx = i; } let bodyPartStr = getFacetString(dataset, "body_parts"); let disStr = getFacetString(dataset, "diseases"); let orgStr = getFacetString(dataset, "organisms"); let projStr = getFacetString(dataset, "projects"); let domStr = getFacetString(dataset, "domains"); let lifeStr = getFacetString(dataset, "life_stages"); let sourceStr = getFacetString(dataset, "sources"); var line = ""; // bootstrap seems to remove the id htmls.push(line); if (!dataset.isSummary) htmls.push(''); if (dataset.sampleCount!==undefined) { var countDesc = prettyNumber(dataset.sampleCount); htmls.push(""+countDesc+""); } if (dataset.datasetCount!==undefined) { htmls.push(""+dataset.datasetCount+" datasets"); } if (dataset.collectionCount!==undefined) { htmls.push(""+dataset.collectionCount+" collections"); } //if (dataset.tags!==undefined) { //for (var tagI = 0; tagI < dataset.tags.length; tagI++) { //var tag = dataset.tags[tagI]; //if (tag==="smartseq2" || tag==="ATAC" || tag==="10x") //continue //htmls.push(""+tag+""); //} //} htmls.push(dataset.shortLabel+""); } htmls.push("
"); // list-group return selIdx; } function getDatasetAttrs(datasets, attrName) { /* return an array of (attrName, "attrName (count)") of all attrNames (e.g. body_parts) in a dataset array */ var valCounts = {}; for (let i=0; i < datasets.length; i++) { let facetObj = datasets[i]; if (facetObj.facets) // facets can be stored on the objects (old) or on a separate ds.facets object (new) facetObj = facetObj.facets; if (facetObj[attrName]===undefined) continue for (let bp of facetObj[attrName]) if (bp in valCounts) valCounts[bp]++; else valCounts[bp] = 1; } let allValues = keys(valCounts); allValues.sort(); var valLabels = {}; for (let i=0; i < allValues.length; i++) { var key = allValues[i]; var count = valCounts[key]; var labelKey = key; if (labelKey==="") labelKey = "-empty-" var label = labelKey+" ("+count+")" valLabels[key] = label; } return Object.entries(valLabels); } function filterDatasetsDom() { /* keep only datasets that fulfill the filters */ // read the current filter values of the dropboxes var categories = ["Body", "Dis", "Org", "Proj", "Stage", "Dom", "Source"]; var filtVals = {}; for (var category of categories) { var vals = $("#tp"+category+"Combo").val(); if (vals===undefined) vals = []; // strip special chars var cleanVals = []; for (var val of vals) cleanVals.push(cleanString(val)); filtVals[category] = cleanVals; } let elList = $(".tpListItem"); var shownCount = 0; var hideCount = 0; for (let el of elList) { // never touch the first/summary element if (el.getAttribute("data-body")==="summary") continue // read the values of the current DOM element var domVals = {}; for (var category of categories) domVals[category] = el.getAttribute("data-"+category.toLowerCase()); var isShown = true; // now compare filtVals and domVals for (var category of categories) { var filtList = filtVals[category]; var domList = domVals[category]; let found = false; if (filtList.length===0) found = true; for (var filtVal of filtList) if (domList.indexOf(filtVal)!=-1) found = true; if (!found) { isShown = false; break; } } if (isShown) { el.style.display=""; shownCount++; } else { el.style.display="none"; hideCount++; } } if (hideCount!==0) $('#tpDatasetCount').text("(filters active, "+shownCount+" datasets shown)"); else $('#tpDatasetCount').text("("+shownCount+" dataset collections)"); } function openDatasetDialog(openDsInfo, selName, openTab) { /* build dataset open dialog, * - openDsInfo is the currently open object or a collection. * - selName is the currently selected dataset in this list * - openTab is optional, "images" opens the image tab */ var datasetList = []; var listGroupHeight = 0; var leftPaneWidth = 400; var title = "Choose Cell Browser Dataset"; // inline functions function openCollOrDataset(selDatasetIdx) { /* click handler, opens either a collection or a dataset */ history.pushState({}, "Cell Browser Main Page", window.location.href); var dsInfo = datasetList[selDatasetIdx]; var datasetName = dsInfo.name; if (dsInfo.isCollection) showCollectionDialog(datasetName); else loadDataset(datasetName, true, dsInfo.md5); $(".ui-dialog-content").dialog("close"); //changeUrl({"bp":null}); } function buildFilter(html, filterVals, filterLabel, urlVar, comboId, comboLabel) { /* build html for a faceting filter */ if (filterVals.length==0) return false; html.push(""+filterLabel+":"); let selPar = getVarSafe(urlVar); if (selPar && selPar!=="") filtList = selPar.split("|"); buildComboBox(html, comboId, filterVals, filtList, comboLabel, 200, {multi:true}); html.push("  "); return true; } function connectOpenPane(selDatasetIdx, datasetList) { /* set all the click handlers for the left open dataset pane */ $("button.list-group-item").eq(selDatasetIdx).css("z-index", "1000"); // fix up first overlap $("button.list-group-item").keypress(function(e) { // load the current dataset when the user presses Return if (e.which === '13') { openCollOrDataset(selDatasetIdx); } }); $(".list-group-item").click( function (ev) { selDatasetIdx = parseInt($(ev.target).data('datasetid')); // index of clicked dataset $(".list-group-item").removeClass("active"); $('#tpDatasetButton_'+selDatasetIdx).bsButton("toggle"); // had to rename .button() in index.html var datasetInfo = datasetList[selDatasetIdx]; openDatasetLoadPane(datasetInfo); }); $(".list-group-item").dblclick( function(ev) { selDatasetIdx = parseInt($(this).data('datasetid')); openCollOrDataset(selDatasetIdx); }); $(".load-dataset").click( function (ev) { ev.preventDefault(); ev.stopPropagation(); selDatasetIdx = parseInt($(this).parents('.list-group-item').data('datasetid')); openCollOrDataset(selDatasetIdx); return false; }); $(".list-group-item").focus( function (event) { selDatasetIdx = parseInt($(event.target).data('datasetid')); // index of clicked dataset // bootstrap has a bug where the blue selection frame is hidden by neighboring buttons // Working around this here by bumping up the current z-index. $("button.list-group-item").css("z-index", "0"); $("button.list-group-item").eq(selDatasetIdx).css("z-index", "1000"); }); } function onFilterChange(ev) { /* called when user changes a filter: updates list of datasets shown */ var filtNames = $(this).val(); var param = null; if (this.id==="tpBodyCombo") param = "bp"; else if (this.id=="tpDisCombo") param = "dis"; else if (this.id=="tpOrgCombo") param = "org"; else if (this.id=="tpProjCombo") param = "proj"; else if (this.id=="tpDomCombo") param = "dom"; else if (this.id=="tpStageCombo") param = "stage"; // change the URL var filtArg = filtNames.join("~"); var urlArgs = {} urlArgs[param] = filtArg; changeUrl(urlArgs); filterDatasetsDom(); } // -- end inline functions gOpenDataset = openDsInfo; var activeIdx = 0; var onlyInfo = false; datasetList = openDsInfo.datasets; if (datasetList===undefined) onlyInfo = true; var noteLines = []; // if this is a collection, not a dataset, change descriptive text in dialog if (datasetList && gOpenDataset.name!=="") { let dsCount = datasetList.length; title = 'Select one dataset from the collection "'+openDsInfo.shortLabel+'"'; title = title.replace(/'/g, "'"); noteLines.push( "

The collection '"+openDsInfo.shortLabel+"' contains "+dsCount+" datasets. " + "Double-click or click 'Open' below.
To move between datasets later in the cell browser, " + "use the 'Collection' dropdown.

"); changeUrl({"ds":openDsInfo.name.replace(/\//g, " ")}); // + is easier to type } let doFilters = false; let filtList = []; let bodyParts = null; let diseases = null; let organisms = null; let projects = null; let lifeStages = null; let domains = null; let sources = null; if (openDsInfo.parents === undefined && openDsInfo.datasets !== undefined) { bodyParts = getDatasetAttrs(openDsInfo.datasets, "body_parts"); diseases = getDatasetAttrs(openDsInfo.datasets, "diseases"); organisms = getDatasetAttrs(openDsInfo.datasets, "organisms"); projects = getDatasetAttrs(openDsInfo.datasets, "projects"); lifeStages = getDatasetAttrs(openDsInfo.datasets, "life_stages"); domains = getDatasetAttrs(openDsInfo.datasets, "domains"); sources = getDatasetAttrs(openDsInfo.datasets, "sources"); // mirror websites are not using the filters at all. So switch off the entire filter UI if they're not used if (bodyParts.length!==0 || diseases.length!==0 || organisms.length!==0 || projects.length!==0 || domains.length!==0 || lifeStages.length!==0 || sources.length!==0) doFilters = true; if (doFilters) { noteLines.push("
Filters:
"); buildFilter(noteLines, bodyParts, "Organ", "body", "tpBodyCombo", "select organs..."); buildFilter(noteLines, diseases, "Disease", "dis", "tpDisCombo", "select diseases..."); buildFilter(noteLines, organisms, "Species", "org", "tpOrgCombo", "select species..."); buildFilter(noteLines, projects, "Project", "proj", "tpProjCombo", "select project..."); noteLines.push("
"); buildFilter(noteLines, lifeStages, "Life Stages", "stage", "tpStageCombo", "select stage..."); buildFilter(noteLines, domains, "Scient. Domain", "dom", "tpDomCombo", "select domain..."); buildFilter(noteLines, sources, "Source DB", "source", "tpSourceCombo", "select db..."); } } // create links to the parents of the dataset if (openDsInfo && openDsInfo.parents && !onlyInfo) { noteLines.push("Go back to: " ); // make the back links let backLinks = []; let allParents = []; let parents = openDsInfo.parents; for (let i=0; i"+parLabel+""); } noteLines.push(backLinks.join(" > ")); } if (onlyInfo) title = "Dataset Information"; else { datasetList.unshift( { shortLabel:"Overview", name:openDsInfo.name, hasFiles:openDsInfo.hasFiles, body_parts:["summary"], isSummary:true, abstract:openDsInfo.abstract }); } var winWidth = window.innerWidth - 0.05*window.innerWidth; var winHeight = window.innerHeight - 0.05*window.innerHeight; var tabsWidth = winWidth - leftPaneWidth - 50; listGroupHeight = winHeight - 100; var htmls = ["
"]; htmls.push(noteLines.join("")); htmls.push("
"); htmls.push("
"); if (onlyInfo) { leftPaneWidth = 0; htmls.push("
"); // keep structure of the page the same, skip the left pane } else { activeIdx = buildListPanel(datasetList, listGroupHeight, leftPaneWidth, htmls, selName); htmls.push("
"); } htmls.push("
"); htmls.push(""); htmls.push("
"); htmls.push("

Loading abstract...

"); htmls.push("
"); htmls.push("
"); htmls.push("

Loading methods...

"); htmls.push("
"); htmls.push("
"); htmls.push("

Loading download instructions...

"); htmls.push("
"); htmls.push("
"); htmls.push("

Loading image data...

"); htmls.push("
"); htmls.push("
"); // tpOpenDialogTabs htmls.push("
"); // tpOpenDialogDatasetDesc //htmls.push("
"); // store the currently selected datasetId in the DOM htmls.push("
"); // tpDatasetBrowser var selDatasetIdx = 0; var buttons = []; if (db!==null) { var cancelLabel = "Cancel"; if (onlyInfo) cancelLabel = "Close"; buttons.push( { text: cancelLabel, click: function() { $( this ).dialog( "close" ); if (openDsInfo.isCollection) openDatasetDialog(openDsInfo, null); // show top-level dialog } }); } $(".ui-dialog-content").dialog("close"); // close the last dialog box showDialogBox(htmls, title, {width: winWidth, height:winHeight, buttons: buttons}); $("#tpOpenDialogTabs").tabs(); // little helper function function activateFilterCombo(valList, comboId) { if (valList) { activateCombobox(comboId, 200); $("#"+comboId).change( onFilterChange ); } } if (doFilters) { activateFilterCombo(bodyParts, "tpBodyCombo"); activateFilterCombo(diseases, "tpDisCombo"); activateFilterCombo(organisms, "tpOrgCombo"); activateFilterCombo(projects, "tpProjCombo"); activateFilterCombo(lifeStages, "tpStageCombo"); activateFilterCombo(domains, "tpDomCombo"); activateFilterCombo(sources, "tpSourceCombo"); } $('.tpBackLink').click( function(ev) { let openDatasetName = $(ev.target).attr('data-open-dataset'); let selDatasetName = $(ev.target).attr('data-sel-dataset'); loadCollectionInfo(openDatasetName, function(newCollInfo) { openDatasetDialog(newCollInfo, selDatasetName); }); changeUrl({"ds":openDatasetName.replace(/\//g, " ")}); }); var focused = document.activeElement; var scroller = $("#tpDatasetList").overlayScrollbars({ }); $(focused).focus(); $("#tabLink1").tab("show"); if (activeIdx!==null && !onlyInfo) { if (activeIdx!==0) scroller.scroll($("#tpDatasetButton_"+activeIdx)); // scroll left pane to current button $("tpDatasetButton_"+activeIdx).addClass("active"); } if (getVarSafe("ds")===undefined) // only filter on the top level filterDatasetsDom(); connectOpenPane(selDatasetIdx, datasetList); // finally, activate the default pane and load its html openDatasetLoadPane(openDsInfo, openTab); } function onHideSelClick(ev) { /* hide all selected cells */ renderer.selectHide(); renderer.drawDots(); updateSelectionButtons(); } function onOnlySelClick(ev) { /* show only selected, hide all unselected cells */ renderer.selectOnlyShow(); renderer.drawDots(); updateSelectionButtons(); } function onShowAllClick(ev) { /* show all cells, hidden or not. Do not touch the selection. */ renderer.unhideAll(); renderer.drawDots(); $("#tpShowAll").hide() } function updateSelectionButtons() { /* remove the selection buttons if that's possible */ let visCount = renderer.getVisibleCount(); let totalCount = renderer.getCount(); let hiddenCount = totalCount - visCount; if (hiddenCount===0) $("#tpShowAll").hide(); else $("#tpShowAll").show(); if (renderer.hasSelected()) { $(".tpSelectButton").show(); } else { $(".tpSelectButton").hide(); } } function buildSelectActions() { /* add buttons for hide selected / unselected to ribbon bar */ if (getById("tpHideSel")!==null) return; let htmls = []; htmls.push(''); htmls.push(''); htmls.push('   '); //htmls.push(''); getById('tpToolBar').insertAdjacentHTML('afterbegin', htmls.join("")); getById('tpHideSel').addEventListener('click', onHideSelClick); getById('tpOnlySel').addEventListener('click', onOnlySelClick); getById('tpShowAll').addEventListener('click', onShowAllClick); } function onSelChange(selection) { /* called each time when the selection has been changed */ var cellIds = []; selection.forEach(function(x) {cellIds.push(x)}); $("#tpSetBackground").parent("li").removeClass("disabled"); updateSelectionButtons(); if (cellIds.length===0 || cellIds===null) { clearMetaAndGene(); clearSelectionState(); $("#tpSetBackground").parent("li").addClass("disabled"); //clearSelectActions(); } else if (cellIds.length===1) { $("#tpHoverHint").hide(); $("#tpSelectHint").show(); var cellId = cellIds[0]; var cellCountBelow = cellIds.length-1; updateMetaBarCustomFields(cellId); db.loadMetaForCell(cellId, function(ci) { updateMetaBarOneCell(ci, cellCountBelow); }, onProgress); } else { $("#tpHoverHint").hide(); $("#tpSelectHint").show(); updateMetaBarManyCells(cellIds); } updateGeneTableColors(cellIds); if ("geneSym" in gLegend) buildViolinPlot(); //var cols = renderer.col.arr; //var selectedLegends = {}; //for (var i = 0; i < gLegend.rows.length; i++) { //selectedLegends[i] = 0; //} //selection.forEach(function(cellId) { //selectedLegends[cols[cellId]]++; //}); //for (var i = 0; i < gLegend.rows.length; i++) { //if (selectedLegends[i] == gLegend.rows[i].count) { //$("#tpLegendCheckbox_" + i).prop("checked", true); //} else { ////$("#tpLegendCheckbox_" + i).prop("checked", false); //} //} //updateLegendGrandCheckbox(); } function onRadiusAlphaChange(radius, alpha) { /* user changed alpha or radius value */ getById("tpSizeInput").value = radius; getById("tpAlphaInput").value = alpha; } function onSaveAsClick() { /* File - Save Image as ... */ var canvas = $("canvas")[0]; canvas.toBlob(function(blob) { saveAs( blob , "cellBrowser.png"); } , "image/png"); } function onSaveAsSvgClick() { /* File - Save Image as vector ... */ renderer.drawDots("svg") renderer.svgLabelWidth = 300; renderer.drawLegendSvg(gLegend) var lines = renderer.getSvgText() var blob = new Blob(lines, {type:"image/svg+xml"}); window.saveAs( blob , "cellBrowser.svg"); renderer.drawDots() } function onSelectAllClick() { /* Edit - select all visible*/ clearSelectionState(); renderer.selectClear(); renderer.selectVisible(); renderer.drawDots(); } function clearSelectionAndDraw() { /* do everything needed to clear the selection */ // clear checkboxes and colored highlight clearSelectionState(); renderer.selectClear(); renderer.drawDots(); } function onSelectNoneClick() { /* Edit - Select None */ clearSelectionAndDraw(); } function onSelectInvertClick() { /* Edit - Invert selection */ clearSelectionState(); renderer.selectInvert(); renderer.drawDots(); } function buildOneComboboxRow(htmls, comboWidth, rowIdx, queryExpr) { /* create one row of combobox elements in the select dialog */ htmls.push('
'); // or ❎ ? htmls.push('× '); // unicode: mult sign htmls.push(''); buildMetaFieldCombo(htmls, "tpSelectMetaComboBox_"+rowIdx, "tpSelectMetaCombo_"+rowIdx, 0); var id = "tpSelectGeneCombo_"+rowIdx; htmls.push(''); htmls.push(''); htmls.push(''); htmls.push(''); htmls.push(''); htmls.push('
'); // tpSelectRow_ htmls.push('

'); } 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; rowIdxy); } function lessThan(x, y) { return (x Name selection */ let title = "Annotate selected "+gSampleDesc+"s"; let htmls = []; htmls.push('

There are '+renderer.getSelection().length+' '+gSampleDesc+' in the current selection.

'); htmls.push('

Name of annotation field:
'); htmls.push('

'); htmls.push('

Annotate selected cells as:
'); htmls.push('

'); htmls.push('

Remove annotations later by clicking Tools > Remove all annotations.

'); var dlgHeight = 400; var dlgWidth = 800; var buttons = [ //"Close and remove all annotations" : function() { //db.getMetaFields().shift(); //rebuildMetaPanel(); //localStorage.removeItem(db.name+"|custom"); //resetCustomAnnotations(); //}, {text:"OK", click: function() { let fieldLabel = $('#tpFieldLabel').val(); if (fieldLabel==="") fieldLabel = "My custom annotations"; let newMetaValue = $("#tpMetaVal").val(); if (newMetaValue==="") return; addNewAnnotation(fieldLabel, newMetaValue, renderer.getSelection()); $( this ).dialog( "close" ); colorByMetaField("custom"); } }]; showDialogBox(htmls, title, {showClose:true, height:dlgHeight, width:dlgWidth, buttons:buttons}); $("#tpMetaVal").focus(); return true; } function onBackgroudSetClick() { // Tools -> Set cells as background if ($("#tpSetBackground").parent("li").hasClass("disabled")) { return; } background = renderer.getSelection(); $("#tpResetBackground").parent("li").removeClass("disabled"); if ("geneSym" in gLegend) buildViolinPlot(); } function onBackgroudResetClick() { // Tools -> Reset background cells background = null; $("#tpResetBackground").parent("li").addClass("disabled"); if ("geneSym" in gLegend) buildViolinPlot(); } function saveQueryList(queryList) { var queryStr = JSURL.stringify(queryList); changeUrl({'select':queryStr}); localStorage.setItem(db.name+"|select", queryStr); } function selectByQueryList(queryList) { /* select cells defined by query list, save to local storage and URL and redraw */ findCellsMatchingQueryList(queryList, function(cellIds) { if (cellIds.length===0) { alert("No matching "+gSampleDesc+"s."); } else { renderer.selectSet(cellIds); saveQueryList(queryList); } }); } function onFindCellsClick() { /* Edit - Find cells */ var dlgHeight = 400; var dlgWidth = 800; var buttons = [ //{ //text:"Cancel", //click:function() { //// save state even if user presses cancel - good idea? //var queryStr = JSURL.stringify(queryList); //var queryList = readSelectForm(); //localStorage.setItem(db.name+"|select", queryStr); //$(this).dialog("close"); //} //}, { text:"OK", click: function() { var queryList = readSelectForm(); selectByQueryList(queryList); $("#tpDialog").dialog("close"); renderer.drawDots(); } } ]; var htmls = []; // build from current query or create a sample query var queries = []; var queryStr = getVar("select"); if (queryStr===undefined) queryStr = localStorage.getItem(db.name+"|select"); if (queryStr===undefined || queryStr===null) queries = [makeSampleQuery()]; else { queries = JSURL.parse(queryStr); } var comboWidth = 250; var query; for (var i=0; i < queries.length; i++) { query = queries[i]; buildOneComboboxRow(htmls, comboWidth, i, query); } htmls.push(""); showDialogBox(htmls, "Find cells based on annotation or gene expression", {showClose:true, height:dlgHeight, width:dlgWidth, buttons:buttons}); for (i=0; i < queries.length; i++) { query = queries[i]; connectOneComboboxRow(comboWidth, i, query); } var rowIdx = queries.length+1; $('#tpSelectAddRowLink').click( function(ev) { var htmls = []; var rowIdx = $(".tpSelectRow").length; var newRowQuery = makeSampleQuery(); buildOneComboboxRow(htmls, comboWidth, rowIdx, newRowQuery); $(htmls.join("")).insertBefore("#tpSelectAddRowLink"); connectOneComboboxRow(comboWidth, rowIdx, newRowQuery); }); } function onMarkClick() { /* Edit - mark selection (with arrows) */ if (gCurrentDataset.markedCellIds===undefined) gCurrentDataset.markedCellIds = {}; var markedIds = gCurrentDataset.markedCellIds; var selIds = keys(gSelCellIds); if (selIds.length>100) { warn("You cannot mark more than 100 "+gSampleDesc+"s"); return; } for (var i = 0; i < selIds.length; i++) { var selId = selIds[i]; markedIds[selId] = true; } plotDots(); renderer.render(stage); } function onMarkClearClick() { gCurrentDataset.markedCellIds = {}; plotDots(); renderer.render(stage); } function cartSave(db) { /* save db.cart dataset long-term changes that may be shared with others, like colors, labels, annotations, etc * For now, save to localStorage and also to the URL. */ var datasetName = db.name; var data = db.cart; var key = "cart"; var fullKey = datasetName+"###"+key; var jsonStr = JSON.stringify(data); var comprStr = LZString.compress(jsonStr); var uriStr = LZString.compressToEncodedURIComponent(jsonStr); localStorage.setItem(fullKey, comprStr); var urlData = {}; if (isEmpty(data)) uriStr = null; urlData[key] = uriStr; changeUrl(urlData); console.log("Saving state: ", data); } function createMetaUiFields(db) { /* This function changes db.metaFields[fieldName], * it adds: .ui.shortLabels, .ui.longLabels, ui.palette and .ui.colors; * ui info fields hold the final data as shown in the ui, they're calculated when the cart is loaded. * apply changes like labels/color/etc stored the userMeta object to db.conf.metaFields. * Potentially clean up the changes and recreate the cart object. * Currently, this only does something for enum fields. * */ if (db.cart===undefined) db.cart = {}; var userMeta = db.cart; if (userMeta===null) alert("the 'cart' argument in the URL is invalid. Please remove cart=xxxx from the URL and reload"); var metaFields = db.conf.metaFields; for (var metaIdx = 0; metaIdx < metaFields.length; metaIdx++) { var metaInfo = metaFields[metaIdx]; var fieldChanges = userMeta[metaInfo.name] || {}; if (metaInfo.type!=="enum") { metaInfo.ui = {}; continue; } // create shortLabels var shortLabels = null; var oldCounts = metaInfo.valCounts; if (oldCounts) { shortLabels = []; for (var i = 0; i < oldCounts.length; i++) shortLabels.push(oldCounts[i][0]); var newLabels = fieldChanges.shortLabels; shortLabels = copyNonEmpty(newLabels, shortLabels); } // create the long labels var longLabels = []; if ("longLabels" in metaInfo) longLabels = cloneArray(metaInfo.longLabels); else longLabels = cloneArray(shortLabels); longLabels = copyNonEmpty(fieldChanges.longLabels, longLabels); // create the colors: configured colors override default colors and cart overrides those var colors = makeColorPalette(cDefQualPalette, metaInfo.valCounts.length); if ("colors" in metaInfo) copyNonNull(metaInfo.colors, colors); var newColors = fieldChanges.colors; colors = copyNonEmpty(newColors, colors); var ui = {}; ui.colors = newColors; ui.longLabels = longLabels; ui.shortLabels = shortLabels; metaInfo.ui = ui; } //var delFields = []; //var delAttrs = []; //if (metaInfo===null) { // field does not exist anymore //delFields.push(fieldName); //continue; //} // remove all fields and attributes that were found to be invalid in the current state // so we don't accumulate crap //var cleanedFields = []; //for (var i = 0; i < delAttrs.length; i++) { //var fieldName = delAttrs[i][0]; //var attrName = delAttrs[i][1]; //delete userMeta[fieldName][attrName]; //cleanedFields.push(fieldName); //} //for (var i = 0; i < delFields.length; i++) { //var fieldName = delFields[i]; //delete userMeta[fieldName]; //cleanedFields.push(fieldName); //} //if (delAttrs.length!==0) //warn("You had previously changed labels or colors or annotations but the dataset has been updated since then. "+ //"As a result, your annotations had to be removed. This concerned the following annotation fields: "+ //cleanedFields.join(", ")); } function cartFieldArrayUpdate(db, metaInfo, key, pos, val) { /* write val into db.cart[fieldName][key][pos], save the dataset cart and apply it. * db.cart[fieldName][key] is an array of arrLen * */ var cart = db.cart; var fieldName = metaInfo.name; var arrLen = metaInfo.valCounts.length; if (!(fieldName in cart)) cart[fieldName] = {}; // init array if (!(key in cart[fieldName])) { var emptyArr = []; for (var i=0; i Name selection "+ "to create a custom annotation field."); } else { resetCustomAnnotations(); } } function onRenameClustersClick() { /* Tools - Rename Clusters */ var htmls = []; htmls.push("

Change labels below. To keep the old name, leave the 'New Name' cell empty. You cannot modify the 'Orig. Name' column.

"); //htmls.push('

To rename a single cluster without this dialog: click onto it in the legend, then click its label.

'); htmls.push('
'); var title = "Rename Clusters"; var dlgHeight = window.innerHeight - 100; var dlgWidth = 650; var clusterField = db.conf.labelField; var data = []; var buttons = [ { text: "Empty All", click : function() { for (var i = 0; i < data.length; i++) { var row = data[i]; row["newName"] = ""; row["mouseOver"] = ""; row["color"] = ""; } grid.invalidate(); } } , { text: "OK", click: function() { Slick.GlobalEditorLock.commitCurrentEdit(); // save currently edited cell to data var shortLabels = []; var longLabels = []; var colors = []; for (var i = 0; i < data.length; i++) { var row = data[i]; if (row["newName"]===row["origName"]) shortLabels.push(""); else shortLabels.push(row["newName"]); longLabels.push(row["mouseOver"]); colors.push(row["color"]); } var fieldMeta = {}; if (!allEmpty(shortLabels)) fieldMeta["shortLabels"] = shortLabels; if (!allEmpty(longLabels)) fieldMeta["longLabels"] = longLabels; if (!allEmpty(colors)) fieldMeta["colors"] = colors; cartOverwrite(db, clusterField, fieldMeta); var metaInfo = db.findMetaInfo(clusterField); renderer.setLabels(metaInfo.ui.shortLabels); // only need to update the legend if the current field is shown if (gLegend.type==="meta" && gLegend.metaInfo.name===clusterField) { //var shortLabels = findMetaInfo(clusterField).ui.shortLabels; legendUpdateLabels(clusterField); buildLegendBar(); } $( this ).dialog( "close" ); renderer.drawDots(); } }, ]; showDialogBox(htmls, title, {showClose:true, height:dlgHeight, width:dlgWidth, buttons:buttons}); var columns = [ {id: "origName", width:100, maxWidth: 200, name: "Orig. Name", field: "origName"}, {id: "newName", width:150, maxWidth: 200, name: "New Name", field: "newName", editor: Slick.Editors.Text}, {id: "mouseOver", width:200, maxWidth: 300, name: "Mouseover Label", field: "mouseOver", editor: Slick.Editors.Text}, //{id: "color", width: 80, maxWidth: 120, name: "Color Code", field: "color", editor: Slick.Editors.Text} ]; var options = { editable: true, enableCellNavigation: true, enableColumnReorder: false, enableAddRow: true, //asyncEditorLoading: false, autoEdit: true }; var metaInfo = db.findMetaInfo(clusterField); var fieldChanges = db.cart[clusterField] || {}; var shortLabels = fieldChanges["shortLabels"]; var longLabels = fieldChanges["longLabels"]; var colors = fieldChanges["colors"]; for (var i = 0; i < metaInfo.valCounts.length; i++) { var shortLabel = ""; if (shortLabels) shortLabel = shortLabels[i]; var longLabel = ""; if (longLabels) longLabel = longLabels[i]; var color = ""; if (colors) color = colors[i]; var valCount = metaInfo.valCounts[i]; var valRow = { "origName":valCount[0], "newName": shortLabel, "mouseOver": longLabel, "color": color }; data.push(valRow); } var grid = new Slick.Grid("#tpGrid", data, columns, options); grid.setSelectionModel(new Slick.CellSelectionModel()); grid.onAddNewRow.subscribe(function (e, args) { var item = args.item; grid.invalidateRow(data.length); data.push(item); grid.updateRowCount(); grid.render(); }); } function selectCellsById(cellIds, hasWildcards, onDone) { /* create a new selection from the cells given the cell IDs, call onDone when ready with missing IDs. */ function onSearchDone(searchRes) { // little helper function var idxArr = searchRes[0]; var notFoundIds = searchRes[1]; renderer.selectSet(idxArr); renderer.drawDots(); if (cellIds.length===1) { let cellId = cellIds[0]; db.loadMetaForCell(cellId, function(ci) { updateMetaBarOneCell(ci, cellCountBelow); }, onProgress); } if (onDone) onDone(notFoundIds); } db.loadFindCellIds(cellIds, onSearchDone, onProgressConsole, hasWildcards); } function onSelectByIdClick() { /* Edit - Find cells by ID */ function onDone(notFoundIds) { if (notFoundIds.length!==0) { $('#tpNotFoundIds').text("Could not find these IDs: "+notFoundIds.join(", ")); $('#tpNotFoundHint').text("Please fix them and click the OK button to try again."); } else $( "#tpDialog" ).dialog( "close" ); } var dlgHeight = 500; var dlgWidth = 500; var htmls = []; var buttons = [ { text:"OK", click : function() { var idListStr = $("#tpIdList").val(); idListStr = idListStr.trim().replace(/\r\n/g,"\n"); var idList = idListStr.split("\n"); var re = new RegExp("\\*"); var hasWildcards = $("#tpHasWildcard")[0].checked; selectCellsById(idList, hasWildcards, onDone); } } ]; htmls.push("
"); htmls.push(" Allow RegEx search, e.g. enter '^TH' to find all IDs that
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.

"); 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+"

"); htmls.push("

Written by: Maximilian Haeussler, Nikolay Markov (U Northwestern), Brian Raney, Lucas Seninge

"); htmls.push("

Testing / User interface / Documentation / Data import / User support: Matt Speir, Brittney Wick

"); htmls.push("

Code contributions by: Pablo Moreno (EBI, UK)

"); htmls.push("

Documentation: Readthedocs

"); htmls.push("

Github Repo: cellBrowser

"); htmls.push("

Paper: Speir et al, Bioinformatics 2021, DOI:10.1093/bioinformatics/btab503/6318386

"); showDialogBox(htmls, title, {showClose:true, height:dlgHeight, width:500}); } function buildMenuBar() { /* draw the menubar at the top */ var htmls = []; htmls.push("
"); htmls.push(''); // navbar htmls.push('
'); // tpMenuBar $(document.body).append(htmls.join("")); $('#tpTransMenu li a').click( onTransClick ); $('#tpSizeMenu li a').click( onSizeClick ); //$('#tpFilterButton').click( onHideSelectedClick ); //$('#tpOnlySelectedButton').click( onShowOnlySelectedClick ); $('#tpZoom100Menu').click( onZoom100Click ); $('#tpSplitMenu').click( onSplitClick ); $('#tpHeatMenu').click( onHeatClick ); $('#tpZoomPlus').click( onZoomInClick ); $('#tpZoomMinus').click( onZoomOutClick ); //$('#tpShowAllButton').click( onShowAllClick ); $('#tpHideShowLabels').click( onHideShowLabelsClick ); $('#tpExportIds').click( onExportIdsClick ); $('#tpSelectById').click( onSelectByIdClick ); $('#tpMark').click( onMarkClick ); $('#tpMarkClear').click( onMarkClearClick ); $('#tpTutorialButton').click( function() { showIntro(false); } ); $('#tpAboutButton').click( onAboutClick ); $('#tpOpenDatasetLink').click( openCurrentDataset ); $('#tpSaveImage').click( onSaveAsClick ); $('#tpSaveImageSvg').click( onSaveAsSvgClick ); $('#tpSelectAll').click( onSelectAllClick ); $('#tpSelectNone').click( onSelectNoneClick ); $('#tpSelectInvert').click( onSelectInvertClick ); $('#tpSelectName').click( onSelectNameClick ); $('#tpSelectComplex').click( onFindCellsClick ); $('#tpRenameClusters').click( onRenameClustersClick ); $('#tpCustomAnnots').click( onCustomAnnotationsClick ); $('#tpSetBackground').click( onBackgroudSetClick ); $('#tpResetBackground').click( onBackgroudResetClick ); //$('#tpCluster').click( onRunClusteringClick ); // This version is more like OSX/Windows: // - menus only open when you click on them // - once you have clicked, they start to open on hover // - a click anywhere else will stop the hovering var doHover = false; $(".nav > .dropdown").click( function(){ doHover = !doHover; return true;} ); $(".nav > .dropdown").hover( function(event) { if (doHover) { $(".dropdown-submenu").removeClass("open"); $(".dropdown").removeClass("open"); $(this).addClass('open'); } }); $(document).click ( function() { doHover= false; }); // when user releases the mouse outside the canvas, remove the zooming marquee $(document).mouseup ( function(ev) { if (ev.target.nodeName!=="canvas") { renderer.resetMarquee(); }} ); $('[data-submenu]').submenupicker(); } function resizeDivs(skipRenderer) { /* resize all divs and the renderer to current window size */ var rendererLeft = metaBarWidth+metaBarMargin; var rendererHeight = window.innerHeight - menuBarHeight - toolBarHeight; var rendererWidth = window.innerWidth - legendBarWidth - rendererLeft; var legendBarLeft = rendererWidth+metaBarMargin+metaBarWidth; var heatWidth, heatHeight; if (db && db.heatmap) { heatWidth = rendererWidth; heatHeight = db.heatmap.height; rendererHeight = rendererHeight - heatHeight; db.heatmap.setSize(heatWidth, heatHeight); let heatTop = window.innerHeight - heatHeight; db.heatmap.div.style.top = heatTop+"px"; db.heatmap.draw(); } $("#tpToolBar").css("width", rendererWidth+"px"); $("#tpToolBar").css("height", toolBarHeight+"px"); $("#tpLeftSidebar").css("height", (window.innerHeight - menuBarHeight)+"px"); // when this is run the first time, these elements don't exist yet. // Note that the whole concept of forcing these DIVs to go up to the screen size is strange, but // I have not found a way in CSS to make them go to the end of the screen. They need to have a fixed size, // as otherwise the scroll bars of tpLegendBar and tpMetaPanel won't appear if ($('#tpMetaPanel').length!==0) $("#tpMetaPanel").css("height", (window.innerHeight - $('#tpMetaPanel').offset().top)+"px"); if ($('#tpLegendRows').length!==0) $("#tpLegendBar").css("height", (window.innerHeight - $('#tpLegendBar').offset().top)+"px"); $('#tpLegendBar').css('left', legendBarLeft+"px"); if (skipRenderer!==true) renderer.setSize(rendererWidth, rendererHeight, true); } var progressUrls = {}; function onProgressConsole(ev) { //console.log(ev); } function onProgress(ev) { /* update progress bars. The DOM elements of these were added in maxPlot (not optimal?) */ console.log(ev); if (ev.text!==undefined) { // image loaders just show a little watermark renderer.setWatermark(ev.text); return; } var url = null; var domEl = ev.currentTarget; if (domEl) url = domEl.responseURL; else url = ev.src; // when loading an image, we're getting the 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'); // lineEl.setAttribute("x1", 5); lineEl.setAttribute("y1", 5); lineEl.setAttribute("x2", 10); lineEl.setAttribute("y2", 5); lineEl.setAttribute("stroke", "black"); lineEl.setAttribute("stroke-width", "2"); let svgEl = getById("tpTraceSvg"); svgEl.textContent = ""; // remove all children svgEl.appendChild(lineEl); let trace = db.traces[cellId]; if (trace===undefined) { svgEl.innerHTML = 'No trace available. Most likely removed due to quality filters.'; return; } let traceWidth = parseInt(getById("tpTraceSvg").getAttribute("width")); let traceHeight = 100.0; let traceMin = 999999; let traceMax = -99999; console.log(trace); // TODO: inverting the trace is that the right thing here? var newTrace = []; for (let i=0; i < trace.length; i++) { newTrace.push(-trace[i]); } trace = newTrace; for (let i=0; i < trace.length; i++) { let val = trace[i]; traceMin = Math.min(traceMin, val); traceMax = Math.max(traceMax, val); } console.log("Min", traceMin, "Max", traceMax); let dataSpan = (traceMax-traceMin); let scaleFact = traceHeight / (dataSpan); let stepX = traceWidth / trace.length ; // number of pixels per point console.log("dataSpan", dataSpan, "scaleFact", scaleFact, "stepX", stepX); let pixYs = []; for (let i=0; i < trace.length; i++) { let val = trace[i]; let pixY = (val-traceMin)*scaleFact; pixYs.push(pixY); console.log(val, pixY); } let htmls = []; for (let i=0; i < trace.length-1; i++) { let x1 = Math.round(i*stepX); let x2 = Math.round((i+1)*stepX); let y1 = Math.round(pixYs[i]); let y2 = Math.round(pixYs[i+1]); htmls.push(""); } svgEl.innerHTML = htmls.join(""); } function buildTraceWindow() { /* called when the traces have been loaded */ renderer.setSize(renderer.getWidth(), renderer.height-traceHeight, true); let divEl = document.createElement('div'); divEl.style.position = "absolute"; divEl.style.left = metaBarWidth+"px"; // from plotHeatmap var canvLeft = metaBarWidth+metaBarMargin; var traceWidth = window.innerWidth - canvLeft - legendBarWidth; divEl.style.width = traceWidth+"px"; divEl.style.height = traceHeight+"px"; divEl.style.left = metaBarWidth+"px"; divEl.style.top = (menuBarHeight+toolBarHeight+renderer.height)+"px"; divEl.id = "tpTraceBar"; document.body.appendChild(divEl); divEl.innerHTML = "
Calcium Trace
"+ ''; let cellId = getVar("cell"); if (cellId) plotTrace(cellId); } function gotTraces(traces) { /* called when the traces have been loaded */ buildTraceWindow(); } function loadAndRenderData() { /* init the basic UI parts, the main renderer in the center, start loading and draw data when ready */ var forcePalName = getVar("pal", null); var loadsDone = 0; var selList = null; // search expression to select, in format accepted by findCellsMatchingQueryList() function doneOnePart() { /* make sure renderer only draws when both coords and other data have loaded */ loadsDone +=1; if (loadsDone===2) { buildLegendBar(); if (db.conf.labelField) setLabelField(getActiveLabelField()); if (forcePalName!==null) { legendChangePaletteAndRebuild(forcePalName); } else renderer.setColors(legendGetColors(gLegend.rows)); renderer.setTitle("Dataset: "+makeFullLabel(db)); if (selList) findCellsMatchingQueryList(selList, function (cellIds) { renderer.selectSet(cellIds); renderer.drawDots(); }); else renderer.drawDots(); // this requires coordinates to be loaded if (getVar("cell")!==undefined) { selectCellsById([getVar("cell")], false, null) } //if (db.conf.multiModal && db.conf.multiModal.splitPrefix) //renderer.split(); if (db.conf.split) { let splitOpts = db.conf.split; //configureRenderer(splitOpts[0]); //renderer.drawDots(); //buildWatermark(); + //buildWatermark(); activateSplit(); configureRenderer(splitOpts); + $("#splitJoinDiv").show(); + $("#splitJoinBox").prop("checked", true); //buildWatermark(); //renderer.drawDots(); changeUrl({"layout":null, "meta":null, "gene":null}); renderer.drawDots(); + } else { + $("#splitJoinDiv").hide(); } } } function guessRadiusAlpha(dotCount) { /* return reasonable radius and alpha values for a number of dots */ if (dotCount<3000) return [4, 0.7]; if (dotCount<6000) return [4, 0.6]; if (dotCount<10000) return [3, 0.5]; if (dotCount<35000) return [2, 0.3]; if (dotCount<80000) return [1, 0.5]; // everything else return [0, 0.3]; } function makeRendConf(dbConf, dotCount) { /* return the 'args' object for the renderer, based on dbConf and count of dots */ var rendConf = {}; var radius = dbConf.radius; var alpha = dbConf.alpha; if (radius===undefined || alpha===undefined) { var radiusAlpha = guessRadiusAlpha(dotCount); if (radius===undefined) radius = radiusAlpha[0]; if (alpha===undefined) alpha = radiusAlpha[1]; } rendConf["radius"] = radius; rendConf["alpha"] = alpha; rendConf["mode"] = "move"; // default mode, one of "move", "select" or "zoom" return rendConf; } var coordIdx = parseInt(getVar("layout", "0")) function gotFirstCoords(coords, info, clusterMids) { /* XX very ugly way to implement promises. Need to rewrite with promise() one day . */ gotCoords(coords, info, clusterMids); chosenSetValue("tpLayoutCombo", coordIdx); doneOnePart(); } var rendConf = makeRendConf(db.conf, db.conf.sampleCount); renderer.initPlot(rendConf); if (db.conf.showLabels===false || db.conf.labelField===undefined || db.conf.labelField===null) { renderer.setLabelField(null); } buildLeftSidebar(); buildToolBar(db.conf.coords, db.conf, metaBarWidth+metaBarMargin, menuBarHeight); buildSelectActions(); db.loadCoords(coordIdx, gotFirstCoords, gotSpatial, onProgress); if ("traces" in db.conf.fileVersions) db.loadTraces(gotTraces); // -- this should probably go into a new function handleVars() or something like that --- if (getVar("select")!==undefined) { selList = JSURL.parse(getVar("select")); } // -- end of handleVars()? if (db.conf.atacSearch) { // peak loading needs the gene -> peak assignment, as otherwise can't show any peaks on the left // so defer the coloring until all the peaks are loaded let onLocsDone = function() { colorByDefaultField(doneOnePart); }; db.loadGeneLocs(db.conf.atacSearch, db.conf.fileVersions.geneLocs, onLocsDone); } else // in gene mode, we can start coloring right away colorByDefaultField(doneOnePart); // pre-load the dataset description file, as the users will often go directly to the info dialog // and the following pre-loads risk blocking this load. var jsonUrl = cbUtil.joinPaths([db.conf.name, "desc.json"]) +"?"+db.conf.md5; fetch(jsonUrl); //if (db.conf.sampleCount < 50000) { if (db.conf.quickGenes) db.preloadGenes(db.conf.quickGenes, function() { updateGeneTableColors(null); if (getVar("heat")==="1") onHeatClick(); }, onProgressConsole, db.conf.binStrategy); db.preloadAllMeta(); //} } function onTransClick(ev) { /* user has clicked transparency menu entry */ var transText = ev.target.innerText; var transStr = transText.slice(0, -1); // remove last char var transFloat = 1.0 - (parseFloat(transStr)/100.0); transparency = transFloat; plotDots(); $("#tpTransMenu").children().removeClass("active"); $("#tpTrans"+transStr).addClass("active"); renderer.render(stage); } function legendSort(sortBy) { /* sort the legend by "name" or "count" */ var rows = gLegend.rows; if (sortBy==="name") { // index 2 is the label rows.sort(function(a, b) { return naturalSort(a.label, b.label); }); } else { // sort this list by count = index 3 rows.sort(function(a, b) { return b.count - a.count; }); // reverse-sort by count } buildLegendBar(); } //function filterCoordsAndUpdate(cellIds, mode) { /* hide/show currently selected cell IDs or "show all". Rebuild the legend and the coloring. */ //if (mode=="hide") //shownCoords = removeCellIds(shownCoords, cellIds); //else if (mode=="showOnly") //shownCoords = showOnlyCellIds(shownCoords, cellIds); //else //shownCoords = allCoords.slice(); //pixelCoords = scaleData(shownCoords); //makeLegendObject(); //buildLegendBar(); //gSelCellIds = {}; //plotDots(); //renderer.render(stage); //menuBarShow("#tpShowAllButton"); //} /* function onHideSelectedClick(ev) { user clicked the hide selected button filterCoordsAndUpdate(gSelCellIds, "hide"); menuBarHide("#tpFilterButton"); menuBarHide("#tpOnlySelectedButton"); menuBarShow("#tpShowAllButton"); ev.preventDefault(); } function onShowOnlySelectedClick(ev) { // user clicked the only selected button filterCoordsAndUpdate(gSelCellIds, "showOnly"); menuBarHide("#tpFilterButton"); menuBarHide("#tpOnlySelectedButton"); menuBarShow("#tpShowAllButton"); ev.preventDefault(); } function onShowAllClick(ev) { // user clicked the show all menu entry //gSelCellIds = {}; filterCoordsAndUpdate(gSelCellIds, "showAll"); shownCoords = allCoords.slice(); // complete copy of list, fastest in Blink pixelCoords = scaleData(shownCoords); makeLegendObject(); buildLegendBar(); gClasses = assignCellClasses(); plotDots(); menuBarHide("#tpFilterButton"); menuBarHide("#tpOnlySelectedButton"); menuBarHide("#tpShowAllButton"); gLegend.lastClicked = null; renderer.render(stage); } */ function onHideShowLabelsClick(ev) { /* user clicked the hide labels / show labels menu entry */ if ($("#tpHideMenuEntry").text()===SHOWLABELSNAME) { renderer.setShowLabels(true); $("#tpHideMenuEntry").text(HIDELABELSNAME); } else { renderer.setShowLabels(false); $("#tpHideMenuEntry").text(SHOWLABELSNAME); } renderer.drawDots(); } function onSizeClick(ev) { /* user clicked circle size menu entry */ var sizeText = ev.target.innerText; var sizeStr = sizeText.slice(0, 1); // keep only first char circleSize = parseInt(sizeStr); $("#tpSizeMenu").children().removeClass("active"); $("#tpSize"+circleSize).addClass("active"); plotDots(); renderer.render(stage); } function onZoom100Click(ev) { /* in addition to zooming (done by maxPlot already), reset the URL */ changeUrl({'zoom':null}); renderer.zoom100(); renderer.drawDots(); $("#tpZoom100Button").blur(); // remove focus ev.preventDefault(); return false; } function activateMode(modeName) { renderer.activateMode(modeName); } function onZoomOutClick(ev) { var zoomRange = renderer.zoomBy(0.8); pushZoomState(zoomRange); renderer.drawDots(); ev.preventDefault(); } function onZoomInClick(ev) { var zoomRange = renderer.zoomBy(1.2); pushZoomState(zoomRange); renderer.drawDots(); ev.preventDefault(); } function onWindowResize(ev) { /* called when window is resized by user */ if (ev.target.id==="tpHeat") // hack: do not do anything if jquery resizable() started this. return; resizeDivs(); } function onColorPaletteClick(ev) { /* called when users clicks a color palette */ var palName = ev.target.getAttribute("data-palette"); if (palName==="default") legendSetColors(gLegend, null) // reset the colors legendChangePaletteAndRebuild(palName); renderer.drawDots(); } function buildEmptyLegendBar(fromLeft, fromTop) { // create an empty right side legend bar var htmls = []; htmls.push("
"); htmls.push("
Legend
"); //htmls.push("
"); htmls.push("
"); htmls.push(""); htmls.push(''); htmls.push("
"); // btn-group //htmls.push("
"); // tpToolbarButtons htmls.push("
"); // tpSidebarHeader //htmls.push("
"); htmls.push("
"); htmls.push("
"); // content htmls.push("
"); // bar $(document.body).append(htmls.join("")); $(".tpColorLink").click( onColorPaletteClick ); } function getTextWidth(text, font) { // re-use canvas object for better performance // http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); var context = canvas.getContext("2d"); context.font = font; var metrics = context.measureText(text); return metrics.width; } function populateTable(table, rows, cells, content) { /* build table from DOM objects */ var is_func = (typeof content === 'function'); if (!table) table = document.createElement('table'); for (var i = 0; i < rows; i++) { var row = document.createElement('tr'); for (var j = 0; j < cells; j++) { row.appendChild(document.createElement('td')); var text = !is_func ? (content + '') : content(table, i, j); row.cells[j].appendChild(document.createTextNode(text)); } table.appendChild(row); } return table; } function legendGetColors(rows) { /* go over the legend lines: create an array of colors in the order of their meta value indexes. * (the values in the legend may be sorted not in the order of their internal indices) */ if (rows===undefined) return []; var colArr = []; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var col = row.color; if (col===null) col = row.defColor; // only use default color if nothing else set var idx = row.intKey; colArr[idx] = col; // 0 = color } return colArr; } function legendUpdateLabels(fieldName) { /* re-copy the labels into the legend rows */ // rows have attributes like these: defColor, currColor, label, count, valueIndex, uniqueKey var shortLabels = db.findMetaInfo(fieldName).ui.shortLabels; var rows = gLegend.rows; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var valIdx = row.intKey; var shortLabel = shortLabels[valIdx]; row.label = shortLabel; } } function legendLabelGetIntKey(legend, findLabel) { /* given a label, find the index in the legend */ let rows = legend.rows; for (let i = 0; i < rows.length; i++) { let row = rows[i]; if (row.label === findLabel) return row.intKey; } return null; } function legendRemoveManualColors(gLegend) { /* remove all manually defined colors from the legend */ // reset the legend object legendSetColors(gLegend, null, "color"); // reset the URL and local storage settings var rows = gLegend.rows; var urlChanges = {}; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var urlVar = COL_PREFIX+row.strKey; localStorage.removeItem(urlVar); urlChanges[urlVar] = null; } changeUrl(urlChanges); } function legendChangePaletteAndRebuild(palName, resetManual) { /* change the legend color palette and put it into the URL */ var success = legendSetPalette(gLegend, palName); if (success) { if (palName==="default") { legendRemoveManualColors(gLegend); changeUrl({"pal":null}); } else changeUrl({"pal":palName}); buildLegendBar(); var colors = legendGetColors(gLegend.rows); renderer.setColors(colors); } } function legendSetColors(legend, colors, keyName) { /* set the colors for all legend rows, keyName can be "color" or "defColor", depending on * whether the current row color or the row default color should be changed. * colors can also be null to reset all values to null. */ if (!keyName) keyName = "color"; var rows = legend.rows; for (let i = 0; i < rows.length; i++) { var colorVal = null; if (colors) colorVal = colors[i]; var legendRow = rows[i]; if ((legendRow.label == "0" && legend.type=="expr") || (likeEmptyString(legendRow.label) && legend.type=="meta")) colorVal = cNullColor; legendRow[keyName] = colorVal; } } function legendSetPalette(legend, origPalName) { /* update the defColor [1] attribute of all legend rows. pal is an array of hex colors. * Will use the predefined colors that are * in the legend.metaInfo.colors configuration, if present. * */ var palName = origPalName; if (origPalName==="default") { if (legend.rowType==="category") palName = datasetQualPalette; else palName = datasetGradPalette; } var rows = legend.rows; var n = rows.length; var pal = null; var usePredefined = false; pal = makeColorPalette(palName, n); // if this is a field for which colors were defined manually during the cbBuild, use them if (legend.metaInfo!==undefined && legend.metaInfo.colors!==undefined && origPalName==="default") { // the order of the color values in the metaInfo object is the same as the order of the order of the values in the // JSON file. But the legend has been sorted now, so we cannot just copy over the array as it is var rows = legend.rows; var predefColors = legend.metaInfo.colors; for (var i=0; i < rows.length; i++) { var origIndex = rows[i].intKey; var col = predefColors[origIndex]; if (col !== null) pal[i] = col; } usePredefined = true; } else pal = makeColorPalette(palName, n); if (pal===null) { alert("Sorry, palette '"+palName+"' does not have "+rows.length+" different colors"); return false; } legendSetColors(legend, pal, "defColor"); legend.palName = palName; // update the dropdown menu $('.tpColorLink').parent().removeClass("active"); // force the menu to the "defaults" entry if we're using predefined colors if (usePredefined) palName = "default"; $('.tpColorLink[data-palette="'+palName+'"]').parent().addClass("active"); return true; } function labelForBinMinMax(binMin, binMax, isAllInt) { /* given the min/max of a numeric value bin, return a good legend for it */ // pretty print the numbers var minDig = 2; //if (binMin % 1 === 0) // % 1 = fractional part //minDig = 0 var maxDig = 2; //if (binMin % 1 === 0) // maxDig = 0 if (isAllInt) { minDig = 0; maxDig = 0 } var legLabel = ""; if (binMax===0 && binMax===0) legLabel = "0"; else if (binMin==="Unknown") legLabel = "Unknown"; else if (binMin!==binMax) { if (Math.abs(binMin) > 1000000) binMin = binMin.toPrecision(4); if (Math.abs(binMax) > 1000000) binMax = binMax.toPrecision(4); if (typeof(binMin)=== 'number') binMin = binMin.toFixed(minDig); if (typeof(binMax)=== 'number') binMax = binMax.toFixed(minDig); legLabel = binMin+' – '+binMax; } else legLabel = binMin.toFixed(minDig); return legLabel; } function makeLegendRowsNumeric(binInfo) { /* return an array of legend lines given bin info from gene expression or a numeric meta field */ var legendRows = []; // figure out if all our ranges are integers var isAllInt = true; for (var binIdx = 0; binIdx < binInfo.length; binIdx++) { let oneBin = binInfo[binIdx]; var binMin = oneBin[0]; var binMax = oneBin[1]; var restMin = binMin - Math.trunc(binMin); var restMax = binMax - Math.trunc(binMax); if (restMin!==0 || restMax!==0) isAllInt = false; } var colIdx = 0; for (var binIdx = 0; binIdx < binInfo.length; binIdx++) { let oneBin = binInfo[binIdx]; var binMin = oneBin[0]; var binMax = oneBin[1]; var count = oneBin[2]; var legendId = binIdx; var legLabel = labelForBinMinMax(binMin, binMax, isAllInt); var uniqueKey = legLabel; // override any color with the color specified in the current URL var savKey = COL_PREFIX+legLabel; var legColor = getVar(savKey, null); if (binMin===0 && binMax===0) { uniqueKey = "noExpr"; legColor = cNullColor; } else if (binMin==="Unknown" && binMax==="Unknown") { uniqueKey = "noExpr"; legColor = cNullColor; } else colIdx++; legendRows.push( { "color": legColor, "defColor":null, "label":legLabel, "count":count, "intKey":binIdx, "strKey":uniqueKey }); } return legendRows; } function makeLegendExpr(geneSym, mouseOver, binInfo, exprVec, decExprVec) { /* build gLegend object for coloring by expression * return the colors as an array of hex codes */ activateTooltip("#tpGeneSym"); var legendRows = makeLegendRowsNumeric(binInfo); gLegend = {}; gLegend.type = "expr"; gLegend.rows = legendRows; var subTitle = null; if (db.isAtacMode()) { let peakCount = geneSym.split("+").length; if (peakCount===1) gLegend.title = "One ATAC peak"; else gLegend.title = ("Sum of "+geneSym.split("+").length) + " ATAC peaks"; } else { // make a best effort to find the gene sym and gene ID if (geneSym.indexOf("+")===-1) { var geneInfo = db.getGeneInfo(geneSym); geneSym = geneInfo.sym; subTitle = geneInfo.id; } else { subTitle = "Sum of "+geneSym.split("+").length+" genes"; } gLegend.title = getGeneLabel()+": "+geneSym; } gLegend.titleHover = mouseOver; gLegend.geneSym = geneSym; gLegend.subTitle = subTitle; gLegend.rowType = "range"; gLegend.exprVec = exprVec; // raw expression values, e.g. floats gLegend.decExprVec = decExprVec; // expression values as deciles, array of bytes gLegend.selectionDirection = "all"; var oldPal = getVar("pal", "default") legendSetPalette(gLegend, oldPal); var colors = legendGetColors(legendRows); return colors; } function alphaNumSearch(genes, saneSym) { /* use alpha-num-only search in gene list for saneSym */ for (var i=0; i does this pose a data protection issue? Need to document. Does it require opt-in ? */ var s = document.createElement( 'script' ); s.setAttribute( 'src', "https://cells.ucsc.edu/js/cbTrackUsage.js" ); s.async = true; document.body.appendChild( s ); } function onGeneClick (event) { /* user clicked on a gene in the gene table */ var locusId = event.target.getAttribute("data-geneId"); // the geneId of the gene var locusLabel = event.target.textContent; $('.tpMetaBox').removeClass('tpMetaSelect'); $('.tpGeneBarCell').removeClass("tpGeneBarCellSelected"); // XX TODO: How find all the elements with this ID? var saneId = onlyAlphaNum(locusId) $('#tpGeneBarCell_'+saneId).addClass("tpGeneBarCellSelected"); colorByLocus(locusId, null, locusLabel); event.stopPropagation(); } function showDialogBox(htmlLines, title, options) { /* show a dialog box with html in it */ $('#tpDialog').remove(); if (options===undefined) options = {}; var addStr = ""; if (options.width!==undefined) addStr = "max-width:"+options.width+"px;"; var maxHeight = $(window).height()-200; // unshift = insert at pos 0 //htmlLines.unshift(""); //htmls.push("
"); var buttons = [{text:"OK", click:onGeneDialogOkClick}]; showDialogBox(htmls, "Genes of interest", {height: 500, width:400, buttons:buttons}); $('#tpGeneDialogOk').click ( onGeneDialogOkClick ); } function onSetRadiusAlphaClick(ev) { let newRadius = parseFloat(getById('tpSizeInput').value); let newAlpha = parseFloat(getById('tpAlphaInput').value); renderer.setRadiusAlpha(newRadius, newAlpha); renderer.drawDots(); } function onGeneLoadComplete() { /* called when all gene expression vectors have been loaded */ console.log("All genes complete"); // Close the dialog box only if all genes were OK. The user needs to see the list of skipped genes if ( $( "#tpNotFoundGenes" ).length===0 ) { $("#tpDialog").dialog("close"); } var cellIds = getSortedCellIds(); // transform the data from tpLoadGeneExpr as gene -> list of values (one per cellId) to // newExprData cellId -> list of values (one per gene) // initialize an empty dict cellId = floats, one per gene var newExprData = {}; var geneCount = gLoad_geneList.length; var cellIdCount = cellIds.length; for (var i = 0; i < cellIdCount; i++) { let cellId = cellIds[i]; newExprData[cellId] = new Float32Array(gLoad_geneList); // XX or a = [] + a.length = x ? } // do the data transform var newGeneFields = []; var newDeciles = {}; for (var geneI = 0; geneI < gLoad_geneList.length; geneI++) { var geneSym = gLoad_geneList[geneI]; var geneInfo = gLoad_geneExpr[geneSym]; var geneId = geneInfo[0]; var geneVec = geneInfo[1]; var deciles = geneInfo[2]; newGeneFields.push( [geneSym, geneId] ); newDeciles[geneSym] = deciles; for (var cellI = 0; cellI < cellIdCount; cellI++) { let cellId = cellIds[cellI]; newExprData[cellId][geneI] = geneVec[cellI]; } } gLoad_geneList = null; gLoad_geneExpr = null; gCurrentDataset.preloadExpr = {}; gCurrentDataset.preloadExpr.genes = newGeneFields; gCurrentDataset.preloadExpr.cellExpr = newExprData; gCurrentDataset.preloadExpr.deciles = newDeciles; } function onGeneDialogOkClick(ev) { /* called the user clicks the OK button on the 'paste your genes' dialog box */ var genes = $('#tpGeneListBox').val().replace(/\r\n/g, "\n").split("\n"); $("#tpDialog").remove(); gLoad_geneList = []; gLoad_geneExpr = {}; var notFoundGenes = []; var baseUrl = gCurrentDataset.baseUrl; var url = cbUtil.joinPaths([baseUrl, "geneMatrix.tsv"]); var validCount = 0; // needed for progressbar later var matrixOffsets = gCurrentDataset.matrixOffsets; for (var i = 0; i < genes.length; i++) { var gene = genes[i]; if (gene.length===0) // skip empty lines continue; if (!(gene in matrixOffsets)) { notFoundGenes.push(gene); continue; } gLoad_geneList.push(gene); var offsetInfo = matrixOffsets[gene]; var start = offsetInfo[0]; var end = start+offsetInfo[1]; jQuery.ajax( { url: url, headers: { Range: "bytes="+start+"-"+end } , geneSymbol : gene, success: onReceiveExprLineProgress }); } var htmls = []; htmls.push("
Loading...
"); if (notFoundGenes.length!==0) { htmls.push("
Could not find the following gene symbols: "); htmls.push(notFoundGenes.join(", ")); htmls.push("
"); //htmls.push(""); //for (var i = 0; i < notFoundGenes.length; i++) { //htmls.push(notFoundGenes[i]); //} //htmls.push("

Could not find the following gene symbols:

"); //showDialogBox(htmls, "Warning", 400); } var showOk = (notFoundGenes.length!==0); showDialogBox(htmls, "Downloading expression data", {width:350, showOk:true}); var progressLabel = $( "#tpProgressLabel" ); $("#tpGeneProgress").progressbar( { value: false, max : gLoad_geneList.length }); } function shortenRange(s) { /* reformat atac range chr1|start|end to chr1:10Mbp */ var parts = s.split("|"); return parts[0]+":"+prettyNumber(parts[1]); } function htmlAddInfoIcon(htmls, helpText, placement) { /* add an info icon with some text to htmls */ var iconHtml = ''; var addAttrs = ""; if (placement!==undefined) addAttrs = " data-placement='"+placement+"'" htmls.push(" "+iconHtml+""); return htmls; } function buildGeneTable(htmls, divId, title, subtitle, geneInfos, noteStr, helpText) { /* create gene expression info table. if htmls is null, update DIV with divId in-place. * geneInfos is array of [gene, mouseover]. gene can be geneId+"|"+symbol. * You must run activateTooltip(".hasTooltip") after adding the htmls. * */ var doUpdate = false; if (htmls===null) { htmls = []; doUpdate = true; } var tableWidth = metaBarWidth; if (title) { htmls.push("
"); htmls.push("
"+title+"
"); if (helpText) { // https://fontawesome.com/icons/circle-info?s=solid htmls = htmlAddInfoIcon(htmls, helpText); } if (subtitle) { htmls.push('
'); htmls.push(subtitle); htmls.push('
'); } htmls.push("
"); // divId_title } if (doUpdate) { $('#'+divId).empty(); } htmls.push("
"); if (geneInfos===undefined || geneInfos===null || geneInfos.length===0) { - if (noteStr!==undefined) + if (noteStr!==undefined && noteStr!==null) htmls.push("
"+noteStr+"
"); htmls.push("
"); return; } var i = 0; while (i < geneInfos.length) { var geneInfo = geneInfos[i]; var geneIdOrSym = geneInfo[0]; var mouseOver = geneInfo[1]; // geneIdOrSym can be just the symbol (if we all we have is symbols) or geneId|symbol var internalId; var label; if (geneIdOrSym.indexOf("|")!==-1) { if (db.isAtacMode()) { label = shortenRange(geneIdOrSym); internalId = geneIdOrSym; //if (mouseOver!==undefined) { //label = mouseOver.split()[0]; //internalId = geneIdOrSym; //} else { // quickGene is a range in format chr|123123|125443 //label = shortenRange(geneIdOrSym); //internalId = geneIdOrSym; //} } else { var parts = geneIdOrSym.split("|"); internalId = parts[0]; label = parts[1]; } } else { internalId = geneIdOrSym; label = internalId; } if (mouseOver===undefined) mouseOver = internalId; htmls.push(''+label+''); i++; } htmls.push("
"); // divId if (doUpdate) { $('#'+divId).html(htmls.join("")); } } function resizeGeneTableDivs(tableId) { /* the size of the DIVs in the gene table depends on the size of the longest DIV in pixels and we know that only once the table is shown. so resize here now */ var tdEls = document.getElementById(tableId).querySelectorAll("span"); var maxWidth = 0; var totalWidth = 0; for (var el of tdEls) { maxWidth = Math.max ( maxWidth, el.offsetWidth+2 ); // 2 pixel borders totalWidth = totalWidth + el.offsetWidth; } // if we have less than one row, make the cells cover the whole row, but limit the total size a little if (totalWidth < (metaBarWidth-(tdEls.length*6))) // 6 pixels for the borders = 2 + 2 + 2 for the selection border. maxWidth = Math.min(70, Math.floor(metaBarWidth/tdEls.length)-6); for (var el of tdEls) { el.style.minWidth = maxWidth+"px"; } } function likeEmptyString(label) { /* some special values like "undefined" and empty string get colored in grey */ return (label===null || label.trim()==="" || label==="none" || label==="None" || label==="unknown" || label==="nd" || label==="n.d." || label==="Unknown" || label==="NaN" || label==="NA" || label==="undefined" || label==="Na"); } function numMetaToBinInfo(fieldInfo) { /* convert a numeric meta field info to look like gene expression info for the legend: * an array of [start, end, count] */ var binInfo = []; var binMethod = fieldInfo.binMethod; if (binMethod==="uniform") { // old method, not used anymore let binMin = fieldInfo.minVal; var stepSize = fieldInfo.stepSize; let binCounts = fieldInfo.binCounts; let binCount = fieldInfo.binCounts.length; for (var i=0; i is a special bin } else { palName = cDefQualPalette; colCount = metaInfo.valCounts.length; } var colors = makeColorPalette(palName, colCount); let colOverrides = metaInfo.ui.colors; if (colOverrides) for (let i=0; i MAXCOLORCOUNT) { warn("This field has "+metaInfo.diffValCount+" different values. Coloring on a "+ "field that has more than "+MAXCOLORCOUNT+" different values is not supported."); return null; } var metaCounts = metaInfo.valCounts; // array of [count, value] // we are going to sort this list later, so we need to keep track of what the original // index in the list was, as every meta data value is stored by its index, not // its label. Simply append the index as [2] of the metaCounts array. for (var valIdx=0; valIdx < metaCounts.length; valIdx++) metaCounts[valIdx].push(valIdx); var oldSortBy = getFromUrl("SORT"); // URL overrides default value if (sortBy===undefined && oldSortBy!==undefined) sortBy = oldSortBy; // default sorting can be specfied with "sortBy" in cellbrowser.conf if (sortBy===undefined && metaInfo.sortBy) sortBy = metaInfo.sortBy; if (sortBy!==undefined && sortBy!=="freq" && sortBy!=="name" && sortBy!=="none") { alert("sortBy is '"+cleanString(sortBy)+' but it can only be "freq" or "name"'); sortBy = undefined; } var fieldName = metaInfo.label; // force field names that look like "cluster" to a rainbow palette // even if they are numbers if (sortBy===undefined) { // should cluster fields be sorted by their name if (metaInfo.type==="float" || metaInfo.type==="int" || (metaCounts.length > 60)) // long lists are easier to grasp if they're sorted by name sortBy = "name"; else if (fieldName.indexOf("luster") || fieldName.indexOf("ouvain") || fieldName.indexOf("res.")) sortBy = "count"; else sortBy = "count"; } // sort like numbers if the strings are mostly numbers, otherwise sort like strings var sortResult = sortPairsBy(metaCounts, sortBy); var countListSorted = sortResult.list; var useGradient = (metaInfo.type==="float" || metaInfo.type==="int"); var rows = []; var shortLabels = metaInfo.ui.shortLabels; var longLabels = metaInfo.ui.longLabels; for (var legRowIdx = 0; legRowIdx < countListSorted.length; legRowIdx++) { var legRowInfo = countListSorted[legRowIdx]; let valIdx = legRowInfo[2]; // index of the original value in metaInfo.valCounts, before we sorted var label = shortLabels[valIdx]; var desc = null; if (longLabels) desc = longLabels[valIdx]; // first use the default palette, then try to get from URL var count = legRowInfo[1]; var uniqueKey = label; if (uniqueKey==="") uniqueKey = "_EMPTY_"; var color = null; // default any color that looks like "NA" or "undefined" to grey if (likeEmptyString(label)) color = cNullColor; // override any color with the color specified in the current URL var savKey = COL_PREFIX+uniqueKey; color = getFromUrl(savKey, color); rows.push( { "color": color, "defColor": color, "label": label, "count": count, "intKey":valIdx, "strKey":uniqueKey, "longLabel" : desc, } ); } legend.rows = rows; legend.isSortedByName = sortResult.isSortedByName; legend.rowType = "category"; legend.selectionDirection = "all"; legendSetPalette(legend, "default"); return legend; } function legendSetTitle(label) { $('#tpLegendTitle').text(label); } function buildLegendForMeta(metaInfo) { /* build the gLegend for a meta field */ var legend = makeLegendMeta(metaInfo); if (legend===null) return; var metaIdx = db.fieldNameToIndex(metaInfo.name); $('.tpMetaBox').removeClass('tpMetaSelect'); $('.tpMetaValue').removeClass('tpMetaValueSelect'); $('#tpMetaBox_'+metaIdx).addClass('tpMetaSelect'); $('#tpMeta_'+metaIdx).addClass('tpMetaValueSelect'); $('.tpGeneBarCell').removeClass('tpGeneBarCellSelected'); //$('#tpLegendTitle').text(legend.metaInfo.label.replace(/_/g, " ")); legendSetTitle(legend.metaInfo.label.replace(/_/g, " ")); return legend; } function onMetaClick (event) { /* called when user clicks a meta data field or label */ var fieldName = event.target.dataset.fieldName; if (isNaN(fieldName)) { // try up one level in the DOM tree, in case the user clicked the little child div in the meta list fieldName = event.target.parentElement.dataset.fieldName; } setColorByDropdown(fieldName); colorByMetaField(fieldName); } function addMetaTipBar(htmls, valFrac, valStr, valFracCategory) { /* add another bar to a simple histogram built from divs */ htmls.push("
 "); htmls.push("
"+(100*valFrac).toFixed(1)+"%
"); htmls.push("
"+valStr); if (valFracCategory !== undefined) { htmls.push(" (" + (100 * valFracCategory).toFixed(1) + "% of all cells with this value)"); } htmls.push("
"); //htmls.push(""+valCount+""); var pxSize = (valFrac * metaTipWidth).toFixed(0); htmls.push("
 
"); htmls.push("
"); } function binInfoToValCounts(binInfo) { /* given an array of (start, end, count), return an array of (label, count) */ var valCounts = []; for (var binIdx = 0; binIdx < binInfo.length; binIdx++) { var binMin = binInfo[binIdx][0]; var binMax = binInfo[binIdx][1]; var count = binInfo[binIdx][2]; var label = labelForBinMinMax(binMin, binMax); valCounts.push( [label, count] ); } return valCounts; } function buildMetaTip(metaInfo, valHist, htmls) { /* build the content of the tooltip that summarizes the multi selection */ var valCounts = metaInfo.valCounts; var shortLabels = metaInfo.ui.shortLabels; if (valCounts===undefined) // for client-side discretized fields, we have to discretize first valCounts = binInfoToValCounts(metaInfo.binInfo); var otherCount = 0; var totalSum = 0; for (var i = 0; i < valHist.length; i++) { var valInfo = valHist[i]; var valCount = valInfo[0]; var valFrac = valInfo[1]; var valIdx = valInfo[2]; var valFracCategory = valInfo[3]; //var valStr = valCounts[valIdx][0]; // 0 = label, 1 = count var label = shortLabels[valIdx]; totalSum += valCount; // only show the top values, summarize everything else into "other" if (i > HISTOCOUNT) { otherCount += valCount; continue; } if (label==="") label = ""+stringEmptyLabel("")+""; addMetaTipBar(htmls, valFrac, label, valFracCategory); } if (otherCount!==0) { var otherFrac = (otherCount / totalSum); addMetaTipBar(htmls, otherFrac, "(other)"); } return htmls; } function metaInfoFromElement(target) { /* get the metaInfo object given a DOM element */ if (target.dataset.fieldName === undefined) target = target.parentNode; if (target.dataset.fieldName === undefined) target = target.parentNode; var fieldName = target.dataset.fieldName; var metaInfo = db.findMetaInfo(fieldName); return metaInfo; } function onSelectSameLinkClick (ev) { /* user clicks the "select same" link in the meta bar */ let parent = ev.target.parentElement; let metaIdx = parent.id.split("_")[1]; let metaBarField = gMeta.rows[metaIdx]; findCellsMatchingQueryList([{"m":metaBarField.field, "eq":metaBarField.value}], function(cellIds) { colorByMetaField(metaBarField.field); renderer.selectSet(cellIds); renderer.drawDots(); } ); } function onMetaMouseOver (event) { /* called when user hovers over meta element: shows the histogram of selected cells */ var metaHist = db.metaHist; // mouseover over spans or divs will not find the id, so look at their parent, which is the main DIV var target = event.target; var metaInfo = metaInfoFromElement(target); var fieldName = metaInfo.name; // change style of this field a little var metaSel = "#tpMetaBox_"+metaInfo.index; $(metaSel).addClass("tpMetaHover"); $(metaSel+" .tpMetaValue").addClass("tpMetaHover"); $(".tpSameLink").remove(); if (gMeta.mode==="single" && metaInfo.type==="enum" && renderer.getSelection().length==1) { $("#tpMeta_"+metaInfo.index).append(""); $('.tpSameLink').on("click", onSelectSameLinkClick); } if (metaHist===undefined || metaHist===null) return; var htmls = []; if (metaInfo.type==="uniqueString") htmls.push("
Cannot summarize: this is a field with unique values
"); else htmls = buildMetaTip(metaInfo, metaHist[fieldName], htmls); $('#tpMetaTip').html(htmls.join("")); // make sure that tooltip doesn't go outside of screen //var tipTop = event.target.offsetTop; var tipTop = event.target.getBoundingClientRect().top-8; var tipHeight = $('#tpMetaTip').height(); var screenHeight = $(window).height(); if (tipTop+tipHeight > screenHeight) tipTop = screenHeight - tipHeight - 8; $('#tpMetaTip').css({top: tipTop+"px", left: metaBarWidth+"px", width:metaTipWidth+"px"}); $('#tpMetaTip').show(); activateTooltip(".tpSameLink"); } function buildComboBox(htmls, id, entries, selIdx, placeholder, width, opts) { /* make html for a combo box and add lines to htmls list. * selIdx is an array of values if opt.multi exists, otherwise it's an int or 'undefined' if none. */ let addStr = ""; if (opts && opts.multi) addStr= " multiple"; htmls.push(''); } function loadCoordSet(coordIdx, labelFieldName) { /* load coordinates and color by meta data */ var newRadius = db.conf.coords[coordIdx].radius; var colorOnMetaField = db.conf.coords[coordIdx].colorOnMeta; renderer.background = null; // remove the background image db.loadCoords(coordIdx, function(coords, info, clusterMids) { gotCoords(coords,info,clusterMids, newRadius); setLabelField(labelFieldName); if (colorOnMetaField!==undefined) { setColorByDropdown(colorOnMetaField); colorByMetaField(colorOnMetaField, undefined); } else renderer.drawDots(); }, gotSpatial, onProgress); } function changeLayout(coordIdx, doNotUpdateUrl) { /* activate a set of coordinates, given the index of a coordinate set */ var labelFieldName = null; var labelFieldVal = $("#tpLabelCombo").val(); if (labelFieldVal!==null) { var labelFieldToken = $("#tpLabelCombo").val().split("_")[1]; if (labelFieldToken!=="none") { var labelFieldIdx = parseInt(labelFieldToken); labelFieldName = db.getMetaFields()[labelFieldIdx].name; } } loadCoordSet(coordIdx, labelFieldName); changeUrl({"layout":coordIdx, "zoom":null}); } function changeLayoutByName(coordName) { /* activate a set of coordinates, given the shortLabel of a coordinate set */ if (coordName===undefined) return; let coordIdx = db.findCoordIdx(coordName); if (coordIdx===undefined) alert("Coordinateset with name "+coordName+" does not exist"); else changeLayout(coordIdx); } function configureRenderer(opts) { /* given an obj with .coords, .meta or .gene, configure the current renderer */ if (opts.coords) changeLayoutByName(opts.coords); if (opts.gene) colorByLocus(opts.gene); if (opts.meta) colorByMetaField(opts.meta); if (opts.labelField) setLabelField(opts.labelField); } function onLayoutChange(ev, params) { /* user changed the layout in the combobox */ var coordIdx = parseInt(params.selected); changeLayout(coordIdx); // remove the focus from the combo box removeFocus(); } function onGeneComboChange(ev) { /* user changed the gene in the combobox */ var geneId = ev.target.value; if (geneId==="") return; // do nothing if user just deleted the current gene if (db.conf.atacSearch) { updatePeakListWithGene(geneId); } else { // in the normal, gene-matrix mode. var locusStr = null; var geneInfo = db.getGeneInfo(geneId); colorByLocus(geneInfo.id); } } function onMetaComboChange(ev, choice) { /* called when user changes the meta field combo box */ //if (choice.selected==="_none") var fieldId = parseInt(choice.selected.split("_")[1]); var fieldName = db.getMetaFields()[fieldId].name; console.log(choice); console.log(ev); colorByMetaField(fieldName); } function onLabelComboChange(ev, choice) { /* called when user changes the label field combo box */ var fieldLabel = choice.selected.split("_")[1]; if (fieldLabel==="none") { setLabelField(null); // = switch off labels changeUrl({"label":null}); } else { var fieldIdx = parseInt(fieldLabel); var fieldName = db.getMetaFields()[fieldIdx].name; setLabelField(fieldName); changeUrl({"label":fieldName}); } renderer.drawDots(); } function showCollectionDialog(collName) { /* load collection with given name and open dialog box for it */ loadCollectionInfo(collName, function(collData) { openDatasetDialog(collData)}); } function onConfigLoaded(datasetName) { /* dataset config JSON is loaded -> build the entire user interface */ // this is a collection if it does not have any field information if (db.conf.sampleDesc) gSampleDesc = db.conf.sampleDesc; else gSampleDesc = "cell"; // allow config to override the default palettes datasetGradPalette = cDefGradPalette; datasetQualPalette = cDefQualPalette; if (db.conf.defQuantPal) datasetGradPalette = db.conf.defQuantPal; if (db.conf.defCatPal) datasetQualPalette = db.conf.defCatPal; if (db.conf.metaBarWidth) metaBarWidth = db.conf.metaBarWidth; else metaBarWidth = 250; renderer.setPos(null, metaBarWidth+metaBarMargin); if (!db.conf.metaFields) { // pablo often has single-dataset installations, there is no need to open the // dataset selection box then. if (db.conf.datasets && db.conf.datasets.length===1 && datasetName==="") // "" is the root dataset loadDataset(db.conf.datasets[0].name, false); else showCollectionDialog(datasetName); return; } let binData = localStorage.getItem(db.name+"|custom"); if (binData) { let jsonStr = LZString.decompress(binData); let customMeta = JSON.parse(jsonStr); db.conf.metaFields.unshift(customMeta); } cartLoad(db); if (getVar("exprGene")) { // show the gene expression viewer buildExprViewWindow() } else { // show the UMAP view let showTutorial = true; loadAndRenderData(); resizeDivs(true); // special URL argument allows to force the info dialog to open if (getVar("openDialog")) { openDatasetDialog(db.conf, db.name); // open Info dialog showTutorial = false; } cartSave(db); // = set the current URL from local storage settings // start the tutorial after a while var introShownBefore = localStorage.getItem("introShown"); if (introShownBefore===undefined || introShownBefore===null) setTimeout(function(){ showIntro(true); }, 3000); // shown after 5 secs } } function loadDataset(datasetName, resetVars, md5) { /* load a dataset and optionally reset all the URL variables. * When a dataset is opened through the UI, the variables have to * be reset, as their values (gene or meta data) may not exist * there. If it's opened via a URL, the variables must stay. */ gRecentGenes = []; // collections are not real datasets, so ask user which one they want if (db!==null && db.heatmap) removeHeatmap(); removeSplit(renderer); db = new CbDbFile(datasetName); cellbrowser.db = db; // easier debugging var vars; if (resetVars) vars = {}; if (datasetName!=="") changeUrl({"ds":datasetName.replace(/\//g, " ")}, vars); // + is easier to type than %23 db.loadConfig(onConfigLoaded, md5); trackEvent("open_dataset", datasetName); trackEventObj("select_content", {content_type: "dataset", item_id: datasetName}); } function loadCollectionInfo(collName, onDone) { /* load collection info and run onDone */ var jsonUrl = cbUtil.joinPaths([collName, "dataset.json"]); cbUtil.loadJson(jsonUrl, onDone); } function onDatasetChange(ev, params) { /* user changed the dataset in the collection dropdown box */ /* jshint validthis: true */ $(this).blur(); removeFocus(); var parts = params.selected.split("?"); var datasetName = parts[0]; var md5 = parts[1]; loadDataset(datasetName, true, md5); } function buildLayoutCombo(coordLabel, htmls, files, id, left, top) { /* files is a list of elements with a shortLabel attribute. Build combobox for them. */ if (!coordLabel) coordLabel = "Layout"; //htmls.push('