Skip to content

Commit

Permalink
Merge branch 'main' into 95-bubble-marker-component
Browse files Browse the repository at this point in the history
  • Loading branch information
chowington committed Aug 8, 2023
2 parents 74cfc94 + 551eb20 commit f57339b
Show file tree
Hide file tree
Showing 15 changed files with 530 additions and 340 deletions.
121 changes: 26 additions & 95 deletions packages/libs/components/src/plots/VolcanoPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
useImperativeHandle,
useRef,
} from 'react';
import { significanceColors } from '../types/plots';
import {
VolcanoPlotData,
VolcanoPlotDataPoint,
} from '../types/plots/volcanoplot';
import { NumberRange } from '../types/general';
import { SignificanceColors } from '../types/plots';
import {
XYChart,
Axis,
Expand All @@ -22,7 +22,6 @@ import {
AnnotationLabel,
} from '@visx/xychart';
import { Group } from '@visx/group';
import { max, min } from 'lodash';
import {
gridStyles,
thresholdLineStyles,
Expand All @@ -38,6 +37,11 @@ import { ToImgopts } from 'plotly.js';
import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot';
import domToImage from 'dom-to-image';

export interface RawDataMinMaxValues {
x: NumberRange;
y: NumberRange;
}

export interface VolcanoPlotProps {
/** Data for the plot. An array of VolcanoPlotDataPoints */
data: VolcanoPlotData | undefined;
Expand Down Expand Up @@ -70,6 +74,8 @@ export interface VolcanoPlotProps {
containerStyles?: CSSProperties;
/** shall we show the loading spinner? */
showSpinner?: boolean;
/** used to determine truncation logic */
rawDataMinMaxValues: RawDataMinMaxValues;
}

const EmptyVolcanoPlotData: VolcanoPlotData = [
Expand Down Expand Up @@ -124,6 +130,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
comparisonLabels,
truncationBarFill,
showSpinner = false,
rawDataMinMaxValues,
} = props;

// Use ref forwarding to enable screenshotting of the plot for thumbnail versions.
Expand All @@ -140,87 +147,24 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
[]
);

/**
* Find mins and maxes of the data and for the plot.
* The standard x axis is the log2 fold change. The standard
* y axis is -log10 raw p value.
*/

// Find maxes and mins of the data itself
const dataXMin = min(data.map((d) => Number(d.log2foldChange))) ?? 0;
const dataXMax = max(data.map((d) => Number(d.log2foldChange))) ?? 0;
const dataYMin = min(data.map((d) => Number(d.pValue))) ?? 0;
const dataYMax = max(data.map((d) => Number(d.pValue))) ?? 0;

// Determine mins, maxes of axes in the plot.
// These are different than the data mins/maxes because
// of the log transform and the little bit of padding, or because axis ranges
// are supplied.
let xAxisMin: number;
let xAxisMax: number;
let yAxisMin: number;
let yAxisMax: number;
const AXIS_PADDING_FACTOR = 0.05; // The padding ensures we don't clip off part of the glyphs that represent
// the most extreme points. We could have also used d3.scale.nice but then we dont have precise control of where
// the extremes are, which is important for user-defined ranges and truncation bars.
// 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;

// X axis
if (independentAxisRange) {
xAxisMin = independentAxisRange.min;
xAxisMax = independentAxisRange.max;
} else {
if (dataXMin && dataXMax) {
// We can use the dataMin and dataMax here because we don't have a further transform
xAxisMin = dataXMin;
xAxisMax = dataXMax;
// Add a little padding to prevent clipping the glyph representing the extreme points
xAxisMin = xAxisMin - (xAxisMax - xAxisMin) * AXIS_PADDING_FACTOR;
xAxisMax = xAxisMax + (xAxisMax - xAxisMin) * AXIS_PADDING_FACTOR;
} else {
xAxisMin = 0;
xAxisMax = 0;
}
}

// Y axis
if (dependentAxisRange) {
yAxisMin = dependentAxisRange.min;
yAxisMax = dependentAxisRange.max;
} else {
if (dataYMin && dataYMax) {
// Standard volcano plots have -log10(raw p value) as the y axis
yAxisMin = -Math.log10(dataYMax);
yAxisMax = -Math.log10(dataYMin);
// Add a little padding to prevent clipping the glyph representing the extreme points
yAxisMin = yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR;
yAxisMax = yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR;
} else {
yAxisMin = 0;
yAxisMax = 0;
}
}
// Set mins, maxes of axes in the plot using axis range props
const xAxisMin = independentAxisRange?.min ?? 0;
const xAxisMax = independentAxisRange?.max ?? 0;
const yAxisMin = dependentAxisRange?.min ?? 0;
const yAxisMax = dependentAxisRange?.max ?? 0;

/**
* Accessors - tell visx which value of the data point we should use and where.
*/

// For the actual volcano plot data
// Only return data if the points fall within the specified range! Otherwise they'll show up on the plot.
const dataAccessors = {
xAccessor: (d: VolcanoPlotDataPoint) => {
const log2foldChange = Number(d?.log2foldChange);

return log2foldChange <= xAxisMax && log2foldChange >= xAxisMin
? log2foldChange
: null;
},
yAccessor: (d: VolcanoPlotDataPoint) => {
const transformedPValue = -Math.log10(Number(d?.pValue));

return transformedPValue <= yAxisMax && transformedPValue >= yAxisMin
? transformedPValue
: null;
},
xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.log2foldChange),
yAccessor: (d: VolcanoPlotDataPoint) => -Math.log10(Number(d?.pValue)),
};

// For all other situations where we need to access point values. For example
Expand Down Expand Up @@ -364,15 +308,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
dataKey={'data'} // unique key
data={data} // data as an array of obejcts (points). Accessed with dataAccessors
{...dataAccessors}
colorAccessor={(d) => {
return assignSignificanceColor(
Number(d.log2foldChange),
Number(d.pValue),
significanceThreshold,
log2FoldChangeThreshold,
significanceColors
);
}}
colorAccessor={(d) => d.significanceColor}
/>
</Group>

Expand Down Expand Up @@ -435,35 +371,30 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
/**
* Assign color to point based on significance and magnitude change thresholds
*/
function assignSignificanceColor(
export function assignSignificanceColor(
log2foldChange: number,
pValue: number,
significanceThreshold: number,
log2FoldChangeThreshold: number,
significanceColors: string[] // Assuming the order is [insignificant, high (up regulated), low (down regulated)]
significanceColors: SignificanceColors
) {
// Name indices of the significanceColors array for easier accessing.
const INSIGNIFICANT = 0;
const HIGH = 1;
const LOW = 2;

// Test 1. If the y value is higher than the significance threshold, just return not significant
if (pValue >= significanceThreshold) {
return significanceColors[INSIGNIFICANT];
return significanceColors['inconclusive'];
}

// Test 2. So the y is significant. Is the x larger than the positive foldChange threshold?
if (log2foldChange >= log2FoldChangeThreshold) {
return significanceColors[HIGH];
return significanceColors['high'];
}

// Test 3. Is the x value lower than the negative foldChange threshold?
if (log2foldChange <= -log2FoldChangeThreshold) {
return significanceColors[LOW];
return significanceColors['low'];
}

// If we're still here, it must be a non significant point.
return significanceColors[INSIGNIFICANT];
return significanceColors['inconclusive'];
}

export default forwardRef(VolcanoPlot);
57 changes: 49 additions & 8 deletions packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { getNormallyDistributedRandomNumber } from './ScatterPlot.storyData';
import { VolcanoPlotData } from '../../types/plots/volcanoplot';
import { NumberRange } from '../../types/general';
import { yellow } from '@veupathdb/coreui/lib/definitions/colors';
import { assignSignificanceColor } from '../../plots/VolcanoPlot';
import { significanceColors } from '../../types/plots';

export default {
title: 'Plots/VolcanoPlot',
Expand Down Expand Up @@ -99,14 +101,52 @@ const Template: Story<TemplateProps> = (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) => {
return {
log2foldChange: l2fc,
pValue: args.data?.volcanoplot.pValue[index],
adjustedPValue: args.data?.volcanoplot.adjustedPValue[index],
pointID: args.data?.volcanoplot.pointID[index],
};
});
args.data?.volcanoplot.log2foldChange
.map((l2fc, index) => {
return {
log2foldChange: l2fc,
pValue: args.data?.volcanoplot.pValue[index],
adjustedPValue: args.data?.volcanoplot.adjustedPValue[index],
pointID: args.data?.volcanoplot.pointID[index],
};
})
.map((d) => ({
...d,
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))
)) ??
0,
max:
(volcanoDataPoints &&
Math.max(
...volcanoDataPoints.map((d) => Number(d.log2foldChange))
)) ??
0,
},
y: {
min:
(volcanoDataPoints &&
Math.min(...volcanoDataPoints.map((d) => Number(d.pValue)))) ??
1,
max:
(volcanoDataPoints &&
Math.max(...volcanoDataPoints.map((d) => Number(d.pValue)))) ??
1,
},
};

