diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/dimensionalityReduction.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/dimensionalityReduction.tsx new file mode 100644 index 0000000000..71ed6b531f --- /dev/null +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/dimensionalityReduction.tsx @@ -0,0 +1,176 @@ +import { useFindEntityAndVariableCollection } from '../../..'; +import { VariableCollectionDescriptor } from '../../../types/variable'; +import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; +import { ComputationConfigProps, ComputationPlugin } from '../Types'; +import { + useConfigChangeHandler, + assertComputationWithConfig, + isNotAbsoluteAbundanceVariableCollection, + partialToCompleteCodec, +} from '../Utils'; +import * as t from 'io-ts'; +import { Computation } from '../../../types/visualization'; +import ScatterBetadivSVG from '../../visualizations/implementations/selectorIcons/ScatterBetadivSVG'; +import { ComputationStepContainer } from '../ComputationStepContainer'; +import './Plugins.scss'; +import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { VariableCollectionSingleSelect } from '../../variableSelectors/VariableCollectionSingleSelect'; +import { IsEnabledInPickerParams } from '../../visualizations/VisualizationTypes'; +import { entityTreeToArray } from '../../../utils/study-metadata'; + +const cx = makeClassNameHelper('AppStepConfigurationContainer'); + +export type DimensionalityReductionConfig = t.TypeOf< + typeof DimensionalityReductionConfig +>; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const DimensionalityReductionConfig = t.partial({ + collectionVariable: VariableCollectionDescriptor, +}); + +const CompleteDimensionalityReductionConfig = partialToCompleteCodec( + DimensionalityReductionConfig +); + +export const plugin: ComputationPlugin = { + configurationComponent: DimensionalityReductionConfiguration, + configurationDescriptionComponent: + DimensionalityReductionConfigDescriptionComponent, + createDefaultConfiguration: () => ({}), + isConfigurationComplete: CompleteDimensionalityReductionConfig.is, + visualizationPlugins: { + scatterplot: scatterplotVisualization + .withOptions({ + getComputedXAxisDetails(config) { + if ( + DimensionalityReductionConfig.is(config) && + config.collectionVariable + ) { + return { + entityId: config.collectionVariable.entityId, + placeholderDisplayName: 'PCA Axis 1', + variableId: 'PC1', + }; + } + }, + getComputedYAxisDetails(config) { + if ( + DimensionalityReductionConfig.is(config) && + config.collectionVariable + ) { + return { + entityId: config.collectionVariable.entityId, + placeholderDisplayName: 'PCA Axis 2', + variableId: 'PC2', + }; + } + }, + hideShowMissingnessToggle: true, + hideTrendlines: true, + hideFacetInputs: true, + hideLogScale: true, + returnPointIds: false, + sendComputedVariablesInRequest: true, + }) + .withSelectorIcon(ScatterBetadivSVG), + }, + isEnabledInPicker: isEnabledInPicker, + studyRequirements: + 'These visualizations are only available for studies with compatible assay data.', +}; + +function DimensionalityReductionConfigDescriptionComponent({ + computation, +}: { + computation: Computation; +}) { + const findEntityAndVariableCollection = useFindEntityAndVariableCollection(); + assertComputationWithConfig(computation, DimensionalityReductionConfig); + const { configuration } = computation.descriptor; + const collectionVariable = + 'collectionVariable' in configuration + ? configuration.collectionVariable + : undefined; + const updatedCollectionVariable = + findEntityAndVariableCollection(collectionVariable); + + return ( +
+

+ Data:{' '} + + {updatedCollectionVariable ? ( + `${updatedCollectionVariable.entity.displayName} > ${updatedCollectionVariable.variableCollection.displayName}` + ) : ( + Not selected + )} + +

+
+ ); +} + +export function DimensionalityReductionConfiguration( + props: ComputationConfigProps +) { + const { + computationAppOverview, + computation, + analysisState, + visualizationId, + changeConfigHandlerOverride, + showStepNumber = true, + } = props; + assertComputationWithConfig(computation, DimensionalityReductionConfig); + const configuration = computation.descriptor.configuration; + + const workspaceChangeConfigHandler = useConfigChangeHandler( + analysisState, + computation, + visualizationId + ); + + const changeConfigHandler = changeConfigHandlerOverride + ? changeConfigHandlerOverride + : workspaceChangeConfigHandler; + + return ( + +
+
+ Data + { + if (typeof value === 'string') return; + changeConfigHandler('collectionVariable', value); + }} + collectionPredicate={isNotAbsoluteAbundanceVariableCollection} + /> +
+
+
+ ); +} + +// Dimensionality reduction's only requirement of the study is that it contains +// at least one collection. +function isEnabledInPicker({ + studyMetadata, +}: IsEnabledInPickerParams): boolean { + if (!studyMetadata) return false; + const entities = entityTreeToArray(studyMetadata.rootEntity); + + // Ensure there are collections in this study. Otherwise, disable app + const studyHasCollections = entities.some( + (entity) => !!entity.collections?.length + ); + + return studyHasCollections; +} diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts index bb1a091e04..9ba9bd4f22 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts @@ -6,18 +6,20 @@ import { plugin as distributions } from './distributions'; import { plugin as countsandproportions } from './countsAndProportions'; import { plugin as abundance } from './abundance'; import { plugin as differentialabundance } from './differentialabundance'; -// import { plugin as differentialexpression } from './differentialExpression'; +import { plugin as differentialexpression } from './differentialExpression'; import { plugin as correlationassaymetadata } from './correlationAssayMetadata'; // mbio import { plugin as correlationassayassay } from './correlationAssayAssay'; // mbio import { plugin as correlation } from './correlation'; // genomics (- vb) import { plugin as selfcorrelation } from './selfCorrelation'; import { plugin as xyrelationships } from './xyRelationships'; +import { plugin as dimensionalityreduction } from './dimensionalityReduction'; export const plugins: Record = { abundance, alphadiv, betadiv, + dimensionalityreduction, differentialabundance, - // differentialexpression, + differentialexpression, correlationassaymetadata, correlationassayassay, correlation, diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index 668981259d..8ed978d7aa 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -286,6 +286,7 @@ interface Options hideTrendlines?: boolean; hideLogScale?: boolean; returnPointIds?: boolean; // Determines whether the backend should return the ids of each point in the scatterplot + sendComputedVariablesInRequest?: boolean; // Determines whether computed variable descriptors should be sent to the backend in the data request. } function ScatterplotViz(props: VisualizationProps) { @@ -332,6 +333,7 @@ function ScatterplotViz(props: VisualizationProps) { computedYAxisDetails, computedOverlayVariableDescriptor, providedOverlayVariableDescriptor, + sendComputedVariablesInRequest, ] = useMemo( () => [ options?.getComputedXAxisDetails?.(computation.descriptor.configuration), @@ -340,6 +342,7 @@ function ScatterplotViz(props: VisualizationProps) { computation.descriptor.configuration ), options?.getOverlayVariable?.(computation.descriptor.configuration), + options?.sendComputedVariablesInRequest ?? false, ], [computation.descriptor.configuration, options] ); @@ -745,8 +748,14 @@ function ScatterplotViz(props: VisualizationProps) { config: { outputEntityId: outputEntity.id, valueSpec: hideTrendlines ? undefined : valueSpecValue, - xAxisVariable: vizConfig.xAxisVariable, - yAxisVariable: vizConfig.yAxisVariable, + xAxisVariable: + sendComputedVariablesInRequest && computedXAxisDescriptor + ? computedXAxisDescriptor + : vizConfig.xAxisVariable, + yAxisVariable: + sendComputedVariablesInRequest && computedYAxisDescriptor + ? computedYAxisDescriptor + : vizConfig.yAxisVariable, overlayVariable: vizConfig.overlayVariable, facetVariable: vizConfig.facetVariable ? [vizConfig.facetVariable] diff --git a/packages/libs/eda/src/lib/notebook/ComputeNotebookCell.tsx b/packages/libs/eda/src/lib/notebook/ComputeNotebookCell.tsx index 059aabd431..dc93b01ada 100644 --- a/packages/libs/eda/src/lib/notebook/ComputeNotebookCell.tsx +++ b/packages/libs/eda/src/lib/notebook/ComputeNotebookCell.tsx @@ -15,6 +15,7 @@ export function ComputeNotebookCell( const { analysisState, cell, isDisabled, wdkState, projectId } = props; const { analysis } = analysisState; if (analysis == null) throw new Error('Cannot find analysis.'); + console.log('compute name', cell.computationName); const { computationName, @@ -48,6 +49,7 @@ export function ComputeNotebookCell( // Ideally it should also use the functional update form of setComputations. // // We'll use a special, simple changeConfigHandler for the computation configuration + console.log('analysis', analysis.descriptor.computations); const changeConfigHandler = (propertyName: string, value?: any) => { if (!computation || !analysis.descriptor.computations[0]) return; @@ -61,7 +63,9 @@ export function ComputeNotebookCell( const existingComputation = analysisState.analysis?.descriptor.computations.find( - (comp) => isEqual(comp.descriptor.configuration, updatedConfiguration) //&& + (comp) => + isEqual(comp.computationId, computationId) && + isEqual(comp.descriptor.configuration, updatedConfiguration) ); if (existingComputation) return; @@ -74,7 +78,13 @@ export function ComputeNotebookCell( }, }; - analysisState.setComputations([updatedComputation]); + analysisState.setComputations((computations) => { + return computations.map((comp) => + comp.computationId === computation.computationId + ? updatedComputation + : comp + ); + }); }; const { jobStatus, createJob } = useComputeJobStatus( diff --git a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx index a1292bd74b..8b94fac591 100644 --- a/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx +++ b/packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx @@ -118,8 +118,11 @@ export function EdaNotebookAnalysis(props: Props) { // This ensures that updates are queued and applied in order, even across // multiple recursive calls. notebookPreset.cells.forEach((cell) => processCell(cell)); + console.log('createdcells'); }, [analysis, setComputations, addVisualization, notebookPreset]); + console.log('notebookpreset', notebookPreset); + // // Now we render the notebook directly from the read-only `notebookPreset`, // fetching computations and visualizations from analysisState.analysis where needed. diff --git a/packages/libs/eda/src/lib/notebook/NotebookPresets.tsx b/packages/libs/eda/src/lib/notebook/NotebookPresets.tsx index 5752e5dfca..5aed8168c1 100644 --- a/packages/libs/eda/src/lib/notebook/NotebookPresets.tsx +++ b/packages/libs/eda/src/lib/notebook/NotebookPresets.tsx @@ -97,6 +97,29 @@ export const presetNotebooks: Record = { /> ), }, + { + type: 'compute', + title: 'Configure PCA', + computationName: 'dimensionalityreduction', + computationId: 'pca_1', + helperText: ( + + ), + cells: [ + { + type: 'visualization', + title: 'PCA Plot', + visualizationName: 'scatterplot', + visualizationId: 'pca_1', + }, + ], + }, { type: 'compute', title: 'Setup DESeq2 Computation', @@ -104,9 +127,9 @@ export const presetNotebooks: Record = { computationId: 'de_1', helperText: ( @@ -154,7 +177,7 @@ export const presetNotebooks: Record = { }, helperText: ( = { title: 'Review and run search', helperText: ( = { if (!analysisState.analysis?.descriptor?.computations?.length) { return
No analysis configuration available
; } - const computation = - analysisState.analysis.descriptor.computations[0]; - if (!computation?.visualizations?.length) { + const differentialExpressionComputation = + analysisState.analysis.descriptor.computations[1]; + if (!differentialExpressionComputation?.visualizations?.length) { return
No visualization configuration available
; } const volcanoPlotConfig = - analysisState.analysis?.descriptor.computations[0] - .visualizations[0].descriptor.configuration; + differentialExpressionComputation.visualizations[0].descriptor + .configuration; if ( !volcanoPlotConfig || @@ -239,7 +262,7 @@ export const presetNotebooks: Record = { }} > To make adjustments, update the volcano plot settings in - step 2. + step 3. ); diff --git a/packages/libs/eda/src/lib/notebook/TextNotebookCell.tsx b/packages/libs/eda/src/lib/notebook/TextNotebookCell.tsx index 1413484c70..123c0343f2 100644 --- a/packages/libs/eda/src/lib/notebook/TextNotebookCell.tsx +++ b/packages/libs/eda/src/lib/notebook/TextNotebookCell.tsx @@ -1,12 +1,18 @@ import ExpandablePanel from '@veupathdb/coreui/lib/components/containers/ExpandablePanel'; import { NotebookCellProps } from './NotebookCell'; import { TextCellDescriptor } from './NotebookPresets'; +import { useMemo } from 'react'; export function TextNotebookCell(props: NotebookCellProps) { const { cell, isDisabled, analysisState } = props; const { text, title, getDynamicContent } = cell; + const dynamicContent = useMemo( + () => getDynamicContent?.(analysisState), + [getDynamicContent, analysisState] + ); + return ( <> {cell.helperText && ( @@ -24,7 +30,7 @@ export function TextNotebookCell(props: NotebookCellProps) { className={'NotebookCellContent' + (isDisabled ? ' disabled' : '')} > {text} - {getDynamicContent && getDynamicContent(analysisState)} + {dynamicContent}