Skip to content

Commit e0b9cf6

Browse files
authored
Merge pull request #2332 from broadinstitute/development
Release 1.106.0
2 parents 249b4eb + 509b2c5 commit e0b9cf6

21 files changed

+1465
-50
lines changed

app/javascript/components/explore/ExploreDisplayTabs.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ export default function ExploreDisplayTabs({
660660
<DotPlot
661661
studyAccession={studyAccession}
662662
{... exploreParamsWithDefaults}
663+
exploreInfo={exploreInfo}
663664
annotationValues={getAnnotationValues(
664665
exploreParamsWithDefaults?.annotation,
665666
exploreParamsWithDefaults?.annotationList?.annotations
@@ -675,6 +676,7 @@ export default function ExploreDisplayTabs({
675676
studyAccession={studyAccession}
676677
{... exploreParamsWithDefaults}
677678
morpheusData={morpheusData}
679+
isVisible={shownTab === 'heatmap'}
678680
dimensions={getPlotDimensions({ showViewOptionsControls, showDifferentialExpressionTable })}
679681
/>
680682
</div>

app/javascript/components/visualization/DotPlot.jsx

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import { withErrorBoundary } from '~/lib/ErrorBoundary'
1111
import LoadingSpinner, { morpheusLoadingSpinner } from '~/lib/LoadingSpinner'
1212
import { fetchServiceWorkerCache } from '~/lib/service-worker-cache'
1313
import { getSCPContext } from '~/providers/SCPContextProvider'
14+
import { getFeatureFlagsWithDefaults } from '~/providers/UserProvider'
15+
import '~/lib/dot-plot-precompute-patch'
1416

1517
export const dotPlotColorScheme = {
1618
// Blue, purple, red. These red and blue hues are accessible, per WCAG.
1719
colors: ['#0000BB', '#CC0088', '#FF0000'],
18-
1920
// TODO: Incorporate expression units, once such metadata is available.
20-
values: [0, 0.5, 1]
21+
values: [0, 0.5, 1],
22+
scalingMode: 'relative'
2123
}
2224

2325
/**
@@ -180,6 +182,17 @@ function patchServiceWorkerCache() {
180182
}
181183
}
182184

185+
/**
186+
* Determines whether to use preprocessed dot plot data
187+
* @param {Object} flags - Feature flags object
188+
* @param {Object} exploreInfo - Explore info containing cluster data
189+
* @returns {boolean} - True if both feature flag is enabled AND cluster has dot plot genes
190+
*/
191+
export function shouldUsePreprocessedData(flags, exploreInfo) {
192+
const hasDotPlotGenes = exploreInfo?.cluster?.hasDotPlotGenes || false
193+
return (flags?.dot_plot_preprocessing_frontend && hasDotPlotGenes) || false
194+
}
195+
183196
/** Renders a Morpheus-powered dot plot for the given URL paths and annotation
184197
* Note that this has a lot in common with Heatmap.js. they are separate for now
185198
* as their display capabilities may diverge (esp. since DotPlot is used in global gene search)
@@ -191,22 +204,27 @@ function patchServiceWorkerCache() {
191204
*/
192205
function RawDotPlot({
193206
studyAccession, genes=[], cluster, annotation={},
194-
subsample, annotationValues, setMorpheusData
207+
subsample, annotationValues, setMorpheusData, exploreInfo, dimensions
195208
}) {
196209
const [graphId] = useState(_uniqueId('dotplot-'))
197210
const { ErrorComponent, showError, setShowError, setErrorContent } = useErrorMessage()
198211

199212
useEffect(() => {
200213
/** Fetch Morpheus data for dot plot */
201214
async function getDataset() {
215+
const flags = getFeatureFlagsWithDefaults()
216+
const usePreprocessed = shouldUsePreprocessedData(flags, exploreInfo)
217+
202218
const [dataset, perfTimes] = await fetchMorpheusJson(
203219
studyAccession,
204220
genes,
205221
cluster,
206222
annotation.name,
207223
annotation.type,
208224
annotation.scope,
209-
subsample
225+
subsample,
226+
false, // mock
227+
usePreprocessed // isPrecomputed
210228
)
211229
logFetchMorpheusDataset(perfTimes, cluster, annotation, genes)
212230

@@ -229,9 +247,15 @@ function RawDotPlot({
229247
annotationValues,
230248
setErrorContent,
231249
setShowError,
232-
genes
250+
genes,
251+
dimensions
233252
})
234-
setMorpheusData(dataset)
253+
// Only share dataset with Heatmap if it's not preprocessed dot plot data
254+
// Preprocessed data has a different format that Heatmap can't consume
255+
const isPreprocessedFormat = !!(dataset?.annotation_name && dataset?.values && dataset?.genes)
256+
if (!isPreprocessedFormat) {
257+
setMorpheusData(dataset)
258+
}
235259
})
236260
}
237261
}, [
@@ -260,13 +284,25 @@ export default DotPlot
260284
/** Render Morpheus dot plot */
261285
export function renderDotPlot({
262286
target, dataset, annotationName, annotationValues,
263-
setShowError, setErrorContent, genes, drawCallback
287+
setShowError, setErrorContent, genes, drawCallback, dimensions
264288
}) {
265289
const $target = $(target)
266290
$target.empty()
267291

268-
// Collapse by mean
269-
const tools = [{
292+
// Check if dataset is pre-computed dot plot data
293+
// Pre-computed data has structure: { annotation_name, values, genes }
294+
let processedDataset = dataset
295+
let isPrecomputed = false
296+
297+
if (dataset && dataset.annotation_name && dataset.values && dataset.genes) {
298+
// This is pre-computed dot plot data - convert it using the patch
299+
// Pass genes array to preserve the original gene order
300+
processedDataset = window.createMorpheusDotPlot(dataset, genes)
301+
isPrecomputed = true
302+
}
303+
304+
// Collapse by mean (only for non-precomputed data)
305+
const tools = isPrecomputed ? [] : [{
270306
name: 'Collapse',
271307
params: {
272308
collapse_method: 'Mean',
@@ -275,30 +311,37 @@ export function renderDotPlot({
275311
collapse_to_fields: [annotationName],
276312
pass_expression: '>',
277313
pass_value: '0',
278-
percentile: '100',
314+
percentile: '75',
279315
compute_percent: true
280316
}
281317
}]
282318

283319
const config = {
284320
shape: 'circle',
285-
dataset,
321+
dataset: processedDataset,
286322
el: $target,
287323
menu: null,
288324
error: morpheusErrorHandler($target, setShowError, setErrorContent),
289-
colorScheme: {
290-
scalingMode: 'relative'
291-
},
292325
focus: null,
293326
tabManager: morpheusTabManager($target),
294327
tools,
295328
loadedCallback: () => logMorpheusPerfTime(target, 'dotplot', genes)
296329
}
297330

331+
// For pre-computed data, tell Morpheus to display series 0 for color
332+
// and use series 1 for sizing (which happens automatically with shape: 'circle')
333+
if (isPrecomputed) {
334+
config.symmetricColorScheme = false
335+
// Tell Morpheus which series to use for coloring the heatmap
336+
config.seriesIndex = 0 // Display series 0 (Mean Expression) for colors
337+
// Explicitly set the size series
338+
config.sizeBySeriesIndex = 1 // Use series 1 (__count) for sizing
339+
}
340+
298341
// Load annotations if specified
299-
config.columnSortBy = [
300-
{ field: annotationName, order: 0 }
301-
]
342+
// config.columnSortBy = [
343+
// { field: annotationName, order: 0 }
344+
// ]
302345
config.columns = [
303346
{ field: annotationName, display: 'text' }
304347
]
@@ -319,12 +362,80 @@ export function renderDotPlot({
319362
config.columnColorModel = annotColorModel
320363

321364

322-
config.colorScheme = dotPlotColorScheme
365+
// Set color scheme (will be overridden for precomputed data below)
366+
if (!isPrecomputed) {
367+
config.colorScheme = dotPlotColorScheme
368+
}
369+
370+
// For precomputed data, configure the sizer to use the __count series
371+
if (isPrecomputed && processedDataset) {
372+
// The color scheme should already have a sizer - we just need to configure it
373+
config.sizeBy = {
374+
seriesName: 'percent',
375+
min: 0,
376+
max: 75
377+
}
378+
379+
// Use relative color scheme for raw expression values
380+
// This will scale colors based on the actual data range across all genes and cell types
381+
config.colorScheme = {
382+
colors: ['#0000BB', '#CC0088', '#FF0000'],
383+
values: [0, 0.5, 1],
384+
scalingMode: 'relative'
385+
}
386+
}
323387

324388
patchServiceWorkerCache()
325389

390+
/** Adjust dot plot height to ensure legend remains visible */
391+
function adjustDotPlotHeight() {
392+
if (!dimensions?.height) {return}
393+
394+
// Get the actual height of the legend dynamically
395+
// Use getBoundingClientRect() for SVG elements instead of offsetHeight
396+
const legendElement = document.querySelector('.dot-plot-legend-container')
397+
const legendHeight = legendElement ? legendElement.getBoundingClientRect().height : 70 // Fallback to 70px
398+
399+
const dotPlotElement = $target[0]
400+
const dotPlotHeight = dotPlotElement.scrollHeight // Use scrollHeight to get full content height
401+
const totalNeededHeight = dotPlotHeight + legendHeight
402+
403+
// If total height exceeds available space, shrink the dot plot
404+
// This ensures the legend remains visible without scrolling
405+
if (totalNeededHeight > dimensions.height) {
406+
const adjustedHeight = dimensions.height - legendHeight
407+
if (adjustedHeight > 100) { // Ensure minimum reasonable height
408+
$target.css('height', `${adjustedHeight}px`)
409+
$target.css('overflow-y', 'auto')
410+
}
411+
} else {
412+
// Reset height if there's enough space
413+
$target.css('height', '')
414+
$target.css('overflow-y', '')
415+
}
416+
}
417+
326418
config.drawCallback = function() {
327419
const dotPlot = this
420+
421+
// After rendering, check if dot plot + legend will fit in available dimensions
422+
// Use setTimeout to ensure Morpheus has fully rendered and layout is complete
423+
if (dimensions?.height) {
424+
setTimeout(() => {
425+
adjustDotPlotHeight()
426+
}, 100) // Wait 100ms for Morpheus to complete layout
427+
428+
// Also listen for window resize events to re-adjust height
429+
const resizeHandler = () => {
430+
adjustDotPlotHeight()
431+
}
432+
433+
// Remove any existing listener to avoid duplicates
434+
$(window).off('resize.dotplot')
435+
// Add new listener
436+
$(window).on('resize.dotplot', resizeHandler)
437+
}
438+
328439
if (drawCallback) {drawCallback(dotPlot)}
329440
}
330441

app/javascript/components/visualization/Heatmap.jsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { renderHeatmap, refitHeatmap } from '~/lib/morpheus-heatmap'
1010
import { withErrorBoundary } from '~/lib/ErrorBoundary'
1111
import LoadingSpinner, { morpheusLoadingSpinner } from '~/lib/LoadingSpinner'
1212

13-
1413
/** renders a morpheus powered heatmap for the given params
1514
* @param genes {Array[String]} array of gene names
1615
* @param cluster {string} the name of the cluster, or blank/null for the study's default
@@ -20,14 +19,39 @@ import LoadingSpinner, { morpheusLoadingSpinner } from '~/lib/LoadingSpinner'
2019
*/
2120
function RawHeatmap({
2221
studyAccession, genes=[], cluster, annotation={}, subsample, heatmapFit, heatmapRowCentering,
23-
morpheusData
22+
morpheusData, isVisible=true
2423
}) {
2524
const [graphId] = useState(_uniqueId('heatmap-'))
25+
const [dataset, setDataset] = useState(morpheusData)
2626
const morpheusHeatmap = useRef(null)
2727
const { ErrorComponent, setShowError, setErrorContent } = useErrorMessage()
28+
29+
// Fetch dataset if not provided via morpheusData prop, but only when tab is visible
30+
useEffect(() => {
31+
async function fetchDataset() {
32+
if (isVisible && !morpheusData && cluster && annotation.name && genes.length > 0) {
33+
const [data] = await fetchMorpheusJson(
34+
studyAccession,
35+
genes,
36+
cluster,
37+
annotation.name,
38+
annotation.type,
39+
annotation.scope,
40+
subsample,
41+
false, // mock
42+
false // usePreprocessed - always use full morpheus data for heatmaps
43+
)
44+
setDataset(data)
45+
} else if (morpheusData) {
46+
setDataset(morpheusData)
47+
}
48+
}
49+
fetchDataset()
50+
}, [isVisible, studyAccession, genes.join(','), cluster, annotation.name, annotation.type, annotation.scope, subsample, morpheusData])
51+
2852
// we can't render until we know what the cluster is, since morpheus requires the annotation name
2953
// so don't try until we've received this, unless we're showing a Gene List
30-
const canRender = !!cluster && !!morpheusData
54+
const canRender = !!cluster && !!dataset
3155

3256
useEffect(() => {
3357
if (canRender) {
@@ -39,7 +63,7 @@ function RawHeatmap({
3963
setShowError(false)
4064
morpheusHeatmap.current = renderHeatmap({
4165
target,
42-
dataset: morpheusData,
66+
dataset,
4367
annotationCellValuesURL: '',
4468
annotationName: annotation.name,
4569
fit: heatmapFit,
@@ -53,7 +77,7 @@ function RawHeatmap({
5377
}, [
5478
studyAccession,
5579
genes.join(','),
56-
morpheusData,
80+
dataset,
5781
cluster,
5882
annotation.name,
5983
annotation.scope,

0 commit comments

Comments
 (0)