Skip to content

Commit

Permalink
Merge pull request #19 from VEuPathDB/volcano
Browse files Browse the repository at this point in the history
Volcano
  • Loading branch information
asizemore authored Apr 17, 2023
2 parents 8435372 + 6d62e39 commit 3f90637
Show file tree
Hide file tree
Showing 9 changed files with 908 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/libs/components/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
loader: require.resolve('babel-loader'),
options: {
plugins: ['@babel/plugin-proposal-nullish-coalescing-operator'],
presets: ['@babel/preset-env', '@babel/preset-react'],
},
});

Expand Down
4 changes: 4 additions & 0 deletions packages/libs/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"@veupathdb/coreui": "workspace:^",
"@visx/axis": "^3.1.0",
"@visx/gradient": "^1.0.0",
"@visx/group": "^1.0.0",
"@visx/hierarchy": "^1.0.0",
"@visx/shape": "^1.4.0",
"@visx/text": "^1.3.0",
"@visx/tooltip": "^1.3.0",
"@visx/visx": "^1.1.0",
"@visx/xychart": "^3.1.0",
"bootstrap": "^4.5.2",
"color-math": "^1.1.3",
"d3": "^7.1.1",
Expand All @@ -37,6 +39,8 @@
"react-leaflet": "^3.2.5",
"react-leaflet-drift-marker": "^3.0.0",
"react-plotly.js": "^2.4.0",
"react-spring": "^9.7.1",
"react-transition-group": "^4.4.1",
"shape2geohash": "^1.2.5"
},
"files": [
Expand Down
267 changes: 267 additions & 0 deletions packages/libs/components/src/plots/VolcanoPlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { significanceColors } from '../types/plots';
import {
VolcanoPlotData,
VolcanoPlotDataPoint,
} from '../types/plots/volcanoplot';
import { NumberRange } from '../types/general';
import {
XYChart,
Axis,
Grid,
GlyphSeries,
Annotation,
AnnotationLineSubject,
} from '@visx/xychart';
import { Group } from '@visx/group';
import { max, min } from 'lodash';
import {
gridStyles,
thresholdLineStyles,
VisxPoint,
axisStyles,
} from './visxVEuPathDB';

export interface VolcanoPlotProps {
/** Data for the plot. An array of VolcanoPlotDataPoints */
data: VolcanoPlotData;
/**
* Used to set the fold change thresholds. Will
* set two thresholds at +/- this number. Affects point colors
*/
log2FoldChangeThreshold: number;
/** Set the threshold for significance. Affects point colors */
significanceThreshold: number;
/** x-axis range */
independentAxisRange?: NumberRange;
/** y-axis range */
dependentAxisRange?: NumberRange;
/**
* Array of size 2 that contains a label for the left and right side
* of the x axis. (Not yet implemented). Expect this to be passed by the viz based
* on the type of data we're using (genes vs taxa vs etc.)
*/
comparisonLabels?: Array<string>;
/** Title of the plot */
plotTitle?: string;
/** marker fill opacity: range from 0 to 1 */
markerBodyOpacity?: number;
/** Height of plot */
height?: number;
/** Width of plot */
width?: number;
}

const EmptyVolcanoPlotData: VolcanoPlotData = [];

/**
* The Volcano Plot displays points on a (magnitude change) by (significance) xy axis.
* The standard volcano plot has -log2(Fold Change) as the x axis and -log10(raw p value)
* on the y axis. The volcano plot also colors the points based on their
* significance and magnitude change to make it easy to spot significantly up or down-regulated genes or taxa.
*/
function VolcanoPlot(props: VolcanoPlotProps) {
const {
data = EmptyVolcanoPlotData,
independentAxisRange, // not yet implemented - expect this to be set by user
dependentAxisRange, // not yet implemented - expect this to be set by user
significanceThreshold,
log2FoldChangeThreshold,
markerBodyOpacity,
height,
width,
} = props;

/**
* 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)));
const dataXMax = max(data.map((d) => Number(d.log2foldChange)));
const dataYMin = min(data.map((d) => Number(d.pValue)));
const dataYMax = max(data.map((d) => Number(d.pValue)));

// 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. The padding
// ensures we don't clip off part of the glyphs that represent
// the most extreme points
let xMin: number;
let xMax: number;
let yMin: number;
let yMax: number;
const AXIS_PADDING_FACTOR = 0.05;

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

// Y axis
if (dataYMin && dataYMax) {
// Standard volcano plots have -log10(raw p value) as the y axis
yMin = -Math.log10(dataYMax);
yMax = -Math.log10(dataYMin);
// Add a little padding to prevent clipping the glyph representing the extreme points
yMin = yMin - (yMax - yMin) * AXIS_PADDING_FACTOR;
yMax = yMax + (yMax - yMin) * AXIS_PADDING_FACTOR;
} else {
yMin = 0;
yMax = 0;
}

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

const dataAccessors = {
xAccessor: (d: VolcanoPlotDataPoint) => {
return Number(d?.log2foldChange);
},
yAccessor: (d: VolcanoPlotDataPoint) => {
return -Math.log10(Number(d?.pValue));
},
};

const thresholdLineAccessors = {
xAccessor: (d: VisxPoint) => {
return d?.x;
},
yAccessor: (d: VisxPoint) => {
return d?.y;
},
};

return (
// Relative positioning so that tooltips are positioned correctly (tooltips are positioned absolutely)
<div style={{ position: 'relative' }}>
{/* The XYChart takes care of laying out the chart elements (children) appropriately.
It uses modularized React.context layers for data, events, etc. The following all becomes an svg,
so use caution when ordering the children (ex. draw axes before data). */}
<XYChart
height={height ?? 300}
xScale={{ type: 'linear', domain: [xMin, xMax] }}
yScale={{ type: 'linear', domain: [yMin, yMax], zero: false }}
width={width ?? 300}
>
{/* Set up the axes and grid lines. XYChart magically lays them out correctly */}
<Grid numTicks={6} lineStyle={gridStyles} />
<Axis orientation="left" label="-log10 Raw P Value" {...axisStyles} />
<Axis orientation="bottom" label="log2 Fold Change" {...axisStyles} />

{/* Draw threshold lines as annotations below the data points. The
annotations use XYChart's theme and dimension context.
The Annotation component holds the context for its children, which is why
we make a new Annotation component for each line.
Another option would be to make Line with LineSeries, but the default hover response
is on the points instead of the line connecting them. */}

{/* Draw horizontal significance threshold */}
{significanceThreshold && (
<Annotation
datum={{
x: 0, // horizontal line so x could be anything
y: -Math.log10(Number(significanceThreshold)),
}}
{...thresholdLineAccessors}
>
<AnnotationLineSubject
orientation="horizontal"
{...thresholdLineStyles}
/>
</Annotation>
)}
{/* Draw both vertical log2 fold change threshold lines */}
{log2FoldChangeThreshold && (
<>
<Annotation
datum={{
x: -log2FoldChangeThreshold,
y: 0, // vertical line so y could be anything
}}
{...thresholdLineAccessors}
>
<AnnotationLineSubject {...thresholdLineStyles} />
</Annotation>
<Annotation
datum={{
x: log2FoldChangeThreshold,
y: 0, // vertical line so y could be anything
}}
{...thresholdLineAccessors}
>
<AnnotationLineSubject {...thresholdLineStyles} />
</Annotation>
</>
)}

{/* The data itself */}
{/* Wrapping in a group in order to change the opacity of points. The GlyphSeries is somehow
a bunch of glyphs which are <circles> so there should be a way to pass opacity
down to those elements, but I haven't found it yet */}
<Group opacity={markerBodyOpacity ?? 1}>
<GlyphSeries
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
);
}}
/>
</Group>
</XYChart>
</div>
);
}

