diff --git a/examples/vanilla/gene-leads.html b/examples/vanilla/gene-leads.html
index e6223633..0e74d9a9 100644
--- a/examples/vanilla/gene-leads.html
+++ b/examples/vanilla/gene-leads.html
@@ -242,6 +242,7 @@
Gene LeadsEnrich your gene search
organism: organism,
container: '#ideogram-container',
// fontFamily: "'Montserrat', sans-serif",
+ showVariantInTooltip: plotGeneFromUrl,
onLoad: plotGeneFromUrl,
onPlotFoundGenes: reportFoundGenes,
onHoverLegend: reportLegendMetrics,
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/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/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 5624f358..2afc1368 100644
--- a/src/js/ideogram.js
+++ b/src/js/ideogram.js
@@ -68,6 +68,15 @@ import {
plotRelatedGenes, getRelatedGenesByType
} from './kit/related-genes';
+import {
+ drawPathway as _drawPathway,
+ getPathwayGenes as _getPathwayGenes
+} from './kit/pathway-viewer.js';
+
+import {
+ initCaches as _initCaches
+} from './init/caches/cache';
+
export default class Ideogram {
constructor(config) {
@@ -340,4 +349,55 @@ 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
+ * @param {Boolean} showClose Whether to show close button
+ * @param {Function} geneNodeHoverFn Function to call upon hovering diagram node
+ */
+ static drawPathway(
+ pwId, sourceGene, destGene,
+ outerSelector,
+ dimensions={height: 440, width: 900},
+ showClose=true,
+ geneNodeHoverFn=undefined,
+ pathwayNodeClickFn=undefined
+ ) {
+ _drawPathway(
+ pwId, sourceGene, destGene,
+ outerSelector,
+ dimensions=dimensions,
+ showClose=showClose,
+ geneNodeHoverFn=geneNodeHoverFn,
+ pathwayNodeClickFn=pathwayNodeClickFn
+ );
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Get list of gene names in pathway
+ *
+ * @param {Object} config Includes organism, useCache, etc.
+ */
+ static getPathwayGenes() {
+ return _getPathwayGenes();
+ }
}
diff --git a/src/js/init/caches/cache.js b/src/js/init/caches/cache.js
index 2476837e..3b10cceb 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,35 @@ 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);
+ if (config.showVariantInTooltip) {
+ 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);
+ if (config.showVariantInTooltip) {
+ cacheFactory('variant', organism, config, cacheDir);
+ }
}
}
}
@@ -140,14 +143,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 +162,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 +214,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 +235,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/pathway-viewer.js b/src/js/kit/pathway-viewer.js
index e64a0733..37f72731 100644
--- a/src/js/kit/pathway-viewer.js
+++ b/src/js/kit/pathway-viewer.js
@@ -1,3 +1,7 @@
+import snarkdown from 'snarkdown';
+import tippy from 'tippy.js';
+import {getTippyConfig} from '../lib';
+
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';
@@ -13,7 +17,7 @@ async function fetchPathwayViewerJson(pwId) {
const response = await fetch(url);
const pathwayJson = await response.json();
- window.pathwayJson = pathwayJson;
+ Ideogram.pathwayJson = pathwayJson;
return pathwayJson;
}
@@ -122,7 +126,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,29 +134,175 @@ 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 =
``;
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) {
+ // 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 /g,
+ ''
+ );
+
+ return htmlWithClassedLinks;
+}
+
+function formatDescription(rawText) {
+ rawText = rawText.replaceAll('\r\n\r\n', '\r\n');
+ rawText = rawText.replaceAll('\r\n', '
');
+ const denoisedPhospho = removePhosphoSitePlusClause(rawText);
+ const denoisedText = removeCptacAssayPortalClause(denoisedPhospho);
+ const linkedText = convertMarkdownLinks(denoisedText);
+ const trimmedText = linkedText.trim();
+ return trimmedText;
+}
+
+function getDescription(pathwayJson) {
+ const rawText =
+ pathwayJson.pathway.comments.filter(
+ c => c.source === 'WikiPathways-description'
+ )[0].content;
+ const descriptionText = formatDescription(rawText);
+
+ 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] ?? ontology;
+ const safeOntology = ontology.replaceAll(' ', '_');
+ const cls = `class="ideoPathwayOntology__${safeOntology}"`;
+
+ if (links === '') return '';
+
+ return `${refinedOntology}: ${links}
`;
+ }).join('');
+
+ if (pathwayAnnotationsList.length === 0) {
+ return '';
+ }
+
+ const style = `style="font-weight: bold"`;
+
+ const pathwayAnnotations =
+ `` +
+ // `
Pathway annotations
` +
+ pathwayAnnotationsList +
+ `
`;
+
+ 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);
+ const footer =
+ `
` +
+ ``;
+ pathwayContainer.insertAdjacentHTML('beforeEnd', footer);
}
/** Fetch and render WikiPathways diagram for given pathway ID */
export async function drawPathway(
pwId, sourceGene, destGene,
- dimensions={height: 440, width: 900}, retryAttempt=0
+ outerSelector='#_ideogramOuterWrap',
+ dimensions={height: 440, width: 900},
+ showClose=true,
+ geneNodeHoverFn,
+ pathwayNodeClickFn,
+ retryAttempt=0
) {
const pvjsScript = document.querySelector(`script[src="${PVJS_URL}"]`);
if (!pvjsScript) {loadPvjsScript();}
@@ -170,7 +320,12 @@ export async function drawPathway(
) {
if (retryAttempt <= 40) {
setTimeout(() => {
- drawPathway(pwId, sourceGene, destGene, dimensions, retryAttempt++);
+ drawPathway(
+ pwId, sourceGene, destGene,
+ outerSelector, dimensions, showClose,
+ geneNodeHoverFn, pathwayNodeClickFn,
+ retryAttempt++
+ );
}, 250);
return;
} else {
@@ -192,7 +347,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();
}
@@ -233,7 +388,9 @@ 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);
// zoomToEntity(sourceEntityId);
@@ -245,4 +402,42 @@ export async function drawPathway(
// Notify listeners of event completion
const ideogramPathwayEvent = new CustomEvent('ideogramDrawPathway', {detail});
document.dispatchEvent(ideogramPathwayEvent);
+
+ // Add mouseover handler to gene nodes in this pathway diagram
+ pathwayContainer.querySelectorAll('g.GeneProduct').forEach(geneNode => {
+ const geneName = geneNode.getAttribute('name');
+ let tooltipContent = geneName;
+ geneNode.addEventListener('mouseover', (event) => {
+ if (geneNodeHoverFn) {
+ tooltipContent = geneNodeHoverFn(event, geneName);
+ geneNode.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/protein.js b/src/js/kit/protein.js
index b5bd885e..23baca2d 100644
--- a/src/js/kit/protein.js
+++ b/src/js/kit/protein.js
@@ -190,9 +190,9 @@ function isEligibleforProteinSvg(gene, ideo) {
return (
ideo.config.showProteinInTooltip &&
!(
- 'proteinCache' in ideo === false ||
- gene in ideo.proteinCache === false ||
- ('spliceExons' in ideo === false || ideo.spliceExons === false)
+ 'proteinCache' in Ideogram === false ||
+ gene in Ideogram.proteinCache === false ||
+ ('spliceExons' in Ideogram === false || Ideogram.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..a2340898 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) {
@@ -1652,7 +1657,20 @@ 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 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,
+ geneNodeHoverFn, pathwayNodeClickFn);
event.stopPropagation();
});
});
@@ -1672,7 +1690,7 @@ function centralizeTooltipPosition() {
function onDidShowAnnotTooltip() {
const ideo = this;
- if (ideo.tissueCache) {
+ if (Ideogram.tissueCache) {
centralizeTooltipPosition();
}
handleTooltipClick(ideo);
@@ -1843,7 +1861,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()) {
@@ -1956,6 +1974,7 @@ const globalKitDefaults = {
showAnnotLabels: true,
showGeneStructureInTooltip: true,
showProteinInTooltip: true,
+ showVariantInTooltip: false,
chrFillColor: {centromere: '#DAAAAA'}
};
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..f32f279c 100644
--- a/src/js/kit/variant.js
+++ b/src/js/kit/variant.js
@@ -326,7 +326,10 @@ export async function getVariantsSvg(
const gene = getGeneFromStructureName(structureName, ideo);
- const cache = ideo.variantCache;
+ const cache = Ideogram.variantCache;
+ if (!cache) {
+ return null;
+ }
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;
}