Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Volcano #19

Merged
merged 40 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
02ef746
feat: draft volcano story
asizemore Mar 1, 2023
84fb7e6
Merge branch 'main' into volcano
asizemore Mar 6, 2023
c0e8326
volcano plot work in progress compiles
asizemore Mar 14, 2023
1aac185
visx test. visx working
asizemore Mar 16, 2023
543514f
drafting visx stories
asizemore Mar 20, 2023
a71b8a1
Merge branch 'main' into volcano
asizemore Mar 20, 2023
46da6d9
fixed volcano story axis
asizemore Mar 20, 2023
40974db
added threshold lines to plotly volcano
asizemore Mar 21, 2023
4345f4e
add threshold lines for volcano
asizemore Mar 22, 2023
ea8dacb
Merge branch 'monorepo-pre-migration' into volcano
dmfalke Mar 22, 2023
777ad32
Merge remote-tracking branch 'components/volcano' into volcano
dmfalke Mar 22, 2023
ab5101f
erge branch 'main' into volcano
asizemore Mar 23, 2023
439c0e5
move volcano visx to its own component
asizemore Mar 27, 2023
19d851b
Merge branch 'main' into volcano
asizemore Mar 29, 2023
18dfc78
format incoming data for visx
asizemore Mar 29, 2023
0b8c448
add basic threshold lines
asizemore Mar 29, 2023
3bc1a4e
improve axis and gridline styles
asizemore Mar 29, 2023
9538298
determine axis ranges from data
asizemore Mar 29, 2023
06320f7
added half a tooltip
asizemore Mar 30, 2023
28bc6dc
Merge branch 'main' into volcano
asizemore Mar 30, 2023
f921d45
use annotation lines for threshold lines
asizemore Mar 31, 2023
1797a64
a little volcano cleanup
asizemore Mar 31, 2023
7063c13
Merge branch 'main' into volcano
asizemore Mar 31, 2023
27c96a7
volcano cleanup and reorganize how data gets formatted
asizemore Apr 6, 2023
3645576
document volcano
asizemore Apr 6, 2023
8c4b458
refactor so volcano data is only one series
asizemore Apr 6, 2023
ff6b5d8
volcano colors points based on input thresholds
asizemore Apr 7, 2023
0a31095
fix volcano story with many points
asizemore Apr 7, 2023
fe81146
fix volcano y axis so that it doesnt always start at 0
asizemore Apr 7, 2023
f52efb5
Merge branch 'main' into volcano
asizemore Apr 7, 2023
9b62891
remove series from volcano
asizemore Apr 10, 2023
aa0ae52
Merge branch 'main' into volcano
asizemore Apr 13, 2023
9469695
synchronous PR review feedback
asizemore Apr 13, 2023
b71d809
volcano data now array of objects and add docs
asizemore Apr 14, 2023
4b033dd
Merge branch 'main' into volcano
asizemore Apr 14, 2023
07b2575
move visx styles and types to own file
asizemore Apr 14, 2023
94d7d41
documentation
asizemore Apr 14, 2023
c2c79b1
Merge branch 'main' into volcano
asizemore Apr 17, 2023
26fba4d
remove unnecessary storybook plugin
asizemore Apr 17, 2023
6d62e39
swap significance_colors indices
asizemore Apr 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/libs/components/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ module.exports = {
test: /\.(js|jsx)$/,
loader: require.resolve('babel-loader'),
options: {
plugins: ['@babel/plugin-proposal-nullish-coalescing-operator'],
plugins: [
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-syntax-class-properties',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? I don't see class properties used anywhere.

Copy link
Contributor

@adnauseum adnauseum Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!! I forgot to ask about this in our PR review! I was curious since it's scoped to storybook.

Ann's probably hacking us 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yes good catch! This was a byproduct of my adventures trying to install some of the visx packages a while back. I just removed it and all seems to work 👍

],
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
274 changes: 274 additions & 0 deletions packages/libs/components/src/plots/VolcanoPlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { PlotProps } from './PlotlyPlot';

import { significanceColors } from '../types/plots';
import { VolcanoPlotData } from '../types/plots/volcanoplot';
import { NumberRange } from '../types/general';
import {
XYChart,
Tooltip,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 'Tooltip' is declared but its value is never read.

Axis,
Grid,
GlyphSeries,
Annotation,
AnnotationLineSubject,
} from '@visx/xychart';
import { Group } from '@visx/group';
import { max, min } from 'lodash';

export interface VolcanoPlotProps extends PlotProps<VolcanoPlotData> {
/**
* Used to set the fold change thresholds. Will
* set two thresholds at +/- this number.
*/
log2FoldChangeThreshold: number;
/** Set the threshold for significance. */
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>;
/** What is this plot's name? */
plotTitle?: string;
/** marker color opacity: range from 0 to 1 */
markerBodyOpacity?: number;
}

const EmptyVolcanoPlotData: VolcanoPlotData = {
foldChange: [],
pValue: [],
adjustedPValue: [],
pointId: [],
};

interface DataPoint {
foldChange: string;
pValue: string;
adjustedPValue: string;
pointId: string;
color: string;
}

/**
* The Volcano Plot displays points on a (magnitude change) by (significance) xy axis.
* It also colors the points based on their significance and magnitude change.
*/
function VolcanoPlot(props: VolcanoPlotProps) {
const {
data = EmptyVolcanoPlotData,
independentAxisRange,
dependentAxisRange,
significanceThreshold,
log2FoldChangeThreshold,
markerBodyOpacity,
...restProps
} = props;

/**
* Find mins and maxes of the data and for the plot
*/

const dataXMin = min(data.foldChange.map(Number));
const dataXMax = max(data.foldChange.map(Number));
const dataYMin = min(data.pValue.map(Number));
const dataYMax = max(data.pValue.map(Number));

// 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.
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Dangling // 👯


let xMin: number;
let xMax: number;
let yMin: number;
let yMax: number;

// Log transform for plotting, and add a little margin for axes
if (dataXMin && dataXMax) {
xMin = Math.log2(dataXMin);
xMax = Math.log2(dataXMax);
xMin = xMin - (xMax - xMin) * 0.05;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/suggestion: 0.05 is the measure of statistical significance, right? Maybe we can save that to a constant called STATISTICAL_SIGNIFICANCE_THRESHOLD?

xMax = xMax + (xMax - xMin) * 0.05;
} else {
xMin = 0;
xMax = 0;
}
if (dataYMin && dataYMax) {
yMin = -Math.log10(dataYMax);
yMax = -Math.log10(dataYMin);
yMin = yMin - (yMax - yMin) * 0.05;
yMax = yMax + (yMax - yMin) * 0.05;
} else {
yMin = 0;
yMax = 0;
}
console.log(yMin);
asizemore marked this conversation as resolved.
Show resolved Hide resolved

/**
* Turn the data (array of arrays) into data points (array of points)
*/

let dataPoints: DataPoint[] = [];

// Loop through the data and return points. Doesn't really matter
// which var of the data we map over.
data.foldChange.forEach((fc, ind: number) => {
dataPoints.push({
foldChange: fc,
pValue: data.pValue[ind],
adjustedPValue: data.adjustedPValue[ind],
pointId: data.pointId[ind],
color: assignSignificanceColor(
Math.log2(Number(fc)),
Number(data.pValue[ind]),
significanceThreshold,
log2FoldChangeThreshold,
significanceColors
),
});
});

/**
* Accessors - tell visx which value of each points we should use and where.
*/

const dataAccessors = {
xAccessor: (d: any) => {
return Math.log2(d?.foldChange);
},
yAccessor: (d: any) => {
return -Math.log10(d?.pValue);
},
};

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

/**
* Plot styles
* (can eventually be moved to a new file and applied as a visx theme)
*/
const thresholdLineStyles = {
stroke: '#aaaaaa',
strokeWidth: 1,
strokeDasharray: 3,
};
const axisStyles = {
stroke: '#bbbbbb',
strokeWidth: 1,
};
const gridStyles = {
stroke: '#dddddd',
strokeWidth: 0.5,
};
console.log(yMin);

return (
// From docs " For correct tooltip positioning, it is important to wrap your
// component in an element (e.g., div) with relative positioning."
Copy link
Contributor

@adnauseum adnauseum Apr 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random: If you or anyone would like to understand absolute/relative positioning, this Codepen might help.

The docs say this because it sounds like the tooltips are positioned absolutely. Absolutely positioned elements position themselves in relation to their nearest parent that's positioned relatively. 🤯

You likely know this, but I just wanted to put it in there in case!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did not know this! Thank you! That was a really clear example

<div style={{ position: 'relative' }}>
<XYChart
height={300}
xScale={{ type: 'linear', domain: [xMin, xMax] }}
yScale={{ type: 'linear', domain: [yMin, yMax], zero: false }}
width={300}
>
<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 */}
{significanceThreshold && (
<Annotation
datum={{
x: 0, // horizontal line so x could be anything
y: -Math.log10(Number(significanceThreshold)),
}}
{...thresholdLineAccessors}
>
<AnnotationLineSubject
orientation="horizontal"
{...thresholdLineStyles}
/>
</Annotation>
)}
{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 */}
<Group opacity={markerBodyOpacity ?? 1}>
<GlyphSeries
dataKey={'data'}
data={dataPoints}
{...dataAccessors}
colorAccessor={(d) => {
return d.color;
}}
/>
</Group>
</XYChart>
</div>
);
}

/**
* Assign color to point based on significance and magnitude change thresholds
*/
function assignSignificanceColor(
xValue: number, // has already been log2 transformed
yValue: number, // the raw pvalue
significanceThreshold: number,
log2FoldChangeThreshold: number,
significanceColors: string[] // Assuming the order is [high, low, not significant]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This isn't at all unclear code or anything complicated, so this is really just a suggestion. I don't know if it makes the code better. Given this assumption, you could name the indicies:

const HIGH = 0;
const LOW = 1;
const INSIGNIFICANT = 2;

 if (yValue >= significanceThreshold) {
    return significanceColors[INSIGNIFICANT];
  }

 if (xValue >= log2FoldChangeThreshold) {
    return significanceColors[HIGH];
  }
...

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

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

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

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

export default VolcanoPlot;
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