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