ideogram
Version:
Chromosome visualization for the web
1,662 lines (1,368 loc) • 66.3 kB
JavaScript
/**
* @fileoverview Kit used in "Related genes" example
*
* This file simplifies client code for reusing a "related genes" ideogram --
* which finds and displays related genes for a searched gene.
*
* Related genes here are either "interacting genes" or "paralogs".
* Interacting genes are genes immediately upstream or downstream of the
* searched gene in a biochemical pathway. Paralogs are evolutionarily
* similar genes in the same species.
*
* Data sources:
* - Interacting genes: WikiPathways
* - Paralogs: Ensembl
* - Genomic coordinates: Ensembl, via MyGene.info
*
* Features provided by this module help users discover and explore genes
* related to their gene of interest.
*
* The reference implementation is available at:
* https://eweitz.github.io/ideogram/related-genes
*/
import {decompressSync, strFromU8} from 'fflate';
import tippy, {hideAll} from 'tippy.js';
import {tippyCss, tippyLightCss} from './tippy-styles';
// import {Pvjs} from 'eweitz-pvjs';
import { drawPathway } from './pathway-viewer';
import {
initAnalyzeRelatedGenes, analyzePlotTimes, analyzeRelatedGenes, timeDiff,
getRelatedGenesByType, getRelatedGenesTooltipAnalytics
} from './analyze-related-genes';
import {
getGeneStructureHtml, addGeneStructureListeners
} from './gene-structure';
import {
sortAnnotsByRank, applyRankCutoff, setAnnotRanks
} from '../annotations/annotations';
import {writeLegend} from '../annotations/legend';
import {getAnnotDomId} from '../annotations/process';
import {
getDir, pluralize, getTextSize, getTippyConfig
} from '../lib';
import {
fetchGpmls, summarizeInteractions, fetchPathwayInteractions
} from './wikipathways';
import {getTissueHtml, addTissueListeners} from './tissue';
import { addVariantListeners } from './variant';
// import {drawAnnotsByLayoutType} from '../annotations/draw';
// import {organismMetadata} from '../init/organism-metadata';
/** Sets DOM IDs for ideo.relatedAnnots; needed to associate labels */
function setRelatedAnnotDomIds(ideo) {
const updated = [];
const sortedChrNames = ideo.chromosomesArray.map((chr) => {
return chr.name;
});
if ('relatedAnnots' in ideo) {
ideo.relatedAnnots = applyAnnotsIncludeList(ideo.relatedAnnots, ideo);
// Count two related annots for same gene as one.
// E.g. gene Foo can both interact with and be paralog of gene Bar
// Instead of count Foo interacting annot and Foo paralog annot as two,
// only count it as one as they are merged in downstream UI.
//
// Searching STAT3 without this block shows the problem this fixes.
const seenNames = {};
ideo.relatedAnnots = ideo.relatedAnnots.filter(annot => {
if (annot.name in seenNames) {
return false;
}
seenNames[annot.name] = 1;
return true;
});
}
// Arrange related annots by chromosome
const annotsByChr = {};
ideo.annots.forEach((annot) => {
const relevanceSortedAnnots = annot.annots.sort((a, b) => {
return -ideo.annotSortFunction(a, b);
});
annotsByChr[annot.chr] = relevanceSortedAnnots;
// }
});
// // Sort related annots by relevance within each chromosome
// const relevanceSortedAnnotsNamesByChr = {};
// Object.entries(annotsByChr).map(([chr, annots]) => {
// if ('annots' in annots) annots = annots.annots;
// annots = setAnnotRanks(annots, ideo);
// // Sort so first annots are drawn last, and thus at top layer
// annots = annots.sort((a, b) => ideo.annotSortFunction(a, b));
// const annotNames = annots.map((annot) => annot.name);
// relevanceSortedAnnotsNamesByChr[chr] = annotNames;
// });
// annotsByChr.annots.sort((a, b) => {
// // Reverse-sort, so first annots are drawn last, and thus at top layer
// return -ideo.annotSortFunction(a, b);
// });
const updatedAnnots = {};
Object.entries(annotsByChr).forEach(([chr, annots]) => {
updatedAnnots[chr] = {chr, annots: []};
annots.forEach((annot, annotIndex) => {
// Annots have DOM IDs keyed by chromosome index and annotation index.
// We reconstruct those here using structures built in two blocks above.
const chrIndex = sortedChrNames.indexOf(chr);
// const annotIndex =
// relevanceSortedAnnotsNamesByChr[chr].indexOf(annot.name);
annot.domId = getAnnotDomId(chrIndex, annotIndex);
updatedAnnots[chr].annots.push(annot);
});
updated.push(updatedAnnots[chr]);
});
ideo.annots = updated;
}
/**
* Determines if interaction node might be a gene
*
* Some interaction nodes are biological processes; this filters out many.
* Filtering these out makes downstream queries faster.
*
* ixn {Object} Interaction from WikiPathways
* gene {Object} Gene from MyGene.info
*/
function maybeGeneSymbol(ixn, gene) {
return (
ixn !== '' &&
!ixn.includes(' ') &&
!ixn.includes('/') && // e.g. Akt/PKB
ixn.toLowerCase() !== gene.name.toLowerCase()
);
}
// /** Helpful for debugging race conditions caused by concurrency */
// const sleep = (delay) => {
// new Promise((resolve) => setTimeout(resolve, delay));
// }
/** Reports if interaction node is a gene and not previously seen */
function isInteractionRelevant(rawIxn, gene, nameId, seenNameIds, ideo) {
let isGeneSymbol;
if ('geneCache' in Ideogram && gene.name) {
isGeneSymbol = rawIxn.toLowerCase() in Ideogram.geneCache.nameCaseMap;
} else {
isGeneSymbol = maybeGeneSymbol(rawIxn, gene);
}
const isNewNameId = !(nameId in seenNameIds);
return isGeneSymbol && isNewNameId;
}
/**
* Retrieves interacting genes from WikiPathways API
*
* Docs:
* https://webservice.wikipathways.org/ui/
* https://www.wikipathways.org/index.php/Help:WikiPathways_Webservice/API
*
* Examples:
* https://webservice.wikipathways.org/findInteractions?query=ACE2&format=json
* https://webservice.wikipathways.org/findInteractions?query=RAD51&format=json
*/
async function fetchInteractions(gene, ideo) {
const ixns = {};
const seenNameIds = {};
const orgNameSimple = ideo.config.organism.replace(/-/g, ' ');
const upperGene = gene.name.toUpperCase();
let data = {result: []};
if (Ideogram.interactionCache) {
if (upperGene in Ideogram.interactionCache) {
data = Ideogram.interactionCache[upperGene];
}
} else {
// const queryString = `?query=${gene.name}&format=json`;
// const url =
// `https://webservice.wikipathways.org/findInteractions${queryString}`;
// const url = `http://localhost:8080/dist/data/cache/${gene.name}.json.gz`;
const url = `https://cdn.jsdelivr.net/npm/ixn2/${upperGene}.json.gz`;
// await sleep(3000);
const response = await fetch(url);
// const data = await response.json();
if (response.ok) {
const blob = await response.blob();
const uint8Array = new Uint8Array(await blob.arrayBuffer());
data = JSON.parse(strFromU8(decompressSync(uint8Array)));
}
}
// For each interaction, get nodes immediately upstream and downstream.
// Filter out pathway nodes that are definitely not gene symbols, then
// group pathways by gene symbol. Each interacting gene can have
// multiple pathways.
data.result.forEach(interaction => {
if (interaction.species.toLowerCase() === orgNameSimple) {
const right = interaction.fields.right.values;
const left = interaction.fields.left.values;
// let mediator = [];
// if ('mediator' in interaction.fields) {
// mediator = interaction.fields.mediator.values;
// console.log('mediator', mediator)
// }
// const rawIxns = right.concat(left, mediator);
const rawIxns = right.concat(left);
const name = interaction.name;
const id = interaction.id;
// rawIxns can contain multiple genes, e.g. when
// a group (i.e. a complex or a set of paralogs)
// interacts with the searched gene
const wrappedRawIxns = rawIxns.map(rawIxn => {
return {name: rawIxn, color: ''};
});
const sortedRawIxns =
sortAnnotsByRank(wrappedRawIxns, ideo).map(i => i.name);
sortedRawIxns.forEach(rawIxn => {
const normRawIxn = rawIxn.toLowerCase();
// Prevent overwriting searched gene. Occurs with e.g. human CD4
if (normRawIxn.includes(gene.name.toLowerCase())) return;
// if (rawIxn === '') return; // Avoid oddly blank placeholders
const nameId = name + id + normRawIxn;
const isRelevant =
isInteractionRelevant(normRawIxn, gene, nameId, seenNameIds, ideo);
if (isRelevant) {
seenNameIds[nameId] = 1;
const ixn = {name, pathwayId: id};
if (normRawIxn in ixns) {
ixns[normRawIxn].push(ixn);
} else {
ixns[normRawIxn] = [ixn];
}
}
});
}
});
const limitIxns = 20; // Maximum number of interacting genes to show
const ixnEntries = Object.entries(ixns);
const numIxns = ixnEntries.length;
let filteredIxns = {};
if (numIxns > limitIxns) {
// Only show up to 20 interacting genes,
// ordered by interest rank of interacting gene.
const ranks = Ideogram.geneCache.interestingNames.map(g => g.toLowerCase());
const ixnGenes = Object.keys(ixns);
const rankedIxnGenes = ixnGenes
.map(gene => {
let rank = 1E10; // Big number, so these rank last
if (ranks.includes(gene)) {
rank = ranks.indexOf(gene) + 1;
}
return [gene, rank];
})
.filter(([gene, _rank]) => gene in ixns)
.sort((a, b) => a[1] - b[1]); // Ascending gene rank order
rankedIxnGenes
.slice(0, limitIxns)
.forEach(([gene, _rank]) => filteredIxns[gene] = ixns[gene]);
} else {
filteredIxns = ixns;
}
return filteredIxns;
}
/**
* Queries MyGene.info API, returns parsed JSON
*
* Docs:
* https://docs.mygene.info/en/v3/
*
* Example:
* https://mygene.info/v3/query?q=symbol:cdk2%20OR%20symbol:brca1&species=9606&fields=symbol,genomic_pos,name
*/
async function fetchMyGeneInfo(queryString) {
const myGeneBase = 'https://mygene.info/v3/query';
const response = await fetch(myGeneBase + queryString + '&size=400');
const data = await response.json();
return data;
}
function parseNameAndEnsemblIdFromMgiGene(gene) {
const name = gene.name;
const id = gene.genomic_pos.ensemblgene;
let ensemblId = id;
if (typeof id === 'undefined') {
// Encountered in AKT3, when querying related genes for MTOR
// A 'chr'omosome value containing _ indicates an alt loci scaffold,
// so ignore that and take the Ensembl ID associated with the
// first position of a primary chromosome.
ensemblId =
gene.genomic_pos.filter(pos => !pos.chr.includes('_'))[0].ensemblgene;
}
return {name, ensemblId};
}
// /**
// * Summarizes genes in a pathway
// *
// * This comprises most of the content for tooltips for pathway genes.
// */
// function describePathwayGene(pathwayGene, searchedGene, pathway, summary) {
// let ixnsDescription = '';
// const pathwaysBase = 'https://www.wikipathways.org/index.php/Pathway:';
// const url = `${pathwaysBase}${pathway.id}`;
// const attrs =
// `href="${url}" ` +
// `target="_blank" ` +
// `title="See pathway diagram in WikiPathways"`;
// ixnsDescription =
// `${summary} ${searchedGene.name} in:</br/>` +
// `<a ${attrs}>${pathway.name}</a>`;
// const {name, ensemblId} = parseNameAndEnsemblIdFromMgiGene(pathwayGene);
// const type = 'pathway gene';
// const descriptionObj = {
// description: ixnsDescription,
// ixnsDescription, ensemblId, name, type
// };
// return descriptionObj;
// }
// /** Limit number of shown interaction links, and enable toggling full list */
// function limitInteractionLinks(links) {
// if (links.length > 5) {
// // Seen in e.g. interacting gene AKT1 for MTOR searched gene
// const numMore = links.length - 5;
// links = links.slice(0, 5);
// const moreText = `${numMore} more ${pluralize('pathway', numMore)}`;
// const attrs = 'id="_ideoIxnLinkToggler" style="font-style: italic"';
// const toggler = `<span ${attrs}>${moreText}</span>`;
// links.push(`<span ${attrs}>${moreText}</span>`);
// }
// return links;
// }
// function toggleInteractionLinks() {
// const ixnLinkToggler = document.querySelector('._ideoIxnLinkToggler');
// }
/**
* Summarizes interactions for a gene
*
* This comprises most of the content for tooltips for interacting genes.
*/
function describeInteractions(gene, ixns, searchedGene) {
const pathwayIds = [];
const pathwayNames = [];
let ixnsDescription = '';
if (typeof ixns !== 'undefined') {
// ixns is undefined when querying e.g. CDKN1B in human
let links = ixns.map(ixn => {
// pathwayIds.push(ixn.pathwayId);
// pathwayNames.push(ixn.name);
// const attrs =
// `class="ideo-pathway-link" ` +
// `title="Click to search for other genes in this pathway" ` +
// `style="cursor: pointer" ` +
// `data-pathway-id="${ixn.pathwayId}" ` +
// `data-pathway-name="${ixn.name}"`;
// return `<a ${attrs}>${ixn.name}</a>`;
const pathwaysBase = 'https://classic.wikipathways.org/index.php/Pathway:';
const url = `${pathwaysBase}${ixn.pathwayId}`;
pathwayIds.push(ixn.pathwayId);
pathwayNames.push(ixn.name);
const attrs =
`class="ideo-pathway-link" ` +
`style="cursor: pointer" ` +
`title="View pathway diagram from WikiPathways" ` +
`data-pathway-id="${ixn.pathwayId}"`;
return `<a ${attrs}>${ixn.name}</a>`;
});
// links = limitInteractionLinks(links);
links = links.join('<br/>');
ixnsDescription =
`Interacts with ${searchedGene.name} in:<br/>${links}`;
}
const {name, ensemblId} = parseNameAndEnsemblIdFromMgiGene(gene);
const type = 'interacting gene';
const descriptionObj = {
description: ixnsDescription,
ixnsDescription, ensemblId, name, type, pathwayIds, pathwayNames
};
return descriptionObj;
}
/** Throw error when searched gene (e.g. "Foo") isn't found */
function throwGeneNotFound(geneSymbol, ideo) {
const organism = ideo.organismScientificName;
throw Error(`"${geneSymbol}" is not a known gene in ${organism}`);
}
/**
* Lookup genes by synonym, a.k.a. alias
*
* E.g. getGeneBySynonym("p53", ideo) returns "TP53"
*/
function getGeneBySynonym(name, ideo) {
if (!Ideogram.synonymCache) return null;
const nameLc = name.toLowerCase();
if (!Ideogram.synonymCache?.nameCaseMap) {
// JIT initialization of canonicalized synonym lookup data.
// Done only once.
const nameCaseMap = {};
for (const gene in Ideogram.synonymCache.byGene) {
const synonyms = Ideogram.synonymCache.byGene[gene];
nameCaseMap[gene.toLowerCase()] = synonyms.map(s => s.toLowerCase());
}
Ideogram.synonymCache.nameCaseMap = 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 Ideogram.geneCache.nameCaseMap[geneLc];
}
}
return null;
}
/**
* Fetch genes from cache
* Construct objects that match format of MyGene.info API response
*/
function fetchGenesFromCache(names, type, ideo) {
const cache = Ideogram.geneCache;
const isSymbol = (type === 'symbol');
const locusMap = isSymbol ? cache.lociByName : cache.lociById;
const nameMap = isSymbol ? cache.idsByName : cache.namesById;
const ensemblGeneIdRegex = /ENS[A-Z]{0,3}G\d{11}/;
const hits = names.map(name => {
let isSynonym = false;
let synonym = null;
if (ensemblGeneIdRegex.test(name)) {
// Omit version if given Ensembl gene ID + version, e.g.
// ENSG00000010404.11 -> ENSG00000010404
name = name.split('.')[0];
}
const isIdentifier = name in cache.namesById;
if (isIdentifier && isSymbol) {
name = cache.namesById[name];
} else {
const nameLc = name.toLowerCase();
if (
!locusMap[name] &&
!cache.nameCaseMap[nameLc] &&
!getGeneBySynonym(name, ideo)
) {
if (isSymbol) {
throwGeneNotFound(name, ideo);
} else {
return;
}
}
// Canonicalize name if it is mistaken in upstream data source.
// This can sometimes happen in WikiPathways, e.g. when searching
// interactions for rat Pten, it includes a result for "PIK3CA".
// In that case, this would correct PIK3CA to be Pik3ca.
if (isSymbol && !locusMap[name]) {
if (cache.nameCaseMap[nameLc]) {
name = cache.nameCaseMap[nameLc];
} else {
synonym = name;
name = getGeneBySynonym(synonym, ideo);
isSynonym = true;
}
}
}
const locus = locusMap[name];
const symbol = isSymbol ? name : nameMap[name];
const ensemblId = isSymbol ? nameMap[name] : name;
const fullName = cache.fullNamesById[ensemblId];
const hit = {
symbol,
name: fullName,
source: 'cache',
genomic_pos: {
chr: locus[0],
start: locus[1],
end: locus[2],
ensemblgene: ensemblId
},
isSynonym,
isIdentifier,
synonym
};
return hit;
});
const hitsWithGenomicPos = hits.filter(hit => hit !== undefined);
return hitsWithGenomicPos;
}
/** Wait for a certain time (delay) in milliseconds */
function wait(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
/**
* Get time to wait before retrying a fail service, gracefully
*
* The returned wait time helps avoid flooding the server
*/
function exponentialBackoffWithJitter(numFailures, baseWaitMs) {
const jitter = 10 * Math.random();
return Math.round(baseWaitMs + jitter) * (numFailures ** 2);
}
async function retryFetch(requestedThing, numLimit, fn, args) {
const numFailed = numFailedFetches[requestedThing];
if (numFailed > numLimit) {
const preamble = 'Failed to fetch from Ideogram third-party service for: ';
throw new TypeError(preamble + requestedThing);
}
numFailedFetches[requestedThing] += 1;
// Exponential backoff
const baseWaitMs = 500;
const waitMilliseconds = exponentialBackoffWithJitter(numFailed, baseWaitMs);
console.log(
`Failed fetch for ${requestedThing} ${numFailed} times, ` +
`retrying in ${waitMilliseconds} ms`
);
await wait(waitMilliseconds);
return await fn(...args);
}
/** Number of times fetches for various things have consecutively failed */
const numFailedFetches = {
genes: 0
};
/** Fetch genes from cache, or, if needed, from MyGene.info API */
async function fetchGenes(names, type, ideo) {
let data;
// Account for single-gene fetch
if (typeof names === 'string') names = [names];
// Query parameter for MyGene.info API
const qParam = names.map(name => `${type}:${name.trim()}`).join(' OR ');
const taxid = ideo.config.taxid;
const queryStringBase = `?q=${qParam}&species=${taxid}&fields=`;
if (Ideogram.geneCache) {
const hits = fetchGenesFromCache(names, type, ideo);
// Asynchronously fetch full name, but don't await the response, because
// full names are only shown upon hovering over an annotation.
hits.forEach((hit) => {
const symbol = hit.symbol;
const fullName = hit.name;
const isSynonym = hit.isSynonym;
const synonym = hit.synonym;
if (symbol in ideo.annotDescriptions.annots) {
ideo.annotDescriptions.annots[symbol].name = fullName;
ideo.annotDescriptions.annots[symbol].isSynonym = hit.isSynonym;
ideo.annotDescriptions.annots[symbol].synonym = hit.synonym;
} else {
ideo.annotDescriptions.annots[symbol] = {
name: fullName,
isSynonym,
synonym
};
}
});
data = {hits, fromGeneCache: true};
} else {
// Fetch gene data from MyGene.info
const queryString = `${queryStringBase}symbol,genomic_pos,name`;
try {
data = await fetchMyGeneInfo(queryString);
} catch (error) {
const isFailedFetch = (error.message === 'Failed to fetch');
if (isFailedFetch && navigator.onLine) {
// Retry fetching 3 times, waiting longer each time
data = await retryFetch('genes', 3, fetchGenes, [names, type, ideo]);
}
}
}
return data;
}
/**
* Retrieves position and other data on interacting genes from MyGene.info
*/
async function fetchInteractionAnnots(interactions, searchedGene, ideo) {
const annots = [];
const symbols = Object.keys(interactions);
if (symbols.length === 0) return annots;
const data = await fetchGenes(symbols, 'symbol', ideo);
data.hits.forEach(gene => {
// If hit lacks position
// or is same as searched gene (e.g. search for human SRC),
// then skip processing
if (
'genomic_pos' in gene === false ||
gene.symbol === searchedGene.name
) {
return;
}
const annot = parseAnnotFromMgiGene(gene, ideo, 'purple');
annots.push(annot);
const ixns = interactions[gene.symbol.toLowerCase()];
const descriptionObj = describeInteractions(gene, ixns, searchedGene);
mergeDescriptions(annot, descriptionObj, ideo);
});
// Fetch GPML files to use when updating interaction descriptions with
// refined direction.
fetchGpmls(ideo);
return annots;
}
/** Fetch paralog positions from MyGeneInfo */
async function fetchParalogPositionsFromMyGeneInfo(
homologs, searchedGene, ideo
) {
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);
data.hits.forEach(gene => {
// If hit lacks position, skip processing
if ('genomic_pos' in gene === false) return;
if ('name' in gene === false) return;
const annot = parseAnnotFromMgiGene(gene, ideo, 'pink');
annots.push(annot);
const description =
Ideogram.tissueCache ? '' : `Paralog of ${searchedGene.name}`;
const {name, ensemblId} = parseNameAndEnsemblIdFromMgiGene(gene);
const type = 'paralogous gene';
const descriptionObj = {description, ensemblId, name, type};
mergeDescriptions(annot, descriptionObj, ideo);
});
return annots;
}
function drawNeighborhoods(neighborhoodAnnots, ideo) {
neighborhoodAnnots = applyAnnotsIncludeList(neighborhoodAnnots, ideo);
ideo.drawAnnots(neighborhoodAnnots, 'overlay', true, true);
moveLegend(ideo);
}
/** Plot paralog neighborhoods */
function plotParalogNeighborhoods(annots, ideo) {
if (!ideo.config.showParalogNeighborhoods) return;
if (ideo.neighborhoodAnnots?.length > 0) {
ideo.neighborhoodAnnots.forEach(annot => {
ideo.annotDescriptions.annots[annot.name] = annot;
});
drawNeighborhoods(ideo.neighborhoodAnnots, ideo);
return;
}
const searchedAnnot = ideo.relatedAnnots[0];
annots = applyAnnotsIncludeList(annots, ideo);
annots.unshift(searchedAnnot);
if (annots.length < 2) return;
// Arrays of paralogs within 10 Mbp of each other
const neighborhoods = {};
neighborhoods[annots[0].chr] = {};
neighborhoods[annots[0].chr][annots[0].start] = [annots[0]];
const windowInt = 2_000_000;
const windowProse = '2 Mbp';
for (let i = 1; i < annots.length; i++) {
const annot = annots[i];
const chr = annot.chr;
const start = annot.start;
if (chr in neighborhoods) {
const starts = Object.keys(neighborhoods[chr]);
for (let j = 0; j < starts.length; j++) {
const startJInt = parseInt(starts[j]);
if (Math.abs(start - startJInt) < windowInt) {
neighborhoods[chr][startJInt].push(annot);
} else {
neighborhoods[chr][start] = [annot];
}
}
} else {
neighborhoods[chr] = {};
neighborhoods[chr][start] = [annot];
}
}
// Big enough to see and hover
const overlayAnnotLength = 15_000_000;
const searchedGene = searchedAnnot.name;
const neighborhoodAnnots =
Object.entries(neighborhoods).map(([chr, neighborhood], index) => {
const start = parseInt(Object.keys(neighborhood)[0]);
let paralogs = Object.values(neighborhood)[0];
if (paralogs.length < 2) {
return {paralogs};
}
let includesSearched = false;
if (paralogs[0].name === searchedAnnot.name) {
paralogs = paralogs.slice(1);
includesSearched = true;
}
// paralogs.map(paralog => {
// console.log(paralog);
// })
const paralogsText = pluralize('paralog', paralogs.length)
const description =
`${paralogs.length} nearby ${paralogsText} of ${searchedGene}`;
const chrLength = ideo.chromosomes[ideo.config.taxid][chr].bpLength;
let annotStart = start - overlayAnnotLength/2;
let annotStop = start + overlayAnnotLength/2;
if (annotStop > chrLength) {
annotStart = start - overlayAnnotLength;
annotStop = chrLength;
} else if (annotStart < 1) {
annotStart = 1;
annotStop = overlayAnnotLength;
};
if ('geneCache' in Ideogram) {
paralogs = paralogs.map(paralog => {
paralog.fullName = Ideogram.geneCache.fullNamesById[paralog.id];
const ranks = Ideogram.geneCache.interestingNames;
if (ranks.includes(paralog.name)) {
paralog.rank = ranks.indexOf(paralog.name) + 1;
} else {
paralog.rank = 1E10;
}
return paralog;
});
}
const key = 'paralogNeighborhood-' + index;
const fStart = start.toLocaleString(); // Format for readability
const displayCoordinates = `chr${chr}:${fStart} ± ${windowProse}`;
const annot = {
name: key,
chr,
start: annotStart,
stop: annotStop,
color: 'pink',
description,
paralogs,
type: 'paralog neighborhood',
displayCoordinates,
includesSearched
};
ideo.annotDescriptions.annots[annot.name] = annot;
return annot;
}).filter(n => n.paralogs.length > 1 || n.includesSearched);
ideo.neighborhoodAnnots = neighborhoodAnnots;
if (neighborhoodAnnots.length > 0) {
drawNeighborhoods(neighborhoodAnnots, ideo);
}
}
/**
* Fetch paralogs of searched gene
*/
async function fetchParalogs(annot, ideo) {
const taxid = ideo.config.taxid;
let homologs;
// Fetch paralogs
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 = Ideogram.paralogCache.paralogsByName;
const nameUc = annot.name.toUpperCase();
const hasParalogs = nameUc in paralogsByName;
homologs = hasParalogs ? paralogsByName[nameUc] : [];
} else {
const params = `&format=condensed&type=paralogues&target_taxon=${taxid}`;
const organismUnderscore = ideo.config.organism.replace('-', '_');
const path = `/homology/id/${organismUnderscore}/${annot.id}?${params}`;
const ensemblHomologs = await Ideogram.fetchEnsembl(path);
homologs = ensemblHomologs.data[0].homologies;
}
// Fetch positions of paralogs
let annots =
await fetchParalogPositionsFromMyGeneInfo(homologs, annot, ideo);
// Omit genes named like "AC113554.1", which is an "accession.version".
// Such accVers are raw and poorly suited here.
annots = annots.filter(annot => {
const isAccVer = annot.name.match(/^AC[0-9.]+$/);
return !isAccVer;
});
return annots;
}
/**
* Filters out placements on alternative loci scaffolds, an advanced
* genome assembly feature we are not concerned with in ideograms.
*
* Example:
* https://mygene.info/v3/query?q=symbol:PTPRC&species=9606&fields=symbol,genomic_pos,name
*/
function getGenomicPos(gene, ideo) {
let genomicPos = null;
if (Array.isArray(gene.genomic_pos)) {
genomicPos = gene.genomic_pos.filter(pos => {
return pos.chr in ideo.chromosomes[ideo.config.taxid];
})[0];
} else {
genomicPos = gene.genomic_pos;
}
return genomicPos;
}
/**
* Transforms MyGene.info (MGI) gene into Ideogram annotation
*/
function parseAnnotFromMgiGene(gene, ideo, color='red') {
const genomicPos = getGenomicPos(gene, ideo);
const annot = {
name: gene.symbol,
chr: genomicPos.chr,
start: genomicPos.start,
stop: genomicPos.end,
id: genomicPos.ensemblgene,
color
};
return annot;
}
/** Return type (interacting, paralogous, or searched) of legend entry */
function getLegendType(li) {
const lcText = li.innerText.toLowerCase();
let type;
if (lcText.includes('interacting')) type = 'interacting';
if (lcText.includes('paralogous')) type = 'paralogous';
if (lcText.includes('searched')) type = 'searched';
return type;
}
/** Return color (purple, pink, or red) of legend entry */
function getLegendEntryColor(li) {
const type = getLegendType(li);
const colorMap = {
'interacting': 'purple',
'paralogous': 'pink',
'searched': 'red'
};
const color = colorMap[type];
return color;
}
/** Highlight / filter upon hovering over legend entry */
function highlightByType(li, ideo) {
li.classList += ' active';
const selectedColor = getLegendEntryColor(li);
if (ideo.config.showAnnotLabels) {
ideo.clearAnnotLabels();
ideo.flattenAnnots().forEach(annot => {
if (annot.color !== selectedColor) {
document.getElementById(annot.domId).style.display = 'none';
}
});
const sortedAnnots = ideo.flattenAnnots().sort((a, b) => {
return ideo.annotSortFunction(a, b);
}).filter(annot => annot.color === selectedColor);
const numLabels = Math.min(sortedAnnots.length, 20);
ideo.fillAnnotLabels(sortedAnnots, numLabels);
}
}
/** Remove highlight / filter upon hovering out of legend entry */
function dehighlightAll(ideo) {
document.querySelectorAll('#_ideogramLegend li').forEach(li => {
li.classList.remove('active');
});
ideo.flattenAnnots().forEach(annot => {
document.getElementById(annot.domId).style.display = null;
});
if (ideo.config.showAnnotLabels) {
const sortedAnnots = ideo.flattenAnnots().sort((a, b) => {
return ideo.annotSortFunction(a, b);
});
ideo.fillAnnotLabels(sortedAnnots);
}
}
function initInteractiveLegend(ideo) {
// Highlight and filter annotations by type on hovering over legend entries
function highlight(event) {
const li = event.target;
highlightByType(li, ideo);
if (ideo.onHoverLegendCallback) {
ideo.onHoverLegendCallback();
}
}
// // Highlight and filter annotations by type on hovering over legend entries
// WIP: 14373b18319e99febd91816fbc0c1b2e0f20f277
// function toggleHighlight(event) {
// const li = event.target;
// return toggleHighlightByType(li, ideo);
// }
function dehighlight() {
dehighlightAll(ideo);
}
const entrySelector = '#_ideogramLegend li._ideoLegendEntry';
document.querySelectorAll(entrySelector).forEach(li => {
// li.addEventListener('click', toggleHighlight);
// WIP: 14373b18319e99febd91816fbc0c1b2e0f20f277
li.addEventListener('mouseenter', highlight);
li.addEventListener('mouseleave', dehighlight);
const legendType = getLegendType(li);
const tippyContentMap = {
'interacting':
'Adjacent to searched gene in a biochemical pathway, ' +
'per WikiPathways',
'paralogous':
'Evolutionarily related to searched gene ' +
'by a duplication event, per Ensembl'
};
// const placement = 'data-tippy-placement="top-end"';
const placement = '';
const tippy =
`data-tippy-content="${tippyContentMap[legendType]}" ${placement}`;
const reset = 'position: inherit; left: inherit';
// const glossary = 'text-decoration: underline dashed;';
// const style = `style="${glossary} ${reset}`;
const style = `style="${reset}"`;
const attrs = `class="_ideoLegendEntry" ${style} ${tippy}`;
if (legendType === 'paralogous') {
li.innerHTML = `<span ${attrs}>Paralogous genes</span>`;
} else if (legendType === 'interacting') {
li.innerHTML = `<span ${attrs}>Interacting genes</span>`;
}
});
const css =
`<style>
${tippyCss}
.tippy-box {
font-size: 12px;
}
.tippy-content {
padding: 3px 7px;
}
#_ideogramLegend li {
padding-left: 5px;
border-radius: 2px;
}
#_ideogramLegend li.active {
color: #00C;
background-color: #EEF;
}
</style>`;
const legendDom = document.querySelector('#_ideogramLegend');
legendDom.insertAdjacentHTML('afterBegin', css);
const tippyConfig = getTippyConfig();
tippyConfig.maxWidth = 180;
tippyConfig.offset = [-30, 10];
ideo.legendTippy =
tippy('._ideoLegendEntry[data-tippy-content]', tippyConfig);
}
function moveLegend(ideo, extraPad=0) {
const ideoInnerDom = document.querySelector('#_ideogramInnerWrap');
const decorPad = setRelatedDecorPad({}).legendPad;
const left = decorPad + 20 + extraPad;
const legendStyle = `position: absolute; top: 15px; left: ${left}px`;
const legend = document.querySelector('#_ideogramLegend');
ideoInnerDom.prepend(legend);
legend.style = legendStyle;
initInteractiveLegend(ideo);
}
/**
* Filter annotations to only include those in configured list
*
* @return {List<Object>} includedAnnots List of filtered annots objects
*/
function applyAnnotsIncludeList(annots, ideo) {
if (ideo.config.annotsInList === 'all') return annots;
const includedAnnots = [];
annots.forEach(annot => {
if (ideo.config.annotsInList.includes(annot.name.toLowerCase())) {
includedAnnots.push(annot);
}
});
return includedAnnots;
}
function filterByAnnotsIncludeList(annots, ideo) {
if (ideo.config.annotsInList === 'all') return annots;
const annotsInList = ideo.config.annotsInList;
const updated = [];
const updatedAnnots = {};
ideo.annots.forEach(chrAnnots => {
const {chr, annots} = chrAnnots;
updatedAnnots[chr] = {chr, annots: []};
annots.forEach((annot) => {
const lcAnnotName = annot.name.toLowerCase();
if (
'relatedAnnots' in ideo &&
!annotsInList.includes(lcAnnotName)
) {
return;
}
updatedAnnots[chr].annots.push(annot);
});
updated.push(updatedAnnots[chr]);
});
return updated;
}
/** Fetch and draw interacting genes, return Promise for annots */
function processInteractions(annot, ideo) {
return new Promise(async (resolve) => {
const t0 = performance.now();
const interactions = await fetchInteractions(annot, ideo);
const annots = await fetchInteractionAnnots(interactions, annot, ideo);
ideo.relatedAnnots.push(...annots);
finishPlotRelatedGenes('interacting', ideo);
ideo.time.rg.interactions = timeDiff(t0);
plotParalogNeighborhoods(annots, ideo);
resolve();
});
}
/** Find and draw paralogs, return Promise for annots */
function processParalogs(annot, ideo) {
return new Promise(async (resolve) => {
const t0 = performance.now();
const annots = await fetchParalogs(annot, ideo);
ideo.relatedAnnots.push(...annots);
finishPlotRelatedGenes('paralogous', ideo);
plotParalogNeighborhoods(annots, ideo);
ideo.time.rg.paralogs = timeDiff(t0);
resolve();
});
}
// /**
// * Sorts gene names consistently.
// *
// * Might also loosely rank by first-discovered or most prominent
// */
// function sortGeneNames(aName, bName) {
// // Rank shorter names above longer names
// if (bName.length !== aName.length) return bName.length - aName.length;
// // Rank names of equal length alphabetically
// return [aName, bName].sort().indexOf(aName) === 0 ? 1 : -1;
// }
/** Sorts by relevance of related type, then rank */
export function sortByRelatedType(a, b) {
var aName, bName, aColor, bColor;
if ('name' in a) {
// Locally processed annotations
aName = a.name;
bName = b.name;
aColor = a.color;
bColor = b.color;
} else {
// Raw annotations
[aName, aColor] = [a[0], a[3]];
[bName, bColor] = [b[0], b[3]];
}
if ('initRank' in a === false) {
// Rank red (searched gene) highest
if (aColor === 'red') return -1;
if (bColor === 'red') return 1;
// Rank purple (interacting gene) above pink (paralogous gene)
if (aColor === 'purple' && bColor === 'pink') return -1;
if (bColor === 'purple' && aColor === 'pink') return 1;
}
return a.rank - b.rank;
// return sortGeneNames(aName, bName);
}
function mergeDescriptions(annot, desc, ideo) {
let mergedDesc;
const descriptions = ideo.annotDescriptions.annots;
if (annot.name in descriptions) {
const otherDesc = descriptions[annot.name];
mergedDesc = desc;
if (desc.type === otherDesc.type) return;
Object.keys(otherDesc).forEach(function(key) {
if (key in mergedDesc === false) {
mergedDesc[key] = otherDesc[key];
}
});
// Object.assign({}, descriptions[annot.name]);
if ('type' in otherDesc && !Ideogram.tissueCache) {
mergedDesc.type += ', ' + otherDesc.type;
mergedDesc.description += `<br/><br/>${otherDesc.description}`;
}
} else {
mergedDesc = desc;
}
ideo.annotDescriptions.annots[annot.name] = mergedDesc;
}
/** Combines paralogs and interacting gene, if name matches */
function mergeAnnots(unmergedAnnots) {
const seenAnnots = {};
let mergedAnnots = [];
unmergedAnnots.forEach((annot) => {
if (annot.name in seenAnnots === false) {
mergedAnnots.push(annot);
seenAnnots[annot.name] = 1;
} else {
if (annot.color === 'purple') {
mergedAnnots = mergedAnnots.map((mergedAnnot) => {
return (annot.name === mergedAnnot.name) ? annot : mergedAnnot;
});
}
}
});
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, config, n) {
setTimeout(() => {
if (n < 40) { // 40 * 50 ms = 2 s
if (!hasTissueCache()) {
waitForTissueCache(geneNames, config, n + 1);
} else {
setTissueExpressions(geneNames, config);
}
}
}, 50);
}
async function setTissueExpressions(geneNames, config) {
if (
!hasTissueCache()
// || !(annot.name in Ideogram.tissueCache.byteRangesByName)
) {
waitForTissueCache(geneNames, config, 0);
return;
}
const tissueExpressionsByGene = {};
const cache = Ideogram.tissueCache;
const promises = [];
geneNames.forEach(async gene => {
const promise = new Promise(async (resolve) => {
const tissueExpressions = await cache.getTissueExpressions(gene, config);
tissueExpressionsByGene[gene] = tissueExpressions;
resolve();
});
promises.push(promise);
});
await Promise.all(promises);
Ideogram.tissueExpressionsByGene = tissueExpressionsByGene;
}
function onBeforeDrawAnnots() {
const ideo = this;
setRelatedAnnotDomIds(ideo);
const geneNames = [];
// Handle differential expression extension
const chrAnnots = ideo.annots;
for (let i = 0; i < chrAnnots.length; i++) {
const annots = chrAnnots[i].annots;
for (let j = 0; j < annots.length; j++) {
const annot = annots[j];
geneNames.push(annot.name);
if (ideo.config.colorMap && annot.differentialExpression?.length) {
const colorMap = ideo.config.colorMap;
const group = annot.differentialExpression[0].group;
annot.color = colorMap[group];
ideo.annots[i].annots[j] = annot;
}
}
}
setTissueExpressions(geneNames, ideo.config);
}
function filterAndDrawAnnots(annots, ideo) {
annots = applyAnnotsIncludeList(annots, ideo);
ideo.drawAnnots(annots);
}
/** Filter, sort, draw annots. Move legend. */
function finishPlotRelatedGenes(type, ideo) {
let annots = ideo.relatedAnnots;
if (annots.length > 1 && ideo.onFindGenesCallback) {
ideo.onFindGenesCallback();
}
annots = mergeAnnots(annots);
filterAndDrawAnnots(annots, ideo);
if (ideo.config.showAnnotLabels) {
const sortedAnnots = ideo.flattenAnnots().sort((a, b) => {
return ideo.annotSortFunction(a, b);
});
ideo.fillAnnotLabels(sortedAnnots);
}
moveLegend(ideo);
analyzePlotTimes(type, ideo);
}
/** Fetch position of searched gene, return corresponding annotation */
async function processSearchedGene(geneSymbol, ideo) {
const t0 = performance.now();
const data = await fetchGenes(geneSymbol, 'symbol', ideo);
if (data.hits.length === 0) {
return;
}
const gene = data.hits.find(hit => {
const genomicPos = getGenomicPos(hit, ideo); // omits alt loci
return genomicPos && genomicPos.ensemblgene;
});
const ensemblId = gene.genomic_pos.ensemblgene;
// Assign tooltip content. Much of the content is often retrieved from
// the gene cache. In that case, all fields except `name` are fetched
// from cache. Occasionally, e.g. often upon the very first search, no
// content is yet available from cache.
let desc = {description: '', ensemblId, type: 'searched gene'};
if (gene.symbol in ideo.annotDescriptions.annots) {
// Most content already set via cache.
// `name` will be set via non-blocking part of `fetchGenes`.
const oldDesc = ideo.annotDescriptions.annots[gene.symbol];
desc = Object.assign(oldDesc, desc);
} else {
// No content has been set yet via cache. In this case, `gene` already
// has all the data needed for the searched gene's tooltip content.
desc.name = gene.name;
}
ideo.annotDescriptions.annots[gene.symbol] = desc;
const annot = parseAnnotFromMgiGene(gene, ideo);
ideo.relatedAnnots.push(annot);
ideo.time.rg.searchedGene = timeDiff(t0);
return annot;
}
function adjustPlaceAndVisibility(ideo) {
var ideoContainerDom = document.querySelector(ideo.config.container);
ideoContainerDom.style.visibility = '';
ideoContainerDom.style.position = 'absolute';
ideoContainerDom.style.width = '100%';
var ideoInnerDom = document.querySelector('#_ideogramInnerWrap');
ideoInnerDom.style.position = 'relative';
ideoInnerDom.style.marginLeft = 'auto';
ideoInnerDom.style.marginRight = 'auto';
ideoInnerDom.style.overflowY = 'hidden';
document.querySelector('#_ideogramMiddleWrap').style.overflowY = 'hidden';
const legendPad = ideo.config.legendPad;
if (typeof ideo.didAdjustIdeogramLegend === 'undefined') {
// Accounts for moving legend when external content at left or right
// is variable upon first rendering plotted genes
var ideoDom = document.querySelector('#_ideogram');
const legendWidth = 160;
ideoInnerDom.style.maxWidth =
(
parseInt(ideoInnerDom.style.maxWidth) +
legendWidth +
legendPad
) + 'px';
ideoDom.style.minWidth =
(parseInt(ideoDom.style.minWidth) + legendPad) + 'px';
ideoDom.style.maxWidth =
(parseInt(ideoDom.style.minWidth) + legendPad) + 'px';
ideoDom.style.position = 'relative';
ideoDom.style.left = legendWidth + 'px';
ideo.didAdjustIdeogramLegend = true;
}
}
// function sortByPathwayIxn(a, b) {
// const aColor = a.color;
// const bColor = b.color;
// // Rank red (searched gene) highest
// if (aColor === 'red') return -1;
// if (bColor === 'red') return 1;
// // Rank not grey above grey
// if (aColor === 'grey' && bColor !== 'grey') return 1;
// if (bColor === 'grey' && aColor !== 'grey') return -1;
// return a.rank - b.rank;
// }
// async function fetchPathwayGeneAnnots(searchedGene, pathway, ideo) {
// const annots = [];
// const pathwayIxns =
// await fetchPathwayInteractions(searchedGene.name, pathway.id, ideo);
// const pathwayGenes = Object.keys(pathwayIxns);
// const data = await fetchGenes(pathwayGenes, 'symbol', ideo);
// const ixnColors = {
// 'Stimulates': 'green',
// 'Stimulated by': 'green',
// 'Necessarily stimulates': 'green',
// 'Necessarily stimulated by': 'green',
// 'Transcribes / translates': 'brown',
// 'Transcribed / translated by': 'brown',
// 'Inhibits': 'red',
// 'Inhibited by': 'red',
// 'Modifies': 'blue',
// 'Modified by': 'blue',
// 'Acts on': 'blue',
// 'Acted on by': 'blue',
// 'Catalyzes': 'orange',
// 'Catalyzed by': 'orange',
// 'Converts': 'orange',
// 'Converted by': 'orange',
// 'Binds': 'black',
// 'Shares pathway with': 'grey'
// };
// data.hits.forEach(gene => {
// // If hit lacks position
// // or is same as searched gene (e.g. search for human SRC),
// // then skip processing
// if (
// 'genomic_pos' in gene === false ||
// gene.symbol === searchedGene.name
// ) {
// return;
// }
// // Account for edge case: cyclic AMP (cAMP) is not "CAMP" gene
// if (gene.symbol === 'cAMP') return;
// const summary = pathwayIxns[gene.symbol];
// const color = ixnColors[summary];
// // if (color !== 'blue') console.log(gene);
// const annot = parseAnnotFromMgiGene(gene, ideo, color);
// annots.push(annot);
// const descriptionObj =
// describePathwayGene(gene, searchedGene, pathway, summary);
// mergeDescriptions(annot, descriptionObj, ideo);
// });
// ideo.annotSortFunction = sortByPathwayIxn;
// const sortedAnnots = annots.sort(sortByPathwayIxn).slice(0, 40);
// return sortedAnnots;
// }
// /**
// *
// */
// async function plotPathwayGenes(searchedGene, pathway, ideo) {
// const headerTitle = 'Genes in pathway';
// initAnnotDescriptions(ideo, headerTitle);
// legendPathwayName = pathway.name;
// ideo.config.legend = pathwayLegend;
// writeLegend(ideo);
// moveLegend(ideo);
// ideo.relatedAnnots = [];
// await processSearchedGene(searchedGene.name, ideo);
// const annots = await fetchPathwayGeneAnnots(searchedGene, pathway, ideo);
// ideo.relatedAnnots.push(...annots);
// finishPlotRelatedGenes('pathway', ideo);
// }
function initAnnotDescriptions(ideo, headerTitle) {
const organism = ideo.getScientificName(ideo.config.taxid);
const version = Ideogram.version;
const headers = [
`# ${headerTitle}`,
`# Organism: ${organism}`,
`# Generated by Ideogram.js version ${version}, https://github.com/eweitz/ideogram`,
`# Generated at ${window.location.href}`
].join('\n');
delete ideo.annotDescriptions;
ideo.annotDescriptions = {headers, annots: {}};
}
/**
* For given gene, finds and draws interacting genes and paralogs
*
* @param geneSymbol {String} Gene symbol, e.g. RAD51
*/
async function plotRelatedGenes(geneSymbol=null) {
const ideo = this;
if (!geneSymbol) {
return plotGeneHints(ideo);
}
ideo.clearAnnotLabels();
const legend = document.querySelector('#_ideogramLegend');
if (legend) legend.remove();
ideo.config = setRelatedDecorPad(ideo.config);
initAnnotDescriptions(ideo, `Related genes for ${geneSymbol}`);
const ideoSel = ideo.selector;
const annotSel = ideoSel + ' .annot';
document.querySelectorAll(annotSel).forEach(el => el.remove());
ideo.startHideAnnotTooltipTimeout();
// Refine style
document.querySelectorAll('.chromosome').forEach(chromosome => {
chromosome.style.cursor = '';
});
adjustPlaceAndVisibility(ideo);
ideo.relatedAnnots = [];
ideo.neighborhoodAnnots = [];
// Fetch positon of searched gene
const annot = await processSearchedGene(geneSymbol, ideo);
if (typeof annot === 'undefined') throwGeneNotFound(geneSymbol, ideo);
ideo.config.legend = relatedLegend;
writeLegend(ideo);
moveLegend(ideo);
await Promise.all([
processInteractions(annot, ideo),
processParalogs(annot, ideo)
]);
ideo.time.rg.total = timeDiff(ideo.time.rg.t0);
analyzeRelatedGenes(ideo);
if (ideo.onPlotFoundGenesCallback) ideo.onPlotFoundGenesCallback();
}
function getAnnotByName(annotName, ideo) {
var annotByName;
ideo.annots.forEach(annotsByChr => {
annotsByChr.annots.forEach(annot => {
if (annotName === annot.name) {
annotByName = annot;
}
});
});
if (annotByName === null) {
annotByName = ideo.annotDescriptions.annots[annotName];
}
return annotByName;
}
/**
* Manage click on pathway links in annotation tooltips
*/
function addPathwayListeners(ideo) {
const pathways = document.querySelectorAll('.ideo-pathway-link');
if (pathways.length > 0 && !ideo.addedPathwayClickHandler) {
pathways.forEach(pathway => {
// pathway.removeEventListener('click', handlePathwayClick);
pathway.addEventListener('click', function(event) {
const target = event.target;
const pathwayId = target.getAttribute('data-pathway-id');
const searchedGene = getSearchedFromDescriptions(ideo);
const interactingGene =
document.querySelector('#ideo-related-gene').textContent;
// const pathwayName = target.getAttribute('data-pathway-name');
// const pathway = {id: pathwayId, name: pathwayName};
// plotPathwayGenes(searchedGene, pathway, ideo);
function geneNodeHoverFn(event, geneName) {
console.log('in geneNodeHoverFn')
return '<div>ok ' + geneName + '</div><div>1234</div>'