8d551f7d3103d3972b7314e27caa2b32d7aa8df8
max
  Thu Oct 23 09:17:05 2025 -0700
dont show color pickers when the field has too many values, small perf improvement, for dev-whole-brain-hqm&meta=Initial_Class_markers_level_2, brittney

diff --git src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
index 5706a1d..3d75a5c 100644
--- src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
+++ src/cbPyLib/cellbrowser/cbWeb/js/cellBrowser.js
@@ -76,52 +76,55 @@
     // 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 nanColor = "DDDDDD"; // light grey
+    //const nanColor = "DDDDDD"; // light grey
 
     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 MAXCOLORCOUNT = 7000;
     const MAXLABELCOUNT = 500;
 
+    // after this number of rows in the legend, no more color pickers are shown
+    const MAXCOLPICK = 300;
+
     // 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
@@ -620,30 +623,31 @@
           $( "#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",
+        "hubUrl" : "UCSC Genome Browser",
         "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",
         "cap_project" : "Cell Annotation Platform",
         "arrayexpress" : "ArrayExpress",
         "ena_project" : "European Nucleotide Archive",
@@ -688,30 +692,35 @@
         // 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 (key==="hubUrl") {
+                url = makeHubUrl(null);
+                urlLabel = "Connect Track Hub";
+            }
+
             if (!url.startsWith("http"))
                 url = descUrls[key]+url;
 
             let parts = []
             parts.push("<a target=_blank href='");
             parts.push(url);
             parts.push("'>");
             parts.push(urlLabel);
             parts.push("</a>");
             let htmlLine = parts.join("");
             frags.push(htmlLine);
         }
         htmls.push(frags.join(", "));
         htmls.push("<br>");
     }
@@ -998,30 +1007,31 @@
 
         if (desc.lab) {
             htmls.push("<b>Lab: </b> "+desc.lab);
             htmls.push("<br>");
         }
         if (desc.institution) {
             htmls.push("<b>Institution: </b> "+desc.institution);
             htmls.push("<br>");
         }
 
 
         htmlAddLink(htmls, desc, "cbDoi");
         htmlAddLink(htmls, desc, "biorxiv_url");
         htmlAddLink(htmls, desc, "paper_url");
         htmlAddLink(htmls, desc, "other_url");
+        htmlAddLink(htmls, db.conf, "hubUrl");
         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, "cap_project");
         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");
 
@@ -1077,30 +1087,31 @@
             console.log(datasetInfo);
 
             if ( datasetInfo.atacSearch) {
                     htmls.push("<b>ATAC-seq search gene models: </b>" + datasetInfo.atacSearch);
                     htmls.push("<br>");
             }
 
             htmls.push("<b>Dataset classification: </b>");
 
             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);
+            buildClassification(htmls, datasetInfo, "assay", "Assay", false);
 
             htmls.push("<p style='padding-top: 8px'>If you use the Cell Browser of this dataset, please cite the " +
                     "original publication and " +
                     "<a href='https://academic.oup.com/bioinformatics/article/37/23/4578/6318386' target=_blank>" +
                     "Speir et al. 2021</a>. Feedback? Email us at <a href='cells@ucsc.edu' " +
                     "target='_blank'>cells@ucsc.edu</a>."+
                     "</p>");
 
             htmls.push("<p style='padding-top: 8px'><small>Cell Browser dataset ID: "+datasetInfo.name+
                     "</small></p>");
 
             }
         }
 
         $( "#pane1" ).html(htmls.join(""));
@@ -1147,40 +1158,42 @@
 
             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 assayStr = getFacetString(dataset, "assays");
             let sourceStr = getFacetString(dataset, "sources");
 
             var line = "<a id='tpDatasetButton_"+i+"' "+
                 "data-body='"+bodyPartStr+"' "+
                 "data-dis='"+disStr+"' "+
                 "data-org='"+orgStr+"' "+
                 "data-proj='"+projStr+"' "+
                 "data-dom='"+domStr+"' "+
                 "data-source='"+sourceStr+"' "+
                 "data-stage='"+lifeStr+"' "+