/**
* Assign color to point based on significance and magnitude change thresholds
*/
function assignSignificanceColor(
log2foldChange: number,
pValue: number,
significanceThreshold: number,
log2FoldChangeThreshold: number,
significanceColors: string[] // Assuming the order is [insignificant, high (up regulated), low (down regulated)]
) {
// 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];
}

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

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

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

export default VolcanoPlot;
29 changes: 29 additions & 0 deletions packages/libs/components/src/plots/visxVEuPathDB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** Helpful styles and types for working with visx */

/**
* Types
*/

// Basic x,y point. Comes in handy for annotations and other non-data plotted elements
export type VisxPoint = {
x?: number;
y?: number;
};

/**
* Plot styles
* (can eventually be moved to a visx theme)
*/
export const thresholdLineStyles = {
stroke: '#aaaaaa',
strokeWidth: 1,
strokeDasharray: 3,
};
export const axisStyles = {
stroke: '#bbbbbb',
strokeWidth: 1,
};
export const gridStyles = {
stroke: '#dddddd',
strokeWidth: 0.5,
};
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,10 @@ function boxMullerTransform() {
return { z0, z1 };
}

function getNormallyDistributedRandomNumber(mean: number, stddev: number) {
export function getNormallyDistributedRandomNumber(
mean: number,
stddev: number
) {
const { z0, z1 } = boxMullerTransform();

return z0 * stddev + mean;
Expand Down
Loading

0 comments on commit 3f90637

Please sign in to comment.