From 3719e8c43dd3423afbc679740230ff3eb589f598 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Tue, 18 Feb 2025 22:09:55 -0500 Subject: [PATCH 01/19] Enable drawing pathway as static method --- karma.conf.js | 6 +++--- src/js/ideogram.js | 25 +++++++++++++++++++++++++ src/js/kit/pathway-viewer.js | 8 ++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 3b4067c7..09d61a4c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -16,10 +16,10 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ 'src/js/index.js', - // 'test/offline/**.test.js', - // 'test/online/**.test.js', + 'test/offline/**.test.js', + 'test/online/**.test.js', // 'test/online/related-genes.test.js', - 'test/offline/gene-structure.test.js', + // 'test/offline/gene-structure.test.js', // 'test/offline/tissue.test.js', {pattern: 'dist/data/**', watched: false, included: false, served: true, nocache: false} ], diff --git a/src/js/ideogram.js b/src/js/ideogram.js index 5624f358..25ae9ee8 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -68,6 +68,10 @@ import { plotRelatedGenes, getRelatedGenesByType } from './kit/related-genes'; +import { + drawPathway as _drawPathway +} from './kit/pathway-viewer.js'; + export default class Ideogram { constructor(config) { @@ -340,4 +344,25 @@ export default class Ideogram { static initGeneLeads(config, annotsInList='all') { return _initGeneLeads(config, annotsInList); } + + /** + * Wrapper for drawing biological pathways using cached WikiPathways data + * + * @param {String} pwId WikiPathways ID, e.g. "WP5109" + * @param {String} sourceGene Symbol of source gene, e.g. "LDLR" + * @param {String} destGene Symbol of destination gene, e.g. "PCSK9" + * @param {String} outerSelector DOM selector of container, e.g. "#my-diagram" + * @param {Object} dimensions Height and width of pathway diagram + */ + static drawPathway( + pwId, sourceGene, destGene, + outerSelector, + dimensions={height: 440, width: 900} + ) { + _drawPathway( + pwId, sourceGene, destGene, + outerSelector, + dimensions=dimensions + ); + } } diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index e64a0733..8ec3e00a 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -152,6 +152,7 @@ function addHeader(pwId, pathwayJson, pathwayContainer) { /** Fetch and render WikiPathways diagram for given pathway ID */ export async function drawPathway( pwId, sourceGene, destGene, + outerSelector='#_ideogramOuterWrap', dimensions={height: 440, width: 900}, retryAttempt=0 ) { const pvjsScript = document.querySelector(`script[src="${PVJS_URL}"]`); @@ -170,7 +171,10 @@ export async function drawPathway( ) { if (retryAttempt <= 40) { setTimeout(() => { - drawPathway(pwId, sourceGene, destGene, dimensions, retryAttempt++); + drawPathway( + pwId, sourceGene, destGene, + outerSelector, dimensions, retryAttempt++ + ); }, 250); return; } else { @@ -192,7 +196,7 @@ export async function drawPathway( const highlights = sourceHighlights.concat(destHighlights); const oldPathwayContainer = document.querySelector(containerSelector); - const ideoContainerDom = document.querySelector('#_ideogramOuterWrap'); + const ideoContainerDom = document.querySelector(outerSelector); if (oldPathwayContainer) { oldPathwayContainer.remove(); } From 7272fe0ab915017751561039d2dfa33760fc6e7a Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Wed, 19 Feb 2025 08:19:16 -0500 Subject: [PATCH 02/19] Make caches statically accessible --- src/js/annotations/annotations.js | 4 +- src/js/ideogram.js | 17 +++++ src/js/init/caches/cache.js | 80 +++++++++++----------- src/js/init/caches/tissue-cache-worker.js | 7 +- src/js/init/caches/variant-cache-worker.js | 6 +- src/js/init/finish-init.js | 2 +- src/js/kit/gene-structure.js | 34 ++++----- src/js/kit/protein.js | 6 +- src/js/kit/related-genes.js | 77 +++++++++++---------- src/js/kit/tissue.js | 15 ++-- src/js/kit/variant.js | 2 +- src/js/kit/wikipathways.js | 2 +- 12 files changed, 137 insertions(+), 115 deletions(-) diff --git a/src/js/annotations/annotations.js b/src/js/annotations/annotations.js index 4acf3f1f..8195858b 100644 --- a/src/js/annotations/annotations.js +++ b/src/js/annotations/annotations.js @@ -287,9 +287,9 @@ export function applyRankCutoff(annots, cutoff, ideo) { export function setAnnotRanks(annots, ideo) { if (annots.length === 0) return annots; if ('initRank' in annots[0] === false) { - if ('geneCache' in ideo === false) return annots; + if ('geneCache' in Ideogram === false) return annots; - const ranks = ideo.geneCache.interestingNames; + const ranks = Ideogram.geneCache.interestingNames; return annots.map(annot => { if (ranks.includes(annot.name)) { diff --git a/src/js/ideogram.js b/src/js/ideogram.js index 25ae9ee8..caf191bf 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -72,6 +72,10 @@ import { drawPathway as _drawPathway } from './kit/pathway-viewer.js'; +import { + initCaches as _initCaches +} from './init/caches/cache'; + export default class Ideogram { constructor(config) { @@ -365,4 +369,17 @@ export default class Ideogram { dimensions=dimensions ); } + + /** + * Wrapper for initializing cached data + * + * @param {Object} config Includes organism, useCache, etc. + */ + static initCaches(config={ + organism: 'homo-sapiens', useCache: true, + awaitCache: true, + showGeneStructureInTooltip: true + }) { + _initCaches(config); + } } diff --git a/src/js/init/caches/cache.js b/src/js/init/caches/cache.js index 2476837e..3aef8ac1 100644 --- a/src/js/init/caches/cache.js +++ b/src/js/init/caches/cache.js @@ -45,9 +45,8 @@ import {parseVariantCacheIndex} from './variant-cache-worker'; * possible completely offline (i.e. a progressive web component) -- but only * once caches are populated. */ -export async function initCaches(ideo) { +export async function initCaches(config) { - const config = ideo.config; if (!config.useCache) return; const organism = config.organism; @@ -60,31 +59,31 @@ export async function initCaches(ideo) { // resolves a Promise, whereas the others return upon completing their // respective initializations. const cachePromise = Promise.all([ - cacheFactory('gene', organism, ideo, cacheDir), - cacheFactory('paralog', organism, ideo, cacheDir), - cacheFactory('interaction', organism, ideo, cacheDir), - cacheFactory('synonym', organism, ideo, cacheDir), + cacheFactory('gene', organism, config, cacheDir), + cacheFactory('paralog', organism, config, cacheDir), + cacheFactory('interaction', organism, config, cacheDir), + cacheFactory('synonym', organism, config, cacheDir), ]); if (config.showGeneStructureInTooltip) { - cacheFactory('geneStructure', organism, ideo, cacheDir); - cacheFactory('protein', organism, ideo, cacheDir); - cacheFactory('tissue', organism, ideo, cacheDir); - cacheFactory('variant', organism, ideo, cacheDir); + cacheFactory('geneStructure', organism, config, cacheDir); + cacheFactory('protein', organism, config, cacheDir); + cacheFactory('tissue', organism, config, cacheDir); + cacheFactory('variant', organism, config, cacheDir); } return cachePromise; } else { - cacheFactory('gene', organism, ideo, cacheDir); - cacheFactory('paralog', organism, ideo, cacheDir); - cacheFactory('interaction', organism, ideo, cacheDir); + cacheFactory('gene', organism, config, cacheDir); + cacheFactory('paralog', organism, config, cacheDir); + cacheFactory('interaction', organism, config, cacheDir); if (config.showGeneStructureInTooltip) { - cacheFactory('geneStructure', organism, ideo, cacheDir); - cacheFactory('protein', organism, ideo, cacheDir); - cacheFactory('synonym', organism, ideo, cacheDir); - cacheFactory('tissue', organism, ideo, cacheDir); - cacheFactory('variant', organism, ideo, cacheDir); + cacheFactory('geneStructure', organism, config, cacheDir); + cacheFactory('protein', organism, config, cacheDir); + cacheFactory('synonym', organism, config, cacheDir); + cacheFactory('tissue', organism, config, cacheDir); + cacheFactory('variant', organism, config, cacheDir); } } } @@ -140,14 +139,14 @@ const allCacheProps = { } }; -function setGeneCache(parsedCache, ideo) { +function setGeneCache(parsedCache) { const [ interestingNames, nameCaseMap, namesById, fullNamesById, idsByName, lociByName, lociById //, sortedAnnots ] = parsedCache; - ideo.geneCache = { + Ideogram.geneCache = { interestingNames, // Array ordered by general or scholarly interest nameCaseMap, // Maps of lowercase gene names to proper gene names namesById, @@ -159,46 +158,46 @@ function setGeneCache(parsedCache, ideo) { }; } -function setParalogCache(parsedCache, ideo) { +function setParalogCache(parsedCache) { const paralogsByName = parsedCache; // Array of paralog Ensembl IDs by (uppercase) gene name - ideo.paralogCache = {paralogsByName}; + Ideogram.paralogCache = {paralogsByName}; } -function setInteractionCache(parsedCache, ideo) { +function setInteractionCache(parsedCache) { const interactionsByName = parsedCache; - ideo.interactionCache = interactionsByName; + Ideogram.interactionCache = interactionsByName; } -function setGeneStructureCache(parsedCache, ideo) { +function setGeneStructureCache(parsedCache) { const featuresByGene = parsedCache; - ideo.geneStructureCache = featuresByGene; + Ideogram.geneStructureCache = featuresByGene; } -function setProteinCache(parsedCache, ideo) { - ideo.proteinCache = parsedCache; +function setProteinCache(parsedCache) { + Ideogram.proteinCache = parsedCache; } -function setSynonymCache(parsedCache, ideo) { - ideo.synonymCache = parsedCache; +function setSynonymCache(parsedCache) { + Ideogram.synonymCache = parsedCache; } -function setTissueCache(parsedCache, ideo) { - ideo.tissueCache = parsedCache; +function setTissueCache(parsedCache) { + Ideogram.tissueCache = parsedCache; } -function setVariantCache(parsedCache, ideo) { - ideo.variantCache = parsedCache; +function setVariantCache(parsedCache) { + Ideogram.variantCache = parsedCache; } -async function cacheFactory(cacheName, orgName, ideo, cacheDir=null) { +async function cacheFactory(cacheName, orgName, config, cacheDir=null) { const cacheProps = allCacheProps[cacheName]; - const debug = ideo.config.debug; + const debug = config.debug; /** - * Fetch cached gene data, transform it usefully, and set it as ideo prop - */ + * Fetch cached gene data, transform it usefully, and set it as Ideogram prop + */ const startTime = performance.now(); let perfTimes = {}; @@ -211,7 +210,6 @@ async function cacheFactory(cacheName, orgName, ideo, cacheDir=null) { // Skip initialization if cache is already populated if (Ideogram[staticProp] && Ideogram[staticProp][orgName]) { // Simplify chief use case, i.e. for single organism - ideo[staticProp] = Ideogram[staticProp][orgName]; return; } @@ -233,8 +231,8 @@ async function cacheFactory(cacheName, orgName, ideo, cacheDir=null) { // cacheWorker.postMessage(message); // cacheWorker.addEventListener('message', event => { // [parsedCache, perfTimes] = event.data; - cacheProps.fn(parsedCache, ideo, orgName); - Ideogram[staticProp][orgName] = ideo[staticProp]; + cacheProps.fn(parsedCache, orgName); + Ideogram[staticProp][orgName] = Ideogram[staticProp]; if (debug) { console.timeEnd(`${cacheName}Cache total`); diff --git a/src/js/init/caches/tissue-cache-worker.js b/src/js/init/caches/tissue-cache-worker.js index 53fe3a8b..6af399bc 100644 --- a/src/js/init/caches/tissue-cache-worker.js +++ b/src/js/init/caches/tissue-cache-worker.js @@ -27,16 +27,15 @@ function processIds(ids) { return processedIds; } -async function getTissueExpressions(gene, ideo) { - const cache = ideo.tissueCache; +async function getTissueExpressions(gene, config) { + const cache = Ideogram.tissueCache; const byteRange = cache.byteRangesByName[gene]; // Easier debuggability - if (!ideo.cacheRangeFetch) ideo.cacheRangeFetch = cacheRangeFetch; + if (!Ideogram.cacheRangeFetch) Ideogram.cacheRangeFetch = cacheRangeFetch; if (!byteRange) return null; - const config = ideo.config; let cacheDir = null; if (config.cacheDir) cacheDir = config.cacheDir; const cacheType = 'tissues'; diff --git a/src/js/init/caches/variant-cache-worker.js b/src/js/init/caches/variant-cache-worker.js index dd8157bc..d2aefe68 100644 --- a/src/js/init/caches/variant-cache-worker.js +++ b/src/js/init/caches/variant-cache-worker.js @@ -171,11 +171,11 @@ function parseVariant(line, variantCache) { async function getVariants(gene, ideo) { const variants = []; - const cache = ideo.variantCache; + const cache = Ideogram.variantCache; const byteRange = cache.byteRangesByName[gene]; // Easier debuggability - if (!ideo.cacheRangeFetch) ideo.cacheRangeFetch = cacheRangeFetch; + if (!Ideogram.cacheRangeFetch) Ideogram.cacheRangeFetch = cacheRangeFetch; if (!byteRange) return []; @@ -188,7 +188,7 @@ async function getVariants(gene, ideo) { const orgName = 'homo-sapiens'; const cacheUrl = getCacheUrl(orgName, cacheDir, cacheType, extension); - const geneLocus = ideo.geneCache.lociByName[gene]; + const geneLocus = Ideogram.geneCache.lociByName[gene]; // Get variant data only for the requested gene const data = await cacheRangeFetch(cacheUrl, byteRange); diff --git a/src/js/init/finish-init.js b/src/js/init/finish-init.js index 5fc182f0..7ccb7ecb 100644 --- a/src/js/init/finish-init.js +++ b/src/js/init/finish-init.js @@ -119,7 +119,7 @@ function finishInit(t0) { if (config.geometry === 'collinear') collinearizeChromosomes(ideo); if (ideo.config.debug) console.time('initCache: Ideogram'); - initCaches(ideo).then(() => { + initCaches(ideo.config).then(() => { if (ideo.config.debug) console.timeEnd('initCache: Ideogram'); if (ideo.onLoadCallback) ideo.onLoadCallback(); }); diff --git a/src/js/kit/gene-structure.js b/src/js/kit/gene-structure.js index a7157ad7..48fc368e 100644 --- a/src/js/kit/gene-structure.js +++ b/src/js/kit/gene-structure.js @@ -113,11 +113,11 @@ async function updateGeneStructure(ideo, offset=0) { const isCanonical = (selectedIndex === 0); const menu = document.querySelector('#_ideoGeneStructureMenu'); menu.options[selectedIndex].selected = true; - const svgResults = await getSvg(structure, ideo, ideo.spliceExons); + const svgResults = await getSvg(structure, ideo, Ideogram.spliceExons); const svg = svgResults[0]; const container = document.querySelector('._ideoGeneStructureSvgContainer'); container.innerHTML = svg; - updateHeader(ideo.spliceExons, isCanonical); + updateHeader(Ideogram.spliceExons, isCanonical); writeFooter(container); ideo.addedSubpartListeners = false; addHoverListeners(ideo); @@ -153,7 +153,7 @@ function getSelectedStructure(ideo, offset=0) { } const gene = getGeneFromStructureName(structureName); const geneStructure = - ideo.geneStructureCache[gene].find(gs => gs.name === structureName); + Ideogram.geneStructureCache[gene].find(gs => gs.name === structureName); return [geneStructure, selectedIndex]; @@ -527,7 +527,7 @@ function addHoverListeners(ideo) { ideo.oneTimeDelayTooltipHideMs = 2000; // wait 2.0 s instead of 0.25 s }); - if (ideo.tissueCache) { + if (Ideogram.tissueCache) { const tooltipFooter = document.querySelector('._ideoTooltipFooter'); tooltipFooter.style.display = 'none'; } @@ -535,7 +535,7 @@ function addHoverListeners(ideo) { container.addEventListener('mouseleave', (event) => { ideo.oneTimeDelayTooltipHideMs = 2000; // See "Without this..." note above - if (ideo.tissueCache) { + if (Ideogram.tissueCache) { const tooltipFooter = document.querySelector('._ideoTooltipFooter'); tooltipFooter.style.display = ''; } @@ -625,7 +625,7 @@ function getSpliceToggleHoverTitle(spliceExons) { } function getSpliceToggle(ideo) { - const spliceExons = ideo.spliceExons; + const spliceExons = Ideogram.spliceExons; const modifier = spliceExons ? '' : 'pre-'; const cls = `class="_ideoSpliceToggle ${modifier}mRNA"`; const checked = spliceExons ? 'checked' : ''; @@ -768,8 +768,8 @@ function updateHeader(spliceExons, isCanonical) { } async function toggleSplice(ideo) { - ideo.spliceExons = !ideo.spliceExons; - const spliceExons = ideo.spliceExons; + Ideogram.spliceExons = !Ideogram.spliceExons; + const spliceExons = Ideogram.spliceExons; const [geneStructure, selectedIndex] = getSelectedStructure(ideo); const isCanonical = (selectedIndex === 0); const svgResult = await getSvg(geneStructure, ideo, spliceExons); @@ -895,13 +895,13 @@ function getSubpartBorderLine(subpart) { // function getSvgList(gene, ideo, spliceExons=false) { // if ( -// 'geneStructureCache' in ideo === false || -// gene in ideo.geneStructureCache === false +// 'geneStructureCache' in Ideogram === false || +// gene in Ideogram.geneStructureCache === false // ) { // return [null]; // } -// const svgList = ideo.geneStructureCache[gene].map(geneStructure => { +// const svgList = Ideogram.geneStructureCache[gene].map(geneStructure => { // return getSvg(geneStructure, ideo, spliceExons); // }); @@ -1131,7 +1131,7 @@ function getMenu(gene, ideo, selectedName) { const containerId = '_ideoGeneStructureMenuContainer'; const style = 'margin-bottom: 4px; margin-top: 4px; clear: both;'; - const structures = ideo.geneStructureCache[gene]; + const structures = Ideogram.geneStructureCache[gene]; if (structures.length === 1) { const name = structures[0].name; @@ -1168,14 +1168,14 @@ export async function getGeneStructureHtml(annot, ideo, isParalogNeighborhood) { if ( ideo.config.showGeneStructureInTooltip && !isParalogNeighborhood && !( - 'geneStructureCache' in ideo === false || - gene in ideo.geneStructureCache === false + 'geneStructureCache' in Ideogram === false || + gene in Ideogram.geneStructureCache === false ) ) { ideo.addedSubpartListeners = false; - if ('spliceExons' in ideo === false) ideo.spliceExons = true; - const spliceExons = ideo.spliceExons; - const structure = ideo.geneStructureCache[gene][0]; + if ('spliceExons' in Ideogram === false) Ideogram.spliceExons = true; + const spliceExons = Ideogram.spliceExons; + const structure = Ideogram.geneStructureCache[gene][0]; const svgResults = await getSvg(structure, ideo, spliceExons); const geneStructureSvg = svgResults[0]; const cls = 'class="_ideoGeneStructureContainer"'; diff --git a/src/js/kit/protein.js b/src/js/kit/protein.js index b5bd885e..48577ba3 100644 --- a/src/js/kit/protein.js +++ b/src/js/kit/protein.js @@ -191,7 +191,7 @@ function isEligibleforProteinSvg(gene, ideo) { ideo.config.showProteinInTooltip && !( 'proteinCache' in ideo === false || - gene in ideo.proteinCache === false || + gene in Ideogram.proteinCache === false || ('spliceExons' in ideo === false || ideo.spliceExons === false) ) ); @@ -220,7 +220,7 @@ function getProteinRect(cds, hasTopology) { * Example: LDLR */ export function getHasTopology(gene, ideo) { - const hasTopology = ideo.proteinCache[gene]?.some(entry => { + const hasTopology = Ideogram.proteinCache[gene]?.some(entry => { return entry.protein.some( feature => isTopologyFeature(feature) ); @@ -238,7 +238,7 @@ export function getProtein( const isEligible = isEligibleforProteinSvg(gene, ideo); if (!isEligible) return ['
', null]; - const entry = ideo.proteinCache[gene].find(d => { + const entry = Ideogram.proteinCache[gene].find(d => { return d.transcriptName === structureName; }); if (!entry) return ['
', null]; diff --git a/src/js/kit/related-genes.js b/src/js/kit/related-genes.js index 42432e61..a7c9e85d 100644 --- a/src/js/kit/related-genes.js +++ b/src/js/kit/related-genes.js @@ -156,8 +156,8 @@ function maybeGeneSymbol(ixn, gene) { /** Reports if interaction node is a gene and not previously seen */ function isInteractionRelevant(rawIxn, gene, nameId, seenNameIds, ideo) { let isGeneSymbol; - if ('geneCache' in ideo && gene.name) { - isGeneSymbol = rawIxn.toLowerCase() in ideo.geneCache.nameCaseMap; + if ('geneCache' in Ideogram && gene.name) { + isGeneSymbol = rawIxn.toLowerCase() in Ideogram.geneCache.nameCaseMap; } else { isGeneSymbol = maybeGeneSymbol(rawIxn, gene); } @@ -186,9 +186,9 @@ async function fetchInteractions(gene, ideo) { let data = {result: []}; - if (ideo.interactionCache) { - if (upperGene in ideo.interactionCache) { - data = ideo.interactionCache[upperGene]; + if (Ideogram.interactionCache) { + if (upperGene in Ideogram.interactionCache) { + data = Ideogram.interactionCache[upperGene]; } } else { @@ -273,7 +273,7 @@ async function fetchInteractions(gene, ideo) { if (numIxns > limitIxns) { // Only show up to 20 interacting genes, // ordered by interest rank of interacting gene. - const ranks = ideo.geneCache.interestingNames.map(g => g.toLowerCase()); + const ranks = Ideogram.geneCache.interestingNames.map(g => g.toLowerCase()); const ixnGenes = Object.keys(ixns); const rankedIxnGenes = ixnGenes .map(gene => { @@ -437,27 +437,27 @@ function throwGeneNotFound(geneSymbol, ideo) { * E.g. getGeneBySynonym("p53", ideo) returns "TP53" */ function getGeneBySynonym(name, ideo) { - if (!ideo.synonymCache) return null; + if (!Ideogram.synonymCache) return null; const nameLc = name.toLowerCase(); - if (!ideo.synonymCache?.nameCaseMap) { + if (!Ideogram.synonymCache?.nameCaseMap) { // JIT initialization of canonicalized synonym lookup data. // Done only once. const nameCaseMap = {}; - for (const gene in ideo.synonymCache.byGene) { - const synonyms = ideo.synonymCache.byGene[gene]; + for (const gene in Ideogram.synonymCache.byGene) { + const synonyms = Ideogram.synonymCache.byGene[gene]; nameCaseMap[gene.toLowerCase()] = synonyms.map(s => s.toLowerCase()); } - ideo.synonymCache.nameCaseMap = nameCaseMap; + Ideogram.synonymCache.nameCaseMap = nameCaseMap; } - const nameCaseMap = ideo.synonymCache.nameCaseMap; + const nameCaseMap = Ideogram.synonymCache.nameCaseMap; for (const geneLc in nameCaseMap) { const synonymsLc = nameCaseMap[geneLc]; if (synonymsLc.includes(nameLc)) { // Got a hit! Return standard gene symbol, e.g. "tp53" -> "TP53". - return ideo.geneCache.nameCaseMap[geneLc]; + return Ideogram.geneCache.nameCaseMap[geneLc]; } } @@ -469,7 +469,7 @@ function getGeneBySynonym(name, ideo) { * Construct objects that match format of MyGene.info API response */ function fetchGenesFromCache(names, type, ideo) { - const cache = ideo.geneCache; + const cache = Ideogram.geneCache; const isSymbol = (type === 'symbol'); const locusMap = isSymbol ? cache.lociByName : cache.lociById; const nameMap = isSymbol ? cache.idsByName : cache.namesById; @@ -604,7 +604,7 @@ async function fetchGenes(names, type, ideo) { const queryStringBase = `?q=${qParam}&species=${taxid}&fields=`; - if (ideo.geneCache) { + if (Ideogram.geneCache) { const hits = fetchGenesFromCache(names, type, ideo); // Asynchronously fetch full name, but don't await the response, because @@ -692,6 +692,7 @@ async function fetchParalogPositionsFromMyGeneInfo( const annots = []; const cached = homologs.length && typeof homologs[0] === 'string'; + console.log('cached', cached) const ensemblIds = cached ? homologs : homologs.map(homolog => homolog.id); const data = await fetchGenes(ensemblIds, 'ensemblgene', ideo); @@ -705,7 +706,7 @@ async function fetchParalogPositionsFromMyGeneInfo( annots.push(annot); const description = - ideo.tissueCache ? '' : `Paralog of ${searchedGene.name}`; + Ideogram.tissueCache ? '' : `Paralog of ${searchedGene.name}`; const {name, ensemblId} = parseNameAndEnsemblIdFromMgiGene(gene); const type = 'paralogous gene'; @@ -810,11 +811,11 @@ function plotParalogNeighborhoods(annots, ideo) { annotStop = overlayAnnotLength; }; - if ('geneCache' in ideo) { + if ('geneCache' in Ideogram) { paralogs = paralogs.map(paralog => { - paralog.fullName = ideo.geneCache.fullNamesById[paralog.id]; + paralog.fullName = Ideogram.geneCache.fullNamesById[paralog.id]; - const ranks = ideo.geneCache.interestingNames; + const ranks = Ideogram.geneCache.interestingNames; if (ranks.includes(paralog.name)) { paralog.rank = ranks.indexOf(paralog.name) + 1; } else { @@ -861,14 +862,14 @@ async function fetchParalogs(annot, ideo) { let homologs; // Fetch paralogs - if (ideo.paralogCache) { + if (Ideogram.paralogCache) { // const baseUrl = 'http://localhost:8080/dist/data/cache/paralogs/'; // const url = `${baseUrl}homo-sapiens/${annot.name}.tsv`; // const response = await fetch(url); // const oneRowTsv = await response.text(); // const rawHomologEnsemblIds = oneRowTsv.split('\t'); // homologs = rawHomologEnsemblIds.map(r => getEnsemblId('ENSG', r)); - const paralogsByName = ideo.paralogCache.paralogsByName; + const paralogsByName = Ideogram.paralogCache.paralogsByName; const nameUc = annot.name.toUpperCase(); const hasParalogs = nameUc in paralogsByName; homologs = hasParalogs ? paralogsByName[nameUc] : []; @@ -1229,7 +1230,7 @@ function mergeDescriptions(annot, desc, ideo) { } }); // Object.assign({}, descriptions[annot.name]); - if ('type' in otherDesc && !ideo.tissueCache) { + if ('type' in otherDesc && !Ideogram.tissueCache) { mergedDesc.type += ', ' + otherDesc.type; mergedDesc.description += `

${otherDesc.description}`; } @@ -1261,39 +1262,43 @@ function mergeAnnots(unmergedAnnots) { return mergedAnnots; } +function hasTissueCache() { + return Ideogram.tissueCache && Object.keys(Ideogram.tissueCache).length > 0; +} + /** * Prevents bug when showing gene leads instantly on page load, * then hovering over an annotation, as in e.g. * https://eweitz.github.io/ideogram/gene-leads */ -function waitForTissueCache(geneNames, ideo, n) { +function waitForTissueCache(geneNames, config, n) { setTimeout(() => { if (n < 40) { // 40 * 50 ms = 2 s - if (!ideo.tissueCache) { - waitForTissueCache(geneNames, ideo, n + 1); + if (!hasTissueCache()) { + waitForTissueCache(geneNames, config, n + 1); } else { - setTissueExpressions(geneNames, ideo); + setTissueExpressions(geneNames, config); } } }, 50); } -async function setTissueExpressions(geneNames, ideo) { +async function setTissueExpressions(geneNames, config) { if ( - !ideo.tissueCache - // || !(annot.name in ideo.tissueCache.byteRangesByName) + !hasTissueCache() + // || !(annot.name in Ideogram.tissueCache.byteRangesByName) ) { - waitForTissueCache(geneNames, ideo, 0); + waitForTissueCache(geneNames, config, 0); return; } const tissueExpressionsByGene = {}; - const cache = ideo.tissueCache; + const cache = Ideogram.tissueCache; const promises = []; geneNames.forEach(async gene => { const promise = new Promise(async (resolve) => { - const tissueExpressions = await cache.getTissueExpressions(gene, ideo); + const tissueExpressions = await cache.getTissueExpressions(gene, config); tissueExpressionsByGene[gene] = tissueExpressions; resolve(); }); @@ -1302,7 +1307,7 @@ async function setTissueExpressions(geneNames, ideo) { await Promise.all(promises); - ideo.tissueExpressionsByGene = tissueExpressionsByGene; + Ideogram.tissueExpressionsByGene = tissueExpressionsByGene; } function onBeforeDrawAnnots() { @@ -1330,7 +1335,7 @@ function onBeforeDrawAnnots() { } } - setTissueExpressions(geneNames, ideo); + setTissueExpressions(geneNames, ideo.config); } function filterAndDrawAnnots(annots, ideo) { @@ -1672,7 +1677,7 @@ function centralizeTooltipPosition() { function onDidShowAnnotTooltip() { const ideo = this; - if (ideo.tissueCache) { + if (Ideogram.tissueCache) { centralizeTooltipPosition(); } handleTooltipClick(ideo); @@ -1843,7 +1848,7 @@ async function decorateAnnot(annot) { const queriedSynonym = descObj.synonym; const synStyle = 'style="font-style: italic"'; synonym = `
Synonym: ${queriedSynonym}
`; - // const synList = ideo.synonymCache.byGene[annot.name]; + // const synList = Ideogram.synonymCache.byGene[annot.name]; // const litSyns = synList.map(s => { // // Emphasize ("highlight") any synonyms that match the user's query // if (s.toLowerCase() === queriedSynonym.toLowerCase()) { diff --git a/src/js/kit/tissue.js b/src/js/kit/tissue.js index ad908c86..7227e2d6 100644 --- a/src/js/kit/tissue.js +++ b/src/js/kit/tissue.js @@ -464,7 +464,7 @@ function getMetricTicks(teObject, height) { function addDetailedCurve(traceDom, ideo) { const gene = traceDom.getAttribute('data-gene'); const tissue = traceDom.getAttribute('data-tissue'); - const tissueExpressions = ideo.tissueExpressionsByGene[gene]; + const tissueExpressions = Ideogram.tissueExpressionsByGene[gene]; let teObject = tissueExpressions.find(t => t.tissue === tissue); const maxWidthPx = 225; // Same width as RNA & protein diagrams @@ -580,7 +580,7 @@ function getExpressionPlotHtml(gene, tissueExpressions, ideo) { }).join(''); let containerStyle = 'style="margin-bottom: 30px;"'; - const hasStructure = gene in ideo.geneStructureCache; + const hasStructure = gene in Ideogram.geneStructureCache; if (!hasStructure) { // e.g. MALAT1 containerStyle = 'style="margin-bottom: 10px;"'; } @@ -605,7 +605,7 @@ function updateTissueExpressionPlot(ideo) { const plotParent = plot.parentElement; const gene = document.querySelector('#ideo-related-gene').innerText; - const tissueExpressions = ideo.tissueExpressionsByGene[gene]; + const tissueExpressions = Ideogram.tissueExpressionsByGene[gene]; const newPlotHtml = getExpressionPlotHtml(gene, tissueExpressions, ideo); @@ -665,7 +665,7 @@ function focusMiniCurve(traceDom, ideo, reset=false) { const refTissue = reset ? null : traceDom.getAttribute('data-tissue'); const numTissues = !ideo.showTissuesMore ? 10 : 3; - let tissueExpressions = ideo.tissueExpressionsByGene[gene]; + let tissueExpressions = Ideogram.tissueExpressionsByGene[gene]; const maxPx = MINI_CURVE_WIDTH; const relative = true; @@ -709,7 +709,10 @@ function focusMiniCurve(traceDom, ideo, reset=false) { } export function getTissueHtml(annot, ideo) { - if (!ideo.tissueCache || !(annot.name in ideo.tissueCache.byteRangesByName)) { + if ( + !Ideogram.tissueCache || + !(annot.name in Ideogram.tissueCache.byteRangesByName) + ) { // e.g. MIR23A return '
'; } @@ -719,7 +722,7 @@ export function getTissueHtml(annot, ideo) { } const gene = annot.name; - const tissueExpressions = ideo.tissueExpressionsByGene[gene]; + const tissueExpressions = Ideogram.tissueExpressionsByGene[gene]; if (!tissueExpressions) return; const tissueHtml = getExpressionPlotHtml(gene, tissueExpressions, ideo); diff --git a/src/js/kit/variant.js b/src/js/kit/variant.js index 18654dcd..78a66f57 100644 --- a/src/js/kit/variant.js +++ b/src/js/kit/variant.js @@ -326,7 +326,7 @@ export async function getVariantsSvg( const gene = getGeneFromStructureName(structureName, ideo); - const cache = ideo.variantCache; + const cache = Ideogram.variantCache; let rawVariants = await cache.getVariants(gene, ideo); diff --git a/src/js/kit/wikipathways.js b/src/js/kit/wikipathways.js index 67c28d21..a14e440b 100644 --- a/src/js/kit/wikipathways.js +++ b/src/js/kit/wikipathways.js @@ -425,7 +425,7 @@ export async function fetchPathwayInteractions(searchedGene, pathwayId, ideo) { nodes.forEach(node => { const label = node.getAttribute('TextLabel'); const normLabel = label.toLowerCase(); - const isKnownGene = normLabel in ideo.geneCache.nameCaseMap; + const isKnownGene = normLabel in Ideogram.geneCache.nameCaseMap; if (isKnownGene) { genes[label] = 1; } From 33b98f6dfd822ce9c7a463cf8b055d8ba8b3b76d Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Mon, 24 Feb 2025 21:11:39 -0500 Subject: [PATCH 03/19] Add pathway description below diagram --- src/js/kit/pathway-viewer.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 8ec3e00a..50617100 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -13,7 +13,7 @@ async function fetchPathwayViewerJson(pwId) { const response = await fetch(url); const pathwayJson = await response.json(); - window.pathwayJson = pathwayJson; + Ideogram.pathwayJson = pathwayJson; return pathwayJson; } @@ -149,6 +149,20 @@ function addHeader(pwId, pathwayJson, pathwayContainer) { }); } +function formatDescription(rawText) { + return rawText.replaceAll('\r\n', '

'); +} + +function addFooter(pathwayJson, pathwayContainer) { + const rawText = + pathwayJson.pathway.comments.filter( + c => c.source === 'WikiPathways-description' + )[0].content; + const descriptionText = formatDescription(rawText); + const description = `
${descriptionText}
`; + pathwayContainer.insertAdjacentHTML('beforeEnd', description); +} + /** Fetch and render WikiPathways diagram for given pathway ID */ export async function drawPathway( pwId, sourceGene, destGene, @@ -239,6 +253,8 @@ export async function drawPathway( const pathwayViewer = new Pvjs(pvjsContainer, pvjsProps); addHeader(pwId, pathwayJson, pathwayContainer); + addFooter(pathwayJson, pathwayContainer); + // zoomToEntity(sourceEntityId); const detail = { From be39c88d415af0752b4fa9cb92294753b158a940 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Mon, 24 Feb 2025 22:13:05 -0500 Subject: [PATCH 04/19] Improve pathway description format --- src/js/kit/pathway-viewer.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 50617100..495a12ca 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -149,8 +149,28 @@ function addHeader(pwId, pathwayJson, pathwayContainer) { }); } +function removeCptacAssayPortalClause(inputText) { + const regex = /Proteins on this pathway have targeted assays available via the \[https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\s+CPTAC Assay Portal\]/g; + return inputText.replace(regex, ''); +} + +function convertMediaWikiLinks(inputText) { + // Regular expression to match the MediaWiki link format + const regex = /\[([^\s]+)\s+([^\]]+)\]/g; + + // Replace the MediaWiki link format with an HTML anchor tag + return inputText.replace(regex, (match, url, text) => { + return `${text}`; + }); +} + function formatDescription(rawText) { - return rawText.replaceAll('\r\n', '

'); + rawText = rawText.replaceAll('\r\n\r\n', '\r\n'); + rawText = rawText.replaceAll('\r\n', '

'); + const denoisedText = removeCptacAssayPortalClause(rawText); + const linkedText = convertMediaWikiLinks(denoisedText); + const trimmedText = linkedText.trim(); + return trimmedText; } function addFooter(pathwayJson, pathwayContainer) { From 091f73e670b5a48b4076dde1799b6133a4bae668 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Tue, 25 Feb 2025 07:15:54 -0500 Subject: [PATCH 05/19] Add pathway annotations to footer --- src/js/kit/pathway-viewer.js | 79 +++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 495a12ca..08415319 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -173,14 +173,81 @@ function formatDescription(rawText) { return trimmedText; } -function addFooter(pathwayJson, pathwayContainer) { +function getDescription(pathwayJson) { const rawText = - pathwayJson.pathway.comments.filter( - c => c.source === 'WikiPathways-description' - )[0].content; + pathwayJson.pathway.comments.filter( + c => c.source === 'WikiPathways-description' + )[0].content; const descriptionText = formatDescription(rawText); - const description = `
${descriptionText}
`; - pathwayContainer.insertAdjacentHTML('beforeEnd', description); + + const style = `style="font-weight: bold"`; + + const description = + `
` + + // `
Description
` + + descriptionText + + `
`; + + return description; +} + +function parsePwAnnotations(entitiesById, keys, ontology) { + const pwKeys = keys.filter(k => entitiesById[k].ontology === ontology); + const pwAnnotations = pwKeys.map(k => entitiesById[k]); + return pwAnnotations; +} + +function getPathwayAnnotations(pathwayJson) { + const entitiesById = pathwayJson.entitiesById; + const keys = Object.keys(entitiesById).filter(k => k.startsWith('http://identifiers.org')); + const sentenceCases = { + 'Cell Type': 'Cell type' + } + const ontologies = [ + 'Cell Type' + // 'Disease', 'Pathway Ontology' // maybe later + ]; + const pathwayAnnotationsList = ontologies.map(ontology => { + const pwAnnotations = parsePwAnnotations(entitiesById, keys, ontology); + const links = pwAnnotations.map(pwa => { + const id = pwa.xrefIdentifier.replace(':', '_'); + const url = `https://purl.obolibrary.org/obo/${id}`; + return `${pwa.term}`; + }).join(', '); + + const refinedOntology = sentenceCases[ontology]; + const safeOntology = ontology.replaceAll(' ', '_'); + const cls = `class="ideoPathwayOntology__${safeOntology}"`; + + return `
${refinedOntology}: ${links}
`; + }); + + if (pathwayAnnotationsList.length === 0) { + return ''; + } + + const style = `style="font-weight: bold"`; + + const pathwayAnnotations = + `
` + + // `
Pathway annotations
` + + pathwayAnnotationsList + + `
`; + + return pathwayAnnotations; +} + +function addFooter(pathwayJson, pathwayContainer) { + const description = getDescription(pathwayJson); + const pathwayAnnotations = getPathwayAnnotations(pathwayJson); + const footer = + `
` + + `
` + + description + + `
` + + pathwayAnnotations + + `
`; + pathwayContainer.insertAdjacentHTML('beforeEnd', footer); } /** Fetch and render WikiPathways diagram for given pathway ID */ From 4fe5487a88500981383687c3cf8c292e70747aa7 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Tue, 25 Feb 2025 07:30:32 -0500 Subject: [PATCH 06/19] Make close button optional for pathway diagram --- src/js/ideogram.js | 6 ++++-- src/js/kit/pathway-viewer.js | 41 ++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/js/ideogram.js b/src/js/ideogram.js index caf191bf..290a2bfd 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -361,12 +361,14 @@ export default class Ideogram { static drawPathway( pwId, sourceGene, destGene, outerSelector, - dimensions={height: 440, width: 900} + dimensions={height: 440, width: 900}, + showClose=true ) { _drawPathway( pwId, sourceGene, destGene, outerSelector, - dimensions=dimensions + dimensions=dimensions, + showClose=showClose ); } diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 08415319..50e5a528 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -122,7 +122,7 @@ function zoomToEntity(entityId, retryAttempt=0) { } /** Add header bar to pathway diagram with name, link, close button, etc. */ -function addHeader(pwId, pathwayJson, pathwayContainer) { +function addHeader(pwId, pathwayJson, pathwayContainer, showClose=true) { const pathwayName = pathwayJson.pathway.name; const url = `https://wikipathways.org/pathways/${pwId}`; const linkAttrs = `href="${url}" target="_blank" style="margin-left: 4px;"`; @@ -130,23 +130,30 @@ function addHeader(pwId, pathwayJson, pathwayContainer) { // Link to full page on WikiPathways, using pathway title const pathwayLink = `${pathwayName}`; - // Close button - const style = - 'style="float: right; background-color: #aaa; border: none; ' + - 'color: white; font-weight: bold; font-size: 16px; padding: 0px 4px; ' + - 'border-radius: 3px; cursor: pointer;"'; - const buttonAttrs = `class="_ideoPathwayCloseButton" ${style}`; - const closeButton = ``; + let closeButton; + if (showClose) { + // Close button + const style = + 'style="float: right; background-color: #aaa; border: none; ' + + 'color: white; font-weight: bold; font-size: 16px; padding: 0px 4px; ' + + 'border-radius: 3px; cursor: pointer;"'; + const buttonAttrs = `class="_ideoPathwayCloseButton" ${style}`; + closeButton = ``; + } else { + closeButton = ''; + } const headerBar = `
${pathwayLink}${closeButton}
`; pathwayContainer.insertAdjacentHTML('afterBegin', headerBar); - const closeButtonDom = document.querySelector('._ideoPathwayCloseButton'); - closeButtonDom.addEventListener('click', function(event) { - const pathwayContainer = document.querySelector(`#${CONTAINER_ID}`); - pathwayContainer.remove(); - }); + if (showClose) { + const closeButtonDom = document.querySelector('._ideoPathwayCloseButton'); + closeButtonDom.addEventListener('click', function(event) { + const pathwayContainer = document.querySelector(`#${CONTAINER_ID}`); + pathwayContainer.remove(); + }); + } } function removeCptacAssayPortalClause(inputText) { @@ -254,7 +261,9 @@ function addFooter(pathwayJson, pathwayContainer) { export async function drawPathway( pwId, sourceGene, destGene, outerSelector='#_ideogramOuterWrap', - dimensions={height: 440, width: 900}, retryAttempt=0 + dimensions={height: 440, width: 900}, + showClose=true, + retryAttempt=0 ) { const pvjsScript = document.querySelector(`script[src="${PVJS_URL}"]`); if (!pvjsScript) {loadPvjsScript();} @@ -274,7 +283,7 @@ export async function drawPathway( setTimeout(() => { drawPathway( pwId, sourceGene, destGene, - outerSelector, dimensions, retryAttempt++ + outerSelector, dimensions, showClose, retryAttempt++ ); }, 250); return; @@ -338,7 +347,7 @@ export async function drawPathway( // const pathwayViewer = new Pvjs(pvjsProps); const pathwayViewer = new Pvjs(pvjsContainer, pvjsProps); - addHeader(pwId, pathwayJson, pathwayContainer); + addHeader(pwId, pathwayJson, pathwayContainer, showClose); addFooter(pathwayJson, pathwayContainer); From 1404f3bef5d5aefc33b63253ca3cd68ecdda373d Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Fri, 7 Mar 2025 08:33:44 -0500 Subject: [PATCH 07/19] Remove Markdown CPTAC link, omit blank pathway annotations --- src/js/kit/pathway-viewer.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 50e5a528..1ca60266 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -158,7 +158,9 @@ function addHeader(pwId, pathwayJson, pathwayContainer, showClose=true) { function removeCptacAssayPortalClause(inputText) { const regex = /Proteins on this pathway have targeted assays available via the \[https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\s+CPTAC Assay Portal\]/g; - return inputText.replace(regex, ''); + const regex2 = /Proteins on this pathway have targeted assays available via the \[CPTAC Assay Portal\]\(https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\)/g; + + return inputText.replace(regex, '').replace(regex2, ''); } function convertMediaWikiLinks(inputText) { @@ -209,7 +211,7 @@ function getPathwayAnnotations(pathwayJson) { const keys = Object.keys(entitiesById).filter(k => k.startsWith('http://identifiers.org')); const sentenceCases = { 'Cell Type': 'Cell type' - } + }; const ontologies = [ 'Cell Type' // 'Disease', 'Pathway Ontology' // maybe later @@ -226,6 +228,8 @@ function getPathwayAnnotations(pathwayJson) { const safeOntology = ontology.replaceAll(' ', '_'); const cls = `class="ideoPathwayOntology__${safeOntology}"`; + if (links === '') return ''; + return `
${refinedOntology}: ${links}
`; }); From aa51e31d5b04cde796052ee2d8c567631807480d Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Mon, 10 Mar 2025 22:37:37 -0400 Subject: [PATCH 08/19] Convert Markdown rather than MediaWiki --- package-lock.json | 11 +++++++++++ package.json | 3 ++- src/js/kit/pathway-viewer.js | 21 +++++++++++++-------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ece2f589..0a338592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "d3-scale": "^4.0.2", "fast-kde": "0.2.1", "fflate": "^0.7.3", + "snarkdown": "^2.0.0", "tippy.js": "6.3.7", "workbox-range-requests": "7.0.0" }, @@ -9268,6 +9269,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/snarkdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/snarkdown/-/snarkdown-2.0.0.tgz", + "integrity": "sha512-MgL/7k/AZdXCTJiNgrO7chgDqaB9FGM/1Tvlcenenb7div6obaDATzs16JhFyHHBGodHT3B7RzRc5qk8pFhg3A==" + }, "node_modules/socket.io": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", @@ -18001,6 +18007,11 @@ } } }, + "snarkdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/snarkdown/-/snarkdown-2.0.0.tgz", + "integrity": "sha512-MgL/7k/AZdXCTJiNgrO7chgDqaB9FGM/1Tvlcenenb7div6obaDATzs16JhFyHHBGodHT3B7RzRc5qk8pFhg3A==" + }, "socket.io": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", diff --git a/package.json b/package.json index 32aee1a0..274bec46 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "fast-kde": "0.2.1", "fflate": "^0.7.3", "tippy.js": "6.3.7", - "workbox-range-requests": "7.0.0" + "workbox-range-requests": "7.0.0", + "snarkdown": "^2.0.0" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 1ca60266..ab51c22d 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -1,3 +1,5 @@ +import snarkdown from 'snarkdown'; + const PVJS_URL = 'https://cdn.jsdelivr.net/npm/@wikipathways/pvjs@5.0.1/dist/pvjs.vanilla.js'; const SVGPANZOOM_URL = 'https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js'; const CONTAINER_ID = '_ideogramPathwayContainer'; @@ -163,21 +165,24 @@ function removeCptacAssayPortalClause(inputText) { return inputText.replace(regex, '').replace(regex2, ''); } -function convertMediaWikiLinks(inputText) { - // Regular expression to match the MediaWiki link format - const regex = /\[([^\s]+)\s+([^\]]+)\]/g; +/** Convert Markdown links to standard ${text}`; - }); + const htmlWithClassedLinks = + html.replace( + //g, + '' + ); + + return htmlWithClassedLinks; } function formatDescription(rawText) { rawText = rawText.replaceAll('\r\n\r\n', '\r\n'); rawText = rawText.replaceAll('\r\n', '

'); const denoisedText = removeCptacAssayPortalClause(rawText); - const linkedText = convertMediaWikiLinks(denoisedText); + const linkedText = convertMarkdownLinks(denoisedText); const trimmedText = linkedText.trim(); return trimmedText; } From 1f3e9462ac6ded4d25c692042f84884c360da1e7 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Mon, 10 Mar 2025 23:39:29 -0400 Subject: [PATCH 09/19] Open pathway description links in new tab --- src/js/kit/pathway-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index ab51c22d..082b7d34 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -172,7 +172,7 @@ function convertMarkdownLinks(markdown) { const htmlWithClassedLinks = html.replace( /
/g, - '' + '' ); return htmlWithClassedLinks; From 4f5bb37e86e8c2e671c954c97df8f257ebae845d Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Thu, 13 Mar 2025 20:25:00 -0400 Subject: [PATCH 10/19] Omit boilerplate PhosPhoSite text in pathway description --- src/js/kit/pathway-viewer.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 082b7d34..c5a773eb 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -158,13 +158,26 @@ function addHeader(pwId, pathwayJson, pathwayContainer, showClose=true) { } } +/** + * + */ function removeCptacAssayPortalClause(inputText) { - const regex = /Proteins on this pathway have targeted assays available via the \[https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\s+CPTAC Assay Portal\]/g; - const regex2 = /Proteins on this pathway have targeted assays available via the \[CPTAC Assay Portal\]\(https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\)/g; + // eslint-disable-next-line max-len + const regex = /Proteins on this pathway have targeted assays available via the \[https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\s+CPTAC Assay Portal\]\./g; + // eslint-disable-next-line max-len + const regex2 = /Proteins on this pathway have targeted assays available via the \[CPTAC Assay Portal\]\(https:\/\/assays\.cancer\.gov\/available_assays\?wp_id=WP\d+\)\./g; return inputText.replace(regex, '').replace(regex2, ''); } + +function removePhosphoSitePlusClause(inputText) { + // eslint-disable-next-line max-len + const regex = 'Phosphorylation sites were added based on information from PhosphoSitePlus (R), www.phosphosite.org.'; + + return inputText.replace(regex, ''); +} + /** Convert Markdown links to standard Date: Sat, 15 Mar 2025 10:09:47 -0400 Subject: [PATCH 12/19] Add disease to pathway annotations --- src/js/kit/pathway-viewer.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index c5a773eb..dead9ef0 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -232,8 +232,9 @@ function getPathwayAnnotations(pathwayJson) { 'Cell Type': 'Cell type' }; const ontologies = [ - 'Cell Type' - // 'Disease', 'Pathway Ontology' // maybe later + 'Cell Type', + 'Disease' + // 'Pathway Ontology' // maybe later ]; const pathwayAnnotationsList = ontologies.map(ontology => { const pwAnnotations = parsePwAnnotations(entitiesById, keys, ontology); @@ -243,14 +244,14 @@ function getPathwayAnnotations(pathwayJson) { return `${pwa.term}`; }).join(', '); - const refinedOntology = sentenceCases[ontology]; + const refinedOntology = sentenceCases[ontology] ?? ontology; const safeOntology = ontology.replaceAll(' ', '_'); const cls = `class="ideoPathwayOntology__${safeOntology}"`; if (links === '') return ''; return `
${refinedOntology}: ${links}
`; - }); + }).join(''); if (pathwayAnnotationsList.length === 0) { return ''; From ccd6d1b55f725e7438a428173242abe6ab886ae3 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Sat, 15 Mar 2025 11:54:26 -0400 Subject: [PATCH 13/19] Enable getting simple list of genes in pathway --- src/js/ideogram.js | 12 +++++++++++- src/js/kit/pathway-viewer.js | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/js/ideogram.js b/src/js/ideogram.js index 290a2bfd..0e4b2186 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -69,7 +69,8 @@ import { } from './kit/related-genes'; import { - drawPathway as _drawPathway + drawPathway as _drawPathway, + getPathwayGenes as _getPathwayGenes } from './kit/pathway-viewer.js'; import { @@ -384,4 +385,13 @@ export default class Ideogram { }) { _initCaches(config); } + + /** + * Get list of gene names in pathway + * + * @param {Object} config Includes organism, useCache, etc. + */ + static getPathwayGenes() { + return _getPathwayGenes(); + } } diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index dead9ef0..41ddd3df 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -268,6 +268,17 @@ function getPathwayAnnotations(pathwayJson) { return pathwayAnnotations; } +/** Get list of unique genes in pathway */ +export function getPathwayGenes() { + const entities = Object.values(Ideogram.pathwayJson.entitiesById); + const genes = entities.filter(entity => { + return ['GeneProduct', 'RNA', 'Protein'].includes(entity.wpType); + }).map(e => e.textContent); + const uniqueGenes = Array.from(new Set(genes)); + return uniqueGenes; +} + + function addFooter(pathwayJson, pathwayContainer) { const description = getDescription(pathwayJson); const pathwayAnnotations = getPathwayAnnotations(pathwayJson); From c9a0f0ce762f03d136b3a44eec81fa6efe357af4 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Sat, 15 Mar 2025 18:29:25 -0400 Subject: [PATCH 14/19] Show customizable tooltip on pathway gene node hover --- src/js/kit/pathway-viewer.js | 33 ++++++++++++++++++++++++++++++++- src/js/kit/related-genes.js | 7 ++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 41ddd3df..39215e14 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -1,4 +1,7 @@ import snarkdown from 'snarkdown'; +import tippy from 'tippy.js'; +import {getTippyConfig} from '../lib'; +import { tippyLightCss } from './tippy-styles'; const PVJS_URL = 'https://cdn.jsdelivr.net/npm/@wikipathways/pvjs@5.0.1/dist/pvjs.vanilla.js'; const SVGPANZOOM_URL = 'https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js'; @@ -292,12 +295,16 @@ function addFooter(pathwayJson, pathwayContainer) { pathwayContainer.insertAdjacentHTML('beforeEnd', footer); } +function addPathwayNodeListeners() { +} + /** Fetch and render WikiPathways diagram for given pathway ID */ export async function drawPathway( pwId, sourceGene, destGene, outerSelector='#_ideogramOuterWrap', dimensions={height: 440, width: 900}, showClose=true, + nodeHoverFn, retryAttempt=0 ) { const pvjsScript = document.querySelector(`script[src="${PVJS_URL}"]`); @@ -318,7 +325,8 @@ export async function drawPathway( setTimeout(() => { drawPathway( pwId, sourceGene, destGene, - outerSelector, dimensions, showClose, retryAttempt++ + outerSelector, dimensions, showClose, nodeHoverFn, + retryAttempt++ ); }, 250); return; @@ -396,4 +404,27 @@ export async function drawPathway( // Notify listeners of event completion const ideogramPathwayEvent = new CustomEvent('ideogramDrawPathway', {detail}); document.dispatchEvent(ideogramPathwayEvent); + + const css = + ``; + pvjsContainer.insertAdjacentHTML('afterBegin', css); + + pathwayContainer.querySelectorAll('g.GeneProduct').forEach(g => { + const geneName = g.getAttribute('name'); + let tooltipContent = geneName; + g.addEventListener('mouseover', (event) => { + if (nodeHoverFn) { + tooltipContent = nodeHoverFn(geneName); + g.setAttribute('data-tippy-content', tooltipContent); + tippy('g.GeneProduct[data-tippy-content]', tippyConfig); + } + }); + + g.setAttribute(`data-tippy-content`, tooltipContent); + }); + + const tippyConfig = getTippyConfig(); + tippy('g.GeneProduct[data-tippy-content]', tippyConfig); } diff --git a/src/js/kit/related-genes.js b/src/js/kit/related-genes.js index 86fd51f4..7e0b4f1c 100644 --- a/src/js/kit/related-genes.js +++ b/src/js/kit/related-genes.js @@ -1657,7 +1657,12 @@ function addPathwayListeners(ideo) { // const pathwayName = target.getAttribute('data-pathway-name'); // const pathway = {id: pathwayId, name: pathwayName}; // plotPathwayGenes(searchedGene, pathway, ideo); - drawPathway(pathwayId, searchedGene, interactingGene); + function nodeHoverFn(geneName) { + console.log('in nodeHoverFn') + return '
ok ' + geneName + '
1234
'; + } + drawPathway(pathwayId, searchedGene, interactingGene, + undefined, undefined, undefined, nodeHoverFn); event.stopPropagation(); }); }); From 45283b8208b17668a91f764073295bbb28fbc643 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Sun, 16 Mar 2025 08:43:13 -0400 Subject: [PATCH 15/19] Update public function signature for drawPathway --- src/js/ideogram.js | 8 ++++++-- src/js/kit/pathway-viewer.js | 3 --- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/js/ideogram.js b/src/js/ideogram.js index 0e4b2186..067429f6 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -358,18 +358,22 @@ export default class Ideogram { * @param {String} destGene Symbol of destination gene, e.g. "PCSK9" * @param {String} outerSelector DOM selector of container, e.g. "#my-diagram" * @param {Object} dimensions Height and width of pathway diagram + * @param {Boolean} showClose Whether to show close button + * @param {Function} nodeHoverFn Function to call upon hovering diagram node */ static drawPathway( pwId, sourceGene, destGene, outerSelector, dimensions={height: 440, width: 900}, - showClose=true + showClose=true, + nodeHoverFn=undefined ) { _drawPathway( pwId, sourceGene, destGene, outerSelector, dimensions=dimensions, - showClose=showClose + showClose=showClose, + nodeHoverFn=nodeHoverFn ); } diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 39215e14..a0898c56 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -295,9 +295,6 @@ function addFooter(pathwayJson, pathwayContainer) { pathwayContainer.insertAdjacentHTML('beforeEnd', footer); } -function addPathwayNodeListeners() { -} - /** Fetch and render WikiPathways diagram for given pathway ID */ export async function drawPathway( pwId, sourceGene, destGene, From 7af537527537a58bd02d4f4cd79cc39ae8c40a0c Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Sun, 16 Mar 2025 08:53:41 -0400 Subject: [PATCH 16/19] Add event to nodeHoverFn signature --- src/js/kit/pathway-viewer.js | 3 +-- src/js/kit/related-genes.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index a0898c56..97bf65dc 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -409,11 +409,10 @@ export async function drawPathway( pvjsContainer.insertAdjacentHTML('afterBegin', css); pathwayContainer.querySelectorAll('g.GeneProduct').forEach(g => { - const geneName = g.getAttribute('name'); let tooltipContent = geneName; g.addEventListener('mouseover', (event) => { if (nodeHoverFn) { - tooltipContent = nodeHoverFn(geneName); + tooltipContent = nodeHoverFn(event, geneName); g.setAttribute('data-tippy-content', tooltipContent); tippy('g.GeneProduct[data-tippy-content]', tippyConfig); } diff --git a/src/js/kit/related-genes.js b/src/js/kit/related-genes.js index 7e0b4f1c..c0dc9027 100644 --- a/src/js/kit/related-genes.js +++ b/src/js/kit/related-genes.js @@ -1657,7 +1657,7 @@ function addPathwayListeners(ideo) { // const pathwayName = target.getAttribute('data-pathway-name'); // const pathway = {id: pathwayId, name: pathwayName}; // plotPathwayGenes(searchedGene, pathway, ideo); - function nodeHoverFn(geneName) { + function nodeHoverFn(event, geneName) { console.log('in nodeHoverFn') return '
ok ' + geneName + '
1234
'; } From 05d8df6c9db46b2ee50cd506f81b98906b690c66 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Sun, 16 Mar 2025 08:59:25 -0400 Subject: [PATCH 17/19] Restore gene name parameter in nodeHoverFn --- src/js/kit/pathway-viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 97bf65dc..1f3af6cc 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -409,6 +409,7 @@ export async function drawPathway( pvjsContainer.insertAdjacentHTML('afterBegin', css); pathwayContainer.querySelectorAll('g.GeneProduct').forEach(g => { + const geneName = g.getAttribute('name'); let tooltipContent = geneName; g.addEventListener('mouseover', (event) => { if (nodeHoverFn) { From eeaa5881a2db3cb0e378d3ba0a06cceb05eaa4ed Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Mon, 17 Mar 2025 07:51:10 -0400 Subject: [PATCH 18/19] Use default tippy theme --- src/js/kit/pathway-viewer.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index 1f3af6cc..b3de7d38 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -1,7 +1,6 @@ import snarkdown from 'snarkdown'; import tippy from 'tippy.js'; import {getTippyConfig} from '../lib'; -import { tippyLightCss } from './tippy-styles'; const PVJS_URL = 'https://cdn.jsdelivr.net/npm/@wikipathways/pvjs@5.0.1/dist/pvjs.vanilla.js'; const SVGPANZOOM_URL = 'https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js'; @@ -402,12 +401,6 @@ export async function drawPathway( const ideogramPathwayEvent = new CustomEvent('ideogramDrawPathway', {detail}); document.dispatchEvent(ideogramPathwayEvent); - const css = - ``; - pvjsContainer.insertAdjacentHTML('afterBegin', css); - pathwayContainer.querySelectorAll('g.GeneProduct').forEach(g => { const geneName = g.getAttribute('name'); let tooltipContent = geneName; From a23101ae5cf7409133d399f9b3c841b5ca8cd676 Mon Sep 17 00:00:00 2001 From: Eric Weitz Date: Tue, 18 Mar 2025 07:52:53 -0400 Subject: [PATCH 19/19] Enable adding click handlers to pathway nodes --- src/js/ideogram.js | 8 ++++--- src/js/kit/pathway-viewer.js | 45 +++++++++++++++++++++++++++--------- src/js/kit/related-genes.js | 14 ++++++++--- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/js/ideogram.js b/src/js/ideogram.js index 067429f6..2afc1368 100644 --- a/src/js/ideogram.js +++ b/src/js/ideogram.js @@ -359,21 +359,23 @@ export default class Ideogram { * @param {String} outerSelector DOM selector of container, e.g. "#my-diagram" * @param {Object} dimensions Height and width of pathway diagram * @param {Boolean} showClose Whether to show close button - * @param {Function} nodeHoverFn Function to call upon hovering diagram node + * @param {Function} geneNodeHoverFn Function to call upon hovering diagram node */ static drawPathway( pwId, sourceGene, destGene, outerSelector, dimensions={height: 440, width: 900}, showClose=true, - nodeHoverFn=undefined + geneNodeHoverFn=undefined, + pathwayNodeClickFn=undefined ) { _drawPathway( pwId, sourceGene, destGene, outerSelector, dimensions=dimensions, showClose=showClose, - nodeHoverFn=nodeHoverFn + geneNodeHoverFn=geneNodeHoverFn, + pathwayNodeClickFn=pathwayNodeClickFn ); } diff --git a/src/js/kit/pathway-viewer.js b/src/js/kit/pathway-viewer.js index b3de7d38..37f72731 100644 --- a/src/js/kit/pathway-viewer.js +++ b/src/js/kit/pathway-viewer.js @@ -300,7 +300,8 @@ export async function drawPathway( outerSelector='#_ideogramOuterWrap', dimensions={height: 440, width: 900}, showClose=true, - nodeHoverFn, + geneNodeHoverFn, + pathwayNodeClickFn, retryAttempt=0 ) { const pvjsScript = document.querySelector(`script[src="${PVJS_URL}"]`); @@ -321,7 +322,8 @@ export async function drawPathway( setTimeout(() => { drawPathway( pwId, sourceGene, destGene, - outerSelector, dimensions, showClose, nodeHoverFn, + outerSelector, dimensions, showClose, + geneNodeHoverFn, pathwayNodeClickFn, retryAttempt++ ); }, 250); @@ -401,20 +403,41 @@ export async function drawPathway( const ideogramPathwayEvent = new CustomEvent('ideogramDrawPathway', {detail}); document.dispatchEvent(ideogramPathwayEvent); - pathwayContainer.querySelectorAll('g.GeneProduct').forEach(g => { - const geneName = g.getAttribute('name'); + // Add mouseover handler to gene nodes in this pathway diagram + pathwayContainer.querySelectorAll('g.GeneProduct').forEach(geneNode => { + const geneName = geneNode.getAttribute('name'); let tooltipContent = geneName; - g.addEventListener('mouseover', (event) => { - if (nodeHoverFn) { - tooltipContent = nodeHoverFn(event, geneName); - g.setAttribute('data-tippy-content', tooltipContent); - tippy('g.GeneProduct[data-tippy-content]', tippyConfig); + geneNode.addEventListener('mouseover', (event) => { + if (geneNodeHoverFn) { + tooltipContent = geneNodeHoverFn(event, geneName); + geneNode.setAttribute('data-tippy-content', tooltipContent); } }); - g.setAttribute(`data-tippy-content`, tooltipContent); + geneNode.setAttribute(`data-tippy-content`, tooltipContent); }); - const tippyConfig = getTippyConfig(); tippy('g.GeneProduct[data-tippy-content]', tippyConfig); + + // Add click handler to pathway nodes in this pathway diagram + if (pathwayNodeClickFn) { + pathwayContainer.querySelectorAll('g.Pathway').forEach(pathwayNode => { + + // Add customizable click handler + pathwayNode.addEventListener('click', (event) => { + const domClasses = Array.from(pathwayNode.classList); + const pathwayId = domClasses + .find(c => c.startsWith('WikiPathways_')) + .split('WikiPathways_')[1]; // e.g. WikiPathways_WP2815 -> WP2815 + + pathwayNodeClickFn(event, pathwayId); + }); + + // Indicate this new pathway can be rendered on click + const tooltipContent = 'Click to show pathway'; + pathwayNode.setAttribute('data-tippy-content', tooltipContent); + }); + + tippy('g.Pathway[data-tippy-content]', tippyConfig); + } } diff --git a/src/js/kit/related-genes.js b/src/js/kit/related-genes.js index c0dc9027..a2340898 100644 --- a/src/js/kit/related-genes.js +++ b/src/js/kit/related-genes.js @@ -1657,12 +1657,20 @@ function addPathwayListeners(ideo) { // const pathwayName = target.getAttribute('data-pathway-name'); // const pathway = {id: pathwayId, name: pathwayName}; // plotPathwayGenes(searchedGene, pathway, ideo); - function nodeHoverFn(event, geneName) { - console.log('in nodeHoverFn') + function geneNodeHoverFn(event, geneName) { + console.log('in geneNodeHoverFn') return '
ok ' + geneName + '
1234
'; } + + function pathwayNodeClickFn(event, pathwayId) { + const pathwayNode = event.target; + console.log('in pathwayNodeClickFn, pathwayNode', pathwayNode); + console.log('in pathwayNodeClickFn, pathwayId', pathwayId); + } + drawPathway(pathwayId, searchedGene, interactingGene, - undefined, undefined, undefined, nodeHoverFn); + undefined, undefined, undefined, + geneNodeHoverFn, pathwayNodeClickFn); event.stopPropagation(); }); });