+                "data-assay='"+assayStr+"' "+
                 "role='button' class='tpListItem list-group-item "+clickClass+"' data-datasetid='"+i+"'>"; // bootstrap seems to remove the id
             htmls.push(line);
 
             if (!dataset.isSummary)
                 htmls.push('<button type="button" class="btn btn-primary btn-xs load-dataset" data-placement="bottom">Open</button>');
 
             if (dataset.sampleCount!==undefined) {
                 var countDesc = prettyNumber(dataset.sampleCount);
                 htmls.push("<span class='badge' style='background-color: #888'>"+countDesc+"</span>");
             }
 
             if (dataset.datasetCount!==undefined) {
                 htmls.push("<span class='badge' style='background-color: #28a745'>"+dataset.datasetCount+" datasets</span>");
             }
 
@@ -1378,30 +1391,32 @@
         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=="tpAssayCombo")
+                param = "assay";
             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;
@@ -1422,55 +1437,58 @@
             noteLines.push( "<p>The collection '"+openDsInfo.shortLabel+"' contains "+dsCount+" datasets. " +
                 "Double-click or click 'Open' below.<br>To move between datasets later in the cell browser, " +
                 "use the 'Collection' dropdown. </p>");
 
             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 assays = 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");
+            assays = getDatasetAttrs(openDsInfo.datasets, "assays");
             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("<div style='margin-right: 10px; font-weight: bold'>Filters: <span id='tpDatasetCount'></span></div>");
 
                 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("<div style='height:4px'></div>");
                 buildFilter(noteLines, lifeStages, "Life Stages", "stage", "tpStageCombo", "select stage...");
                 buildFilter(noteLines, domains, "Scient. Domain", "dom", "tpDomCombo", "select domain...");
+                buildFilter(noteLines, assays, "Assay", "assay", "tpAssayCombo", "select assay...");
                 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<parents.length; i++) {
                 let parentInfo = parents[i];
                 let parName = parentInfo[0];
@@ -1577,30 +1595,31 @@
         // 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(domains, "tpAssayCombo");
             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();
@@ -3094,31 +3113,30 @@
                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
@@ -3501,42 +3519,54 @@
             // update the "recent genes" div
             for (var i = 0; i < gRecentGenes.length; i++) {
                 // remove previous gene entry with the same symbol
                 var recIntId = gRecentGenes[i][0];
                 if (!db.isAtacMode())
                     recIntId = recIntId.split("|")[0]; // only keep geneId for comparisons
                 if (recIntId===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()) {
+                for (let qg of db.conf.quickGenes) {
+                    if (qg[0]===locusStr) {
+                        geneDesc = qg[1];
+                        //selectizeSetValue(geneDesc);
+                        let geneId = db.mustFindOneGeneExact(geneDesc);
+                        updatePeakListWithGene(geneId);
+                        updatePeakListCheckboxes(locusStr);
+                    }
+                }
                 //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";
                 }
             }
 
+            if (db.isAtacMode()) {
+                selectizeSetValue(geneDesc)
+            }
             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);
 
     }
@@ -3982,40 +4012,41 @@
     }
 
     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" */
+        /* sort the legend by "name" or "count" or "orderKey"*/
         var rows = gLegend.rows;
 
         if (sortBy==="name") {
             // index 2 is the label
             rows.sort(function(a, b) { return naturalSort(a.label, b.label); });
-        }
-        else {
+        } else if (sortBy==="count") {
             // sort this list by count = index 3
             rows.sort(function(a, b) { return b.count - a.count; }); // reverse-sort by count
+        } else {
+            rows.sort(function(a, b) { return a.orderKey - b.orderKey; }); // sort by order key
         }
         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);
 