const volcanoPlotProps: VolcanoPlotProps = {
data: volcanoDataPoints,
Expand All @@ -118,6 +158,7 @@ const Template: Story<TemplateProps> = (args) => {
dependentAxisRange: args.dependentAxisRange,
truncationBarFill: args.truncationBarFill,
showSpinner: args.showSpinner,
rawDataMinMaxValues,
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useEffect } from 'react';
import { useState } from 'react';
import { useRef } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import { Story } from '@storybook/react/types-6-0';
import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot';
import { range } from 'lodash';
import { VolcanoPlotData } from '../../types/plots/volcanoplot';
import { getNormallyDistributedRandomNumber } from './ScatterPlot.storyData';
import { assignSignificanceColor } from '../../plots/VolcanoPlot';
import { significanceColors } from '../../types/plots';

export default {
title: 'Plots/VolcanoPlot',
Expand Down Expand Up @@ -63,22 +65,50 @@ const Template: Story<TemplateProps> = (args) => {
}, []);

// Wrangle data to get it into the nice form for plot component.
const volcanoDataPoints: VolcanoPlotData =
data.volcanoplot.log2foldChange.map((l2fc, index) => {
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,
significanceColor: assignSignificanceColor(
Number(d.log2foldChange),
Number(d.pValue),
args.significanceThreshold,
args.log2FoldChangeThreshold,
significanceColors
),
}));

