From e6634ce5f49856d4db8283f84d3748112cc8d5b5 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Wed, 27 Sep 2023 20:22:01 -0400 Subject: [PATCH 1/9] draft api changes --- .../libs/components/src/plots/VolcanoPlot.tsx | 48 +++++++++++-------- .../components/src/types/plots/volcanoplot.ts | 9 +++- .../eda/src/lib/core/api/DataClient/types.ts | 10 ++-- .../computations/plugins/abundance.tsx | 6 +-- .../computations/plugins/alphaDiv.tsx | 6 +-- .../computations/plugins/betadiv.tsx | 6 +-- .../plugins/differentialabundance.tsx | 38 +++++++++++++-- .../ScatterplotVisualization.tsx | 9 +++- .../visualizations/options/types.ts | 9 +++- .../libs/eda/src/lib/core/types/variable.ts | 10 ++++ 10 files changed, 109 insertions(+), 42 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 1347097f46..509ec1368e 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -8,6 +8,7 @@ import { import { VolcanoPlotData, VolcanoPlotDataPoint, + VolcanoPlotStats, } from '../types/plots/volcanoplot'; import { NumberRange } from '../types/general'; import { SignificanceColors, significanceColors } from '../types/plots'; @@ -46,13 +47,13 @@ export interface RawDataMinMaxValues { } export interface VolcanoPlotProps { - /** Data for the plot. An array of VolcanoPlotDataPoints */ + /** Data for the plot. An effectSizeLabel and an array of VolcanoPlotDataPoints */ data: VolcanoPlotData | undefined; /** * Used to set the fold change thresholds. Will * set two thresholds at +/- this number. Affects point colors */ - log2FoldChangeThreshold: number; + effectSizeThreshold: number; /** Set the threshold for significance. Affects point colors */ significanceThreshold: number; /** x-axis range */ @@ -83,10 +84,15 @@ export interface VolcanoPlotProps { minPValueCap?: number; } -const EmptyVolcanoPlotData: VolcanoPlotData = [ - { log2foldChange: '0', pValue: '1' }, +const EmptyVolcanoPlotStats: VolcanoPlotStats = [ + { effectSize: '0', pValue: '1' }, ]; +const EmptyVolcanoPlotData: VolcanoPlotData = { + effectSizeLabel: 'log2(FoldChange)', + statistics: EmptyVolcanoPlotStats, +}; + const MARGIN_DEFAULT = 50; interface TruncationRectangleProps { @@ -130,7 +136,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { independentAxisRange, dependentAxisRange, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, markerBodyOpacity, containerClass = 'web-components-plot', containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, @@ -155,6 +161,8 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { [] ); + const effectSizeLabel = data?.effectSizeLabel; + // Set maxes and mins of the data itself from rawDataMinMaxValues prop const { min: dataXMin, max: dataXMax } = rawDataMinMaxValues.x; const { min: dataYMin, max: dataYMax } = rawDataMinMaxValues.y; @@ -193,10 +201,8 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { * Check whether each threshold line is within the graph's axis ranges so we can * prevent the line from rendering outside the graph. */ - const showNegativeFoldChangeThresholdLine = - -log2FoldChangeThreshold > xAxisMin; - const showPositiveFoldChangeThresholdLine = - log2FoldChangeThreshold < xAxisMax; + const showNegativeFoldChangeThresholdLine = -effectSizeThreshold > xAxisMin; + const showPositiveFoldChangeThresholdLine = effectSizeThreshold < xAxisMax; const showSignificanceThresholdLine = -Math.log10(Number(significanceThreshold)) > yAxisMin && -Math.log10(Number(significanceThreshold)) < yAxisMax; @@ -207,7 +213,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { // For the actual volcano plot data. Y axis points are capped at -Math.log10(minPValueCap) const dataAccessors = { - xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.log2foldChange), + xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.effectSize), yAccessor: (d: VolcanoPlotDataPoint) => d.pValue === '0' ? -Math.log10(minPValueCap) @@ -268,7 +274,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { {/* Set up the axes and grid lines. XYChart magically lays them out correctly */} - + {/* X axis annotations */} {comparisonLabels && @@ -316,13 +322,13 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { /> )} - {/* Draw both vertical log2 fold change threshold lines */} - {log2FoldChangeThreshold && ( + {/* Draw both vertical effect size threshold lines */} + {effectSizeThreshold && ( <> {showNegativeFoldChangeThresholdLine && ( ) { {showPositiveFoldChangeThresholdLine && ( ) { d.significanceColor} findNearestDatumOverride={findNearestDatumXY} @@ -432,7 +438,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { >
  • - log2 Fold Change: {data?.log2foldChange} + {effectSizeLabel}: {data?.effectSize}
  • P Value: {data?.pValue} @@ -507,10 +513,10 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { * Assign color to point based on significance and magnitude change thresholds */ export function assignSignificanceColor( - log2foldChange: number, + effectSize: number, pValue: number, significanceThreshold: number, - log2FoldChangeThreshold: number, + effectSizeThreshold: number, significanceColors: SignificanceColors ) { // Test 1. If the y value is higher than the significance threshold, just return not significant @@ -519,12 +525,12 @@ export function assignSignificanceColor( } // Test 2. So the y is significant. Is the x larger than the positive foldChange threshold? - if (log2foldChange >= log2FoldChangeThreshold) { + if (effectSize >= effectSizeThreshold) { return significanceColors['high']; } // Test 3. Is the x value lower than the negative foldChange threshold? - if (log2foldChange <= -log2FoldChangeThreshold) { + if (effectSize <= -effectSizeThreshold) { return significanceColors['low']; } diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index 51a9fe163a..74b179a435 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,7 +1,7 @@ // Can remove the | undefined from most of these after some other backend work is merged export type VolcanoPlotDataPoint = { // log2foldChange becomes the x axis. Also used for coloring points - log2foldChange?: string; + effectSize?: string; // pValue will be negative log transformed for the y axis. Also // needed as is (untransformed) in the tooltip and when coloring points pValue?: string; @@ -15,4 +15,9 @@ export type VolcanoPlotDataPoint = { displayLabels?: string[]; }; -export type VolcanoPlotData = Array; +export type VolcanoPlotStats = Array; + +export type VolcanoPlotData = { + effectSizeLabel: string; + statistics: VolcanoPlotStats; +}; diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index c9bae7c184..6eeeebada2 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -357,16 +357,20 @@ export const ScatterplotResponse = intersection([ // The volcano plot response type MUST be the same as the VolcanoPlotData type defined in the components package export type VolcanoPlotResponse = TypeOf; -// TEMP - Many of these can be simplified after some backend work is merged (microbiomeComputations #37) -export const VolcanoPlotResponse = array( +export const VolcanoPlotStatistics = array( partial({ - log2foldChange: string, + effectSize: string, pValue: string, adjustedPValue: string, pointID: string, }) ); +export const VolcanoPlotResponse = type({ + effectSizeLabel: string, + statistics: VolcanoPlotStatistics, +}); + export interface VolcanoPlotRequestParams { studyId: string; filters: Filter[]; diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx index e5ac58d28e..b6b059ba51 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx @@ -1,6 +1,6 @@ import { useStudyMetadata } from '../../..'; import { useCollectionVariables } from '../../../hooks/workspace'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { boxplotVisualization } from '../../visualizations/implementations/BoxplotVisualization'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; @@ -19,7 +19,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type AbundanceConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const AbundanceConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, rankingMethod: t.string, }); @@ -170,7 +170,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx index eac9b0c18d..76101e76bf 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx @@ -1,5 +1,5 @@ import { useCollectionVariables, useStudyMetadata } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { boxplotVisualization } from '../../visualizations/implementations/BoxplotVisualization'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; @@ -18,7 +18,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type AlphaDivConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const AlphaDivConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, alphaDivMethod: t.string, }); @@ -153,7 +153,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx index 67022b1381..fe8846c1c6 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx @@ -1,5 +1,5 @@ import { useCollectionVariables, useStudyMetadata } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; import { isEqual, partial } from 'lodash'; @@ -18,7 +18,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type BetaDivConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const BetaDivConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, betaDivDissimilarityMethod: t.string, }); @@ -156,7 +156,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index d3b8a91438..8e59e89c11 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -5,7 +5,10 @@ import { usePromise, useStudyMetadata, } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { volcanoPlotVisualization } from '../../visualizations/implementations/VolcanoPlotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; import { isEqual, partial } from 'lodash'; @@ -67,7 +70,7 @@ const Comparator = t.intersection([ // eslint-disable-next-line @typescript-eslint/no-redeclare export const DifferentialAbundanceConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, comparator: Comparator, differentialAbundanceMethod: t.string, }); @@ -151,6 +154,10 @@ function DifferentialAbundanceConfigDescriptionComponent({ ); } +// Include available methods in this array. +// TODO do we need the display names different to these internal strings? +const DIFFERENTIAL_ABUNDANCE_METHODS = ['DESeq', 'Maaslin']; + export function DifferentialAbundanceConfiguration( props: ComputationConfigProps ) { @@ -208,11 +215,12 @@ export function DifferentialAbundanceConfiguration( })); }, [collections]); + // TODO presumably to keep the saved analyses from breaking, we need to maintain support for a variableId const selectedCollectionVar = useMemo(() => { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); @@ -280,6 +288,12 @@ export function DifferentialAbundanceConfiguration( } ); + const differentialAbundanceMethod = useMemo(() => { + if (configuration && 'differentialAbundanceMethod' in configuration) { + return configuration.differentialAbundanceMethod; + } + }, [configuration]); + return ( + +
    + Method + ({ + value: method, + display: method, + }))} + /> +
    ); 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 f45617e6f9..0e51d8b8c6 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 @@ -20,7 +20,10 @@ import { } from '../../../hooks/workspace'; import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; import { BirdsEyeView } from '../../BirdsEyeView'; @@ -243,7 +246,9 @@ interface Options getComputedYAxisDetails?( config: unknown ): ComputedVariableDetails | undefined; - getComputedOverlayVariable?(config: unknown): VariableDescriptor | undefined; + getComputedOverlayVariable?( + config: unknown + ): VariableDescriptor | VariableCollectionDescriptor | undefined; hideTrendlines?: boolean; hideLogScale?: boolean; } diff --git a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts index e8cc10e6ad..7032ede4d4 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts +++ b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts @@ -1,11 +1,16 @@ import { ReactNode } from 'react'; import { OverlayConfig } from '../../../api/DataClient'; import { Filter } from '../../../types/filter'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { Computation } from '../../../types/visualization'; export interface XAxisOptions { - getXAxisVariable?: (computeConfig: unknown) => VariableDescriptor | undefined; + getXAxisVariable?: ( + computeConfig: unknown + ) => VariableDescriptor | VariableCollectionDescriptor | undefined; } export interface OverlayOptions { diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index c1afd30d77..a7d83b6758 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -16,3 +16,13 @@ export const StringVariableValue = t.intersection([ value: t.string, }), ]); + +const _VariableCollectionBase = t.type({ + entityId: t.string, + collectionId: t.string, +}); + +export type VariableCollectionDescriptor = t.TypeOf< + typeof VariableCollectionDescriptor +>; +export const VariableCollectionDescriptor = _VariableCollectionBase; From 32e708eec943c4343fafe1195d666fc894cc2343 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Thu, 28 Sep 2023 22:06:50 -0400 Subject: [PATCH 2/9] wip --- .../computations/plugins/abundance.tsx | 4 +- .../computations/plugins/alphaDiv.tsx | 4 +- .../computations/plugins/betadiv.tsx | 4 +- .../plugins/differentialabundance.tsx | 4 +- .../implementations/BoxplotVisualization.tsx | 22 ++++-- .../ScatterplotVisualization.tsx | 14 +++- .../VolcanoPlotVisualization.tsx | 27 +++---- packages/libs/eda/src/lib/core/types/study.ts | 1 + .../libs/eda/src/lib/core/types/variable.ts | 12 ++++ .../eda/src/lib/core/utils/study-metadata.ts | 71 ++++++++++++++++++- 10 files changed, 133 insertions(+), 30 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx index b6b059ba51..1728f6c7e3 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx @@ -158,7 +158,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -170,7 +170,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx index 76101e76bf..d12ed5a99a 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx @@ -141,7 +141,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -153,7 +153,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx index fe8846c1c6..392be442c9 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx @@ -144,7 +144,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -156,7 +156,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 8e59e89c11..017aa258fc 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -207,7 +207,7 @@ export function DifferentialAbundanceConfiguration( }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -220,7 +220,7 @@ export function DifferentialAbundanceConfiguration( if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx index 516c48e8ba..0503f7fdac 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx @@ -15,7 +15,10 @@ import { } from '../../../hooks/workspace'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; import { useDataClient, useStudyMetadata } from '../../../hooks/workspace'; -import { VariableDescriptor } from '../../../types/variable'; +import { + isVariableDescriptor, + VariableDescriptor, +} from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -90,7 +93,10 @@ import { truncationConfig } from '../../../utils/truncation-config-utils'; import Notification from '@veupathdb/components/lib/components/widgets//Notification'; import { useDefaultAxisRange } from '../../../hooks/computeDefaultAxisRange'; // alphadiv abundance this should be used for collection variable -import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; +import { + findEntityAndDynamicData, + getTreeNode, +} from '../../../utils/study-metadata'; // type of computedVariableMetadata for computation apps such as alphadiv and abundance import { BoxplotRequestParams, @@ -652,12 +658,11 @@ function BoxplotViz(props: VisualizationProps) { // alphadiv abundance findEntityAndVariable does not work properly for collection variable const independentAxisEntityAndVariable = useMemo( - () => - findCollectionVariableEntityAndVariable(entities, providedXAxisVariable), + () => findEntityAndDynamicData(entities, providedXAxisVariable), [entities, providedXAxisVariable] ); const independentAxisLabel = - independentAxisEntityAndVariable?.variable.displayName ?? + getTreeNode(independentAxisEntityAndVariable)?.displayName ?? variableDisplayWithUnit(xAxisVariable) ?? 'X-axis'; @@ -752,6 +757,7 @@ function BoxplotViz(props: VisualizationProps) { /> ); + // TODO understand how we know this is a collection without checking isCollection? // List variables in a collection one by one in the variable coverage table. Create these extra rows // here and then append to the variable coverage table rows array. const collectionVariableMetadata = data.value?.computedVariableMetadata?.find( @@ -797,7 +803,11 @@ function BoxplotViz(props: VisualizationProps) { role: 'X-axis', required: true, display: independentAxisLabel, - variable: providedXAxisVariable ?? vizConfig.xAxisVariable, + variable: + isVariableDescriptor(providedOverlayVariable) && + providedOverlayVariable != null + ? providedOverlayVariable + : vizConfig.xAxisVariable, }, ...additionalVariableCoverageTableRows, { 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 0e51d8b8c6..b3da60c46a 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 @@ -23,6 +23,7 @@ import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from import { VariableDescriptor, VariableCollectionDescriptor, + isVariableDescriptor, } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -852,7 +853,9 @@ function ScatterplotViz(props: VisualizationProps) { if (computedOverlayVariableDescriptor) { return findCollectionVariableEntityAndVariable( entities, - computedOverlayVariableDescriptor + isVariableDescriptor(computedOverlayVariableDescriptor) + ? computedOverlayVariableDescriptor + : undefined )?.variable.displayName; } return variableDisplayWithUnit(overlayVariable); @@ -1912,7 +1915,9 @@ function ScatterplotViz(props: VisualizationProps) { }, { role: 'Y-axis', - required: !computedOverlayVariableDescriptor?.variableId, + required: isVariableDescriptor(computedOverlayVariableDescriptor) + ? !computedOverlayVariableDescriptor?.variableId + : false, display: dependentAxisLabel, variable: !computedOverlayVariableDescriptor && computedYAxisDescriptor @@ -1924,7 +1929,10 @@ function ScatterplotViz(props: VisualizationProps) { required: !!computedOverlayVariableDescriptor, display: legendTitle, variable: - computedOverlayVariableDescriptor ?? vizConfig.overlayVariable, + isVariableDescriptor(computedOverlayVariableDescriptor) && + computedOverlayVariableDescriptor != null + ? computedOverlayVariableDescriptor + : vizConfig.overlayVariable, }, ...additionalVariableCoverageTableRows, { diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 686add6c79..0bf64c75d9 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -190,10 +190,14 @@ function VolcanoPlotViz(props: VisualizationProps) { x: { min: 0, max: 0 }, y: { min: 1, max: 1 }, }; - const dataXMin = min(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataXMax = max(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataYMin = min(data.value.map((d) => Number(d.pValue))) ?? 0; - const dataYMax = max(data.value.map((d) => Number(d.pValue))) ?? 0; + const dataXMin = + min(data.value.statistics.map((d) => Number(d.effectSize))) ?? 0; + const dataXMax = + max(data.value.statistics.map((d) => Number(d.effectSize))) ?? 0; + const dataYMin = + min(data.value.statistics.map((d) => Number(d.pValue))) ?? 0; + const dataYMax = + max(data.value.statistics.map((d) => Number(d.pValue))) ?? 0; return { x: { min: dataXMin, max: dataXMax }, y: { min: dataYMin, max: dataYMax }, @@ -249,14 +253,14 @@ function VolcanoPlotViz(props: VisualizationProps) { */ const finalData = useMemo(() => { if (data.value && independentAxisRange && dependentAxisRange) { - const cleanedData = data.value + const cleanedData = data.value.statistics // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot. .filter((d) => { - const log2foldChange = Number(d?.log2foldChange); + const effectSize = Number(d?.effectSize); const transformedPValue = -Math.log10(Number(d?.pValue)); return ( - log2foldChange <= independentAxisRange.max && - log2foldChange >= independentAxisRange.min && + effectSize <= independentAxisRange.max && + effectSize >= independentAxisRange.min && transformedPValue <= dependentAxisRange.max && transformedPValue >= dependentAxisRange.min ); @@ -285,7 +289,7 @@ function VolcanoPlotViz(props: VisualizationProps) { pointIDs: pointID ? [pointID] : undefined, displayLabels: displayLabel ? [displayLabel] : undefined, significanceColor: assignSignificanceColor( - Number(d.log2foldChange), + Number(d.effectSize), Number(d.pValue), significanceThreshold, log2FoldChangeThreshold, @@ -294,7 +298,7 @@ function VolcanoPlotViz(props: VisualizationProps) { }; }) // Sort data in ascending order for tooltips to work most effectively - .sort((a, b) => Number(a.log2foldChange) - Number(b.log2foldChange)); + .sort((a, b) => Number(a.effectSize) - Number(b.effectSize)); // Here we're going to loop through the cleanedData to aggregate any data with shared coordinates. // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: @@ -304,8 +308,7 @@ function VolcanoPlotViz(props: VisualizationProps) { for (const entry of cleanedData) { const foundIndex = aggregatedData.findIndex( (d: VolcanoPlotDataPoint) => - d.log2foldChange === entry.log2foldChange && - d.pValue === entry.pValue + d.log2foldChange === entry.effectSize && d.pValue === entry.pValue ); if (foundIndex === -1) { aggregatedData.push(entry); diff --git a/packages/libs/eda/src/lib/core/types/study.ts b/packages/libs/eda/src/lib/core/types/study.ts index fecdf8125c..ef83c4f8d4 100644 --- a/packages/libs/eda/src/lib/core/types/study.ts +++ b/packages/libs/eda/src/lib/core/types/study.ts @@ -202,6 +202,7 @@ export const Variable = t.union([ export type VariableTreeNode = t.TypeOf; export const VariableTreeNode = t.union([Variable, VariableCategory]); +// TODO change to VariableCollectionTreeNode export type CollectionVariableTreeNode = t.TypeOf< typeof CollectionVariableTreeNode >; diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index a7d83b6758..033b777239 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -26,3 +26,15 @@ export type VariableCollectionDescriptor = t.TypeOf< typeof VariableCollectionDescriptor >; export const VariableCollectionDescriptor = _VariableCollectionBase; + +export function isVariableDescriptor( + object: any +): object is VariableDescriptor { + return 'entityId' in object && 'variableId' in object; +} + +export function isVariableCollectionDescriptor( + object: any +): object is VariableCollectionDescriptor { + return 'entityId' in object && 'collectionId' in object; +} diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index e4d770658d..6145ac7f1d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -1,11 +1,17 @@ import { keyBy } from 'lodash'; import { find } from '@veupathdb/wdk-client/lib/Utils/IterableUtils'; import { + CollectionVariableTreeNode, MultiFilterVariable, StudyEntity, VariableTreeNode, } from '../types/study'; -import { VariableDescriptor } from '../types/variable'; +import { + VariableCollectionDescriptor, + VariableDescriptor, + isVariableCollectionDescriptor, + isVariableDescriptor, +} from '../types/variable'; import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; export function entityTreeToArray(rootEntity: StudyEntity) { @@ -17,6 +23,35 @@ export interface EntityAndVariable { variable: VariableTreeNode; } +export interface EntityAndVariableCollection { + entity: StudyEntity; + variableCollection: CollectionVariableTreeNode; +} + +export function isEntityAndVariable(object: any): object is EntityAndVariable { + return 'entity' in object && 'variable' in object; +} + +export function isEntityAndVariableCollection( + object: any +): object is EntityAndVariableCollection { + return 'entity' in object && 'variableCollection' in object; +} + +export function getTreeNode( + entityAndDynamicData: + | EntityAndVariable + | EntityAndVariableCollection + | undefined +): VariableTreeNode | CollectionVariableTreeNode | undefined { + if (entityAndDynamicData == null) return undefined; + if (isEntityAndVariable(entityAndDynamicData)) { + return entityAndDynamicData.variable; + } else if (isEntityAndVariableCollection(entityAndDynamicData)) { + return entityAndDynamicData.variableCollection; + } +} + export function findEntityAndVariable( entities: Iterable, variableDescriptor?: VariableDescriptor @@ -36,6 +71,40 @@ export function findEntityAndVariable( return { entity, variable }; } +export function findEntityAndVariableCollection( + entities: Iterable, + variableCollectionDescriptor?: VariableCollectionDescriptor +): EntityAndVariableCollection | undefined { + if (variableCollectionDescriptor == null) return undefined; + const entity = find( + (entity) => entity.id === variableCollectionDescriptor.entityId, + entities + ); + const variableCollection = + entity && + find( + (variableCollection) => + variableCollection.id === variableCollectionDescriptor.collectionId, + entity.collections ?? [] + ); + if (entity == null || variableCollection == null) return undefined; + return { entity, variableCollection }; +} + +export function findEntityAndDynamicData( + entities: Iterable, + dynamicDataDescriptor?: VariableDescriptor | VariableCollectionDescriptor +): EntityAndVariable | EntityAndVariableCollection | undefined { + if (dynamicDataDescriptor == null) return undefined; + if (isVariableDescriptor(dynamicDataDescriptor)) { + return findEntityAndVariable(entities, dynamicDataDescriptor); + } else if (isVariableCollectionDescriptor(dynamicDataDescriptor)) { + return findEntityAndVariableCollection(entities, dynamicDataDescriptor); + } else { + return undefined; + } +} + export function makeEntityDisplayName(entity: StudyEntity, isPlural: boolean) { return !isPlural ? entity.displayName From 2fe70af48092feb84953d9c1ec55017eac04ed6d Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Fri, 29 Sep 2023 12:38:25 -0400 Subject: [PATCH 3/9] check for undefined when testing obj type --- .../computations/plugins/differentialabundance.tsx | 14 ++++++++++++++ packages/libs/eda/src/lib/core/types/variable.ts | 6 ++++++ .../libs/eda/src/lib/core/utils/study-metadata.ts | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 017aa258fc..c9df5e2788 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -117,6 +117,10 @@ function DifferentialAbundanceConfigDescriptionComponent({ 'comparator' in configuration ? findEntityAndVariable(configuration.comparator.variable) : undefined; + const differentialAbundanceMethod = + 'differentialAbundanceMethod' in configuration + ? configuration.differentialAbundanceMethod + : undefined; const updatedCollectionVariable = collections.find((collectionVar) => isEqual( @@ -150,6 +154,16 @@ function DifferentialAbundanceConfigDescriptionComponent({ )} +

    + Method:{' '} + + {differentialAbundanceMethod ? ( + differentialAbundanceMethod + ) : ( + Not selected + )} + +

    ); } diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index 033b777239..7f13cda8a9 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -30,11 +30,17 @@ export const VariableCollectionDescriptor = _VariableCollectionBase; export function isVariableDescriptor( object: any ): object is VariableDescriptor { + if (!object) { + return false; + } return 'entityId' in object && 'variableId' in object; } export function isVariableCollectionDescriptor( object: any ): object is VariableCollectionDescriptor { + if (!object) { + return false; + } return 'entityId' in object && 'collectionId' in object; } diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index 6145ac7f1d..9c966b171d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -29,12 +29,18 @@ export interface EntityAndVariableCollection { } export function isEntityAndVariable(object: any): object is EntityAndVariable { + if (!object) { + return false; + } return 'entity' in object && 'variable' in object; } export function isEntityAndVariableCollection( object: any ): object is EntityAndVariableCollection { + if (!object) { + return false; + } return 'entity' in object && 'variableCollection' in object; } From 9af1635c1d95bb8e05b300cce1819a795e41f50e Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Fri, 29 Sep 2023 12:42:44 -0400 Subject: [PATCH 4/9] remove hardcoded deseq method --- .../components/computations/plugins/differentialabundance.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index c9df5e2788..9fb81295e8 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -190,9 +190,6 @@ export function DifferentialAbundanceConfiguration( const filters = analysisState.analysis?.descriptor.subset.descriptor; const findEntityAndVariable = useFindEntityAndVariable(filters); - // For now, set the method to DESeq2. When we add the next method, we can just add it here (no api change!) - if (configuration) configuration.differentialAbundanceMethod = 'DESeq'; - // Include known collection variables in this array. const collections = useCollectionVariables(studyMetadata.rootEntity); if (collections.length === 0) From f8d02b0c2c22d86bf7889afe7afaae2f340be9a3 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Sun, 1 Oct 2023 23:03:06 -0400 Subject: [PATCH 5/9] update stories --- .../src/stories/plots/VolcanoPlot.stories.tsx | 196 ++++++++++-------- .../stories/plots/VolcanoPlotRef.stories.tsx | 96 +++++---- .../plugins/differentialabundance.tsx | 9 +- 3 files changed, 163 insertions(+), 138 deletions(-) diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 06d1188a5f..5010f1eb8c 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -12,7 +12,7 @@ export default { title: 'Plots/VolcanoPlot', component: VolcanoPlot, argTypes: { - log2FoldChangeThreshold: { + effectSizeThreshold: { control: { type: 'range', min: 0.5, max: 10, step: 0.01 }, }, significanceThreshold: { @@ -26,65 +26,71 @@ export default { // of objects for actual use :) interface VEuPathDBVolcanoPlotData { volcanoplot: { - log2foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointID: string[]; + effectSizeLabel: string; + statistics: { + effectSize: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointID: string[]; + }; }; } // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - log2foldChange: [ - '2', - '3', - '0.5', - '-0.1', - '1', - '-0.5', - '-1.2', - '4', - '0.2', - '-8', - '-4', - '-3', - '-8.2', - '7', - ], - pValue: [ - '0.001', - '0.0001', - '0.01', - '0.001', - '0.98', - '1', - '0.8', - '1', - '0.6', - '0.001', - '0.0001', - '0.002', - '0', - '0', - ], - adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02', '0', '0'], - pointID: [ - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'buzz', - 'lightyear', - ], + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: [ + '2', + '3', + '0.5', + '-0.1', + '1', + '-0.5', + '-1.2', + '4', + '0.2', + '-8', + '-4', + '-3', + '-8.2', + '7', + ], + pValue: [ + '0.001', + '0.0001', + '0.01', + '0.001', + '0.98', + '1', + '0.8', + '1', + '0.6', + '0.001', + '0.0001', + '0.002', + '0', + '0', + ], + adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02', '0', '0'], + pointID: [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'buzz', + 'lightyear', + ], + }, }, }; @@ -92,21 +98,24 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { const nPoints = 300; const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { volcanoplot: { - log2foldChange: range(1, nPoints).map((p) => - String(Math.log2(Math.abs(getNormallyDistributedRandomNumber(0, 5)))) - ), - pValue: range(1, nPoints).map((p) => String(Math.random() / 2)), - adjustedPValue: range(1, nPoints).map((p) => - String(nPoints * Math.random()) - ), - pointID: range(1, nPoints).map((p) => String(p)), + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: range(1, nPoints).map((p) => + String(Math.log2(Math.abs(getNormallyDistributedRandomNumber(0, 5)))) + ), + pValue: range(1, nPoints).map((p) => String(Math.random() / 2)), + adjustedPValue: range(1, nPoints).map((p) => + String(nPoints * Math.random()) + ), + pointID: range(1, nPoints).map((p) => String(p)), + }, }, }; interface TemplateProps { data: VEuPathDBVolcanoPlotData | undefined; markerBodyOpacity: number; - log2FoldChangeThreshold: number; + effectSizeThreshold: number; significanceThreshold: number; adjustedPValueGate: number; independentAxisRange?: NumberRange; @@ -119,51 +128,54 @@ interface TemplateProps { const Template: Story = (args) => { // Process input data. Take the object of arrays and turn it into // an array of data points. Note the backend will do this for us! - const volcanoDataPoints: VolcanoPlotData | undefined = - args.data?.volcanoplot.log2foldChange - .map((l2fc, index) => { + const volcanoDataPoints: VolcanoPlotData | undefined = { + effectSizeLabel: args.data?.volcanoplot.effectSizeLabel ?? '', + statistics: + args.data?.volcanoplot.statistics.effectSize.map((effectSize, index) => { return { - log2foldChange: l2fc, - pValue: args.data?.volcanoplot.pValue[index], - adjustedPValue: args.data?.volcanoplot.adjustedPValue[index], - pointID: args.data?.volcanoplot.pointID[index], + effectSize: effectSize, + pValue: args.data?.volcanoplot.statistics.pValue[index], + adjustedPValue: + args.data?.volcanoplot.statistics.adjustedPValue[index], + pointID: args.data?.volcanoplot.statistics.pointID[index], + significanceColor: assignSignificanceColor( + Number(effectSize), + Number(args.data?.volcanoplot.statistics.pValue[index]), + args.significanceThreshold, + args.effectSizeThreshold, + significanceColors + ), }; - }) - .map((d) => ({ - ...d, - pointIDs: d.pointID ? [d.pointID] : undefined, - significanceColor: assignSignificanceColor( - Number(d.log2foldChange), - Number(d.pValue), - args.significanceThreshold, - args.log2FoldChangeThreshold, - significanceColors - ), - })); + }) ?? [], + }; const rawDataMinMaxValues = { x: { min: (volcanoDataPoints && Math.min( - ...volcanoDataPoints.map((d) => Number(d.log2foldChange)) + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) )) ?? 0, max: (volcanoDataPoints && Math.max( - ...volcanoDataPoints.map((d) => Number(d.log2foldChange)) + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) )) ?? 0, }, y: { min: (volcanoDataPoints && - Math.min(...volcanoDataPoints.map((d) => Number(d.pValue)))) ?? + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + )) ?? 1, max: (volcanoDataPoints && - Math.max(...volcanoDataPoints.map((d) => Number(d.pValue)))) ?? + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + )) ?? 1, }, }; @@ -171,7 +183,7 @@ const Template: Story = (args) => { const volcanoPlotProps: VolcanoPlotProps = { data: volcanoDataPoints, significanceThreshold: args.significanceThreshold, - log2FoldChangeThreshold: args.log2FoldChangeThreshold, + effectSizeThreshold: args.effectSizeThreshold, markerBodyOpacity: args.markerBodyOpacity, comparisonLabels: args.comparisonLabels, independentAxisRange: args.independentAxisRange, @@ -197,7 +209,7 @@ export const Simple = Template.bind({}); Simple.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 1, + effectSizeThreshold: 1, significanceThreshold: 0.01, comparisonLabels: ['up in group a', 'up in group b'], independentAxisRange: { min: -9, max: 9 }, @@ -211,7 +223,7 @@ export const ManyPoints = Template.bind({}); ManyPoints.args = { data: dataSetVolcanoManyPoints, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 3, + effectSizeThreshold: 3, significanceThreshold: 0.01, independentAxisRange: { min: -9, max: 9 }, dependentAxisRange: { min: 0, max: 9 }, @@ -226,7 +238,7 @@ export const Truncation = Template.bind({}); Truncation.args = { data: dataSetVolcano, markerBodyOpacity: 0.5, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, significanceThreshold: 0.01, independentAxisRange: { min: -3, max: 3 }, dependentAxisRange: { min: 1, max: 3 }, @@ -238,7 +250,7 @@ export const Spinner = Template.bind({}); Spinner.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 1, + effectSizeThreshold: 1, significanceThreshold: 0.01, comparisonLabels: ['up in group a', 'up in group b'], independentAxisRange: { min: -8, max: 9 }, @@ -251,7 +263,7 @@ export const Empty = Template.bind({}); Empty.args = { data: undefined, markerBodyOpacity: 0, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, significanceThreshold: 0.05, independentAxisRange: { min: -9, max: 9 }, dependentAxisRange: { min: -1, max: 9 }, diff --git a/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx index 65bd5e9de8..70b1d10a43 100644 --- a/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx @@ -17,7 +17,7 @@ export default { interface TemplateProps { data: VEuPathDBVolcanoPlotData; markerBodyOpacity: number; - log2FoldChangeThreshold: number; + effectSizeThreshold: number; significanceThreshold: number; adjustedPValueGate: number; comparisonLabels?: string[]; @@ -27,23 +27,29 @@ interface TemplateProps { // Generate fake data interface VEuPathDBVolcanoPlotData { volcanoplot: { - log2foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointID: string[]; + effectSizeLabel: string; + statistics: { + effectSize: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointID: string[]; + }; }; } const nPoints = 20; const data: VEuPathDBVolcanoPlotData = { volcanoplot: { - log2foldChange: range(1, nPoints).map((p) => - String(Math.log2(Math.abs(getNormallyDistributedRandomNumber(0, 5)))) - ), - pValue: range(1, nPoints).map((p) => String(Math.random() / 2)), - adjustedPValue: range(1, nPoints).map((p) => - String(nPoints * Math.random()) - ), - pointID: range(1, nPoints).map((p) => String(p)), + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: range(1, nPoints).map((p) => + String(Math.log2(Math.abs(getNormallyDistributedRandomNumber(0, 5)))) + ), + pValue: range(1, nPoints).map((p) => String(Math.random() / 2)), + adjustedPValue: range(1, nPoints).map((p) => + String(nPoints * Math.random()) + ), + pointID: range(1, nPoints).map((p) => String(p)), + }, }, }; @@ -65,46 +71,54 @@ const Template: Story = (args) => { }, []); // Wrangle data to get it into the nice form for plot component. - const volcanoDataPoints: VolcanoPlotData = data.volcanoplot.log2foldChange - .map((l2fc, index) => { - return { - log2foldChange: l2fc, - pValue: data.volcanoplot.pValue[index], - adjustedPValue: data.volcanoplot.adjustedPValue[index], - pointID: data.volcanoplot.pointID[index], - }; - }) - .map((d) => ({ - ...d, - pointID: d.pointID ? [d.pointID] : undefined, - significanceColor: assignSignificanceColor( - Number(d.log2foldChange), - Number(d.pValue), - args.significanceThreshold, - args.log2FoldChangeThreshold, - significanceColors - ), - })); + const volcanoDataPoints: VolcanoPlotData = { + effectSizeLabel: data.volcanoplot.effectSizeLabel, + statistics: data.volcanoplot.statistics.effectSize.map( + (effectSize, index) => { + return { + effectSize: effectSize, + pValue: data.volcanoplot.statistics.pValue[index], + adjustedPValue: data.volcanoplot.statistics.adjustedPValue[index], + pointID: data.volcanoplot.statistics.pointID[index], + significanceColor: assignSignificanceColor( + Number(effectSize), + Number(data.volcanoplot.statistics.pValue[index]), + args.significanceThreshold, + args.effectSizeThreshold, + significanceColors + ), + }; + } + ), + }; const rawDataMinMaxValues = { x: { min: - Math.min(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ?? - 0, + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) + ) ?? 0, max: - Math.max(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ?? - 0, + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) + ) ?? 0, }, y: { - min: Math.min(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0, - max: Math.max(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0, + min: + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + ) ?? 0, + max: + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + ) ?? 0, }, }; const volcanoPlotProps: VolcanoPlotProps = { data: volcanoDataPoints, significanceThreshold: args.significanceThreshold, - log2FoldChangeThreshold: args.log2FoldChangeThreshold, + effectSizeThreshold: args.effectSizeThreshold, markerBodyOpacity: args.markerBodyOpacity, comparisonLabels: args.comparisonLabels, rawDataMinMaxValues, @@ -128,7 +142,7 @@ export const ToImage = Template.bind({}); ToImage.args = { data: data, significanceThreshold: 0.05, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, markerBodyOpacity: 0.9, comparisonLabels: ['a', 'b'], }; diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 9fb81295e8..bcd1e6a418 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -169,7 +169,6 @@ function DifferentialAbundanceConfigDescriptionComponent({ } // Include available methods in this array. -// TODO do we need the display names different to these internal strings? const DIFFERENTIAL_ABUNDANCE_METHODS = ['DESeq', 'Maaslin']; export function DifferentialAbundanceConfiguration( @@ -210,10 +209,10 @@ export function DifferentialAbundanceConfiguration( const collectionVarItems = useMemo(() => { return collections .filter((collectionVar) => { - return collectionVar.normalizationMethod - ? !collectionVar.isProportion && - collectionVar.normalizationMethod === 'NULL' && - !collectionVar.displayName?.includes('pathway') + return collectionVar.normalizationMethod // i guess diy stuff doesnt have this prop? + ? // !collectionVar.isProportion && + // collectionVar.normalizationMethod === 'NULL' && + !collectionVar.displayName?.includes('pathway') : true; }) .map((collectionVar) => ({ From e72fd2dc850575369afb933710d86846e42a3cc9 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Mon, 2 Oct 2023 10:30:42 -0400 Subject: [PATCH 6/9] fix up volcano viz --- .../VolcanoPlotVisualization.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 0bf64c75d9..344b45bd9a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -36,6 +36,7 @@ import DataClient, { import { VolcanoPlotData, VolcanoPlotDataPoint, + VolcanoPlotStats, } from '@veupathdb/components/lib/types/plots/volcanoplot'; import VolcanoSVG from './selectorIcons/VolcanoSVG'; import { NumberOrDate } from '@veupathdb/components/lib/types/general'; @@ -85,7 +86,7 @@ export const volcanoPlotVisualization = createVisualizationPlugin({ function createDefaultConfig(): VolcanoPlotConfig { return { - log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, + effectSizeThreshold: DEFAULT_FC_THRESHOLD, significanceThreshold: DEFAULT_SIG_THRESHOLD, markerBodyOpacity: DEFAULT_MARKER_OPACITY, independentAxisRange: undefined, @@ -96,7 +97,7 @@ function createDefaultConfig(): VolcanoPlotConfig { export type VolcanoPlotConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const VolcanoPlotConfig = t.partial({ - log2FoldChangeThreshold: t.number, + effectSizeThreshold: t.number, significanceThreshold: t.number, markerBodyOpacity: t.number, independentAxisRange: NumberRange, @@ -245,8 +246,8 @@ function VolcanoPlotViz(props: VisualizationProps) { const significanceThreshold = vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD; - const log2FoldChangeThreshold = - vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD; + const effectSizeThreshold = + vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD; /** * This version of the data will get passed to the VolcanoPlot component @@ -292,7 +293,7 @@ function VolcanoPlotViz(props: VisualizationProps) { Number(d.effectSize), Number(d.pValue), significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, significanceColors ), }; @@ -304,11 +305,11 @@ function VolcanoPlotViz(props: VisualizationProps) { // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: // Yes? => update the matched aggregatedData element's pointID array to include the pointID of the matching entry // No? => just push the entry onto the aggregatedData array since no match was found - const aggregatedData: VolcanoPlotData = []; + const aggregatedData: VolcanoPlotStats = []; for (const entry of cleanedData) { const foundIndex = aggregatedData.findIndex( (d: VolcanoPlotDataPoint) => - d.log2foldChange === entry.effectSize && d.pValue === entry.pValue + d.effectSize === entry.effectSize && d.pValue === entry.pValue ); if (foundIndex === -1) { aggregatedData.push(entry); @@ -335,14 +336,17 @@ function VolcanoPlotViz(props: VisualizationProps) { } } } - return aggregatedData; + return { + effectSizeLabel: data.value.effectSizeLabel, + statistics: Object.values(aggregatedData), + }; } }, [ data.value, independentAxisRange, dependentAxisRange, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, entities, ]); @@ -354,7 +358,7 @@ function VolcanoPlotViz(props: VisualizationProps) { [significanceColors['high']]: 0, [significanceColors['low']]: 0, }; - for (const entry of finalData) { + for (const entry of finalData.statistics) { if (entry.significanceColor) { // Recall that finalData combines data with shared coords into one point in order to display a // single tooltip that lists all the pointIDs for that shared point. This means we need to use @@ -401,11 +405,11 @@ function VolcanoPlotViz(props: VisualizationProps) { /** * VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined. * In order to display an empty viz, EmptyVolcanoPlotData is defined as: - * const EmptyVolcanoPlotData: VolcanoPlotData = [{log2foldChange: '0', pValue: '1'}]; + * const EmptyVolcanoPlotData: VolcanoPlotData = [{effectSize: '0', pValue: '1'}]; */ - data: finalData ? Object.values(finalData) : undefined, + data: finalData ?? undefined, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, /** * Since we are rendering a single point in order to display an empty viz, let's hide the data point * by setting the marker opacity to 0 when data.value doesn't exist @@ -602,11 +606,11 @@ function VolcanoPlotViz(props: VisualizationProps) { - updateVizConfig({ log2FoldChangeThreshold: Number(newValue) }) + updateVizConfig({ effectSizeThreshold: Number(newValue) }) } label="log2(Fold Change)" minValue={0} - value={vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD} + value={vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD} containerStyles={{ marginRight: 10 }} /> From a55bb987cc01ea9bac63092e9d3f48c94715a06f Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:18:55 -0400 Subject: [PATCH 7/9] address some review feedback --- packages/libs/components/src/plots/VolcanoPlot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 509ec1368e..2cf096f82c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -161,7 +161,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { [] ); - const effectSizeLabel = data?.effectSizeLabel; + const effectSizeLabel = data.effectSizeLabel; // Set maxes and mins of the data itself from rawDataMinMaxValues prop const { min: dataXMin, max: dataXMax } = rawDataMinMaxValues.x; From 7af0c41ec13caf1c23f087942563d9417e9d2181 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:24:44 -0400 Subject: [PATCH 8/9] effect size threshold label --- .../implementations/VolcanoPlotVisualization.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 344b45bd9a..a093726e0b 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -157,6 +157,7 @@ function VolcanoPlotViz(props: VisualizationProps) { config: {}, computeConfig: computationConfiguration, }; + const response = await dataClient.getVisualizationData( computation.descriptor.type, visualization.descriptor.type, @@ -608,7 +609,7 @@ function VolcanoPlotViz(props: VisualizationProps) { onValueChange={(newValue?: NumberOrDate) => updateVizConfig({ effectSizeThreshold: Number(newValue) }) } - label="log2(Fold Change)" + label={finalData?.effectSizeLabel ?? 'Effect Size'} minValue={0} value={vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD} containerStyles={{ marginRight: 10 }} From 28d59f1b62a22893ee0668d5a7a704aaee5925b7 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:35:58 -0400 Subject: [PATCH 9/9] move some helpers to study-metadata --- .../implementations/BoxplotVisualization.tsx | 6 ++---- .../ScatterplotVisualization.tsx | 21 ++++++++++++------- .../libs/eda/src/lib/core/types/variable.ts | 18 ---------------- .../eda/src/lib/core/utils/study-metadata.ts | 20 ++++++++++++++++-- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx index 0503f7fdac..bcfafdff7a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx @@ -15,10 +15,7 @@ import { } from '../../../hooks/workspace'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; import { useDataClient, useStudyMetadata } from '../../../hooks/workspace'; -import { - isVariableDescriptor, - VariableDescriptor, -} from '../../../types/variable'; +import { VariableDescriptor } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -96,6 +93,7 @@ import { useDefaultAxisRange } from '../../../hooks/computeDefaultAxisRange'; import { findEntityAndDynamicData, getTreeNode, + isVariableDescriptor, } from '../../../utils/study-metadata'; // type of computedVariableMetadata for computation apps such as alphadiv and abundance import { 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 b3da60c46a..2354c4795b 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 @@ -18,12 +18,15 @@ import { useStudyEntities, useStudyMetadata, } from '../../../hooks/workspace'; -import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; +import { + findEntityAndDynamicData, + getTreeNode, + isVariableDescriptor, +} from '../../../utils/study-metadata'; import { VariableDescriptor, VariableCollectionDescriptor, - isVariableDescriptor, } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -851,12 +854,14 @@ function ScatterplotViz(props: VisualizationProps) { const legendTitle = useMemo(() => { if (computedOverlayVariableDescriptor) { - return findCollectionVariableEntityAndVariable( - entities, - isVariableDescriptor(computedOverlayVariableDescriptor) - ? computedOverlayVariableDescriptor - : undefined - )?.variable.displayName; + return getTreeNode( + findEntityAndDynamicData( + entities, + isVariableDescriptor(computedOverlayVariableDescriptor) + ? computedOverlayVariableDescriptor + : undefined + ) + )?.displayName; } return variableDisplayWithUnit(overlayVariable); }, [entities, overlayVariable, computedOverlayVariableDescriptor]); diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index 7f13cda8a9..a7d83b6758 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -26,21 +26,3 @@ export type VariableCollectionDescriptor = t.TypeOf< typeof VariableCollectionDescriptor >; export const VariableCollectionDescriptor = _VariableCollectionBase; - -export function isVariableDescriptor( - object: any -): object is VariableDescriptor { - if (!object) { - return false; - } - return 'entityId' in object && 'variableId' in object; -} - -export function isVariableCollectionDescriptor( - object: any -): object is VariableCollectionDescriptor { - if (!object) { - return false; - } - return 'entityId' in object && 'collectionId' in object; -} diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index 9c966b171d..5bf8c9a57d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -9,8 +9,6 @@ import { import { VariableCollectionDescriptor, VariableDescriptor, - isVariableCollectionDescriptor, - isVariableDescriptor, } from '../types/variable'; import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; @@ -44,6 +42,24 @@ export function isEntityAndVariableCollection( return 'entity' in object && 'variableCollection' in object; } +export function isVariableDescriptor( + object: any +): object is VariableDescriptor { + if (!object) { + return false; + } + return 'entityId' in object && 'variableId' in object; +} + +export function isVariableCollectionDescriptor( + object: any +): object is VariableCollectionDescriptor { + if (!object) { + return false; + } + return 'entityId' in object && 'collectionId' in object; +} + export function getTreeNode( entityAndDynamicData: | EntityAndVariable