@@ -4277,80 +4308,75 @@
         }
     }
 
     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;
         var palIdx = 0;
         var hasNan = false;
         for (let i = 0; i < rows.length; i++) {
             var colorVal = null;
             var legendRow = rows[i];
+            // any meta legend with a "special" value, so 0 or empty gets grey
             if ((i==0 && legendRow.label == "0" && legend.type=="expr" && !hasNan) || 
                 (likeEmptyString(legendRow.label) && legend.type=="meta")) {
                 colorVal = cNullColor;
-            } else if ((legendRow.label == "-12345.00" && legend.type=="expr")) {
-                legendRow.label = "NaN";
+            // gene expression diagrams: grey if NA
+            } else if ((legendRow.label == "NaN" && legend.type=="expr")) {
+                //legendRow.label = "NaN";
                 colorVal = cNullColor;
                 hasNan = true;
             } else if (colors) {
                 colorVal = colors[palIdx];
                 palIdx++;
             }
 
             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;
 
         // the number of colors needed is not the number of legend rows, because some values
         // do not get a color from the palette, e.g. "Unknown" and "0" rows
         //var n = rows.length;
-        var n = legend.rows.length;
-        var hasNan = false;
-        var hasZero = false;
+        var n = rows.length;
+        var hasSpecialBin = false;
         for (var row of rows) {
-            if (row.strKey==="noExpr")
-                hasZero = true;
-            if (row.strKey=="nan")
-                hasNan=true;
+            if (row.strKey==="noExpr" || row.strKey=="NaN")
+                hasSpecialBin = true;
         }
 
-        if (hasZero)
-            n--;
-        if (hasNan)
+        if (hasSpecialBin)
             n--;
-        if (hasZero && hasNan)
-            n++;
 
         var pal = null;
         var usePredefined = false;
 
         // 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
             pal = makeColorPalette(palName, rows.length);
             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)
@@ -4387,30 +4413,32 @@
         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==="NaN")
+            legLabel = "NaN";
         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+' &ndash; '+binMax;
         }
         else
             legLabel = binMin.toFixed(minDig);
         return legLabel;
@@ -4740,32 +4768,37 @@
         $("#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 humanizeRange(s) {
         /* reformat atac range chr1|start|end to chr1:xxx-yyy */
-        var parts = s.split("|");
-        return parts[0]+":"+parts[1]+"-"+parts[2];
+        let ranges = s.split("+");
+        let humanRanges = [];
+        for (let oneRange of ranges) {
+            var parts = oneRange.split("|");
+            humanRanges.push( parts[0]+":"+parts[1]+"-"+parts[2] );
+        }
+        return humanRanges.join(" and ");
     }
 
     function htmlAddInfoIcon(htmls, helpText, placement) {
         /* add an info icon with some text to htmls */
         var iconHtml = '<svg style="width:0.9em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 128c17.67 0 32 14.33 32 32c0 17.67-14.33 32-32 32S224 177.7 224 160C224 142.3 238.3 128 256 128zM296 384h-80C202.8 384 192 373.3 192 360s10.75-24 24-24h16v-64H224c-13.25 0-24-10.75-24-24S210.8 224 224 224h32c13.25 0 24 10.75 24 24v88h16c13.25 0 24 10.75 24 24S309.3 384 296 384z"/></svg>';
         var addAttrs = "";
         if (placement!==undefined)
             addAttrs = " data-placement='"+placement+"'"
         htmls.push("<span class='hasTooltip' title='"+helpText+"'"+addAttrs+">&nbsp;"+iconHtml+"</span>");
         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. 
@@ -4809,33 +4842,35 @@
         }
 
         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)
+                    if (mouseOver) {
+                        // inverse label <> mouseover for ATAC, much easier to read
+                        label = mouseOver;
+                    }
                     mouseOver = humanizeRange(geneIdOrSym);
-                        
                 } else {
                     var parts = geneIdOrSym.split("|");
                     internalId = parts[0];
                     label = parts[1];
                 }
             } else {
                 internalId = geneIdOrSym;
                 label = internalId;
             }
 
             if (mouseOver===undefined)
                 mouseOver = internalId;
 
             htmls.push('<span title="'+mouseOver+'" style="width: fit-content;" data-geneId="'+internalId+'" id="tpGeneBarCell_'+onlyAlphaNum(internalId)+'" class="hasTooltip tpGeneBarCell">'+label+'</span>');
             i++;
