1db5c89d97fdd3c3580c4ba9b18eba6b5029c97d
max
  Thu Sep 17 05:09:56 2026 -0700
hgSearch: highlight MANE transcript(s) in RefSeq protein-position search results, refs #38285

#Preview2 week - bugs introduced now will need a build patch to fix
A gene-symbol + codon-range search (e.g. "BRCA1 100-200") maps to every
RefSeq isoform prediction sharing that genomic footprint, which can be
dozens of near-identical NP_ accessions with no indication of which one
is the clinically-relevant MANE transcript. Cross-reference the mane
bigGenePred track by genomic overlap + protein accession and call out
the MANE Select/Plus Clinical transcript(s) in their own section above
the rest, gated behind showManeInSearch (default off) in hg.conf.

diff --git src/hg/js/hgSearch.js src/hg/js/hgSearch.js
index e3fee756915..c775ea590b9 100644
--- src/hg/js/hgSearch.js
+++ src/hg/js/hgSearch.js
@@ -350,31 +350,31 @@
         });
         parentDiv.css('height', "auto");
     }
 
     function updateFilters(uiState) {
         if (typeof uiState.categs !== "undefined") {
             filtersToJstree();
             makeCategoryTree();
         }
     }
 
     function clearOldFacetCounts() {
         $("[id*='extraInfo']").remove();
     }
 
