@@ -451,7 +451,7 @@ function buildAliasResults(resolvedGenes, sourceTaxid, targetTaxids) {
451451
452452 for ( const { query, proteinId } of found ) {
453453 html += `<div class="result-section">` ;
454- html += `<div class="result-section-title"><span class="result-gene-badge">${ esc ( query ) } </span>` ;
454+ html += `<div class="result-section-title"><span class="result-gene-badge" data-pid=" ${ esc ( proteinId ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( query ) } </span>` ;
455455 const name = getPreferredName ( proteinId , sourceTaxid ) ;
456456 if ( name !== query ) html += ` → ${ esc ( name ) } ` ;
457457 html += `</div>` ;
@@ -483,8 +483,8 @@ function buildAliasResults(resolvedGenes, sourceTaxid, targetTaxids) {
483483 const matchAliases = ( targetData . aliases ?. [ matchPid ] || [ ] ) . slice ( 0 , 5 ) . join ( ', ' ) ;
484484 html += `<tr>
485485 <td>${ italicSpeciesName ( targetName ) } </td>
486- <td><code>${ esc ( matchPid ) } </code></td>
487- <td>${ esc ( matchName ) } </td>
486+ <td><code data-pid=" ${ esc ( matchPid ) } " data-taxid=" ${ esc ( targetTaxid ) } " >${ esc ( matchPid ) } </code></td>
487+ <td data-pid=" ${ esc ( matchPid ) } " data-taxid=" ${ esc ( targetTaxid ) } " >${ esc ( matchName ) } </td>
488488 <td class="alias-text">${ esc ( matchAliases ) } </td>
489489 </tr>` ;
490490 foundMatch = true ;
@@ -521,7 +521,7 @@ function buildPPIResults(resolvedGenes, sourceTaxid) {
521521
522522 for ( const { query, proteinId } of found ) {
523523 html += `<div class="result-section">` ;
524- html += `<div class="result-section-title"><span class="result-gene-badge">${ esc ( query ) } </span>` ;
524+ html += `<div class="result-section-title"><span class="result-gene-badge" data-pid=" ${ esc ( proteinId ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( query ) } </span>` ;
525525 const name = getPreferredName ( proteinId , sourceTaxid ) ;
526526 if ( name !== query ) html += ` → ${ esc ( name ) } ` ;
527527 html += `</div>` ;
@@ -550,8 +550,8 @@ function buildPPIResults(resolvedGenes, sourceTaxid) {
550550 const scoreClass = s >= 900 ? 'score-high' : s >= 700 ? 'score-med' : 'score-low' ;
551551 const annotation = data . info ?. [ p ] ?. annotation || '' ;
552552 html += `<tr>
553- <td><code>${ esc ( p ) } </code></td>
554- <td>${ esc ( iName ) } </td>
553+ <td><code data-pid=" ${ esc ( p ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( p ) } </code></td>
554+ <td data-pid=" ${ esc ( p ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( iName ) } </td>
555555 <td><span class="score ${ scoreClass } ">${ s } </span></td>
556556 <td class="alias-text">${ esc ( truncate ( annotation , 100 ) ) } </td>
557557 </tr>` ;
@@ -578,7 +578,7 @@ function buildPPINetwork(resolvedGenes, sourceTaxid) {
578578
579579 const getNameFn = ( pid ) => getPreferredName ( pid , sourceTaxid ) ;
580580
581- const result = window . PPINetwork . buildPPINetworkSVG ( resolvedGenes , data . ppi , data . info , state . scoreThreshold , getNameFn ) ;
581+ const result = window . PPINetwork . buildPPINetworkSVG ( resolvedGenes , data . ppi , data . info , state . scoreThreshold , getNameFn , sourceTaxid ) ;
582582
583583 container . innerHTML = '' ;
584584 if ( result && result . svg ) {
@@ -632,7 +632,7 @@ function renderHubGenesTable(nodes, container, taxid) {
632632
633633 return `
634634 <tr ${ rowClass } >
635- <td>
635+ <td data-pid=" ${ esc ( n . id ) } " data-taxid=" ${ esc ( taxid ) } " >
636636 <div><strong>${ esc ( name ) } </strong></div>
637637 <div style="font-size:0.75rem;color:var(--text-muted)">${ esc ( n . id ) } </div>
638638 </td>
@@ -670,7 +670,7 @@ function buildGOResults(resolvedGenes, sourceTaxid) {
670670 if ( ! goTerms || goTerms . length === 0 ) continue ;
671671
672672 html += `<div class="result-section">` ;
673- html += `<div class="result-section-title"><span class="result-gene-badge">${ esc ( query ) } </span></div>` ;
673+ html += `<div class="result-section-title"><span class="result-gene-badge" data-pid=" ${ esc ( proteinId ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( query ) } </span></div>` ;
674674
675675 const grouped = { } ;
676676 for ( const t of goTerms ) {
@@ -750,7 +750,7 @@ function buildKEGGResults(resolvedGenes, sourceTaxid) {
750750 if ( keggTerms . length === 0 && pathwayMatches . length === 0 && keggLikeTerms . length === 0 ) continue ;
751751
752752 html += `<div class="result-section">` ;
753- html += `<div class="result-section-title"><span class="result-gene-badge">${ esc ( query ) } </span>` ;
753+ html += `<div class="result-section-title"><span class="result-gene-badge" data-pid=" ${ esc ( proteinId ) } " data-taxid=" ${ esc ( sourceTaxid ) } " >${ esc ( query ) } </span>` ;
754754 if ( prefName !== query ) html += ` → ${ esc ( prefName ) } ` ;
755755 html += `</div>` ;
756756
@@ -1210,4 +1210,150 @@ function makeTableSortable(table) {
12101210 Array . from ( tbody . querySelectorAll ( 'tr' ) ) . forEach ( ( tr , i ) => tr . dataset . originalIndex = i ) ;
12111211}
12121212
1213+ // ===== Gene Tooltip =====
1214+ const tooltipEl = document . getElementById ( 'gene-tooltip' ) ;
1215+ let tooltipTimer = null ;
1216+
1217+ function buildTooltipHTML ( pid , taxid ) {
1218+ const data = state . cache [ taxid ] ;
1219+ if ( ! data ) return null ;
1220+
1221+ const info = data . info ?. [ pid ] ;
1222+ const name = info ?. name || pid ;
1223+ const annotation = info ?. annotation || '' ;
1224+ const size = info ?. size || '' ;
1225+ const goTerms = ( data . go ?. [ pid ] || [ ] ) . slice ( 0 , 4 ) ;
1226+ const pathwayNames = data . keggPathways ?. pathways || { } ;
1227+
1228+ // KEGG gene_pathways is keyed by gene name, not protein ID — try multiple keys
1229+ let keggPaths = [ ] ;
1230+ if ( data . keggPathways ?. gene_pathways ) {
1231+ const gp = data . keggPathways . gene_pathways ;
1232+ if ( gp [ pid ] ) keggPaths = gp [ pid ] ;
1233+ else if ( gp [ name ] ) keggPaths = gp [ name ] ;
1234+ else if ( data . aliases ?. [ pid ] ) {
1235+ for ( const alias of data . aliases [ pid ] ) {
1236+ if ( gp [ alias ] ) { keggPaths = gp [ alias ] ; break ; }
1237+ }
1238+ }
1239+ }
1240+
1241+ let html = `<div class="gene-tooltip-header">` ;
1242+ html += `<span class="gene-tooltip-name">${ esc ( name ) } </span>` ;
1243+ if ( name !== pid ) html += `<span class="gene-tooltip-pid">${ esc ( pid ) } </span>` ;
1244+ if ( size ) html += `<span class="gene-tooltip-size">${ esc ( size ) } aa</span>` ;
1245+ html += `</div>` ;
1246+
1247+ if ( annotation ) {
1248+ html += `<div class="gene-tooltip-annotation">${ esc ( annotation ) } </div>` ;
1249+ }
1250+
1251+ if ( goTerms . length > 0 ) {
1252+ html += `<div class="gene-tooltip-section">` ;
1253+ html += `<div class="gene-tooltip-section-title">GO Terms</div>` ;
1254+ html += `<ul class="gene-tooltip-terms">` ;
1255+ for ( const t of goTerms ) {
1256+ html += `<li><a href="https://amigo.geneontology.org/amigo/term/${ encodeURIComponent ( t . term ) } " target="_blank" rel="noopener">${ esc ( t . term ) } </a> ${ esc ( t . description ) } </li>` ;
1257+ }
1258+ html += `</ul></div>` ;
1259+ }
1260+
1261+ if ( keggPaths . length > 0 ) {
1262+ html += `<div class="gene-tooltip-section">` ;
1263+ html += `<div class="gene-tooltip-section-title">KEGG Pathways</div>` ;
1264+ html += `<ul class="gene-tooltip-terms">` ;
1265+ for ( const kp of keggPaths . slice ( 0 , 3 ) ) {
1266+ const cleanId = kp . replace ( / ^ p a t h : / , '' ) ;
1267+ const desc = pathwayNames [ cleanId ] || pathwayNames [ kp ] || cleanId ;
1268+ html += `<li><a href="https://www.genome.jp/entry/${ encodeURIComponent ( cleanId ) } " target="_blank" rel="noopener">${ esc ( cleanId ) } </a> ${ esc ( desc ) } </li>` ;
1269+ }
1270+ html += `</ul></div>` ;
1271+ }
1272+
1273+ // Reference links
1274+ html += `<div class="gene-tooltip-refs">` ;
1275+ html += `<a href="https://www.uniprot.org/uniprot/${ encodeURIComponent ( pid ) } " target="_blank" rel="noopener">UniProt</a>` ;
1276+ html += `<a href="https://string-db.org/network/${ encodeURIComponent ( taxid + '.' + pid ) } " target="_blank" rel="noopener">STRING</a>` ;
1277+ html += `</div>` ;
1278+
1279+ return html ;
1280+ }
1281+
1282+ function showGeneTooltip ( pid , taxid , event ) {
1283+ if ( ! pid || ! taxid ) return ;
1284+ clearTimeout ( tooltipTimer ) ;
1285+
1286+ tooltipTimer = setTimeout ( ( ) => {
1287+ const html = buildTooltipHTML ( pid , taxid ) ;
1288+ if ( ! html ) return ;
1289+
1290+ tooltipEl . innerHTML = html ;
1291+ tooltipEl . hidden = false ;
1292+
1293+ // Position near cursor, avoiding viewport overflow
1294+ const pad = 12 ;
1295+ const rect = tooltipEl . getBoundingClientRect ( ) ;
1296+ let x = event . clientX + pad ;
1297+ let y = event . clientY + pad ;
1298+
1299+ // Measure after making visible but before showing
1300+ tooltipEl . style . left = x + 'px' ;
1301+ tooltipEl . style . top = y + 'px' ;
1302+ tooltipEl . classList . add ( 'visible' ) ;
1303+
1304+ // Adjust if overflowing
1305+ const ttRect = tooltipEl . getBoundingClientRect ( ) ;
1306+ if ( ttRect . right > window . innerWidth - 8 ) {
1307+ x = event . clientX - ttRect . width - pad ;
1308+ }
1309+ if ( ttRect . bottom > window . innerHeight - 8 ) {
1310+ y = event . clientY - ttRect . height - pad ;
1311+ }
1312+ tooltipEl . style . left = Math . max ( 4 , x ) + 'px' ;
1313+ tooltipEl . style . top = Math . max ( 4 , y ) + 'px' ;
1314+ } , 200 ) ;
1315+ }
1316+
1317+ function hideGeneTooltip ( ) {
1318+ clearTimeout ( tooltipTimer ) ;
1319+ tooltipEl . classList . remove ( 'visible' ) ;
1320+ tooltipTimer = setTimeout ( ( ) => {
1321+ tooltipEl . hidden = true ;
1322+ } , 150 ) ;
1323+ }
1324+
1325+ // Event delegation for [data-pid] elements in results
1326+ document . addEventListener ( 'mouseover' , ( e ) => {
1327+ // Don't hide if mouse enters the tooltip itself
1328+ if ( tooltipEl . contains ( e . target ) ) {
1329+ clearTimeout ( tooltipTimer ) ;
1330+ return ;
1331+ }
1332+ const target = e . target . closest ( '[data-pid]' ) ;
1333+ if ( target ) {
1334+ const pid = target . dataset . pid ;
1335+ const taxid = target . dataset . taxid ;
1336+ showGeneTooltip ( pid , taxid , e ) ;
1337+ }
1338+ } ) ;
1339+
1340+ document . addEventListener ( 'mouseout' , ( e ) => {
1341+ const target = e . target . closest ( '[data-pid]' ) ;
1342+ if ( target ) {
1343+ // Check if mouse is moving to the tooltip
1344+ const related = e . relatedTarget ;
1345+ if ( related && tooltipEl . contains ( related ) ) return ;
1346+ hideGeneTooltip ( ) ;
1347+ }
1348+ } ) ;
1349+
1350+ // Hide tooltip when leaving the tooltip itself
1351+ tooltipEl . addEventListener ( 'mouseleave' , ( ) => {
1352+ hideGeneTooltip ( ) ;
1353+ } ) ;
1354+
1355+ // Expose for network.js
1356+ window . showGeneTooltip = showGeneTooltip ;
1357+ window . hideGeneTooltip = hideGeneTooltip ;
1358+
12131359document . addEventListener ( 'DOMContentLoaded' , init ) ;
0 commit comments