@@ -5040,30 +5075,31 @@
             // 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,
+                "orderKey" : 0,
             } );
         }
 
         legend.rows = rows;
         legend.isSortedByName = sortResult.isSortedByName;
         legend.rowType = "category";
         legend.selectionDirection = "all";
         legendSetPalette(legend, "default");
         return legend;
     }
 
     function legendSetTitle(label) {
         $('#tpLegendTitle').text(label);
     }
 
@@ -5411,30 +5447,33 @@
         // 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);
 
+        var hubUrl = makeHubUrl(null);
+        $('#tpOpenGenome').attr("href", hubUrl);
+
         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);
@@ -5774,31 +5813,31 @@
         if (foundRanges.length === 0) {
             htmls.push("No peaks around this gene");
         }
         else
             for (let rangeInfo of foundRanges) {
                 let foundStart = rangeInfo[0];
                 let foundEnd = rangeInfo[1];
                 //let label = chrom+":"+foundStart+"-"+foundEnd;
                 let dist = foundStart-searchStart;
                 let label = prettySeqDist(dist, true);
                 let regLen = foundEnd-foundStart;
                 if (regLen!==0)
                     label += ", "+(foundEnd-foundStart)+" bp long";
                 let checkBoxId = "range:"+chrom+":"+foundStart+":"+foundEnd+":"+dist;
                 htmls.push("<div class='tpPeak'>");
-                htmls.push("<input style='margin-right: 4px' id='"+checkBoxId+"' type='checkbox'>");
+                htmls.push("<input class='tpPeakCheckbox' style='margin-right: 4px' id='"+checkBoxId+"' type='checkbox'>");
                 htmls.push("<label for='"+checkBoxId+"'>"+label+"</label>");
                 htmls.push("</div>");
                 i++;
             }
         var divEl = document.getElementById("tpPeakList");
         divEl.innerHTML = htmls.join(""); // set the DIV
         classAddListener("tpPeak", "input", onPeakChange);
     }
 
     function onPeakAll(ev) {
         /* select all peaks */
         let peaks = peakListGetPeaksWith("off");
         let peakNames = [];
         if (peaks.length>100) {
             alert("More than 100 peaks to select. This will take too long. Please contact us if you need this feature.");
@@ -5888,30 +5927,49 @@
             return callback();
         this.clearOptions();
         var genes = db.findGenes(query);
         selectizeSendGenes(genes, callback);
     }
 
     function updatePeakListWithGene(geneId) {
         /* update the peak list box with all peaks close to a gene's TSS */
         var peaksInView = db.findRangesByGene(geneId);
         var gene = db.getGeneInfoAtac(geneId);
         peakListShowTitle(gene.sym, gene.chrom, gene.chromStart);
         peakListShowRanges(gene.chrom, peaksInView.ranges, gene.chromStart);
         changeUrl({"locusGene":geneId});
     }
 
+    function updatePeakListCheckboxes(rangeStr) {
+        /* check all the boxes of peaks for rangeStr, which has format chr7|115585018|115585471+chr7|115596153|115596958 */
+
+        let checkRanges = rangeStr.split("+");
+        for (let el of document.getElementsByClassName("tpPeakCheckbox")) {
+            // id is something like "range:chr7:115193275:115194105:-404621"
+            let idStr = el.id;
+            let parts = idStr.split(":");
+            let chrom = parts[1];
+            let start = parts[2];
+            let end = parts[3];
+            let range = chrom+"|"+start+"|"+end;
+            console.log(range, checkRanges);
+            if (checkRanges.includes(range)) {
+                el.checked=true;
+            }
+        }
+    }
+
     function comboLoadAtac(query, callback) {
         /* The load() function for selectize for ATAC datasets.
          * called when the user types something into the gene box, calls callback with matching gene symbols or peaks
          * or shows the peaks in the peakList box */
         if (!query.length)
             return callback();
 
         this.clearOptions();
         this.renderCache = {};
 
         var range = cbUtil.parseRange(query);
         if (range===null) {
             if (!db.geneToTss || db.geneToTss===undefined)
                 db.indexGenesAtac();
 
@@ -7129,30 +7187,31 @@
 
         var nameParts = dataset.name.split("/");
         var parentName = null;
         if (nameParts.length > 1) {
             //buildCollectionCombo(htmls, "tpCollectionCombo", 330, nextLeft, 0);
             buildCollectionCombo(htmls, "tpCollectionCombo", 330, null, 0);
             nameParts.pop();
             parentName = nameParts.join("/");
         }
 
         htmls.push("</div>");
 
         $(document.body).append(htmls.join(""));
 
         $('#tpOpenXena').click(onXenaButtonClick);
+
         $('#tpOpenGenome').click(onGenomeButtonClick);
 
         activateTooltip('.tpIconButton');
         activateTooltip('#tpOpenUcsc');
         activateTooltip('#tpOpenDatasetButton');
         activateTooltip('#tpOpenExprButton');
         activateTooltip('#tpOpenImgButton');
 
         $('#tpButtonInfo').click( function() { openDatasetDialog(db.conf, db.name) } );
 
         $('#tpOpenImgButton').click( function() { openDatasetDialog(db.conf, db.name, "images") } );
 
         activateCombobox("tpLayoutCombo", layoutComboWidth);
 
         if (parentName!==null) {
@@ -7454,35 +7513,37 @@
         htmls.push("</div>"); // tpAnnotTab
 
         htmls.push("<div id='tpGeneTab'>");
 
         buildGeneCombo(htmls, "tpGeneCombo", 0, metaBarWidth-10);
 
         htmls.push('<div id="splitJoinDiv"><input class="form-check-input" type="checkbox" id="splitJoinBox" name="splitJoin" value="splitJoin" /> <label for="splitJoinBox">Show on both sides</label></div>');
 
         if (db.conf.atacSearch)
             buildPeakList(htmls);
 
         var geneLabel = getGeneLabel();
         var recentHelp = "Shown below are the 10 most recently searched genes. Click any gene to color the plot on the right-hand side by the gene.";
 
         buildGeneTable(htmls, "tpRecentGenes", "Recent "+geneLabel+"s",
-            "Hover or select cells to update colors here<br>Click to color by gene", gRecentGenes, null, recentHelp);
+            "Hover or select cells to update colors here<br>Click to color by "+gFeatDesc, gRecentGenes, null, recentHelp);
 
         var noteStr = "No genes or peaks defined: Use quickGenesFile in cellbrowser.conf.";
         var geneHelp = "The dataset genes were defined by the dataset submitter, publication author or data wrangler at UCSC. " +
             "Click any of them to color the plot on the right hand side by the gene.";
+        if (db.conf.atacSearch)
+            geneHelp = "Predefined dataset ranges were defined by the dataset submitter. Click any to color by a list of loci, so a sum of the peaks contained in the range. The exact peaks are listed on mouse over.";
         buildGeneTable(htmls, "tpQuickGenes", "Dataset "+geneLabel+"s", null, db.conf.quickGenes, noteStr, geneHelp);
 
         htmls.push("</div>"); // tpGeneTab
 
         htmls.push("<div id='tpLayoutTab'>");
         buildLayoutCombo(db.conf.coordLabel, htmls, db.conf.coords, "tpLayoutCombo", 0, 2);
         htmls.push("</div>"); // tpLayoutTab
 
         htmls.push("</div>"); // tpLeftSidebar
 
         $(document.body).append(htmls.join(""));
 
         resizeGeneTableDivs("tpRecentGenes");
         resizeGeneTableDivs("tpQuickGenes");
 
@@ -8178,52 +8239,55 @@
         if (status==="none")
             renderer.selectClear();
 
         renderer.drawDots();
     }
 
     function legendColorOnlyChecked(ev) {
         /* re-assign colors from palette, for only checked rows. Or reset all colors. */
 
         if (gLegend.isColorOnlyChecked===undefined || gLegend.isColorOnlyChecked===false) {
             // make a new palette and assign to checked legend rows, otherwise grey
             let rows = gLegend.rows;
             let checkedCount = 0;
             for (let rowIdx=0; rowIdx<rows.length; rowIdx++) {
                 let row = rows[rowIdx];
-                if (row.isChecked)
+                if (row.isChecked) {
                     checkedCount++;
+                    row.orderKey = -1;
+                }
             }
 
             if (checkedCount===0) {
                 alert("No entries selected. Select a few entries in the legend with the checkboxes, then click this button again.");
                 return;
             }
                 
             let pal = makeColorPalette(gLegend.palName, checkedCount);
 
             let palIdx = 0;
             for (let rowIdx=0; rowIdx<rows.length; rowIdx++) {
                 let row = rows[rowIdx];
                 if (row.isChecked) {
                     row.color = pal[palIdx];
                     palIdx++;
                 }
                 else
                     row.color = cNullColor;
             }
             gLegend.isColorOnlyChecked = true;
+            legendSort("orderKey");
 
         } else {
             // reset the colors and uncheck all checkboxes
             legendRemoveManualColors(gLegend);
             //legendSetPalette(gLegend, gLegend.palName);
             legendSetCheckboxes("none");
             gLegend.isColorOnlyChecked = false;
         }
 
         let colors = legendGetColors(gLegend.rows);
         renderer.setColors(colors);
         renderer.drawDots();
 
         buildLegendBar();
     }
@@ -8277,30 +8341,32 @@
         }
         
         var blob = new Blob([lines.join("\n")], {type: "text/plain;charset=utf-8"});
         saveAs(blob, "plotLegend.tsv");
     }
 
     function onLegendCheckboxClick(ev) {
         /* user clicked the small checkboxes in the legend */
         var valIdx = parseInt(ev.target.getAttribute("data-value-index")); // index of this value in original array (before sort)
         var rowIdx = parseInt(ev.target.id.split("_")[1]); // index of this row in the current legend (after sorting)
 
         let isChecked = null
         if (ev.target.checked) {
             renderer.selectByColor(valIdx);
             isChecked = true;
+            gLegend.isColorOnlyChecked=false;
+            $("#tpLegendColorChecked").text("Recolor checked values");
         } else {
             //ev.target.checked = false; // why is this necessary?
             renderer.unselectByColor(valIdx);
             isChecked = false;
         }
         ev.target.checked = isChecked;
         gLegend.rows[rowIdx].isChecked = isChecked;
         renderer.drawDots();
     }
 
     function buildMinMaxPart(htmls) {
         /* create the min/max and apply/reset buttons */
         htmls.push("<div>");
         htmls.push("<table style='margin: 4px; margin-top: 6px'>");
         htmls.push("<tr>");
@@ -8360,31 +8426,31 @@
         var rows = gLegend.rows;
 
         var legTitle = gLegend.title;
         var subTitle = gLegend.subTitle;
 
         htmls.push('<span id="tpLegendTitle" title="' +gLegend.titleHover+'">'+legTitle+"</span>");
         if (subTitle)
             htmls.push('<div id="tpLegendSubTitle" >'+subTitle+"</div>");
 
         htmls.push('<div class="tpHint">Click buttons to select '+gSampleDesc+'s</small></div>');
         htmls.push("<small><button id='tpLegendAll'>All</button>");
         htmls.push("<button id='tpLegendNone'>None</button>");
         htmls.push("<button id='tpLegendInvert'>Invert</button>");
         htmls.push("<button id='tpLegendNotNull'>&gt; 0</button></small>");
 
-        let buttonText = "Recolor checked fields";
+        let buttonText = "Recolor checked values";
         if (gLegend.isColorOnlyChecked===true) {
             buttonText = "Reset colors";
         } 
 
         htmls.push("<button id='tpLegendColorChecked'>"+buttonText+"</button></small>");
 
         htmls.push("</div>"); // title
         htmls.push('<div id="tpLegendHeader"><span id="tpLegendCol1"></span><span id="tpLegendCol2"></span></div>');
         htmls.push('<div id="tpLegendRows">');
 
         // get the sum of all, to calculate frequency
         var sum = 0;
         for (var i = 0; i < rows.length; i++) {
             let count = rows[i].count;
             sum += count;
@@ -8427,31 +8493,35 @@
             else {
                 if (label.length > 20)
                     mouseOver = label;
             }
 
             var classStr = "tpLegend";
             var line = "<div id='tpLegend_" +valueIndex+ "' class='" +classStr+ "'>";
             htmls.push(line);
 
             let checkedStr = "";
             if (row.isChecked)
                 checkedStr = " checked";
 
             htmls.push("<input class='tpLegendCheckbox' data-value-index='"+valueIndex+"' "+
                 "id='tpLegendCheckbox_"+i+"' type='checkbox'"+checkedStr+">");
+
+            if (gLegend.rows.length < MAXCOLPICK)
                 htmls.push("<input class='tpColorPicker' id='tpLegendColorPicker_"+i+"' />");
+            else
+                htmls.push("<div title='Cannot change color manually - too many legend entries' style='display: inline-block; background-color: #"+colorHex+"; width:14px; height:14px; margin-right: 3px' class='' id='tpLegendColor"+i+"'>&nbsp;</div>");
 
             htmls.push("<span class='"+labelClass+"' id='tpLegendLabel_"+i+"' data-placement='auto top' title='"+mouseOver+"'>");
             htmls.push(label);
             htmls.push("</span>");
             var prec = 1;
             if (freq<1)
                 //prec = minPrec;
                 prec = 1+countLeadingZerosAfterDecimal(freq) // one more digit than the smallest frequency
 
             htmls.push("<span class='tpLegendCount' title='"+count+" of "+sum+"'>"+freq.toFixed(prec)+"%</span>");
             htmls.push("</span>");
 
             htmls.push("</div>");
         }
         htmls.push('</div>'); // tpLegendRows
@@ -8513,30 +8583,31 @@
         // activate the color pickers
         for (let i = 0; i < colors.length; i++) {
             var colInfo = colors[i];
             var rowIdx = colInfo[0];
             var hexCode = colInfo[1];
 
             var opt = {
                 hideAfterPaletteSelect : true,
                 color : hexCode,
                 showPalette: true,
                 allowEmpty : true,
                 showInput: true,
                 preferredFormat: "hex",
                 change: onColorPickerChange
                 }
+            if (gLegend.rows.length < MAXCOLPICK)
                 $("#tpLegendColorPicker_"+rowIdx).spectrum(opt);
         }
 
         buildViolinPlot();
     }
 
     function onColorPickerChange(color, ev) {
         /* called when user manually selects a color in the legend with the color picker */
         console.log(ev);
         /* jshint validthis: true */
         var valueIdx = parseInt(this.id.split("_")[1]);
         var rows = gLegend.rows;
         var clickedRow = rows[valueIdx];
         var oldColorHex = clickedRow.color;
         var defColorHex = clickedRow.defColor;
@@ -9763,26 +9834,28 @@
         renderer.onNoLabelHover = onNoClusterNameHover;
         renderer.onCellClick = onCellClickOrHover;
         renderer.onCellHover = onCellClickOrHover;
         renderer.onNoCellHover = clearMetaAndGene;
         renderer.onLineHover = onLineHover;
         renderer.onZoom100Click = onZoom100Click;
         renderer.onSelChange = onSelChange;
         renderer.onRadiusAlphaChange = onRadiusAlphaChange;
         renderer.canvas.addEventListener("mouseleave", hideTooltip);
 
         loadDataset(datasetName, false, rootMd5);
     }
 
     // only export these functions
     return {
-        "main":main
+        "main":main,
+        "selectizeClear" : selectizeClear,
+        "selectizeSetValue" : selectizeSetValue
     }
 
 }();
 
 
 
 function _tpReset() {
 /* for debugging: reset the intro setting */
     localStorage.removeItem("introShown");
 }