-    function printMatches(list, matches, title, searchDesc, doShowMore) {
+    function printMatches(list, matches, title, searchDesc, doShowMore, maneSection) {
         var printCount = 0;
         _.each(matches, function(match, printCount) {
             var position = match.position.split(':');
             var url, matchTitle;
             if (title === "helpDocs") {
                 url = position[0];
                 matchTitle = "<b>" + position[1].replace(/_/g, " ") + "</b>";
             } else if (title === "publicHubs") {
                 var hubUrl = position[0] + ":" + position[1];
                 var dbName = position[2];
                 var track = position[3];
                 var hubShortLabel = position[4];
                 var hubLongLabel = position[5];
                 url = "hgTrackUi?hubUrl=" + hubUrl + "&g=" + track + "&db=" + dbName;
                 matchTitle = "<b>" + hubShortLabel + "</b>";
@@ -397,33 +397,40 @@
                 if (goToHgTracks) {
                     url = "hgTracks?db=" + db + "&" + hgTracksTitle + "=pack&position=" + match.position;
                     if (match.hgFindMatches) {
                         url += "&hgFind.matches=" + match.hgFindMatches;
                     }
                     if (match.extraSel) {
                         url += "&" + match.extraSel;
                     }
                     if (match.highlight) {
                         url += url[url.length-1] !== '&' ? '&' : '';
                         url += "addHighlight=" + encodeURIComponent(match.highlight);
                     }
                 } else {
                     url = "hgc?db=" + db + "&g=" + hgcTitle + "&i=" + match.position + "&c=0&o=0&l=0&r=0" ;
                 }
-                matchTitle = match.posName;
-                //if (match.canonical === true)
-                matchTitle = "<b>" + matchTitle + "</b>";
+                if (maneSection && match.maneProtAcc) {
+                    // match.posName can be a "/"-joined group of transcripts that all
+                    // share this genomic footprint; show only the actual MANE one.
+                    matchTitle = "<b>" + match.maneProtAcc + "</b>";
+                } else {
+                    matchTitle = "<b>" + match.posName + "</b>";
+                }
+                if (maneSection && match.maneStatus) {
+                    matchTitle = match.maneStatus + ": " + matchTitle;
+                }
             }
             if (printCount < 500) {
                 let newListObj = document.createElement("li");
                 let className = "searchResult ";
                 if (doShowMore || printCount + 1 > 10) {
                     className += title + "_hidden";
                     if (!doShowMore) {
                         newListObj.style.display = "none";
                     }
                 }
                 newListObj.className = className;
                 let a = document.createElement("a");
                 newListObj.appendChild(a);
                 a.href = url;
                 a.innerHTML = matchTitle; // need the bold in the title so use innerHTML here
@@ -507,39 +514,47 @@
     function collapseNode() {
         var toCollapse = this.parentNode.childNodes[3];
         var isHidden  = toCollapse.style.display === "none";
         if (isHidden)
             {
             toCollapse.style = 'display:';
             this.src = "../images/remove_sm.gif";
             }
         else
             {
             toCollapse.style = 'display: none';
             this.src = "../images/add_sm.gif";
             }
     }
 
+    function nonManeMatches(matches) {
+        /* If any of matches are MANE transcripts, they get their own section elsewhere
+         * (see updateSearchResults), so exclude them here to avoid showing the same
+         * transcript twice. If none are MANE, return matches unchanged. */
+        let maneMatches = matches.filter(function(m) { return m.maneStatus; });
+        return maneMatches.length > 0 ? matches.filter(function(m) { return !m.maneStatus; }) : matches;
+    }
+
     function printMoreResults(trackName, nodeAfter) {
         /* Print the 11-500 result before nodeAfter */
         let results = uiState.resultHash[trackName];
         let title = results.name;
         let searchDesc = results.description;
         // show the 11-500th elements
         // after this, only CSS is used to show hide them, since they are part of the
         // page already
-        printMatches(nodeAfter, results.matches.slice(10), title, searchDesc, true);
+        printMatches(nodeAfter, nonManeMatches(results.matches).slice(10), title, searchDesc, true);
     }
 
     function updateSearchResults(uiState) {
         var parentDiv = $("#searchResults");
         if (uiState && typeof uiState.search !== "undefined") {
             $("#searchBarSearchString").val(uiState.search);
         } else {
             // back button all the way to the beginning
             $("#searchBarSearchString").val("");
         }
         if (uiState && uiState.positionMatches && uiState.positionMatches.length > 0) {
             // clear the old search results if there were any:
             parentDiv.empty();
 
             // create the elements that will hold results:
@@ -566,60 +581,83 @@
                 noLiStyle.value = "liNoStyle";
                 newListObj.setAttributeNode(noLiStyle);
                 let inp = document.createElement("input");
                 inp.type = "hidden";
                 inp.id = idKey + categoryCount;
                 inp.value = "0";
                 newListObj.appendChild(inp);
                 let ctrlImg = document.createElement("img");
                 ctrlImg.height = "18";
                 ctrlImg.width = "18";
                 ctrlImg.id = idKey + categoryCount + "_button";
                 ctrlImg.src = "../images/remove_sm.gif";
                 newListObj.appendChild(ctrlImg);
                 let descText = document.createTextNode(" " + searchDesc + ":");
                 newListObj.appendChild(descText);
+                // If any of the matches are MANE transcripts (RefSeq protein-position
+                // searches only), call them out in their own small section above the
+                // full list, since with many isoform predictions the MANE transcript(s)
+                // can otherwise be hard to spot.
+                let maneMatches = matches.filter(function(m) { return m.maneStatus; });
+                if (maneMatches.length > 0) {
+                    let maneHeader = document.createElement("div");
+                    maneHeader.className = "maneSectionHeader";
+                    maneHeader.textContent = "MANE transcript" + (maneMatches.length > 1 ? "s" : "");
+                    newListObj.appendChild(maneHeader);
+                    let maneList = document.createElement("ul");
+                    printMatches(maneList, maneMatches, title, searchDesc, false, true);
+                    newListObj.appendChild(maneList);
+                }
+                // The rest of the matches, excluding any already shown above as MANE
+                // transcripts (if there were none, this is just the full match list).
+                let otherMatches = nonManeMatches(matches);
+                if (maneMatches.length > 0 && otherMatches.length > 0) {
+                    let otherHeader = document.createElement("div");
+                    otherHeader.className = "maneSectionHeader";
+                    otherHeader.textContent = "Other transcripts";
+                    newListObj.appendChild(otherHeader);
+                }
                 // Now loop through each actual hit on this table and unpack onto list
                 let subList = document.createElement("ul");
                 // only print the first 10 at first
-                printMatches(subList, matches.slice(0,10), title, searchDesc, false);
-                if (matches.length > 10) {
+                printMatches(subList, otherMatches.slice(0,10), title, searchDesc, false);
+                if (otherMatches.length > 10) {
                     let idStr = idKey + "_" + categoryCount;
                     let showMoreLi = document.createElement("li");
                     showMoreLi.id = idStr;
                     showMoreLi.classList.add("liNoStyle","searchResult");
                     let showMoreInp = document.createElement("input");
                     showMoreInp.type = "hidden";
                     showMoreInp.value = '0';
                     showMoreInp.id = showMoreLi.id + "showMore";
                     showMoreLi.appendChild(showMoreInp);
                     let showMoreImg = document.createElement("img");
                     showMoreImg.height = "18";
                     showMoreImg.width = "18";
                     showMoreImg.id = showMoreLi.id + "_showMoreButton";
                     showMoreImg.src = "../images/add_sm.gif";
                     showMoreLi.appendChild(showMoreImg);
                     let showMoreDiv = document.createElement("div");
                     showMoreDiv.id = idStr + "_showMoreDiv";
                     showMoreDiv.classList.add("showMoreDiv");
                     let showMoreA = document.createElement("a");
                     showMoreA.id = idStr + "_showMoreLink";
                     let newText = "";
-                    if (matches.length > 500) {
-                        newText = " Show 490 (out of " + (matches.length) + " total) more matches for " + searchDesc;
+                    if (otherMatches.length > 500) {
+                        newText = " Show 490 (out of " + (otherMatches.length) + " total) more matches for " + searchDesc;
                     } else {
-                        newText = " Show " + (matches.length - 10) + " more matches for " + searchDesc;
+                        newText = " Show " + (otherMatches.length - 10) + " more matches for " + searchDesc;
                     }
                     showMoreA.textContent = newText;
                     showMoreDiv.appendChild(showMoreA);
                     showMoreLi.appendChild(showMoreDiv);
                     subList.appendChild(showMoreLi);
                 }
                 newListObj.append(subList);
                 newList.append(newListObj);
 
                 // make result list collapsible:
                 $('#'+idKey+categoryCount+"_button").click(collapseNode);
                 $('#'+idKey+"_" +categoryCount+"_showMoreButton").click(showMoreResults);
                 $('#'+idKey + "_" + categoryCount + "_showMoreLink").click(showMoreResults);
                 categoryCount += 1;
             });