c7fcdde6db52ba01fbabfa695b49205bf7261a33
max
  Tue Aug 4 07:40:32 2026 -0700
hgBlat/hgc: QA fixes for the new BLAT table view and alignment page

Address Gerardo's QA findings on the new BLAT UI:
- Table view: label each hit with its own query (new Query column +
"Queries" count in the summary) when the search has multiple queries,
reading cfg.multiQuery / hit.qName from the payload.
- Table view: strip the "hub_NNN_" prefix from the Assembly field so hub
assemblies no longer show a doubled prefix.
- Alignment page: strip the same prefix from the organism in the page title
for non-GenArk assembly hubs (blatAsmLabel fallback path).
- Table view: do not pre-select hit #1; show a prompt until the user clicks
a row, since top hits are often tied.
- Table view: add a divider between the Browser / New tab / Alignment links.
- Alignment page: sidebar link now reads "Side by Side Alignment" to match
the section heading.
- Alignment page: comma-format coordinates and base counts in the summary,
matching the table view and the rest of the browser.

refs #37893

diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index 19eebb0ed1a..6a0d49d2320 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -72,31 +72,32 @@
     .blatShareMsg { font-size:14px; color:var(--muted); }
     .blatShareInput { flex:1; min-width:260px; font-size:13px; padding:5px 8px;
         border:1px solid var(--btnLine); border-radius:3px; background:#fff; }
     #blatTable { font-size:13px; width:100%; border-collapse:collapse; }
     #blatTable thead th { background:var(--headrow); border-bottom:1px solid var(--headrowLine);
         font-size:12px; color:var(--navy); font-weight:700; padding:8px 12px; white-space:nowrap; }
     #blatTable tbody td { padding:8px 12px; border-bottom:1px solid var(--lineSoft); white-space:nowrap;
         vertical-align:middle; }
     #blatTable tbody tr { cursor:pointer; }
     #blatTable tbody tr:nth-child(even) { background:var(--stripe); }
     #blatTable tbody tr:hover { background:var(--hover); }
     #blatTable tbody tr.blatSel { background:var(--sel); box-shadow:inset 3px 0 0 var(--navy); }
     #blatTable td.num, #blatTable th.num { text-align:right; font-variant-numeric:tabular-nums; }
     #blatTable td.rankCol { color:var(--faint); }
     #blatTable td.strandCol { text-align:center; color:var(--muted); }
