Skip to content

Commit

Permalink
Merge pull request #565 from VEuPathDB/fix-528-volcano-point-floors
Browse files Browse the repository at this point in the history
Incorporate p value floor from backend
  • Loading branch information
asizemore authored Oct 30, 2023
2 parents 38c98c7 + a22951a commit a8a7b4f
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 37 deletions.
77 changes: 58 additions & 19 deletions packages/libs/components/src/plots/VolcanoPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export interface RawDataMinMaxValues {
y: NumberRange;
}

export interface StatisticsFloors {
/** The minimum allowed p value. Useful for protecting the plot against taking the log of pvalue=0. Points with true pvalue <= pValueFloor will get plotted at -log10(pValueFloor).
* Any points with pvalue <= the pValueFloor will show "P Value <= {pValueFloor}" in the tooltip.
*/
pValueFloor: number;
/** The minimum allowed adjusted p value. Ideally should be calculated in conjunction with the pValueFloor. Currently used
* only to update the tooltip with this information, but later will be used to control the y axis location, similar to pValueFloor.
*/
adjustedPValueFloor?: number;
}

export interface VolcanoPlotProps {
/** Data for the plot. An effectSizeLabel and an array of VolcanoPlotDataPoints */
data: VolcanoPlotData | undefined;
Expand Down Expand Up @@ -80,8 +91,10 @@ export interface VolcanoPlotProps {
showSpinner?: boolean;
/** used to determine truncation logic */
rawDataMinMaxValues: RawDataMinMaxValues;
/** The maximum possible y axis value. Points with pValue=0 will get plotted at -log10(minPValueCap). */
minPValueCap?: number;
/** Minimum (floor) values for p values and adjusted p values. Will set a cap on the maximum y axis values
* at which points can be plotted. This information will also be shown in tooltips for floored points.
*/
statisticsFloors?: StatisticsFloors;
}

const EmptyVolcanoPlotStats: VolcanoPlotStats = [
Expand All @@ -93,6 +106,10 @@ const EmptyVolcanoPlotData: VolcanoPlotData = {
statistics: EmptyVolcanoPlotStats,
};

export const DefaultStatisticsFloors: StatisticsFloors = {
pValueFloor: 0, // Do not floor by default
};

const MARGIN_DEFAULT = 50;

interface TruncationRectangleProps {
Expand Down Expand Up @@ -144,7 +161,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
truncationBarFill,
showSpinner = false,
rawDataMinMaxValues,
minPValueCap = 2e-300,
statisticsFloors = DefaultStatisticsFloors,
} = props;

// Use ref forwarding to enable screenshotting of the plot for thumbnail versions.
Expand All @@ -167,34 +184,44 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
const { min: dataXMin, max: dataXMax } = rawDataMinMaxValues.x;
const { min: dataYMin, max: dataYMax } = rawDataMinMaxValues.y;

// When dataYMin = 0, there must be a point with pvalue = 0, which means the plot will try in vain to draw a point at -log10(0) = Inf.
// When this issue arises, one should set a pValueFloor >= 0 so that the point with pValue = 0
// will be able to be plotted sensibly.
if (dataYMin === 0 && statisticsFloors.pValueFloor <= 0) {
throw new Error(
'Found data point with pValue = 0. Cannot create a volcano plot with a point at -log10(0) = Inf. Please use the statisticsFloors prop to set a pValueFloor >= 0.'
);
}

// Set mins, maxes of axes in the plot using axis range props
// The y axis max should not be allowed to exceed -log10(minPValueCap)
// The y axis max should not be allowed to exceed -log10(pValueFloor)
const xAxisMin = independentAxisRange?.min ?? 0;
const xAxisMax = independentAxisRange?.max ?? 0;
const yAxisMin = dependentAxisRange?.min ?? 0;
const yAxisMax = dependentAxisRange?.max
? dependentAxisRange.max > -Math.log10(minPValueCap)
? -Math.log10(minPValueCap)
? dependentAxisRange.max > -Math.log10(statisticsFloors.pValueFloor)
? -Math.log10(statisticsFloors.pValueFloor)
: dependentAxisRange.max
: 0;

// Do we need to show the special annotation for the case when the y axis is maxxed out?
const showCappedDataAnnotation = yAxisMax === -Math.log10(minPValueCap);
const showFlooredDataAnnotation =
yAxisMax === -Math.log10(statisticsFloors.pValueFloor);

// Truncation indicators
// If we have truncation indicators, we'll need to expand the plot range just a tad to
// ensure the truncation bars appear. The folowing showTruncationBar variables will
// be either 0 (do not show bar) or 1 (show bar).
// The y axis has special logic because it gets capped at -log10(minPValueCap)
// The y axis has special logic because it gets capped at -log10(pValueFloor) and we dont want to
// show the truncation bar if the annotation will be shown!
const showXMinTruncationBar = Number(dataXMin < xAxisMin);
const showXMaxTruncationBar = Number(dataXMax > xAxisMax);
const xTruncationBarWidth = 0.02 * (xAxisMax - xAxisMin);

const showYMinTruncationBar = Number(-Math.log10(dataYMax) < yAxisMin);
const showYMaxTruncationBar =
dataYMin === 0
? Number(-Math.log10(minPValueCap) > yAxisMax)
: Number(-Math.log10(dataYMin) > yAxisMax);
const showYMaxTruncationBar = Number(
-Math.log10(dataYMin) > yAxisMax && !showFlooredDataAnnotation
);
const yTruncationBarHeight = 0.02 * (yAxisMax - yAxisMin);

/**
Expand All @@ -211,12 +238,12 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
* Accessors - tell visx which value of the data point we should use and where.
*/

// For the actual volcano plot data. Y axis points are capped at -Math.log10(minPValueCap)
// For the actual volcano plot data. Y axis points are capped at -Math.log10(pValueFloor)
const dataAccessors = {
xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.effectSize),
yAccessor: (d: VolcanoPlotDataPoint) =>
d.pValue === '0'
? -Math.log10(minPValueCap)
Number(d.pValue) <= statisticsFloors.pValueFloor
? -Math.log10(statisticsFloors.pValueFloor)
: -Math.log10(Number(d?.pValue)),
};

Expand Down Expand Up @@ -266,7 +293,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
findNearestDatumOverride={findNearestDatumXY}
margin={{
top: MARGIN_DEFAULT,
right: showCappedDataAnnotation ? 150 : MARGIN_DEFAULT,
right: showFlooredDataAnnotation ? 150 : MARGIN_DEFAULT,
left: MARGIN_DEFAULT,
bottom: MARGIN_DEFAULT,
}}
Expand Down Expand Up @@ -351,7 +378,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
)}

