@@ -11,13 +11,15 @@ import { withErrorBoundary } from '~/lib/ErrorBoundary'
1111import LoadingSpinner , { morpheusLoadingSpinner } from '~/lib/LoadingSpinner'
1212import { fetchServiceWorkerCache } from '~/lib/service-worker-cache'
1313import { getSCPContext } from '~/providers/SCPContextProvider'
14+ import { getFeatureFlagsWithDefaults } from '~/providers/UserProvider'
15+ import '~/lib/dot-plot-precompute-patch'
1416
1517export 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 */
192205function 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 */
261285export 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
0 commit comments