-    #blatTable td.actionsCol a { color:var(--accent); font-weight:700; margin-right:12px; }
+    #blatTable td.actionsCol a { color:var(--accent); font-weight:700; }
+    #blatTable td.actionsCol .blatActSep { color:var(--line); margin:0 10px; }
     #blatTable a { color:var(--accent); text-decoration:none; }
     #blatTable a:hover { color:var(--accentHover); text-decoration:underline; }
     .blatRowHint { padding:11px 18px 2px; font-size:13px; color:var(--muted); }
     .blatLocus { max-width:230px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
     .chrNote { color:var(--faint); cursor:help; margin-left:4px; }
     .blatIdWrap { display:flex; align-items:center; gap:9px; justify-content:flex-end; }
     .blatIdBar { flex:0 0 54px; height:8px; background:#dce4ee; border:1px solid var(--headrowLine); overflow:hidden; }
     .blatIdBar > i { display:block; height:100%; }
     .blatIdPct { font-size:13px; font-weight:700; width:48px; text-align:right; font-variant-numeric:tabular-nums; }
     .blatCov { position:relative; display:block; width:150px; height:10px; background:#dce4ee; border:1px solid var(--headrowLine); }
     .blatCov > i { position:absolute; top:0; bottom:0; background:var(--accent); }
     #blatTable_filter { float:left; margin:0 0 10px; }
     #blatTable_filter input { width:300px; max-width:55vw; border:1px solid var(--btnLine);
         border-radius:3px; padding:5px 9px; font-size:14px; }
     #blatTable_wrapper { padding:6px 18px 4px; }
@@ -117,33 +118,35 @@
     document.head.appendChild(st);
 }
 
 // ---- cell renderers ------------------------------------------------------
 
 function blatPositionCell(hit) {
     var note = hit.chromNote ?
         ` <span class="chrNote" title="${blatEsc(hit.chromNote)}">&#9432;</span>` : '';
     return `<a class="blatPos" href="${hit.browserUrl}">${blatEsc(hit.chrom)}:` +
         `${blatFmt(hit.tStart)}-${blatFmt(hit.tEnd)}</a>${note}`;
 }
 
 function blatActionsCell(hit) {
     // detailsUrl is the base-by-base alignment page (htcUserAli on a fresh search, htcBlatAlign on a
     // shared-link reopen); it is always present now, but guard in case a future caller omits it.
-    var align = hit.detailsUrl ? `<a href="${hit.detailsUrl}">Alignment</a>` : '';
-    return `<a href="${hit.browserUrl}">Browser</a>` +
-        `<a target="_blank" href="${hit.newTabUrl}">New&nbsp;tab</a>` + align;
+    var parts = [`<a href="${hit.browserUrl}">Browser</a>`,
+        `<a target="_blank" href="${hit.newTabUrl}">New&nbsp;tab</a>`];
+    if (hit.detailsUrl) { parts.push(`<a href="${hit.detailsUrl}">Alignment</a>`); }
+    // a thin divider between the links makes the grouping clearer
+    return parts.join('<span class="blatActSep">|</span>');
 }
 
 function blatLocusCell(hit, cfg) {
     if (!hit.locusText) { return ''; }
     var inner;
     if (hit.locusType && hit.locusGenes && hit.locusGenes.length) {
         var links = hit.locusGenes.map(function(g) {
             return `<a href="${cfg.geneUrlBase}${encodeURIComponent(g)}">${blatEsc(g)}</a>`;
         }).join('-');
         inner = `${blatEsc(hit.locusType)} ${links}`;
     } else {
         inner = blatEsc(hit.locusText);
     }
     return `<div class="blatLocus" title="${blatEsc(hit.locusText)}">${inner}</div>`;
 }
@@ -153,38 +156,45 @@
     return `<span class="blatIdWrap"><span class="blatIdBar">` +
         `<i style="width:${hit.identity}%;background:${c}"></i></span>` +
         `<span class="blatIdPct" style="color:${c}">${hit.identity.toFixed(1)}%</span></span>`;
 }
 
 function blatCoverageCell(hit) {
     var left = (hit.qStart - 1) / hit.qSize * 100;
     var width = (hit.qEnd - hit.qStart + 1) / hit.qSize * 100;
     var tip = `query ${blatFmt(hit.qStart)}–${blatFmt(hit.qEnd)} of ${blatFmt(hit.qSize)} bp`;
     return `<span class="blatCov" title="${tip}"><i style="left:${left.toFixed(1)}%;` +
         `width:${width.toFixed(1)}%"></i></span>`;
 }
 
 // ---- summary strip + detail panel ---------------------------------------
 