{/* infinity y data annotation line */}
{showCappedDataAnnotation && (
{showFlooredDataAnnotation && (
<Annotation
datum={{
x: xAxisMax,
Expand Down Expand Up @@ -441,11 +468,23 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
<span>{effectSizeLabel}:</span> {data?.effectSize}
</li>
<li>
<span>P Value:</span> {data?.pValue}
<span>P Value:</span>{' '}
{data?.pValue
? Number(data.pValue) <= statisticsFloors.pValueFloor
? '<= ' + statisticsFloors.pValueFloor
: data?.pValue
: 'n/a'}
</li>
<li>
<span>Adjusted P Value:</span>{' '}
{data?.adjustedPValue ?? 'n/a'}
{data?.adjustedPValue
? statisticsFloors.adjustedPValueFloor &&
Number(data.adjustedPValue) <=
statisticsFloors.adjustedPValueFloor &&
Number(data.pValue) <= statisticsFloors.pValueFloor
? '<= ' + statisticsFloors.adjustedPValueFloor
: data?.adjustedPValue
: 'n/a'}
</li>
</ul>
</div>
Expand Down
56 changes: 42 additions & 14 deletions packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot';
import VolcanoPlot, {
StatisticsFloors,
VolcanoPlotProps,
} from '../../plots/VolcanoPlot';
import { Story, Meta } from '@storybook/react/types-6-0';
import { range } from 'lodash';
import { getNormallyDistributedRandomNumber } from './ScatterPlot.storyData';
Expand Down Expand Up @@ -70,8 +73,8 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = {
'0.001',
'0.0001',
'0.002',
'0',
'0',
'1e-90',
'0.00000002',
],
adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02', '0', '0'],
pointID: [
Expand Down Expand Up @@ -123,6 +126,7 @@ interface TemplateProps {
comparisonLabels?: string[];
truncationBarFill?: string;
showSpinner?: boolean;
statisticsFloors?: StatisticsFloors;
}

const Template: Story<TemplateProps> = (args) => {
Expand All @@ -131,22 +135,27 @@ const Template: Story<TemplateProps> = (args) => {
const volcanoDataPoints: VolcanoPlotData | undefined = {
effectSizeLabel: args.data?.volcanoplot.effectSizeLabel ?? '',
statistics:
args.data?.volcanoplot.statistics.effectSize.map((effectSize, index) => {
return {
effectSize: effectSize,
pValue: args.data?.volcanoplot.statistics.pValue[index],
adjustedPValue:
args.data?.volcanoplot.statistics.adjustedPValue[index],
pointID: args.data?.volcanoplot.statistics.pointID[index],
args.data?.volcanoplot.statistics.effectSize
.map((effectSize, index) => {
return {
effectSize: effectSize,
pValue: args.data?.volcanoplot.statistics.pValue[index],
adjustedPValue:
args.data?.volcanoplot.statistics.adjustedPValue[index],
pointID: args.data?.volcanoplot.statistics.pointID[index],
};
})
.map((d) => ({
...d,
pointIDs: d.pointID ? [d.pointID] : undefined,
significanceColor: assignSignificanceColor(
Number(effectSize),
Number(args.data?.volcanoplot.statistics.pValue[index]),
Number(d.effectSize),
Number(d.pValue),
args.significanceThreshold,
args.effectSizeThreshold,
significanceColors
),
};
}) ?? [],
})) ?? [],
};

const rawDataMinMaxValues = {
Expand Down Expand Up @@ -191,6 +200,7 @@ const Template: Story<TemplateProps> = (args) => {
truncationBarFill: args.truncationBarFill,
showSpinner: args.showSpinner,
rawDataMinMaxValues,
statisticsFloors: args.statisticsFloors,
};

return (
Expand Down Expand Up @@ -268,3 +278,21 @@ Empty.args = {
independentAxisRange: { min: -9, max: 9 },
dependentAxisRange: { min: -1, max: 9 },
};

// With a pvalue floor
const testStatisticsFloors: StatisticsFloors = {
pValueFloor: 0.006,
adjustedPValueFloor: 0.01,
};
export const FlooredPValues = Template.bind({});
FlooredPValues.args = {
data: dataSetVolcano,
markerBodyOpacity: 0.8,
effectSizeThreshold: 1,
significanceThreshold: 0.01,
comparisonLabels: ['up in group a', 'up in group b'],
independentAxisRange: { min: -9, max: 9 },
dependentAxisRange: { min: -1, max: 9 },
showSpinner: false,
statisticsFloors: testStatisticsFloors,
};
14 changes: 10 additions & 4 deletions packages/libs/eda/src/lib/core/api/DataClient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,16 @@ export const VolcanoPlotStatistics = array(
})
);

export const VolcanoPlotResponse = type({
effectSizeLabel: string,
statistics: VolcanoPlotStatistics,
});
export const VolcanoPlotResponse = intersection([
type({
effectSizeLabel: string,
statistics: VolcanoPlotStatistics,
}),
partial({
pValueFloor: string,
adjustedPValueFloor: string,
}),
]);

export interface VolcanoPlotRequestParams {
studyId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const DifferentialAbundanceConfig = t.type({
collectionVariable: VariableCollectionDescriptor,
comparator: Comparator,
differentialAbundanceMethod: t.string,
pValueFloor: t.string,
});

// Check to ensure the entirety of the configuration is filled out before enabling the
Expand Down Expand Up @@ -188,6 +189,11 @@ export function DifferentialAbundanceConfiguration(
configuration.differentialAbundanceMethod =
DIFFERENTIAL_ABUNDANCE_METHODS[0];

// Set the pValueFloor here. May change for other apps.
// Note this is intentionally different than the default pValueFloor used in the Volcano component. By default
// that component does not floor the data, but we know we want the diff abund computation to use a floor.
if (configuration) configuration.pValueFloor = '1e-200';

// Include known collection variables in this array.
const collections = useCollectionVariables(studyMetadata.rootEntity);
if (collections.length === 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import VolcanoPlot, {
VolcanoPlotProps,
assignSignificanceColor,
RawDataMinMaxValues,
StatisticsFloors,
DefaultStatisticsFloors,
} from '@veupathdb/components/lib/plots/VolcanoPlot';

import * as t from 'io-ts';
Expand Down Expand Up @@ -415,6 +417,15 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
]
: [];

// Record any floors for the p value and adjusted p value sent to us from the backend.
const statisticsFloors: StatisticsFloors =
data.value && data.value.pValueFloor
? {
pValueFloor: Number(data.value.pValueFloor),
adjustedPValueFloor: Number(data.value.adjustedPValueFloor),
}
: DefaultStatisticsFloors;

const volcanoPlotProps: VolcanoPlotProps = {
/**
* VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined.
Expand All @@ -437,6 +448,7 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
* confusing behavior where selecting group values displays on the empty viz placeholder.
*/
comparisonLabels: data.value ? comparisonLabels : [],
statisticsFloors,
showSpinner: data.pending,
truncationBarFill: yellow[300],
independentAxisRange,
Expand Down

0 comments on commit a8a7b4f

Please sign in to comment.