Skip to content

Commit

Permalink
Merge pull request #2172 from broadinstitute/ew-more-pathway-nodes
Browse files Browse the repository at this point in the history
Scalable expression overlays for pathway diagrams (SCP-5710)
  • Loading branch information
eweitz authored Nov 15, 2024
2 parents 1922013 + d60279f commit cbefd8b
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 47 deletions.
136 changes: 97 additions & 39 deletions app/javascript/lib/pathway-expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,31 +145,49 @@ export async function renderBackgroundDotPlot(
/** Get unique genes in pathway diagram, ranked by global interest */
export function getPathwayGenes(ranks) {
const dataNodes = Array.from(document.querySelectorAll('#_ideogramPathwayContainer g.DataNode'))
const geneNodes = dataNodes.filter(
dataNode => Array.from(dataNode.classList).some(cls => cls.startsWith('Ensembl_ENS'))
)
const geneNodes = []
for (let i = 0; i < dataNodes.length; i++) {
const dataNode = dataNodes[i]
const classes = dataNode.classList

for (let j = 0; j < classes.length; j++) {
const cls = classes[j]
const isGene = ['geneproduct', 'rna', 'protein'].includes(cls.toLowerCase())
if (isGene) {
geneNodes.push(dataNode)
break
}
}
}

const genes = geneNodes.map(
node => {return { domId: node.id, name: node.querySelector('text').textContent }}
)

const rankedGenes = genes
.filter(gene => ranks.includes(gene.name))
.sort((a, b) => ranks.indexOf(a.name) - ranks.indexOf(b.name))

return rankedGenes
}

/** Get up to 50 genes from pathway, including searched gene and interacting gene */
function getDotPlotGenes(searchedGene, interactingGene, pathwayGenes) {
/** Slice array into batches of a given size */
function sliceIntoBatches(arr, batchSize) {
const result = []
for (let i = 0; i < arr.length; i += batchSize) {
result.push(arr.slice(i, i + batchSize))
}
return result
}

/** Get genes from pathway, in batches of up to 50 genes, eliminating duplicates */
export function getDotPlotGeneBatches(pathwayGenes) {
const genes = pathwayGenes.map(g => g.name)
const uniqueGenes = Array.from(new Set(genes))
const dotPlotGenes = uniqueGenes.slice(0, 50)
if (!dotPlotGenes.includes(searchedGene)) {
dotPlotGenes[dotPlotGenes.length - 2] = searchedGene
}
if (!dotPlotGenes.includes(interactingGene)) {
dotPlotGenes[dotPlotGenes.length - 1] = interactingGene
}

return dotPlotGenes
const dotPlotGeneBatches = sliceIntoBatches(uniqueGenes, 50)

return dotPlotGeneBatches
}

/**
Expand Down Expand Up @@ -307,18 +325,33 @@ function writeLoadingIndicator(loadingCls) {
headerLink.insertAdjacentHTML('afterend', loading)
}

/** Merge new and old dot plots metrics */
function mergeDotPlotMetrics(newMetrics, oldMetrics) {
Object.entries(oldMetrics).map(([label, oldGeneMetrics]) => {
const newGeneMetrics = newMetrics[label]
if (!newGeneMetrics) {
return
}
newMetrics[label] = Object.assign(newGeneMetrics, oldGeneMetrics)
})

return newMetrics
}

/** Color pathway gene nodes by expression */
function renderPathwayExpression(
async function renderPathwayExpression(
searchedGene, interactingGene,
ideogram, dotPlotParams
) {
let allDotPlotMetrics = {}

const ranks = ideogram.geneCache.interestingNames
const pathwayGenes = getPathwayGenes(ranks)
const dotPlotGenes = getDotPlotGenes(searchedGene, interactingGene, pathwayGenes, ideogram)

const dotPlotGeneBatches = getDotPlotGeneBatches(pathwayGenes)
const { studyAccession, cluster, annotation } = dotPlotParams

let numDraws = 0
let numRenders = 0

const annotationLabels = getEligibleLabels()

Expand All @@ -329,63 +362,88 @@ function renderPathwayExpression(
function backgroundDotPlotDrawCallback(dotPlot) {
// The first render is for uncollapsed cell-x-gene metrics (heatmap),
// the second render is for collapsed label-x-gene metrics (dotplot)

numDraws += 1
if (numDraws === 1) {return}

const dotPlotMetrics = getDotPlotMetrics(dotPlot)

if (!dotPlotMetrics) {
// Occurs upon resizing window, artifact of internal Morpheus handling
// of pre-dot-plot heatmap matrix. No user-facing impact.
return
}
writePathwayExpressionHeader(loadingCls, dotPlotMetrics, annotationLabels, pathwayGenes)

if (!annotationLabels.includes(Object.keys(dotPlotMetrics)[0])) {
// Another protection for computing only for dot plots, not heatmaps
return
}

allDotPlotMetrics = mergeDotPlotMetrics(dotPlotMetrics, allDotPlotMetrics)

writePathwayExpressionHeader(loadingCls, allDotPlotMetrics, annotationLabels, pathwayGenes)

const annotationLabel = annotationLabels[0]
colorPathwayGenesByExpression(pathwayGenes, dotPlotMetrics, annotationLabel)
colorPathwayGenesByExpression(pathwayGenes, allDotPlotMetrics, annotationLabel)

if (numRenders <= dotPlotGeneBatches.length) {
numRenders += 1
// Future optimization: render background dot plot one annotation at a time. This would
// speed up initial pathway expression overlay rendering, and increase the practical limit
// on number of genes that could be retrieved via SCP API Morpheus endpoint.
renderBackgroundDotPlot(
studyAccession, dotPlotGeneBatches[numRenders], cluster, annotation,
'All', annotationLabels, backgroundDotPlotDrawCallback,
'#related-genes-ideogram-container'
)
}
}

// Future optimization: render background dot plot one annotation at a time. This would
// speed up initial pathway expression overlay rendering, and increase the practical limit
// on number of genes that could be retrieved via SCP API Morpheus endpoint.
renderBackgroundDotPlot(
studyAccession, dotPlotGenes, cluster, annotation,
studyAccession, dotPlotGeneBatches[0], cluster, annotation,
'All', annotationLabels, backgroundDotPlotDrawCallback,
'#related-genes-ideogram-container'
)
}

/** Draw pathway diagram */
function drawPathway(event, dotPlotParams, ideogram) {
// Hide popover instantly upon drawing pathway; don't wait ~2 seconds
const ideoTooltip = document.querySelector('._ideogramTooltip')
ideoTooltip.style.opacity = 0
ideoTooltip.style.pointerEvents = 'none'

// Ensure popover for pathway diagram doesn't appear over gene search autocomplete,
// while still appearing over default visualizations.
const container = document.querySelector('#_ideogramPathwayContainer')
container.style.zIndex = 2

const details = event.detail
const searchedGene = details.sourceGene
const interactingGene = details.destGene
renderPathwayExpression(
searchedGene, interactingGene, ideogram,
dotPlotParams
)
}

/**
* Add and remove event listeners for Ideogram's `ideogramDrawPathway` event
*
* This sets up the pathway expression overlay
*/
export function manageDrawPathway(studyAccession, cluster, annotation, ideogram) {

const flags = getFeatureFlagsWithDefaults()
if (!flags?.show_pathway_expression) {return}

const dotPlotParams = { studyAccession, cluster, annotation }
if (annotation.type === 'group') {
document.removeEventListener('ideogramDrawPathway')
document.removeEventListener('ideogramDrawPathway', drawPathway)
document.addEventListener('ideogramDrawPathway', event => {

// Hide popover instantly upon drawing pathway; don't wait ~2 seconds
const ideoTooltip = document.querySelector('._ideogramTooltip')
ideoTooltip.style.opacity = 0
ideoTooltip.style.pointerEvents = 'none'

// Ensure popover for pathway diagram doesn't appear over gene search autocomplete,
// while still appearing over default visualizations.
const container = document.querySelector('#_ideogramPathwayContainer')
container.style.zIndex = 2

const details = event.detail
const searchedGene = details.sourceGene
const interactingGene = details.destGene
renderPathwayExpression(
searchedGene, interactingGene, ideogram,
dotPlotParams
)
drawPathway(event, dotPlotParams, ideogram)
})
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"crossfilter2": "^1.5.4",
"exifreader": "4.6.0",
"fflate": "^0.7.3",
"ideogram": "1.47.0",
"ideogram": "1.48.0",
"jquery": "3.5.1",
"jquery-ui": "1.13.2",
"morpheus-app": "1.0.18",
Expand Down
3 changes: 3 additions & 0 deletions test/js/lib/pathway-expression.test-data.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit cbefd8b

Please sign in to comment.