-function blatSummaryStrip(cfg) {
+function blatSummaryStrip(cfg, queryCount) {
     var stat = (k, v) => `<div class="blatStat"><span class="k">${k}</span>` +
         `<span class="v">${v}</span></div>`;
     var div = '<span class="blatDiv"></span>';
-    var stats = stat('Query', blatEsc(cfg.queryName)) + div +
-        stat('Length', blatFmt(cfg.querySize) + ' bp') + div +
-        stat('Assembly', blatEsc(cfg.organism) + ' / ' + blatEsc(cfg.db)) + div +
+    var assembly = stat('Assembly', blatEsc(cfg.organism) + ' / ' + blatEsc(cfg.db)) + div +
         stat('Hits', blatFmt(cfg.hitCount));
+    var stats;
+    if (cfg.multiQuery) {
+        // With more than one query sequence a single query name/length would be wrong, so show the
+        // number of distinct queries; each hit's own query is in the table's Query column.
+        stats = stat('Queries', blatFmt(queryCount)) + div + assembly;
+    } else {
+        stats = stat('Query', blatEsc(cfg.queryName)) + div +
+            stat('Length', blatFmt(cfg.querySize) + ' bp') + div + assembly;
+    }
     var actions = '';
     // "Share a link" saves a durable anonymous session that reopens these results; it only works
     // when a stable custom track was made from them (cfg.canShare, i.e. autoBigPsl is on).
     if (cfg.canShare) {
         actions += '<button type="button" class="blatPill" id="blatShareBtn">Share a link</button>';
     }
     if (cfg.viewAllUrl) {
         actions += `<a class="blatPill" href="${cfg.viewAllUrl}">View all in browser</a>`;
     }
     // custom-track Rename/Delete buttons (emitted by hgBlat.c) get relocated here after render
     return `<div class="blatStrip">${stats}<span class="blatStripActions">${actions}</span></div>`;
 }
 
 var BLAT_TILE_TIPS = {
     'Score': 'BLAT score: matches minus mismatches and gap penalties. Higher is better.',
@@ -236,32 +246,33 @@
 
 function blatSet(id, prop, val) {
     var e = document.getElementById(id);
     if (!e) { return; }
     if (prop === 'text') { e.textContent = val; }
     else if (prop === 'html') { e.innerHTML = val; }
     else if (prop === 'href') { e.setAttribute('href', val); }
     else if (prop === 'color') { e.style.color = val; }
 }
 
 function blatRenderDetail(hit) {
     if (!hit || !document.getElementById('blatDetail')) { return; }
     if (!document.getElementById('dvScore')) { blatDetailSkeleton(); }
     var idc = blatIdColor(hit.identity);
     var locus = hit.locusText ? blatEsc(hit.locusText) + ' · ' : '';
+    var q = hgBlatData.config.multiQuery ? blatEsc(hit.qName) + ' · ' : '';
     blatSet('dvLoc', 'html',
-        `#${hit.rank} · ${locus}${blatEsc(hit.chrom)}:${blatFmt(hit.tStart)}-${blatFmt(hit.tEnd)}`);
+        `#${hit.rank} · ${q}${locus}${blatEsc(hit.chrom)}:${blatFmt(hit.tStart)}-${blatFmt(hit.tEnd)}`);
     blatSet('dvScore', 'text', blatFmt(hit.score));
     blatSet('dvIdentity', 'text', hit.identity.toFixed(1) + '%');
     blatSet('dvIdentity', 'color', idc);
     blatSet('dvMatches', 'text', blatFmt(hit.matches));
     blatSet('dvMismatch', 'text', blatFmt(hit.misMatch));
     blatSet('dvGaps', 'text', blatFmt(hit.gaps));
     blatSet('dvBlocks', 'text', blatFmt(hit.blocks));
     blatSet('dvStrand', 'text', hit.strand);
     blatSet('dvQspan', 'text', blatFmt(hit.qStart) + '–' + blatFmt(hit.qEnd));
     blatSet('dvBrowser', 'href', hit.browserUrl);
     blatSet('dvNewTab', 'href', hit.newTabUrl);
     // Show the Alignment box whenever a base-by-base alignment page is available (htcUserAli on a
     // fresh search, htcBlatAlign on a shared-link reopen); hide it only if detailsUrl is missing.
     var alignBox = document.getElementById('dvAlignBox');
     if (alignBox) { alignBox.style.display = hit.detailsUrl ? '' : 'none'; }
@@ -275,30 +286,31 @@
 
 function blatSelect(dt, rank) {
     blatSelectedRank = rank;
     $('#blatTable tbody tr').each(function() {
         var d = dt.row(this).data();
         $(this).toggleClass('blatSel', !!d && d.rank === rank);
     });
     var hit = hgBlatData.hits.find(h => h.rank === rank);
     blatRenderDetail(hit);
 }
 
 // ---- header tooltips (reuse the browser's title -> mouseover system) -----
 
 var BLAT_HEADER_TIPS = {
     '#': 'Rank by the chosen sort order',
+    'Query': 'The query sequence this hit came from',
     'Position': 'Genomic location of the match (1-based). Click to open the Genome Browser.',
     'Actions': 'Open the match in the browser, see the alignment details, or open in a new tab',
     'Locus': 'Nearest gene(s), and whether the hit falls in an exon, intron, or intergenic region',
     'Score': 'BLAT score: matches minus mismatches and gap penalties. Higher is better.',
     'Identity': 'Percent identity of the aligned bases',
     'Strand': 'Genome strand the query matched (+ or -)',
     'Query coverage': 'Which part of the query aligned (blue) across its full length',
     'Span': 'Length of the match on the genome (bp). Larger than the query length means ' +
         'the alignment crosses introns or deletions.'
 };
 
 function blatApplyTooltips() {
     $('#blatTable thead th').each(function() {
         var tip = BLAT_HEADER_TIPS[$(this).text().trim()];
         if (tip) { $(this).attr('title', tip); }
@@ -364,59 +376,63 @@
     blatInjectStyle();
 
     var back = cfg.backUrl ?
         `<a class="blatPill" title="${blatEsc(cfg.backPos)}" href="${cfg.backUrl}">Back to browser</a>` : '';
     // "Old BLAT result page" re-renders the classic list from this session's fresh trash files and
     // clears the blatNewPage preference (blatNewPage=0), so future searches use the classic page
     // until the user opts back in.  Only offered on a fresh search (cfg.canOldPage), not a reopen.
     var oldPage = cfg.canOldPage ?
         `<a class="blatPill" title="Switch back to the classic BLAT results page" ` +
         `href="hgBlat?blatNewPage=0&blatReopen=1&hgsid=${encodeURIComponent(cfg.hgsid)}">Old BLAT result page</a>` : '';
     var headHtml =
         `<div class="blatHead"><span class="t">BLAT <span class="sub">results</span></span>` +
         `<span class="blatHeadActions">${oldPage}${back}` +
         `<a class="blatPill primary" href="${cfg.newSearchUrl}">New BLAT search</a></span></div>`;
 
+    var queryCount = new Set(hits.map(h => h.qName)).size;
+
     var th = [];
     th.push('<th class="num">#</th>');
+    if (cfg.multiQuery) { th.push('<th>Query</th>'); }
     th.push('<th>Position</th>');
     th.push('<th>Actions</th>');
     if (cfg.hasLocus) { th.push('<th>Locus</th>'); }
     th.push('<th class="num">Score</th>');
     th.push('<th class="num">Identity</th>');
     th.push('<th>Strand</th>');
     th.push('<th>Query coverage</th>');
     th.push('<th class="num">Span</th>');
 
     // detail dock sits above the table: with long hit lists a bottom dock scrolls out of view
     document.getElementById('blatResults').innerHTML =
-        `<div class="blatCard">${headHtml}${blatSummaryStrip(cfg)}` +
+        `<div class="blatCard">${headHtml}${blatSummaryStrip(cfg, queryCount)}` +
         `<div id="blatShareBox" class="blatShareBox" style="display:none"></div>` +
         `<div id="blatDetail" class="blatDetail"></div>` +
         `<div class="blatRowHint">Click any row to inspect it in the panel above.</div>` +
         `<table id="blatTable" class="display" style="width:100%"><thead><tr>${th.join('')}</tr></thead></table></div>`;
 
     // relocate the C-emitted custom-track buttons into the summary strip
     var actionBar = document.querySelector('.blatStripActions');
     ['renameFormItem', 'deleteCtForm'].forEach(function(id) {
         var el = document.getElementById(id);
         if (el && actionBar) { actionBar.appendChild(el); }
     });
     $('#blatShareBtn').on('click', blatShareLink);
 
     var columns = [];
     columns.push({ data: 'rank', className: 'num rankCol' });
+    if (cfg.multiQuery) { columns.push({ data: 'qName', className: 'queryCol' }); }
     columns.push({ data: null, orderable: false, className: 'blatPos',
         render: (d, type, row) => (type === 'display' ? blatPositionCell(row) : row.chrom + ':' + row.tStart) });
     columns.push({ data: null, orderable: false, className: 'actionsCol',
         render: (d, type, row) => (type === 'display' ? blatActionsCell(row) : '') });
     if (cfg.hasLocus) {
         columns.push({ data: 'locusText',
             render: (d, type, row) => (type === 'display' ? blatLocusCell(row, cfg) : (d || '')) });
     }
     columns.push({ data: 'score', className: 'num' });
     columns.push({ data: 'identity', className: 'num',
         render: (d, type, row) => (type === 'display' ? blatIdentityCell(row) : d) });
     columns.push({ data: 'strand', className: 'strandCol' });
     columns.push({ data: null, className: 'covCol', orderable: false,
         render: (d, type, row) => (type === 'display' ? blatCoverageCell(row) :
             (row.qEnd - row.qStart + 1)) });
@@ -432,24 +448,27 @@
     });
 
     $('#blatTable tbody').on('click', 'tr', function(ev) {
         if ($(ev.target).closest('a').length) { return; }   // let links work normally
         var d = dt.row(this).data();
         if (d) { blatSelect(dt, d.rank); }
     });
 
     // Keep the selected-row highlight after sort/filter.  Header tooltips are wired once below (the
     // <thead> persists across draws); we deliberately do NOT re-run convertTitleTagsToMouseovers on
     // every draw, as it re-scans the whole document and adds global listeners on each call.
     dt.on('draw', function() {
         if (blatSelectedRank !== null) { blatSelect(dt, blatSelectedRank); }
     });
 
-    if (hits.length) { blatSelect(dt, hits[0].rank); }
+    // No hit is pre-selected: several hits are often tied on score/identity, so picking one for the
+    // user is misleading.  The detail panel shows a prompt until a row is clicked.
+    document.getElementById('blatDetail').innerHTML =
+        `<div class="dhead"><span class="lab">Select a hit below to see its alignment details.</span></div>`;
     blatApplyTooltips();
 }
 
 $(document).ready(function() {
     if (typeof hgBlatData !== 'undefined' && document.getElementById('blatResults')) {
         blatBuild();
     }
 });