const rawDataMinMaxValues = {
x: {
min:
Math.min(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ??
0,
max:
Math.max(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ??
0,
},
y: {
min: Math.min(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0,
max: Math.max(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0,
},
};

const volcanoPlotProps: VolcanoPlotProps = {
data: volcanoDataPoints,
significanceThreshold: args.significanceThreshold,
log2FoldChangeThreshold: args.log2FoldChangeThreshold,
markerBodyOpacity: args.markerBodyOpacity,
comparisonLabels: args.comparisonLabels,
rawDataMinMaxValues,
independentAxisRange: { min: -9, max: 9 },
dependentAxisRange: { min: 0, max: 9 },
};

return (
Expand Down
13 changes: 10 additions & 3 deletions packages/libs/components/src/types/plots/addOns.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* Additional reusable modules to extend PlotProps and PlotData props
*/

import { CSSProperties } from 'react';
import { BarLayoutOptions, OrientationOptions } from '.';
import { scaleLinear } from 'd3-scale';
Expand Down Expand Up @@ -329,8 +328,16 @@ export const gradientConvergingColorscaleMap = scaleLinear<string>()
.range(ConvergingGradientColorscale)
.interpolate(interpolateLab);

// Significance colors (not significant, high, low)
export const significanceColors = ['#B5B8B4', '#AC3B4E', '#0E8FAB'];
export type SignificanceColors = {
inconclusive: string;
high: string;
low: string;
};
export const significanceColors: SignificanceColors = {
inconclusive: '#B5B8B4',
high: '#AC3B4E',
low: '#0E8FAB',
};

/** truncated axis flags */
export type AxisTruncationAddon = {
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/components/src/types/plots/volcanoplot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type VolcanoPlotDataPoint = {
adjustedPValue?: string;
// Used for tooltip
pointID?: string;
// Used to determine color of data point in the plot
significanceColor?: string;
};

export type VolcanoPlotData = Array<VolcanoPlotDataPoint>;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const StudyResponse = type({
});

export interface DistributionRequestParams {
filters: Filter[];
filters?: Filter[];
binSpec?: {
displayRangeMin: number | string;
displayRangeMax: number | string;
Expand Down
Loading

0 comments on commit f57339b

Please sign in to comment.