From 02ef7466dcef0dd4cee2c764924a7caf2920cabc Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 1 Mar 2023 08:07:47 -0500 Subject: [PATCH 01/28] feat: draft volcano story --- .../src/stories/plots/VolcanoPlot.stories.tsx | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100755 packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx new file mode 100755 index 0000000000..04b247c62e --- /dev/null +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -0,0 +1,238 @@ +import React, { useState } from 'react'; +import ScatterPlot, { ScatterPlotProps } from '../../plots/ScatterPlot'; +import { min, max, lte, gte } from 'lodash'; +// import { dataSetProcess, xAxisRange, yAxisRange } from './ScatterPlot.storyData'; +import { Story, Meta } from '@storybook/react/types-6-0'; +// test to use RadioButtonGroup directly instead of ScatterPlotControls +import { NumberRange } from '../../types/general'; + +import { ScatterPlotData } from '../../types/plots'; + +export default { + title: 'Plots/VolcanoPlot', + component: VolcanoPlot, +} as Meta; + +interface VEuPathDBVolcanoPlotData { + volcanoplot: { + data: Array<{ + foldChange: string[]; + adjustedPValue: string[]; + pointId: string[]; + overlayValue: string; + }>; + }; +} + +// Let's make some fake data! +const dataSetVolcano: VEuPathDBVolcanoPlotData = { + volcanoplot: { + data: [ + { + foldChange: ['2', '3'], + adjustedPValue: ['0.001', '0.0001'], + pointId: ['a', 'b'], + overlayValue: 'positive', + }, + { + foldChange: ['-1', '0', '1', '0.5', '-0.5', '4', '-5'], + adjustedPValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], + pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], + overlayValue: 'none', + }, + { + foldChange: ['-2', '-3', '-4'], + adjustedPValue: ['0.001', '0.0001', '0.002'], + pointId: ['j', 'k', 'l'], + overlayValue: 'negative', + }, + ], + }, +}; + +// These can go into addons eventually. I'd expect other vizs that involve significance to use these as well +// These are NOT the final proposed colors +const highMedLowColors = ['#dd1111', '#bbbbbb', '#1111dd']; + +const plotTitle = 'Volcano erupt!'; + +interface TemplateProps { + data: VEuPathDBVolcanoPlotData; + markerBodyOpacity: number; + // foldChangeHighGate: number; + // foldChangeLowGate: number; // we can't have gates unless we mimic the backend updating the data format when we change gates + adjustedPValueGate: number; +} + +const Template: Story = (args) => { + const { dataSetProcess: datasetProcess } = processVolcanoData( + args.data, + highMedLowColors + ); + console.log(datasetProcess); + + // Better to break into a high and low prop? Would be more clear + const foldChangeGates = [-1.5, 1.5]; + + const comparisonLabels = ['group a', 'group b']; + + /** + * Volcano knows + * x and y label (always fold change and pvalue) + */ + + /** + * datasetProcess has three or fewer series + * has columms for foldChange, adjustedPValue, pointId, significanceDirection (naming help!!) + * + */ + + const independentAxisRange = [-5, 5]; // Determined by the data and symmetric around 0 by default? + const dependentAxisRange = [0, 0.2]; // By default max determined by data and min at 0 + + return ( +
+ + data={datasetProcess} + foldChangeGates={foldChangeGates} + comparisonLabels={comparisonLabels} + adjustedPValueGate={args.adjustedPValueGate} + markerBodyOpacity={args.markerBodyOpacity} + plotTitle={plotTitle} + independentAxisRange={independentAxisRange} + dependentAxisRange={dependentAxisRange} + +
+ ); +}; + +export const Default = Template.bind({}); +Default.args = { + data: dataSetVolcano, + markerBodyOpacity: 0.8, +}; + +// this process input data function is similar to scatter's but not the same. +// would probably be worth revisiting what is in common and factoring accordingly +function processVolcanoData( + dataSet: VEuPathDBVolcanoPlotData, + colors: string[] +): { + dataSetProcess: ScatterPlotData; + xAxisRange: NumberRange; + yAxisRange: NumberRange; +} { + // set variables for x- and yaxis ranges + let xMin: number = 0; + let xMax: number = 0; + let yMin: number = 0; + let yMax: number = 0; + + let processedDataSeries: any = []; + dataSet.volcanoplot.data.forEach(function (el: any, index: number) { + // initialize variables: setting with union type for future, but this causes typescript issue in the current version + let xSeriesValue: T[] = []; + let ySeriesValue: T[] = []; + + // set rgbValue here per dataset with a default color + // Add check for len(colors) = number of series + let rgbValue: number[] = hexToRgb(colors[index]); + + let scatterPointColor: string = ''; + + // series is for scatter plot + if (el.series) { + // check the number of x = number of y + if (el.series.foldChange.length !== el.series.adjustedPValue.length) { + console.log( + 'x length=', + el.series.foldChange.length, + ' y length=', + el.series.adjustedPValue.length + ); + alert('The number of X data is not equal to the number of Y data'); + throw new Error( + 'The number of X data is not equal to the number of Y data' + ); + } + + /* + * set variables for x-/y-axes ranges including x,y data points: considering Date data for X as well + * This is for finding global min/max values among data arrays for better display of the plot(s) + */ + + xMin = + xMin < Math.min(...(xSeriesValue as number[])) + ? xMin + : Math.min(...(xSeriesValue as number[])); + xMax = + xMax > Math.max(...(xSeriesValue as number[])) + ? xMax + : Math.max(...(xSeriesValue as number[])); + + // check if this Y array consists of numbers & add type assertion + if (index == 0) { + yMin = Math.min(...ySeriesValue); + yMax = Math.max(...ySeriesValue); + } else { + yMin = + yMin < Math.min(...ySeriesValue) ? yMin : Math.min(...ySeriesValue); + yMax = + yMax > Math.max(...ySeriesValue) ? yMax : Math.max(...ySeriesValue); + } + + // use global opacity for coloring + scatterPointColor = + 'rgba(' + + rgbValue[0] + + ',' + + rgbValue[1] + + ',' + + rgbValue[2] + + ',' + + 0.8 + + ')'; + + // add scatter data considering input options + processedDataSeries.push({ + x: xSeriesValue, + y: ySeriesValue, + name: el.label, + mode: 'markers', + // type: 'scattergl', + type: 'scatter', + marker: { color: scatterPointColor, size: 12 }, + }); + } + + // make some margin for y-axis range (5% of range for now) + if (typeof yMin == 'number' && typeof yMax == 'number') { + yMin = yMin - (yMax - yMin) * 0.05; + yMax = yMax + (yMax - yMin) * 0.05; + } + }); + + return { + dataSetProcess: { series: processedDataSeries }, + xAxisRange: { min: xMin, max: xMax }, + yAxisRange: { min: yMin, max: yMax }, + }; +} + +// change HTML hex code to rgb array +const hexToRgb = (hex?: string): [number, number, number] => { + if (!hex) return [0, 0, 0]; + const fullHex = hex.replace( + /^#?([a-f\d])([a-f\d])([a-f\d])$/i, + (m: string, r: string, g: string, b: string): string => + '#' + r + r + g + g + b + b + ); + const hexDigits = fullHex.substring(1); + const matches = hexDigits.match(/.{2}/g); + if (matches == null) return [0, 0, 0]; + return matches.map((x: string) => parseInt(x, 16)) as [ + number, + number, + number + ]; +}; From c0e83267df9d692c7dd6537f57669eed8484fcf8 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 14 Mar 2023 16:56:43 -0400 Subject: [PATCH 02/28] volcano plot work in progress compiles --- .../libs/components/src/plots/VolcanoPlot.tsx | 225 ++++++++++++++++++ .../src/stories/plots/VolcanoPlot.stories.tsx | 31 ++- .../components/src/types/plots/volcanoplot.ts | 31 +++ 3 files changed, 277 insertions(+), 10 deletions(-) create mode 100755 packages/libs/components/src/plots/VolcanoPlot.tsx create mode 100755 packages/libs/components/src/types/plots/volcanoplot.ts diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx new file mode 100755 index 0000000000..98743b72c9 --- /dev/null +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -0,0 +1,225 @@ +import { useMemo } from 'react'; +import { makePlotlyPlotComponent, PlotProps } from './PlotlyPlot'; +// truncation +import { + OrientationAddon, + OrientationDefault, + AxisTruncationAddon, + independentAxisLogScaleAddon, + DependentAxisLogScaleAddon, + ScatterPlotData, +} from '../types/plots'; +import { VolcanoPlotData } from '../types/plots/volcanoplot'; +// add Shape for truncation +import { Layout, Shape } from 'plotly.js'; +import { NumberRange } from '../types/general'; + +// import truncation util functions +import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-truncations'; +import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; +import { tickSettings } from '../utils/tick-settings'; +import * as ColorMath from 'color-math'; + +export interface VolcanoPlotProps + extends PlotProps, + // truncation + OrientationAddon, + independentAxisLogScaleAddon, + DependentAxisLogScaleAddon, + AxisTruncationAddon { + /** x-axis range: required for confidence interval - not really */ + independentAxisRange?: NumberRange; + /** y-axis range: required for confidence interval */ + dependentAxisRange?: NumberRange; + foldChangeGates?: Array; + comparisonLabels?: Array; + adjustedPValueGate?: number; + plotTitle?: string; + + /** marker color opacity: range from 0 to 1 */ + markerBodyOpacity?: number; +} + +const EmptyVolcanoPlotData: ScatterPlotData = { + series: [], +}; + +/** + * This component handles several plots such as marker, line, confidence interval, + * density, and combinations of plots like marker + line + confidence interval + */ +const VolcanoPlot = makePlotlyPlotComponent( + 'VolcanoPlot', + (props: VolcanoPlotProps) => { + const { + data = EmptyVolcanoPlotData, + independentAxisRange, + dependentAxisRange, + // independentAxisLabel, + // dependentAxisLabel, + // independentValueType, + // dependentValueType, + // truncation + orientation = OrientationDefault, + axisTruncationConfig, + independentAxisLogScale = false, + dependentAxisLogScale = false, + markerBodyOpacity, + ...restProps + } = props; + + // truncation axis range + const standardIndependentAxisRange = independentAxisRange; + const extendedIndependentAxisRange = extendAxisRangeForTruncations( + standardIndependentAxisRange, + axisTruncationConfig?.independentAxis, + 'number', + true, // addPadding + independentAxisLogScale + ); + + // truncation + const standardDependentAxisRange = dependentAxisRange; + const extendedDependentAxisRange = extendAxisRangeForTruncations( + standardDependentAxisRange, + axisTruncationConfig?.dependentAxis, + 'number', + true, // addPadding + dependentAxisLogScale + ); + + // make rectangular layout shapes for truncated axis/missing data + const truncatedAxisHighlighting: + | Partial[] + | undefined = useMemo(() => { + if (data.series.length > 0) { + const filteredTruncationLayoutShapes = truncationLayoutShapes( + orientation, + standardIndependentAxisRange, // send undefined for independentAxisRange + standardDependentAxisRange, + extendedIndependentAxisRange, // send undefined for independentAxisRange + extendedDependentAxisRange, + axisTruncationConfig + ); + + return filteredTruncationLayoutShapes; + } else { + return []; + } + }, [ + standardDependentAxisRange, + extendedDependentAxisRange, + orientation, + data, + axisTruncationConfig, + ]); + + const layout: Partial = { + hovermode: 'closest', + xaxis: { + title: 'log2 Fold Change', + // truncation + range: data.series.length + ? [ + extendedIndependentAxisRange?.min, + extendedIndependentAxisRange?.max, + ].map((val) => + independentAxisLogScale && val != null + ? Math.log10(val as number) + : val + ) + : undefined, + zeroline: false, // disable yaxis line + // make plot border + mirror: true, + // date or number type (from variable.type): no log scale for date + type: undefined, + tickfont: data.series.length ? {} : { color: 'transparent' }, + ...tickSettings( + independentAxisLogScale, + extendedIndependentAxisRange, + 'number' + ), + }, + yaxis: { + title: '-log10 P Value', + // with the truncated axis, negative values need to be checked for log scale + range: data.series.length + ? [ + extendedDependentAxisRange?.min, + extendedDependentAxisRange?.max, + ].map((val) => + dependentAxisLogScale && val != null + ? Math.log10(val as number) + : val + ) + : undefined, + // range: undefined, + zeroline: false, // disable xaxis line + // make plot border + mirror: true, + // date or number type (from variable.type): no log scale for date + type: undefined, + tickfont: data.series.length ? {} : { color: 'transparent' }, + ...tickSettings( + dependentAxisLogScale, + extendedDependentAxisRange, + 'number' + ), + }, + // add truncatedAxisHighlighting for layout.shapes + shapes: truncatedAxisHighlighting, + }; + + // change data here for marker opacity + const finalData = useMemo(() => { + return data.series.map((d: any) => ({ + ...d, + marker: { + ...d.marker, + color: + d.marker == null + ? undefined + : markerBodyOpacity != null + ? Array.isArray(d.marker.color) + ? d.marker.color.map((color: string) => + ColorMath.evaluate( + color + + ' @a ' + + (markerBodyOpacity * 100).toString() + + '%' + ).result.css() + ) + : ColorMath.evaluate( + d.marker.color + + ' @a ' + + (markerBodyOpacity * 100).toString() + + '%' + ).result.css() + : d.marker.color, + // need to set marker.line for a transparent case (opacity != 1) + line: + d.marker == null + ? undefined + : { + ...d.marker.line, + width: + markerBodyOpacity != null + ? markerBodyOpacity === 0 + ? 1 + : 0 + : 1, + }, + }, + })); + }, [data, markerBodyOpacity]); + + return { + data: finalData, + layout, + ...restProps, + }; + } +); + +export default VolcanoPlot; diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 04b247c62e..be86456396 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import ScatterPlot, { ScatterPlotProps } from '../../plots/ScatterPlot'; -import { min, max, lte, gte } from 'lodash'; +import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; +// import { min, max, lte, gte } from 'lodash'; // import { dataSetProcess, xAxisRange, yAxisRange } from './ScatterPlot.storyData'; import { Story, Meta } from '@storybook/react/types-6-0'; // test to use RadioButtonGroup directly instead of ScatterPlotControls @@ -17,6 +17,7 @@ interface VEuPathDBVolcanoPlotData { volcanoplot: { data: Array<{ foldChange: string[]; + pValue: string[]; adjustedPValue: string[]; pointId: string[]; overlayValue: string; @@ -30,19 +31,22 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { data: [ { foldChange: ['2', '3'], - adjustedPValue: ['0.001', '0.0001'], + pValue: ['0.001', '0.0001'], + adjustedPValue: ['0.01', '0.001'], pointId: ['a', 'b'], overlayValue: 'positive', }, { foldChange: ['-1', '0', '1', '0.5', '-0.5', '4', '-5'], - adjustedPValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], + pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], + adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], overlayValue: 'none', }, { foldChange: ['-2', '-3', '-4'], - adjustedPValue: ['0.001', '0.0001', '0.002'], + pValue: ['0.001', '0.0001', '0.002'], + adjustedPValue: ['0.01', '0.001', '0.02'], pointId: ['j', 'k', 'l'], overlayValue: 'negative', }, @@ -87,13 +91,20 @@ const Template: Story = (args) => { * */ - const independentAxisRange = [-5, 5]; // Determined by the data and symmetric around 0 by default? - const dependentAxisRange = [0, 0.2]; // By default max determined by data and min at 0 + const independentAxisRange = { + min: -5, + max: 5, + }; + // Determined by the data and symmetric around 0 by default? + const dependentAxisRange = { + min: 0, + max: 0.2, + }; // By default max determined by data and min at 0 return (
- - data={datasetProcess} + = (args) => { plotTitle={plotTitle} independentAxisRange={independentAxisRange} dependentAxisRange={dependentAxisRange} - + />
); }; diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts new file mode 100755 index 0000000000..c2f4d36eeb --- /dev/null +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -0,0 +1,31 @@ +export type VolcanoPlotDataSeries = { + /** x/y data */ + x: (number | null)[] | string[]; + y: (number | null)[] | string[]; + /** legend text */ + name?: string; + /** plot style */ + mode?: 'markers'; + /** plot with marker: scatter plot with raw data */ + marker?: { + /** marker color */ + color?: string; + /** marker size: no unit */ + size?: number; + /** marker's perimeter setting */ + line?: { + /** marker's perimeter color */ + color?: string; + /** marker's perimeter color: no unit */ + width?: number; + }; + symbol?: string; + }; + /** opacity of points? */ + opacity?: number; +}; + +export type VolcanoPlotData = { + /** an array of data series (aka traces) */ + series: VolcanoPlotDataSeries[]; +}; From 1aac18514da7e4ee5c57635875d683bc8e01654c Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 16 Mar 2023 15:25:27 -0400 Subject: [PATCH 03/28] visx test. visx working --- packages/libs/components/.storybook/main.js | 6 +- packages/libs/components/package.json | 2 + .../src/stories/plots/ScatterVisx.stories.tsx | 57 +++ .../src/stories/plots/VolcanoPlot.stories.tsx | 41 +- packages/libs/components/yarn.lock | 395 +++++++++++++++++- 5 files changed, 478 insertions(+), 23 deletions(-) create mode 100644 packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx diff --git a/packages/libs/components/.storybook/main.js b/packages/libs/components/.storybook/main.js index 9394c710da..3b7dfc1d28 100644 --- a/packages/libs/components/.storybook/main.js +++ b/packages/libs/components/.storybook/main.js @@ -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', + ], + presets: ['@babel/preset-env', '@babel/preset-react'], }, }); diff --git a/packages/libs/components/package.json b/packages/libs/components/package.json index c7a3c55f1a..007bd8db39 100755 --- a/packages/libs/components/package.json +++ b/packages/libs/components/package.json @@ -17,6 +17,7 @@ "@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", @@ -36,6 +37,7 @@ "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" }, diff --git a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx new file mode 100644 index 0000000000..4ab01881e3 --- /dev/null +++ b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx @@ -0,0 +1,57 @@ +import { XYChart, Tooltip, Axis, Grid, LineSeries } from '@visx/xychart'; +import ScatterPlot from '../../../lib/plots/ScatterPlot'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { DivIcon } from 'leaflet'; + +export default { + title: 'Plots/VisxScatter', + component: ScatterPlot, +} as Meta; + +const data1 = [ + { x: '2020-01-01', y: 50 }, + { x: '2020-01-02', y: 10 }, + { x: '2020-01-03', y: 20 }, +]; + +const data2 = [ + { x: '2020-01-01', y: 30 }, + { x: '2020-01-02', y: 40 }, + { x: '2020-01-03', y: 80 }, +]; + +interface TemplateProps {} + +const Template: Story = (args) => { + const accessors = { + xAccessor: (d: any) => d.x, + yAccessor: (d: any) => d.y, + }; + + return ( + + + + + + ( +
+
+ {tooltipData!.nearestDatum!.key} +
+ {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} + {', '} + {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} +
+ )} + /> +
+ ); +}; + +export const Default = Template.bind({}); diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index be86456396..1331a598e7 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -69,6 +69,7 @@ interface TemplateProps { } const Template: Story = (args) => { + console.log(args.data); const { dataSetProcess: datasetProcess } = processVolcanoData( args.data, highMedLowColors @@ -98,7 +99,7 @@ const Template: Story = (args) => { // Determined by the data and symmetric around 0 by default? const dependentAxisRange = { min: 0, - max: 0.2, + max: 0.03, }; // By default max determined by data and min at 0 return ( @@ -142,8 +143,8 @@ function processVolcanoData( let processedDataSeries: any = []; dataSet.volcanoplot.data.forEach(function (el: any, index: number) { // initialize variables: setting with union type for future, but this causes typescript issue in the current version - let xSeriesValue: T[] = []; - let ySeriesValue: T[] = []; + let xSeries = []; + let ySeries = []; // set rgbValue here per dataset with a default color // Add check for len(colors) = number of series @@ -152,14 +153,14 @@ function processVolcanoData( let scatterPointColor: string = ''; // series is for scatter plot - if (el.series) { + if (el) { // check the number of x = number of y - if (el.series.foldChange.length !== el.series.adjustedPValue.length) { + if (el.foldChange.length !== el.adjustedPValue.length) { console.log( 'x length=', - el.series.foldChange.length, + el.foldChange.length, ' y length=', - el.series.adjustedPValue.length + el.adjustedPValue.length ); alert('The number of X data is not equal to the number of Y data'); throw new Error( @@ -172,24 +173,26 @@ function processVolcanoData( * This is for finding global min/max values among data arrays for better display of the plot(s) */ + // ANN needs log 2 adjusting! + xSeries = el.foldChange; + ySeries = el.adjustedPValue; + xMin = - xMin < Math.min(...(xSeriesValue as number[])) + xMin < Math.min(...(xSeries as number[])) ? xMin - : Math.min(...(xSeriesValue as number[])); + : Math.min(...(xSeries as number[])); xMax = - xMax > Math.max(...(xSeriesValue as number[])) + xMax > Math.max(...(xSeries as number[])) ? xMax - : Math.max(...(xSeriesValue as number[])); + : Math.max(...(xSeries as number[])); // check if this Y array consists of numbers & add type assertion if (index == 0) { - yMin = Math.min(...ySeriesValue); - yMax = Math.max(...ySeriesValue); + yMin = Math.min(...ySeries); + yMax = Math.max(...ySeries); } else { - yMin = - yMin < Math.min(...ySeriesValue) ? yMin : Math.min(...ySeriesValue); - yMax = - yMax > Math.max(...ySeriesValue) ? yMax : Math.max(...ySeriesValue); + yMin = yMin < Math.min(...ySeries) ? yMin : Math.min(...ySeries); + yMax = yMax > Math.max(...ySeries) ? yMax : Math.max(...ySeries); } // use global opacity for coloring @@ -206,8 +209,8 @@ function processVolcanoData( // add scatter data considering input options processedDataSeries.push({ - x: xSeriesValue, - y: ySeriesValue, + x: xSeries, + y: ySeries, name: el.label, mode: 'markers', // type: 'scattergl', diff --git a/packages/libs/components/yarn.lock b/packages/libs/components/yarn.lock index 7c6d3bdd1a..7d26c978bf 100644 --- a/packages/libs/components/yarn.lock +++ b/packages/libs/components/yarn.lock @@ -2859,6 +2859,92 @@ resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-1.1.1.tgz#827fd05bb542cf874116176d8ef48d5b12163f81" integrity sha512-7PGLWa9MZ5x/cWy8EH2VzI4T8q5WpuHbixzCDXqixP/WyqwIrg5NDUPgYuFnB4IEIZF+6nA265mYzswFo/h1Pw== +"@react-spring/animated@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.1.tgz#0f2d78184ee0cce703acd41abb87ea56765b5713" + integrity sha512-EX5KAD9y7sD43TnLeTNG1MgUVpuRO1YaSJRPawHNRgUWYfILge3s85anny4S4eTJGpdp5OoFV2kx9fsfeo0qsw== + dependencies: + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/core@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.1.tgz#cfe176a48ee0a05545b1af5f2fbae718b50e9a99" + integrity sha512-8K9/FaRn5VvMa24mbwYxwkALnAAyMRdmQXrARZLcBW2vxLJ6uw9Cy3d06Z8M12kEqF2bDlccaCSDsn2bSz+Q4A== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/rafz" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/konva@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.7.1.tgz#25640892f88bde06c3ab96c875e5f7408abbce43" + integrity sha512-74svXHtUJi6Tvk9mNLUV1/1WfU8MdWsTK6JUpvmJr/rUr8r3FdOokk22icbgEg6AjxCkIf5e2WFovCCHUSyS0w== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/core" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/native@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.7.1.tgz#3f397f946fc9a7dd4d7d432f8c0a4726d7723751" + integrity sha512-dHWeH0UuE+Rxc3YZFLp8Aq0RBP07sdOgI7pLVG46OzkMRs2RtJeWJxB6UXIWAgcYDqWDk2REAPhLD3ItDl0tDQ== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/core" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/rafz@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.1.tgz#bdfea463fcb5ddc4e7253a8fa3870dd52ebbc59a" + integrity sha512-JSsrRfbEJvuE3w/uvU3mCTuWwpQcBXkwoW14lBgzK9XJhuxmscGo59AgJUpFkGOiGAVXFBGB+nEXtSinFsopgw== + +"@react-spring/shared@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.1.tgz#29611bb63d0c9e1ac18b6ced7aa4db1d48d136f3" + integrity sha512-R2kZ+VOO6IBeIAYTIA3C1XZ0ZVg/dDP5FKtWaY8k5akMer9iqf5H9BU0jyt3Qtxn0qQY7whQdf6MTcWtKeaawg== + dependencies: + "@react-spring/rafz" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/three@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.7.1.tgz#0dab3b5e96bb6e10db0a1363938e46fc68a861e4" + integrity sha512-5leUe0PDwIIw1M3GN3788zwTY4Ykyy+kNvQmg9+Hqs1DN3T8J1ovRTGwqWfGAu4ApTta9p5BH7SWNxxt3NO59Q== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/core" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/types@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.1.tgz#b540752a479d210c6fb68d2b1d5ff35556df4308" + integrity sha512-yBcyfKUeZv9wf/ZFrQszvhSPuDx6Py6yMJzpMnS+zxcZmhXPeOCKZSHwqrUz1WxvuRckUhlgb7eNI/x5e1e8CA== + +"@react-spring/web@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.1.tgz#a9ee730d06c686b8432cd20f41683b1acb9b6300" + integrity sha512-6uUE5MyKqdrJnIJqlDN/AXf3i8PjOQzUuT26nkpsYxUGOk7c+vZVPcfrExLSoKzTb9kF0i66DcqzO5fXz/Z1AA== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/core" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + +"@react-spring/zdog@~9.7.1": + version "9.7.1" + resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.7.1.tgz#474a1366d7b71d623e0dff0e37a243b505e8c1a6" + integrity sha512-FeDws+7ZSoi91TUjxKnq3xmdOW6fthmqky6zSPIZq1NomeyO7+xwbxjtu15IqoWG4DJ9pouVZDijvBQXUNl0Mw== + dependencies: + "@react-spring/animated" "~9.7.1" + "@react-spring/core" "~9.7.1" + "@react-spring/shared" "~9.7.1" + "@react-spring/types" "~9.7.1" + "@restart/context@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02" @@ -4213,7 +4299,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.7.tgz#14a57b0539f8929015f8ad96490de50a16211040" integrity sha512-fvht6DOYKzqmXjMb/+xfgkmrWM4SD7rMA/ZbM+gGwr9ZTuIDfky95J8CARtaJo/ExeWyS0xGVdL2gqno2zrQ0Q== -"@types/d3-interpolate@*": +"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== @@ -4276,6 +4362,13 @@ dependencies: "@types/d3-time" "*" +"@types/d3-scale@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" + integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== + dependencies: + "@types/d3-time" "*" + "@types/d3-selection@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.1.tgz#e57b01ab69b18b380f68db97b76ceefe62f17191" @@ -4310,6 +4403,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.1.1.tgz#6cf3a4242c3bbac00440dfb8ba7884f16bedfcbf" integrity sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw== +"@types/d3-time@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + "@types/d3-timer@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" @@ -4520,6 +4618,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.167.tgz#ce7d78553e3c886d4ea643c37ec7edc20f16765e" integrity sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw== +"@types/lodash@^4.14.172": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + "@types/luxon@^1.25.0": version "1.25.0" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.25.0.tgz#3d6fe591fac874f48dd225cb5660b2b785a21a05" @@ -4958,6 +5061,19 @@ prop-types "^15.5.10" react-use-measure "2.0.1" +"@visx/annotation@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@visx/annotation/-/annotation-3.0.1.tgz#007c0030155d95897a74422a9d3aad65c7ef82a7" + integrity sha512-otf2AZDlt/XCpOBG0gkPowXVerLJW5yXPFis94Km0bj629htyKOsY5GeJKlBeB89ddxdi4zWHkaFxGFJ79Pqog== + dependencies: + "@types/react" "*" + "@visx/drag" "3.0.1" + "@visx/group" "3.0.0" + "@visx/text" "3.0.0" + classnames "^2.3.1" + prop-types "^15.5.10" + react-use-measure "^2.0.4" + "@visx/axis@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-1.3.0.tgz#e4804f7f8cd612c326905cd49c2da8f8f9ba7a79" @@ -4973,6 +5089,20 @@ classnames "^2.2.5" prop-types "^15.6.0" +"@visx/axis@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-3.1.0.tgz#775b221e5abfdec25304c607eeae8cda1a8bade6" + integrity sha512-JDj/1VYx0JO0pHFtwoFtYcnqdoZFh/dpHImEl169S5nTslSFlIoNTXA/ekpBP6ELkEZ59gmF1X5k29x6MFBwCA== + dependencies: + "@types/react" "*" + "@visx/group" "3.0.0" + "@visx/point" "3.0.1" + "@visx/scale" "3.0.0" + "@visx/shape" "3.0.0" + "@visx/text" "3.0.0" + classnames "^2.3.1" + prop-types "^15.6.0" + "@visx/bounds@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@visx/bounds/-/bounds-1.0.0.tgz#a54eb43a7ddf1bb7f0cd6bf8bf5f94fa5ec562b8" @@ -4982,6 +5112,15 @@ "@types/react-dom" "*" prop-types "^15.5.10" +"@visx/bounds@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/bounds/-/bounds-3.0.0.tgz#cf357cbff90a1fe5f95eb9d9288dd8794b744a7f" + integrity sha512-YQaSSER9erxlhppzRms6cvYdKqcIwk6eksrGdbJkBoHobhPo1JCIUXlmrA4qgrEnXInPJpueGE+PE5F+Dk12DA== + dependencies: + "@types/react" "*" + "@types/react-dom" "*" + prop-types "^15.5.10" + "@visx/brush@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/brush/-/brush-1.3.0.tgz#3f5fd7d7eadc702efdac0bf327630d482de4f1c4" @@ -5009,6 +5148,14 @@ "@types/d3-shape" "^1.3.1" d3-shape "^1.0.6" +"@visx/curve@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/curve/-/curve-3.0.0.tgz#c54568472e00a38483c58cf52e4a6ddb2887c2d4" + integrity sha512-kvHJDLBeczTQ87ZExSTfRxej06l6o6UiQ0NHf9+xpAin06y6Qk1ThOHHWJTGM6KGzwlu7jEauJGHwZs6nMhDvA== + dependencies: + "@types/d3-shape" "^1.3.1" + d3-shape "^1.0.6" + "@visx/drag@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/drag/-/drag-1.3.0.tgz#79d022854d2a85e14cbcf5cc5d82dcfcd6bb8d46" @@ -5018,6 +5165,16 @@ "@visx/event" "1.3.0" prop-types "^15.5.10" +"@visx/drag@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@visx/drag/-/drag-3.0.1.tgz#753d5f471d4e31679ca4fddcb476cf2d5c7eb6e4" + integrity sha512-yi2AB/unUfNYBRKS4pmUOuz8MjaAAYjsQGYcD/s4LqeQjd+lBZF7CuNcYZ/maGNQAEUfgLr2czIzADanOMtMaw== + dependencies: + "@types/react" "*" + "@visx/event" "3.0.1" + "@visx/point" "3.0.1" + prop-types "^15.5.10" + "@visx/event@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/event/-/event-1.3.0.tgz#bdbcf40910faf873bcfa972c8ba2c5a192c2dd6a" @@ -5026,6 +5183,14 @@ "@types/react" "*" "@visx/point" "1.0.0" +"@visx/event@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@visx/event/-/event-3.0.1.tgz#d5358f52ff5ef30036d955bd2b68b96472ff2d6f" + integrity sha512-tK1EUYQLLStBuoCMbm8LJ3VbDyCVI8HjT0pMRQxm+C75FSIVWvrThgrfrC9sWOFnEMEYWspZO7hI5zjsPKjLQA== + dependencies: + "@types/react" "*" + "@visx/point" "3.0.1" + "@visx/geo@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@visx/geo/-/geo-1.0.0.tgz#d28d05301eb560a150c0775f9c6e86d0fb8ae0ce" @@ -5053,6 +5218,18 @@ d3-shape "^1.2.0" prop-types "^15.6.2" +"@visx/glyph@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/glyph/-/glyph-3.0.0.tgz#218a96aa0ccba95dc77e46ab08d26ad89198f3a8" + integrity sha512-r1B0IocfWfhTABKjam0qqsWKjxLxZfGwefnwn8IcfELSd9iAUtLbI/46nP4roQRHhB/Wl3RBbgA97fZw8f1MxA== + dependencies: + "@types/d3-shape" "^1.3.1" + "@types/react" "*" + "@visx/group" "3.0.0" + classnames "^2.3.1" + d3-shape "^1.2.0" + prop-types "^15.6.2" + "@visx/gradient@1.0.0", "@visx/gradient@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@visx/gradient/-/gradient-1.0.0.tgz#51a9fe47eb5155620369b3fa8f33fb51c3f36881" @@ -5075,6 +5252,20 @@ classnames "^2.2.5" prop-types "^15.6.2" +"@visx/grid@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@visx/grid/-/grid-3.0.1.tgz#d91085ed92e7e1c0c2e58710bc33b2f0f33b8e74" + integrity sha512-cln5CVvFG58C5Uz1Uf0KRBFmGmgD1NALOQdYDu5yPsTuY2yLzVYPvCIlYBMdUtE0uzfNq972SmkZHfZYs03jxQ== + dependencies: + "@types/react" "*" + "@visx/curve" "3.0.0" + "@visx/group" "3.0.0" + "@visx/point" "3.0.1" + "@visx/scale" "3.0.0" + "@visx/shape" "3.0.0" + classnames "^2.3.1" + prop-types "^15.6.2" + "@visx/group@1.0.0", "@visx/group@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@visx/group/-/group-1.0.0.tgz#d47ac94abec1d191602a501a4fdf7455dfaad0be" @@ -5085,6 +5276,15 @@ classnames "^2.2.5" prop-types "^15.6.2" +"@visx/group@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.0.0.tgz#e7f9752599bcc7e141ff5317a2a9a502577ab8df" + integrity sha512-SFjXhTMcsaVAb1/TVL1KM5vn8gQTIVgSx0ATdDl4BJSFp2ym1lO8LY4jpV4SFweaHnWxVwrrfGLTn5QsYnvmjQ== + dependencies: + "@types/react" "*" + classnames "^2.3.1" + prop-types "^15.6.2" + "@visx/heatmap@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@visx/heatmap/-/heatmap-1.0.0.tgz#205f6e0ba8d70c491285ec10d2cbfc5c07865205" @@ -5167,6 +5367,24 @@ resolved "https://registry.yarnpkg.com/@visx/point/-/point-1.0.0.tgz#c93cd540989ded394aab8d56cbdc7ca4891c3dd9" integrity sha512-0L3ILwv6ro0DsQVbA1lo8fo6q3wvIeSTt9C8NarUUkoTNSFZaJtlmvwg2238r8fwwmSv0v9QFBj1hBz4o0bHrg== +"@visx/point@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@visx/point/-/point-3.0.1.tgz#77587ddaabf6f3023f09f8a0ce33a2c27c9d64c8" + integrity sha512-S5WOBMgEP2xHcgs3A2BFB2vwzrk0tMmn3PGZAbQJ+lu4HlnalDP72klUnxLTH8xclNNvpUHtHM5eLIJXyHx6Pw== + +"@visx/react-spring@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@visx/react-spring/-/react-spring-3.1.0.tgz#dad4770e74a3880da862cac104bad61e466cee01" + integrity sha512-8GHfXvJwnscGgf5UVCMeKw8Genq+cllcQJ8T6aAe1uC2ttYa2InmRIPsJEoA7VDLcxNIJgvw1bRd22QrgT9LtA== + dependencies: + "@types/react" "*" + "@visx/axis" "3.1.0" + "@visx/grid" "3.0.1" + "@visx/scale" "3.0.0" + "@visx/text" "3.0.0" + classnames "^2.3.1" + prop-types "^15.6.2" + "@visx/responsive@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/responsive/-/responsive-1.3.0.tgz#4df0c9d9e2f72ac36e4e1c895b4624626209cca3" @@ -5178,6 +5396,16 @@ prop-types "^15.6.1" resize-observer-polyfill "1.5.1" +"@visx/responsive@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/responsive/-/responsive-3.0.0.tgz#e183c54ce04cffe756378872d30ac88c66a137ac" + integrity sha512-immnxQwOWlrxbnlCIqJWuDpPfrM6tglgMTN1WsyXyGluLMJqhuuxqxllfXaRPkQFS4fcvs66KCEELdazh96U2w== + dependencies: + "@types/lodash" "^4.14.172" + "@types/react" "*" + lodash "^4.17.21" + prop-types "^15.6.1" + "@visx/scale@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-1.3.0.tgz#6b48537d4ee29a4a565de5a85d39b7ca1f89b572" @@ -5202,6 +5430,18 @@ d3-scale "^3.2.3" d3-time "^1.1.0" +"@visx/scale@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-3.0.0.tgz#727123f0c930d3346a4473e926831c45997e0312" + integrity sha512-WSf+wrxZEvu5TPGfGTafzzX1MbogbIxfD9ZKM9p7xfw65v23G0dNMy4bqVBUbOJigONoQkIZyqQ+gz5AJ/ioIg== + dependencies: + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-time" "^2.0.0" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-time "^2.1.1" + "@visx/shape@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/shape/-/shape-1.3.0.tgz#fab11b683a4d3d648ce76da5372a8bd75031eae2" @@ -5221,6 +5461,24 @@ lodash "^4.17.15" prop-types "^15.5.10" +"@visx/shape@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/shape/-/shape-3.0.0.tgz#a1d4bd0e12cc94c164252f175997932a09c24652" + integrity sha512-t6lpP9bIA1vwChDwiOUWl92ro29XF/M8IVNWRA0pm4LGxGGTACvxG3Agfcdi3JprahUVqPpnRCwuR36PDanq3Q== + dependencies: + "@types/d3-path" "^1.0.8" + "@types/d3-shape" "^1.3.1" + "@types/lodash" "^4.14.172" + "@types/react" "*" + "@visx/curve" "3.0.0" + "@visx/group" "3.0.0" + "@visx/scale" "3.0.0" + classnames "^2.3.1" + d3-path "^1.0.5" + d3-shape "^1.2.0" + lodash "^4.17.21" + prop-types "^15.5.10" + "@visx/shape@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@visx/shape/-/shape-1.4.0.tgz#a8ab713c7df775db357341618c7c0fdbb813f272" @@ -5253,6 +5511,18 @@ prop-types "^15.7.2" reduce-css-calc "^1.3.0" +"@visx/text@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/text/-/text-3.0.0.tgz#9099c3605027b9ab4c54bde97518a648136c3629" + integrity sha512-LW6v5T/gpd9RGw83/ScXncYc6IlcfzXTpaN8WbbxLRI65gdvSqrykwAMR0cbpQmzoVFuZXljqOf0QslHGnBg1w== + dependencies: + "@types/lodash" "^4.14.172" + "@types/react" "*" + classnames "^2.3.1" + lodash "^4.17.21" + prop-types "^15.7.2" + reduce-css-calc "^1.3.0" + "@visx/tooltip@1.3.0", "@visx/tooltip@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/tooltip/-/tooltip-1.3.0.tgz#32b3969eed51cc83e7f16d61d929258ef52b50ca" @@ -5265,6 +5535,17 @@ prop-types "^15.5.10" react-use-measure "2.0.1" +"@visx/tooltip@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/tooltip/-/tooltip-3.0.0.tgz#9076f0d43e74c7f307b0320b0ec2460cc7fdbedf" + integrity sha512-a+ZzlE/vVxQgW83k/Ypj721K09IKG4JRHVb7YDxiQnAawkJe9rkTxGoAIXD6PrqvERa+rSISgUWHAxuee5MnhA== + dependencies: + "@types/react" "*" + "@visx/bounds" "3.0.0" + classnames "^2.3.1" + prop-types "^15.5.10" + react-use-measure "^2.0.4" + "@visx/visx@^1.1.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/visx/-/visx-1.3.0.tgz#417ec5309ffed3e1d392375ed955109a3efaf6d0" @@ -5311,6 +5592,44 @@ d3-voronoi "^1.1.2" prop-types "^15.6.1" +"@visx/voronoi@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@visx/voronoi/-/voronoi-3.0.0.tgz#24a3513252dde1d6e85b88144e7a65b6d5936275" + integrity sha512-ySX7+Ic+irfgZQMij/0RJnryETonuKDWA3Upw3V6YtIiodPOEQ5w8FW8TvEbhaBlAUfSwQtHJ5ECvv3ZDrJa2A== + dependencies: + "@types/d3-voronoi" "^1.1.9" + "@types/react" "*" + classnames "^2.3.1" + d3-voronoi "^1.1.2" + prop-types "^15.6.1" + +"@visx/xychart@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@visx/xychart/-/xychart-3.1.0.tgz#94c4646a975301b8ab24d9ee1b1651ce3e0b6784" + integrity sha512-s7NOgYGL83KICTwkSoo0g88ORZemCxp3MgIr3z4z6zm6ypKhArRRqXqPrmkoRfnx53kitzmpisH71lncymz0XA== + dependencies: + "@types/lodash" "^4.14.172" + "@types/react" "*" + "@visx/annotation" "3.0.1" + "@visx/axis" "3.1.0" + "@visx/event" "3.0.1" + "@visx/glyph" "3.0.0" + "@visx/grid" "3.0.1" + "@visx/react-spring" "3.1.0" + "@visx/responsive" "3.0.0" + "@visx/scale" "3.0.0" + "@visx/shape" "3.0.0" + "@visx/text" "3.0.0" + "@visx/tooltip" "3.0.0" + "@visx/voronoi" "3.0.0" + classnames "^2.3.1" + d3-array "^2.6.0" + d3-interpolate-path "2.2.1" + d3-shape "^2.0.0" + lodash "^4.17.21" + mitt "^2.1.0" + prop-types "^15.6.2" + "@visx/zoom@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@visx/zoom/-/zoom-1.3.0.tgz#3e2e28d7020ffe7f4f42cda236352c2c152007b3" @@ -6778,6 +7097,11 @@ classnames@^2.2.5, classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== +classnames@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-css@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" @@ -7500,6 +7824,13 @@ d3-array@1, d3-array@^1.2.1: resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== +d3-array@2, d3-array@^2.6.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3: version "3.1.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c" @@ -7676,7 +8007,12 @@ d3-hierarchy@^1.1.4, d3-hierarchy@^1.1.9: resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: +d3-interpolate-path@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/d3-interpolate-path/-/d3-interpolate-path-2.2.1.tgz#fd8ff20a90aff3f380bcd1c15305e7b531e55d07" + integrity sha512-6qLLh/KJVzls0XtMsMpcxhqMhgVEN7VIbR/6YGZe2qlS8KDgyyVB20XcmGnDyB051HcefQXM/Tppa9vcANEA4Q== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3, d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -7702,6 +8038,11 @@ d3-path@1, d3-path@^1.0.5: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + "d3-path@1 - 3", d3-path@3: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" @@ -7740,7 +8081,7 @@ d3-scale-chromatic@3: d3-color "1 - 3" d3-interpolate "1 - 3" -d3-scale@4: +d3-scale@4, d3-scale@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== @@ -7781,6 +8122,13 @@ d3-shape@^1.0.6, d3-shape@^1.2.0: dependencies: d3-path "1" +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + "d3-time-format@2 - 3": version "3.0.0" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" @@ -7819,6 +8167,13 @@ d3-time@1, d3-time@^1.1.0: dependencies: d3-array "2 - 3" +d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + d3-timer@1: version "1.0.10" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" @@ -7920,6 +8275,11 @@ debounce@^1.2.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -10536,6 +10896,11 @@ internal-slot@^1.0.3: resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -12194,6 +12559,11 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" +mitt@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" + integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -14003,6 +14373,18 @@ react-sizeme@^3.0.1: shallowequal "^1.1.0" throttle-debounce "^3.0.1" +react-spring@^9.7.1: + version "9.7.1" + resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.7.1.tgz#8acfed700823490a4d9d4cf131c5fea12d1aaa93" + integrity sha512-o2+r2DNQDVEuefiz33ZF76DPd/gLq3kbdObJmllGF2IUfv2W6x+ZP0gR97QYCSR4QLbmOl1mPKUBbI+FJdys2Q== + dependencies: + "@react-spring/core" "~9.7.1" + "@react-spring/konva" "~9.7.1" + "@react-spring/native" "~9.7.1" + "@react-spring/three" "~9.7.1" + "@react-spring/web" "~9.7.1" + "@react-spring/zdog" "~9.7.1" + react-syntax-highlighter@^13.5.3: version "13.5.3" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6" @@ -14055,6 +14437,13 @@ react-use-measure@2.0.1: dependencies: debounce "^1.2.0" +react-use-measure@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.1.tgz#5824537f4ee01c9469c45d5f7a8446177c6cc4ba" + integrity sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig== + dependencies: + debounce "^1.2.1" + react@^16.13.1: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" From 543514f3b348974d17ab5aaa25e87a5254fd6526 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 20 Mar 2023 05:30:00 -0400 Subject: [PATCH 04/28] drafting visx stories --- .../src/stories/plots/ScatterVisx.stories.tsx | 7 +- .../src/stories/plots/VolcanoPlot.stories.tsx | 2 +- .../src/stories/plots/VolcanoVisx.stories.tsx | 298 ++++++++++++++++++ 3 files changed, 303 insertions(+), 4 deletions(-) create mode 100755 packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx diff --git a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx index 4ab01881e3..96d17f811c 100644 --- a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx @@ -1,4 +1,4 @@ -import { XYChart, Tooltip, Axis, Grid, LineSeries } from '@visx/xychart'; +import { XYChart, Tooltip, Axis, Grid, GlyphSeries } from '@visx/xychart'; import ScatterPlot from '../../../lib/plots/ScatterPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; import { DivIcon } from 'leaflet'; @@ -31,9 +31,10 @@ const Template: Story = (args) => { return ( + - - + + = (args) => { // Determined by the data and symmetric around 0 by default? const dependentAxisRange = { min: 0, - max: 0.03, + max: 8, }; // By default max determined by data and min at 0 return ( diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx new file mode 100755 index 0000000000..7783959b5e --- /dev/null +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -0,0 +1,298 @@ +import React, { useState } from 'react'; +import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; +import { XYChart, Tooltip, Axis, Grid, GlyphSeries } from '@visx/xychart'; +// import { min, max, lte, gte } from 'lodash'; +// import { dataSetProcess, xAxisRange, yAxisRange } from './ScatterPlot.storyData'; +import { Story, Meta } from '@storybook/react/types-6-0'; +// test to use RadioButtonGroup directly instead of ScatterPlotControls +import { NumberRange } from '../../types/general'; + +import { ScatterPlotData } from '../../types/plots'; +import { AxisBottom } from '@visx/visx'; +import { scaleLinear } from '@visx/scale'; + +export default { + title: 'Plots/VolcanoPlotVisx', + component: VolcanoPlot, +} as Meta; + +interface VEuPathDBVolcanoPlotData { + volcanoplot: { + data: Array<{ + foldChange: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointId: string[]; + overlayValue: string; + }>; + }; +} + +// Let's make some fake data! +const dataSetVolcano: VEuPathDBVolcanoPlotData = { + volcanoplot: { + data: [ + { + foldChange: ['2', '3'], + pValue: ['0.001', '0.0001'], + adjustedPValue: ['0.01', '0.001'], + pointId: ['a', 'b'], + overlayValue: 'positive', + }, + { + foldChange: ['-1', '0', '1', '0.5', '-0.5', '4', '-5'], + pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], + adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], + pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], + overlayValue: 'none', + }, + { + foldChange: ['-2', '-3', '-4'], + pValue: ['0.001', '0.0001', '0.002'], + adjustedPValue: ['0.01', '0.001', '0.02'], + pointId: ['j', 'k', 'l'], + overlayValue: 'negative', + }, + ], + }, +}; + +// These can go into addons eventually. I'd expect other vizs that involve significance to use these as well +// These are NOT the final proposed colors +const highMedLowColors = ['#dd1111', '#bbbbbb', '#1111dd']; + +const plotTitle = 'Volcano erupt!'; + +interface TemplateProps { + data: VEuPathDBVolcanoPlotData; + markerBodyOpacity: number; + // foldChangeHighGate: number; + // foldChangeLowGate: number; // we can't have gates unless we mimic the backend updating the data format when we change gates + adjustedPValueGate: number; +} + +const Template: Story = (args) => { + console.log(args.data); + const { dataSetProcess: datasetProcess } = processVolcanoData( + args.data, + highMedLowColors + ); + console.log(datasetProcess); + + // Better to break into a high and low prop? Would be more clear + const foldChangeGates = [-1.5, 1.5]; + + const comparisonLabels = ['group a', 'group b']; + + /** + * Volcano knows + * x and y label (always fold change and pvalue) + */ + + /** + * datasetProcess has three or fewer series + * has columms for foldChange, adjustedPValue, pointId, significanceDirection (naming help!!) + * + */ + + const independentAxisRange = { + min: -5, + max: 5, + }; + // Determined by the data and symmetric around 0 by default? + const dependentAxisRange = { + min: 0, + max: 0.03, + }; // By default max determined by data and min at 0 + + const accessors = { + xAccessor: (d: any) => Number(d.x), + yAccessor: (d: any) => Number(d.y), + }; + + const bottomScale = scaleLinear({ + domain: [-4, 4], + range: [-1, 8], + nice: true, + }); + + // ANN RETURN TO THE AXIS ISSUE + + return ( + + + + + + {datasetProcess.series.map((series, i) => { + console.log(series); + return ( + + ); + })} + {/* + */} + ( +
+
+ {tooltipData!.nearestDatum!.key} +
+ {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} + {', '} + {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} +
+ )} + /> +
+ ); +}; + +export const Default2 = Template.bind({}); +Default2.args = { + data: dataSetVolcano, + markerBodyOpacity: 0.8, +}; + +// this process input data function is similar to scatter's but not the same. +// would probably be worth revisiting what is in common and factoring accordingly +function processVolcanoData( + dataSet: VEuPathDBVolcanoPlotData, + colors: string[] +): { + dataSetProcess: ScatterPlotData; + xAxisRange: NumberRange; + yAxisRange: NumberRange; +} { + // set variables for x- and yaxis ranges + let xMin: number = 0; + let xMax: number = 0; + let yMin: number = 0; + let yMax: number = 0; + + let processedDataSeries: any = []; + dataSet.volcanoplot.data.forEach(function (el: any, index: number) { + // initialize variables: setting with union type for future, but this causes typescript issue in the current version + let xSeries = []; + let ySeries: never[] = []; + + // set rgbValue here per dataset with a default color + // Add check for len(colors) = number of series + let rgbValue: number[] = hexToRgb(colors[index]); + + let scatterPointColor: string = ''; + + // series is for scatter plot + if (el) { + // check the number of x = number of y + if (el.foldChange.length !== el.adjustedPValue.length) { + console.log( + 'x length=', + el.foldChange.length, + ' y length=', + el.adjustedPValue.length + ); + alert('The number of X data is not equal to the number of Y data'); + throw new Error( + 'The number of X data is not equal to the number of Y data' + ); + } + + /* + * set variables for x-/y-axes ranges including x,y data points: considering Date data for X as well + * This is for finding global min/max values among data arrays for better display of the plot(s) + */ + + // ANN needs log 2 adjusting! + xSeries = el.foldChange; + ySeries = el.adjustedPValue; + + xMin = + xMin < Math.min(...(xSeries as number[])) + ? xMin + : Math.min(...(xSeries as number[])); + xMax = + xMax > Math.max(...(xSeries as number[])) + ? xMax + : Math.max(...(xSeries as number[])); + + // check if this Y array consists of numbers & add type assertion + if (index == 0) { + yMin = Math.min(...ySeries); + yMax = Math.max(...ySeries); + } else { + yMin = yMin < Math.min(...ySeries) ? yMin : Math.min(...ySeries); + yMax = yMax > Math.max(...ySeries) ? yMax : Math.max(...ySeries); + } + + // use global opacity for coloring + scatterPointColor = + 'rgba(' + + rgbValue[0] + + ',' + + rgbValue[1] + + ',' + + rgbValue[2] + + ',' + + 0.8 + + ')'; + + // add scatter data considering input options + const points = xSeries.map((x: any, i: any) => { + return { + x: Number(x), + y: Number(ySeries[i]), + }; + }); + console.log(points); + processedDataSeries.push(points); + } + + // make some margin for y-axis range (5% of range for now) + if (typeof yMin == 'number' && typeof yMax == 'number') { + yMin = yMin - (yMax - yMin) * 0.05; + yMax = yMax + (yMax - yMin) * 0.05; + } + }); + + return { + dataSetProcess: { series: processedDataSeries }, + xAxisRange: { min: xMin, max: xMax }, + yAxisRange: { min: yMin, max: yMax }, + }; +} + +// change HTML hex code to rgb array +const hexToRgb = (hex?: string): [number, number, number] => { + if (!hex) return [0, 0, 0]; + const fullHex = hex.replace( + /^#?([a-f\d])([a-f\d])([a-f\d])$/i, + (m: string, r: string, g: string, b: string): string => + '#' + r + r + g + g + b + b + ); + const hexDigits = fullHex.substring(1); + const matches = hexDigits.match(/.{2}/g); + if (matches == null) return [0, 0, 0]; + return matches.map((x: string) => parseInt(x, 16)) as [ + number, + number, + number + ]; +}; From 46da6d927304c81e629e57c04d3de894416b4fe2 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 20 Mar 2023 11:13:17 -0400 Subject: [PATCH 05/28] fixed volcano story axis --- packages/libs/components/package.json | 1 + .../src/stories/plots/ScatterVisx.stories.tsx | 29 +++++++++---- .../src/stories/plots/VolcanoVisx.stories.tsx | 42 ++++++++----------- packages/libs/components/yarn.lock | 2 +- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/libs/components/package.json b/packages/libs/components/package.json index 096d2aaa3e..dba67ffa68 100755 --- a/packages/libs/components/package.json +++ b/packages/libs/components/package.json @@ -10,6 +10,7 @@ "dependencies": { "@typescript-eslint/eslint-plugin": "^5.46.0", "@typescript-eslint/parser": "^5.46.0", + "@visx/axis": "^3.1.0", "@visx/gradient": "^1.0.0", "@visx/group": "^1.0.0", "@visx/hierarchy": "^1.0.0", diff --git a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx index 96d17f811c..3c3f54a760 100644 --- a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx @@ -2,6 +2,8 @@ import { XYChart, Tooltip, Axis, Grid, GlyphSeries } from '@visx/xychart'; import ScatterPlot from '../../../lib/plots/ScatterPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; import { DivIcon } from 'leaflet'; +import { AxisBottom } from '@visx/axis'; +import { scaleLinear } from '@visx/scale'; export default { title: 'Plots/VisxScatter', @@ -9,15 +11,15 @@ export default { } as Meta; const data1 = [ - { x: '2020-01-01', y: 50 }, - { x: '2020-01-02', y: 10 }, - { x: '2020-01-03', y: 20 }, + { x: 10, y: 50 }, + { x: 20, y: 10 }, + { x: 5, y: 20 }, ]; const data2 = [ - { x: '2020-01-01', y: 30 }, - { x: '2020-01-02', y: 40 }, - { x: '2020-01-03', y: 80 }, + { x: 3, y: 30 }, + { x: 40, y: 40 }, + { x: 1, y: 80 }, ]; interface TemplateProps {} @@ -28,9 +30,20 @@ const Template: Story = (args) => { yAccessor: (d: any) => d.y, }; + const bottomScale = scaleLinear({ + domain: [-4, 4], + range: [-1, 8], + nice: true, + }); + return ( - - + + + {/* */} diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index 7783959b5e..326acfe9ae 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -10,6 +10,7 @@ import { NumberRange } from '../../types/general'; import { ScatterPlotData } from '../../types/plots'; import { AxisBottom } from '@visx/visx'; import { scaleLinear } from '@visx/scale'; +import ControlsHeader from '../../../lib/components/typography/ControlsHeader'; export default { title: 'Plots/VolcanoPlotVisx', @@ -40,14 +41,14 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { overlayValue: 'positive', }, { - foldChange: ['-1', '0', '1', '0.5', '-0.5', '4', '-5'], + foldChange: ['0.5', '0', '1', '0.5', '0.1', '4', '0.2'], pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], overlayValue: 'none', }, { - foldChange: ['-2', '-3', '-4'], + foldChange: ['0.01', '0.02', '0.03'], pValue: ['0.001', '0.0001', '0.002'], adjustedPValue: ['0.01', '0.001', '0.02'], pointId: ['j', 'k', 'l'], @@ -72,12 +73,10 @@ interface TemplateProps { } const Template: Story = (args) => { - console.log(args.data); const { dataSetProcess: datasetProcess } = processVolcanoData( args.data, highMedLowColors ); - console.log(datasetProcess); // Better to break into a high and low prop? Would be more clear const foldChangeGates = [-1.5, 1.5]; @@ -106,8 +105,12 @@ const Template: Story = (args) => { }; // By default max determined by data and min at 0 const accessors = { - xAccessor: (d: any) => Number(d.x), - yAccessor: (d: any) => Number(d.y), + xAccessor: (d: any) => { + return Math.log2(Number(d.x)); + }, + yAccessor: (d: any) => { + return -Math.log10(Number(d.y)); + }, }; const bottomScale = scaleLinear({ @@ -116,36 +119,26 @@ const Template: Story = (args) => { nice: true, }); - // ANN RETURN TO THE AXIS ISSUE - return ( - + - - - {datasetProcess.series.map((series, i) => { - console.log(series); + + {datasetProcess.series.map((series, index) => { return ( ); })} - {/* - */} - = (args) => { {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} )} - /> + /> */} ); }; @@ -261,7 +254,6 @@ function processVolcanoData( y: Number(ySeries[i]), }; }); - console.log(points); processedDataSeries.push(points); } diff --git a/packages/libs/components/yarn.lock b/packages/libs/components/yarn.lock index 7d26c978bf..894160ad0d 100644 --- a/packages/libs/components/yarn.lock +++ b/packages/libs/components/yarn.lock @@ -5089,7 +5089,7 @@ classnames "^2.2.5" prop-types "^15.6.0" -"@visx/axis@3.1.0": +"@visx/axis@3.1.0", "@visx/axis@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-3.1.0.tgz#775b221e5abfdec25304c607eeae8cda1a8bade6" integrity sha512-JDj/1VYx0JO0pHFtwoFtYcnqdoZFh/dpHImEl169S5nTslSFlIoNTXA/ekpBP6ELkEZ59gmF1X5k29x6MFBwCA== From 40974db9733d41664bb3545aa68c35902608bc00 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 21 Mar 2023 14:26:35 -0400 Subject: [PATCH 06/28] added threshold lines to plotly volcano --- .../libs/components/src/plots/VolcanoPlot.tsx | 48 ++++++++- .../src/stories/plots/VolcanoPlot.stories.tsx | 26 ++--- .../src/stories/plots/VolcanoVisx.stories.tsx | 98 ++++++++++++------- 3 files changed, 123 insertions(+), 49 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 98743b72c9..b99e976e97 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -65,6 +65,8 @@ const VolcanoPlot = makePlotlyPlotComponent( independentAxisLogScale = false, dependentAxisLogScale = false, markerBodyOpacity, + adjustedPValueGate, + foldChangeGates, ...restProps } = props; @@ -168,7 +170,51 @@ const VolcanoPlot = makePlotlyPlotComponent( ), }, // add truncatedAxisHighlighting for layout.shapes - shapes: truncatedAxisHighlighting, + // shapes: truncatedAxisHighlighting, + shapes: [ + { + type: 'line', + x0: 0, + y0: adjustedPValueGate, + x1: 1, + y1: adjustedPValueGate, + line: { + color: 'rgb(0.3.0.35.0.35)', + width: 1, + dash: 'dash', + }, + xref: 'paper', + layer: 'below', + }, + { + type: 'line', + x0: foldChangeGates![0], + y0: 0, + x1: foldChangeGates![0], + y1: 1, + line: { + color: 'rgb(0.3.0.35.0.35)', + width: 1, + dash: 'dash', + }, + yref: 'paper', + layer: 'below', + }, + { + type: 'line', + x0: foldChangeGates![1], + y0: 0, + x1: foldChangeGates![1], + y1: 1, + line: { + color: 'rgb(0.3.0.35.0.35)', + width: 1, + dash: 'dash', + }, + yref: 'paper', + layer: 'below', + }, + ], }; // change data here for marker opacity diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index e7517add46..d0ecd3f960 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -30,23 +30,23 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { data: [ { - foldChange: ['2', '3'], + foldChange: ['6', '3'], pValue: ['0.001', '0.0001'], adjustedPValue: ['0.01', '0.001'], pointId: ['a', 'b'], overlayValue: 'positive', }, { - foldChange: ['-1', '0', '1', '0.5', '-0.5', '4', '-5'], - pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], - adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], + foldChange: ['0.2', '0.8', '1', '0.5', '0.5', '4', '1.2'], + pValue: ['0.01', '0.0001', '0.2', '0.1', '0.3', '0.1', '0.4'], + adjustedPValue: ['0.1', '0.001', '2', '1', '7', '1', '4'], pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], overlayValue: 'none', }, { - foldChange: ['-2', '-3', '-4'], - pValue: ['0.001', '0.0001', '0.002'], - adjustedPValue: ['0.01', '0.001', '0.02'], + foldChange: ['0.02', '0.03', '0.06'], + pValue: ['0.001', '0.0001', '0.0014'], + adjustedPValue: ['0.01', '0.001', '0.014'], pointId: ['j', 'k', 'l'], overlayValue: 'negative', }, @@ -77,7 +77,8 @@ const Template: Story = (args) => { console.log(datasetProcess); // Better to break into a high and low prop? Would be more clear - const foldChangeGates = [-1.5, 1.5]; + const foldChangeGates = [-1, 1]; + const adjustedPValueGate = 1.6; const comparisonLabels = ['group a', 'group b']; @@ -98,7 +99,7 @@ const Template: Story = (args) => { }; // Determined by the data and symmetric around 0 by default? const dependentAxisRange = { - min: 0, + min: -1, max: 8, }; // By default max determined by data and min at 0 @@ -108,7 +109,7 @@ const Template: Story = (args) => { data={datasetProcess} // call it PlotlyScatterData??? foldChangeGates={foldChangeGates} comparisonLabels={comparisonLabels} - adjustedPValueGate={args.adjustedPValueGate} + adjustedPValueGate={adjustedPValueGate} markerBodyOpacity={args.markerBodyOpacity} plotTitle={plotTitle} independentAxisRange={independentAxisRange} @@ -173,9 +174,8 @@ function processVolcanoData( * This is for finding global min/max values among data arrays for better display of the plot(s) */ - // ANN needs log 2 adjusting! - xSeries = el.foldChange; - ySeries = el.adjustedPValue; + xSeries = el.foldChange.map((fc: number) => Math.log2(fc)); + ySeries = el.adjustedPValue.map((apv: number) => -Math.log10(apv)); xMin = xMin < Math.min(...(xSeries as number[])) diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index 326acfe9ae..02a488c9b4 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -1,6 +1,13 @@ import React, { useState } from 'react'; import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; -import { XYChart, Tooltip, Axis, Grid, GlyphSeries } from '@visx/xychart'; +import { + XYChart, + Tooltip, + Axis, + Grid, + GlyphSeries, + LineSeries, +} from '@visx/xychart'; // import { min, max, lte, gte } from 'lodash'; // import { dataSetProcess, xAxisRange, yAxisRange } from './ScatterPlot.storyData'; import { Story, Meta } from '@storybook/react/types-6-0'; @@ -11,6 +18,8 @@ import { ScatterPlotData } from '../../types/plots'; import { AxisBottom } from '@visx/visx'; import { scaleLinear } from '@visx/scale'; import ControlsHeader from '../../../lib/components/typography/ControlsHeader'; +import { Line } from '@visx/shape'; +import { Group } from '@visx/group'; export default { title: 'Plots/VolcanoPlotVisx', @@ -25,6 +34,7 @@ interface VEuPathDBVolcanoPlotData { adjustedPValue: string[]; pointId: string[]; overlayValue: string; + id: string; }>; }; } @@ -39,6 +49,7 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001'], pointId: ['a', 'b'], overlayValue: 'positive', + id: 'id1', }, { foldChange: ['0.5', '0', '1', '0.5', '0.1', '4', '0.2'], @@ -46,6 +57,7 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], overlayValue: 'none', + id: 'id2', }, { foldChange: ['0.01', '0.02', '0.03'], @@ -53,6 +65,7 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001', '0.02'], pointId: ['j', 'k', 'l'], overlayValue: 'negative', + id: 'id3', }, ], }, @@ -106,10 +119,10 @@ const Template: Story = (args) => { const accessors = { xAccessor: (d: any) => { - return Math.log2(Number(d.x)); + return d.x; }, yAccessor: (d: any) => { - return -Math.log10(Number(d.y)); + return d.y; }, }; @@ -120,41 +133,57 @@ const Template: Story = (args) => { }); return ( - - - - - {datasetProcess.series.map((series, index) => { - return ( - - ); - })} - {/* + + + + + {datasetProcess.series.map((series, index) => { + console.log(String(index) + 'a'); + return ( + + ); + })} + {/* */} + {/* ( -
-
- {tooltipData!.nearestDatum!.key} + renderTooltip={({ tooltipData, colorScale }) => { + console.log(tooltipData!.nearestDatum!); + return ( +
+
+ {tooltipData!.nearestDatum!.key} +
+ {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} + {', '} + {accessors.yAccessor(tooltipData!.nearestDatum!.datum)}
- {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} - {', '} - {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} -
- )} + ) + } + } /> */} - + + + ); }; @@ -213,9 +242,8 @@ function processVolcanoData( * This is for finding global min/max values among data arrays for better display of the plot(s) */ - // ANN needs log 2 adjusting! - xSeries = el.foldChange; - ySeries = el.adjustedPValue; + xSeries = el.foldChange.map((fc: number) => Math.log2(fc)); + ySeries = el.adjustedPValue.map((apv: number) => -Math.log10(apv)); xMin = xMin < Math.min(...(xSeries as number[])) From 4345f4e92c15f697c3785142b88df4065fc6822e Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 22 Mar 2023 11:50:41 -0400 Subject: [PATCH 07/28] add threshold lines for volcano --- .../src/stories/plots/VolcanoVisx.stories.tsx | 100 +++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index 02a488c9b4..e5e04abf5c 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -133,57 +133,83 @@ const Template: Story = (args) => { }); return ( - - - - - - {datasetProcess.series.map((series, index) => { - console.log(String(index) + 'a'); - return ( - - ); - })} - {/* */} - {/* + + + + {datasetProcess.series.map((series, index) => { + console.log(String(index) + 'a'); + return ( + + ); + })} + + + + { - console.log(tooltipData!.nearestDatum!); + // console.log(tooltipData!.nearestDatum!); return (
-
+
{tooltipData!.nearestDatum!.key}
{accessors.xAccessor(tooltipData!.nearestDatum!.datum)} {', '} {accessors.yAccessor(tooltipData!.nearestDatum!.datum)}
- ) - } - } - /> */} - - - + ); }; From 439c0e52a53da78264c040f1d96fbcbd76b70835 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 27 Mar 2023 17:01:26 -0400 Subject: [PATCH 08/28] move volcano visx to its own component --- .../libs/components/src/plots/VolcanoPlot.tsx | 311 ++++++------------ .../src/stories/plots/VolcanoVisx.stories.tsx | 88 +---- .../components/src/types/plots/volcanoplot.ts | 28 +- 3 files changed, 104 insertions(+), 323 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index dba067e739..eb12449440 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -7,12 +7,21 @@ import { AxisTruncationAddon, independentAxisLogScaleAddon, DependentAxisLogScaleAddon, - ScatterPlotData, } from '../types/plots'; -import { VolcanoPlotData } from '../types/plots/volcanoplot'; +import { + VolcanoPlotData, + VolcanoPlotDataSeries, +} from '../types/plots/volcanoplot'; // add Shape for truncation -import { Layout, Shape } from 'plotly.js'; import { NumberRange } from '../types/general'; +import { + XYChart, + Tooltip, + Axis, + Grid, + GlyphSeries, + LineSeries, +} from '@visx/xychart'; // import truncation util functions import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-truncations'; @@ -21,7 +30,7 @@ import { tickSettings } from '../utils/tick-settings'; import * as ColorMath from 'color-math'; export interface VolcanoPlotProps - extends PlotProps, + extends PlotProps, // truncation OrientationAddon, independentAxisLogScaleAddon, @@ -40,231 +49,99 @@ export interface VolcanoPlotProps markerBodyOpacity?: number; } -const EmptyVolcanoPlotData: ScatterPlotData = { - series: [], +const EmptyVolcanoPlotData: VolcanoPlotData = { + data: [], }; /** * This component handles several plots such as marker, line, confidence interval, * density, and combinations of plots like marker + line + confidence interval */ -const VolcanoPlot = makePlotlyPlotComponent( - 'VolcanoPlot', - (props: VolcanoPlotProps) => { - const { - data = EmptyVolcanoPlotData, - independentAxisRange, - dependentAxisRange, - // independentAxisLabel, - // dependentAxisLabel, - // independentValueType, - // dependentValueType, - // truncation - orientation = OrientationDefault, - axisTruncationConfig, - independentAxisLogScale = false, - dependentAxisLogScale = false, - markerBodyOpacity, - adjustedPValueGate, - foldChangeGates, - ...restProps - } = props; +function VolcanoPlot(props: VolcanoPlotProps) { + const { + data = EmptyVolcanoPlotData, + independentAxisRange, + dependentAxisRange, + // independentAxisLabel, + // dependentAxisLabel, + // independentValueType, + // dependentValueType, + // truncation + orientation = OrientationDefault, + axisTruncationConfig, + independentAxisLogScale = false, + dependentAxisLogScale = false, + markerBodyOpacity, + adjustedPValueGate, + foldChangeGates, + ...restProps + } = props; - // truncation axis range - const standardIndependentAxisRange = independentAxisRange; - const extendedIndependentAxisRange = extendAxisRangeForTruncations( - standardIndependentAxisRange, - axisTruncationConfig?.independentAxis, - 'number', - true, // addPadding - independentAxisLogScale - ); + // add truncation - // truncation - const standardDependentAxisRange = dependentAxisRange; - const extendedDependentAxisRange = extendAxisRangeForTruncations( - standardDependentAxisRange, - axisTruncationConfig?.dependentAxis, - 'number', - true, // addPadding - dependentAxisLogScale - ); + // process the data. unzip and zip + function formatData(series: VolcanoPlotDataSeries) { + // data: [ + // { + // foldChange: ['2', '3'], + // pValue: ['0.001', '0.0001'], + // adjustedPValue: ['0.01', '0.001'], + // pointId: ['a', 'b'], + // overlayValue: 'positive', + // id: 'id1', + // }, - // make rectangular layout shapes for truncated axis/missing data - const truncatedAxisHighlighting: Partial[] | undefined = - useMemo(() => { - if (data.series.length > 0) { - const filteredTruncationLayoutShapes = truncationLayoutShapes( - orientation, - standardIndependentAxisRange, // send undefined for independentAxisRange - standardDependentAxisRange, - extendedIndependentAxisRange, // send undefined for independentAxisRange - extendedDependentAxisRange, - axisTruncationConfig - ); + // assume at least foldChange is there (should be type error if not!) + let seriesPoints = []; + } - return filteredTruncationLayoutShapes; - } else { - return []; - } - }, [ - standardDependentAxisRange, - extendedDependentAxisRange, - orientation, - data, - axisTruncationConfig, - ]); + // this should be -log2 etc. + const dataAccessors = { + xAccessor: (d: any) => { + return d.foldChange; + }, + yAccessor: (d: any) => { + console.log('accessing ys'); + return d.adjustedPValue; + }, + }; - const layout: Partial = { - hovermode: 'closest', - xaxis: { - title: 'log2 Fold Change', - // truncation - range: data.series.length - ? [ - extendedIndependentAxisRange?.min, - extendedIndependentAxisRange?.max, - ].map((val) => - independentAxisLogScale && val != null - ? Math.log10(val as number) - : val - ) - : undefined, - zeroline: false, // disable yaxis line - // make plot border - mirror: true, - // date or number type (from variable.type): no log scale for date - type: undefined, - tickfont: data.series.length ? {} : { color: 'transparent' }, - ...tickSettings( - independentAxisLogScale, - extendedIndependentAxisRange, - 'number' - ), - }, - yaxis: { - title: '-log10 P Value', - // with the truncated axis, negative values need to be checked for log scale - range: data.series.length - ? [ - extendedDependentAxisRange?.min, - extendedDependentAxisRange?.max, - ].map((val) => - dependentAxisLogScale && val != null - ? Math.log10(val as number) - : val - ) - : undefined, - // range: undefined, - zeroline: false, // disable xaxis line - // make plot border - mirror: true, - // date or number type (from variable.type): no log scale for date - type: undefined, - tickfont: data.series.length ? {} : { color: 'transparent' }, - ...tickSettings( - dependentAxisLogScale, - extendedDependentAxisRange, - 'number' - ), - }, - // add truncatedAxisHighlighting for layout.shapes - // shapes: truncatedAxisHighlighting, - shapes: [ - { - type: 'line', - x0: 0, - y0: adjustedPValueGate, - x1: 1, - y1: adjustedPValueGate, - line: { - color: 'rgb(0.3.0.35.0.35)', - width: 1, - dash: 'dash', - }, - xref: 'paper', - layer: 'below', - }, - { - type: 'line', - x0: foldChangeGates![0], - y0: 0, - x1: foldChangeGates![0], - y1: 1, - line: { - color: 'rgb(0.3.0.35.0.35)', - width: 1, - dash: 'dash', - }, - yref: 'paper', - layer: 'below', - }, - { - type: 'line', - x0: foldChangeGates![1], - y0: 0, - x1: foldChangeGates![1], - y1: 1, - line: { - color: 'rgb(0.3.0.35.0.35)', - width: 1, - dash: 'dash', - }, - yref: 'paper', - layer: 'below', - }, - ], - }; + const thresholdLineAccessors = { + xAccessor: (d: any) => { + return d.x; + }, + yAccessor: (d: any) => { + return d.y; + }, + }; - // change data here for marker opacity - const finalData = useMemo(() => { - return data.series.map((d: any) => ({ - ...d, - marker: { - ...d.marker, - color: - d.marker == null - ? undefined - : markerBodyOpacity != null - ? Array.isArray(d.marker.color) - ? d.marker.color.map((color: string) => - ColorMath.evaluate( - color + - ' @a ' + - (markerBodyOpacity * 100).toString() + - '%' - ).result.css() - ) - : ColorMath.evaluate( - d.marker.color + - ' @a ' + - (markerBodyOpacity * 100).toString() + - '%' - ).result.css() - : d.marker.color, - // need to set marker.line for a transparent case (opacity != 1) - line: - d.marker == null - ? undefined - : { - ...d.marker.line, - width: - markerBodyOpacity != null - ? markerBodyOpacity === 0 - ? 1 - : 0 - : 1, - }, - }, - })); - }, [data, markerBodyOpacity]); + console.log('data'); + console.log(data); + console.log(data.data[0] as unknown as any[]); - return { - data: finalData, - layout, - ...restProps, - }; - } -); + // Issue is that the data needs to be like [{x1 y2}, {x2 y2}, ...] + + return ( + + + + + {/* { + data.data.map((series : any, index : any) => { + console.log(series); + return ( */} + + + ); +} export default VolcanoPlot; diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index f47e29487d..eb56b78cb6 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -86,11 +86,6 @@ interface TemplateProps { } const Template: Story = (args) => { - const { dataSetProcess: datasetProcess } = processVolcanoData( - args.data, - highMedLowColors - ); - // Better to break into a high and low prop? Would be more clear const foldChangeGates = [-1.5, 1.5]; @@ -131,86 +126,11 @@ const Template: Story = (args) => { range: [-1, 8], nice: true, }); + const volcanoPlotProps: VolcanoPlotProps = { + data: dataSetVolcano.volcanoplot, + }; - return ( - - - - - {datasetProcess.series.map((series, index) => { - console.log(String(index) + 'a'); - return ( - - ); - })} - - - - { - // console.log(tooltipData!.nearestDatum!); - return ( -
-
- {tooltipData!.nearestDatum!.key} -
- {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} - {', '} - {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} -
- ); - }} - /> -
- ); + return ; }; export const Default2 = Template.bind({}); diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index c2f4d36eeb..cd8dd2e44b 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,31 +1,15 @@ export type VolcanoPlotDataSeries = { /** x/y data */ - x: (number | null)[] | string[]; - y: (number | null)[] | string[]; - /** legend text */ - name?: string; - /** plot style */ - mode?: 'markers'; - /** plot with marker: scatter plot with raw data */ - marker?: { - /** marker color */ - color?: string; - /** marker size: no unit */ - size?: number; - /** marker's perimeter setting */ - line?: { - /** marker's perimeter color */ - color?: string; - /** marker's perimeter color: no unit */ - width?: number; - }; - symbol?: string; - }; + foldChange: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointId: string[]; + overlayValue: string; /** opacity of points? */ opacity?: number; }; export type VolcanoPlotData = { /** an array of data series (aka traces) */ - series: VolcanoPlotDataSeries[]; + data: VolcanoPlotDataSeries[]; }; From 18dfc7885c00a8a213f16c9889f9a1a295516299 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 29 Mar 2023 10:22:06 -0400 Subject: [PATCH 09/28] format incoming data for visx --- .../libs/components/src/plots/VolcanoPlot.tsx | 59 ++++++++++--------- .../src/stories/plots/VolcanoVisx.stories.tsx | 4 -- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index eb12449440..7614606bb1 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -81,28 +81,34 @@ function VolcanoPlot(props: VolcanoPlotProps) { // process the data. unzip and zip function formatData(series: VolcanoPlotDataSeries) { - // data: [ - // { - // foldChange: ['2', '3'], - // pValue: ['0.001', '0.0001'], - // adjustedPValue: ['0.01', '0.001'], - // pointId: ['a', 'b'], - // overlayValue: 'positive', - // id: 'id1', - // }, - // assume at least foldChange is there (should be type error if not!) - let seriesPoints = []; + let seriesPoints: { + foldChange: string; + pValue: string; + adjustedPValue: string; + pointId: string; + }[] = []; + series.foldChange.forEach((value: string, index: number) => { + seriesPoints.push({ + foldChange: value, + pValue: series.pValue[index], + adjustedPValue: series.adjustedPValue[index], + pointId: series.pointId[index], + }); + }); + + return seriesPoints; } + const formattedData = data.data.map((series) => formatData(series)); + // this should be -log2 etc. const dataAccessors = { xAccessor: (d: any) => { - return d.foldChange; + return Math.log2(d.foldChange); }, yAccessor: (d: any) => { - console.log('accessing ys'); - return d.adjustedPValue; + return -Math.log10(d.adjustedPValue); }, }; @@ -115,12 +121,6 @@ function VolcanoPlot(props: VolcanoPlotProps) { }, }; - console.log('data'); - console.log(data); - console.log(data.data[0] as unknown as any[]); - - // Issue is that the data needs to be like [{x1 y2}, {x2 y2}, ...] - return ( - {/* { - data.data.map((series : any, index : any) => { - console.log(series); - return ( */} - + {formattedData.map((series: any, index: any) => { + console.log(series); + return ( + + ); + })} ); } diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index eb56b78cb6..ad69e48e04 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -34,7 +34,6 @@ interface VEuPathDBVolcanoPlotData { adjustedPValue: string[]; pointId: string[]; overlayValue: string; - id: string; }>; }; } @@ -49,7 +48,6 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001'], pointId: ['a', 'b'], overlayValue: 'positive', - id: 'id1', }, { foldChange: ['0.5', '0', '1', '0.5', '0.1', '4', '0.2'], @@ -57,7 +55,6 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], overlayValue: 'none', - id: 'id2', }, { foldChange: ['0.01', '0.02', '0.03'], @@ -65,7 +62,6 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { adjustedPValue: ['0.01', '0.001', '0.02'], pointId: ['j', 'k', 'l'], overlayValue: 'negative', - id: 'id3', }, ], }, From 0b8c4488f042515c90f64848b2aea96b82f47d17 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 29 Mar 2023 15:56:37 -0400 Subject: [PATCH 10/28] add basic threshold lines --- .../libs/components/src/plots/VolcanoPlot.tsx | 99 +++++++++++--- .../src/stories/plots/VolcanoVisx.stories.tsx | 123 +----------------- 2 files changed, 80 insertions(+), 142 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 7614606bb1..0cf62f9bdc 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -28,6 +28,7 @@ import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-trun import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; import { tickSettings } from '../utils/tick-settings'; import * as ColorMath from 'color-math'; +import { rgb } from 'd3'; export interface VolcanoPlotProps extends PlotProps, @@ -40,7 +41,7 @@ export interface VolcanoPlotProps independentAxisRange?: NumberRange; /** y-axis range: required for confidence interval */ dependentAxisRange?: NumberRange; - foldChangeGates?: Array; + foldChangeGate?: number; comparisonLabels?: Array; adjustedPValueGate?: number; plotTitle?: string; @@ -73,7 +74,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { dependentAxisLogScale = false, markerBodyOpacity, adjustedPValueGate, - foldChangeGates, + foldChangeGate, ...restProps } = props; @@ -120,28 +121,84 @@ function VolcanoPlot(props: VolcanoPlotProps) { return d.y; }, }; + console.log(adjustedPValueGate); + + const thresholdLineStyles = { + stroke: '#cccccc', + strokeWidth: 1, + strokeDasharray: 3, + }; + + const xMin = -7; + const xMax = 7; + const yMin = -2; + const yMax = 4; + const volcanoColors = ['#fa1122', '#cccccc', '#2211fa']; return ( - - - - - {formattedData.map((series: any, index: any) => { - console.log(series); - return ( - + + + + + + {/* Draw threshold lines below data points */} + {adjustedPValueGate && ( + + )} + {foldChangeGate && ( + + )} + {foldChangeGate && ( + - ); - })} - + )} + {formattedData.map((series: any, index: any) => { + console.log(series); + return ( + { + return volcanoColors[index]; + }} + /> + ); + })} + +
); } diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx index ad69e48e04..cef08cc910 100755 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx @@ -124,6 +124,8 @@ const Template: Story = (args) => { }); const volcanoPlotProps: VolcanoPlotProps = { data: dataSetVolcano.volcanoplot, + adjustedPValueGate: 0.1, + foldChangeGate: 2, }; return ; @@ -137,124 +139,3 @@ Default2.args = { // this process input data function is similar to scatter's but not the same. // would probably be worth revisiting what is in common and factoring accordingly -function processVolcanoData( - dataSet: VEuPathDBVolcanoPlotData, - colors: string[] -): { - dataSetProcess: ScatterPlotData; - xAxisRange: NumberRange; - yAxisRange: NumberRange; -} { - // set variables for x- and yaxis ranges - let xMin: number = 0; - let xMax: number = 0; - let yMin: number = 0; - let yMax: number = 0; - - let processedDataSeries: any = []; - dataSet.volcanoplot.data.forEach(function (el: any, index: number) { - // initialize variables: setting with union type for future, but this causes typescript issue in the current version - let xSeries = []; - let ySeries: never[] = []; - - // set rgbValue here per dataset with a default color - // Add check for len(colors) = number of series - let rgbValue: number[] = hexToRgb(colors[index]); - - let scatterPointColor: string = ''; - - // series is for scatter plot - if (el) { - // check the number of x = number of y - if (el.foldChange.length !== el.adjustedPValue.length) { - console.log( - 'x length=', - el.foldChange.length, - ' y length=', - el.adjustedPValue.length - ); - alert('The number of X data is not equal to the number of Y data'); - throw new Error( - 'The number of X data is not equal to the number of Y data' - ); - } - - /* - * set variables for x-/y-axes ranges including x,y data points: considering Date data for X as well - * This is for finding global min/max values among data arrays for better display of the plot(s) - */ - - xSeries = el.foldChange.map((fc: number) => Math.log2(fc)); - ySeries = el.adjustedPValue.map((apv: number) => -Math.log10(apv)); - - xMin = - xMin < Math.min(...(xSeries as number[])) - ? xMin - : Math.min(...(xSeries as number[])); - xMax = - xMax > Math.max(...(xSeries as number[])) - ? xMax - : Math.max(...(xSeries as number[])); - - // check if this Y array consists of numbers & add type assertion - if (index == 0) { - yMin = Math.min(...ySeries); - yMax = Math.max(...ySeries); - } else { - yMin = yMin < Math.min(...ySeries) ? yMin : Math.min(...ySeries); - yMax = yMax > Math.max(...ySeries) ? yMax : Math.max(...ySeries); - } - - // use global opacity for coloring - scatterPointColor = - 'rgba(' + - rgbValue[0] + - ',' + - rgbValue[1] + - ',' + - rgbValue[2] + - ',' + - 0.8 + - ')'; - - // add scatter data considering input options - const points = xSeries.map((x: any, i: any) => { - return { - x: Number(x), - y: Number(ySeries[i]), - }; - }); - processedDataSeries.push(points); - } - - // make some margin for y-axis range (5% of range for now) - if (typeof yMin == 'number' && typeof yMax == 'number') { - yMin = yMin - (yMax - yMin) * 0.05; - yMax = yMax + (yMax - yMin) * 0.05; - } - }); - - return { - dataSetProcess: { series: processedDataSeries }, - xAxisRange: { min: xMin, max: xMax }, - yAxisRange: { min: yMin, max: yMax }, - }; -} - -// change HTML hex code to rgb array -const hexToRgb = (hex?: string): [number, number, number] => { - if (!hex) return [0, 0, 0]; - const fullHex = hex.replace( - /^#?([a-f\d])([a-f\d])([a-f\d])$/i, - (m: string, r: string, g: string, b: string): string => - '#' + r + r + g + g + b + b - ); - const hexDigits = fullHex.substring(1); - const matches = hexDigits.match(/.{2}/g); - if (matches == null) return [0, 0, 0]; - return matches.map((x: string) => parseInt(x, 16)) as [ - number, - number, - number - ]; -}; From 3bc1a4e42aed2100b011897d541454800233719a Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 29 Mar 2023 16:17:31 -0400 Subject: [PATCH 11/28] improve axis and gridline styles --- .../libs/components/src/plots/VolcanoPlot.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 0cf62f9bdc..c07dec4ebf 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -29,6 +29,7 @@ import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; import { tickSettings } from '../utils/tick-settings'; import * as ColorMath from 'color-math'; import { rgb } from 'd3'; +import { LineSubject } from '@visx/visx'; export interface VolcanoPlotProps extends PlotProps, @@ -129,6 +130,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { strokeDasharray: 3, }; + const axisStyles = { + stroke: '#bbbbbb', + strokeWidth: 1, + }; + const xMin = -7; const xMax = 7; const yMin = -2; @@ -145,9 +151,12 @@ function VolcanoPlot(props: VolcanoPlotProps) { yScale={{ type: 'linear', domain: [-2, 4] }} width={300} > - - - + + + {/* Draw threshold lines below data points */} {adjustedPValueGate && ( From 953829892c896a0a9db4b502cd8fc18ded93a727 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 29 Mar 2023 17:11:11 -0400 Subject: [PATCH 12/28] determine axis ranges from data --- .../libs/components/src/plots/VolcanoPlot.tsx | 68 ++++++++++++++++--- .../src/stories/plots/VolcanoVisx.stories.tsx | 2 +- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index c07dec4ebf..bf5cdfbb68 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -22,6 +22,7 @@ import { GlyphSeries, LineSeries, } from '@visx/xychart'; +import { max, min } from 'lodash'; // import truncation util functions import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-truncations'; @@ -38,9 +39,9 @@ export interface VolcanoPlotProps independentAxisLogScaleAddon, DependentAxisLogScaleAddon, AxisTruncationAddon { - /** x-axis range: required for confidence interval - not really */ + /** x-axis range: */ independentAxisRange?: NumberRange; - /** y-axis range: required for confidence interval */ + /** y-axis range: */ dependentAxisRange?: NumberRange; foldChangeGate?: number; comparisonLabels?: Array; @@ -81,6 +82,55 @@ function VolcanoPlot(props: VolcanoPlotProps) { // add truncation + // Axis ranges + // Let's do something dumb for now... + let xMin: number | undefined; + let xMax: number | undefined; + let yMin: number | undefined; + let yMax: number | undefined; + + data.data.forEach((series, index: number) => { + if (index == 0) { + xMin = min(series.foldChange.map((fc) => Math.log2(Number(fc)))); + xMax = max(series.foldChange.map((fc) => Math.log2(Number(fc)))); + yMin = min(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))); + yMax = max(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))); + } else { + xMin = min([ + xMin, + min(series.foldChange.map((fc) => Math.log2(Number(fc)))), + ]); + xMax = max([ + xMax, + max(series.foldChange.map((fc) => Math.log2(Number(fc)))), + ]); + yMin = min([ + yMin, + min(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))), + ]); + yMax = max([ + yMax, + max(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))), + ]); + } + }); + + // Add a little margin for axes + if (xMin && xMax) { + xMin = xMin - (xMax - xMin) * 0.05; + xMax = xMax + (xMax - xMin) * 0.05; + } else { + xMin = 0; + xMax = 0; + } + if (yMin && yMax) { + yMin = yMin - (yMax - yMin) * 0.05; + yMax = yMax + (yMax - yMin) * 0.05; + } else { + yMin = 0; + yMax = 0; + } + // process the data. unzip and zip function formatData(series: VolcanoPlotDataSeries) { // assume at least foldChange is there (should be type error if not!) @@ -90,7 +140,9 @@ function VolcanoPlot(props: VolcanoPlotProps) { adjustedPValue: string; pointId: string; }[] = []; + series.foldChange.forEach((value: string, index: number) => { + // Find axis ranges seriesPoints.push({ foldChange: value, pValue: series.pValue[index], @@ -122,10 +174,9 @@ function VolcanoPlot(props: VolcanoPlotProps) { return d.y; }, }; - console.log(adjustedPValueGate); const thresholdLineStyles = { - stroke: '#cccccc', + stroke: '#aaaaaa', strokeWidth: 1, strokeDasharray: 3, }; @@ -135,10 +186,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { strokeWidth: 1, }; - const xMin = -7; - const xMax = 7; - const yMin = -2; - const yMax = 4; + // move the following to addOns? maybe leave here until needed in another plot const volcanoColors = ['#fa1122', '#cccccc', '#2211fa']; return ( @@ -147,8 +195,8 @@ function VolcanoPlot(props: VolcanoPlotProps) {
Date: Thu, 30 Mar 2023 14:19:17 -0400 Subject: [PATCH 13/28] added half a tooltip --- .../libs/components/src/plots/VolcanoPlot.tsx | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index bf5cdfbb68..15601e398b 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -1,5 +1,6 @@ -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; import { makePlotlyPlotComponent, PlotProps } from './PlotlyPlot'; + // truncation import { OrientationAddon, @@ -22,6 +23,7 @@ import { GlyphSeries, LineSeries, } from '@visx/xychart'; +import { useTooltip } from '@visx/tooltip'; import { max, min } from 'lodash'; // import truncation util functions @@ -30,7 +32,6 @@ import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; import { tickSettings } from '../utils/tick-settings'; import * as ColorMath from 'color-math'; import { rgb } from 'd3'; -import { LineSubject } from '@visx/visx'; export interface VolcanoPlotProps extends PlotProps, @@ -156,22 +157,21 @@ function VolcanoPlot(props: VolcanoPlotProps) { const formattedData = data.data.map((series) => formatData(series)); - // this should be -log2 etc. const dataAccessors = { xAccessor: (d: any) => { - return Math.log2(d.foldChange); + return Math.log2(d?.foldChange); }, yAccessor: (d: any) => { - return -Math.log10(d.adjustedPValue); + return -Math.log10(d?.adjustedPValue); }, }; const thresholdLineAccessors = { xAccessor: (d: any) => { - return d.x; + return d?.x; }, yAccessor: (d: any) => { - return d.y; + return d?.y; }, }; @@ -189,6 +189,19 @@ function VolcanoPlot(props: VolcanoPlotProps) { // move the following to addOns? maybe leave here until needed in another plot const volcanoColors = ['#fa1122', '#cccccc', '#2211fa']; + // tooltip?? + const { + tooltipData, + tooltipLeft, + tooltipTop, + tooltipOpen, + showTooltip, + hideTooltip, + } = useTooltip(); + + console.log('tooltipdata'); + console.log(tooltipData); + return ( // From docs " For correct tooltip positioning, it is important to wrap your // component in an element (e.g., div) with relative positioning." @@ -209,12 +222,12 @@ function VolcanoPlot(props: VolcanoPlotProps) { {/* Draw threshold lines below data points */} {adjustedPValueGate && ( @@ -226,6 +239,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { { x: -Math.log2(foldChangeGate), y: yMax }, ]} dataKey="foldChangeLineLow" + enableEvents={false} {...thresholdLineStyles} {...thresholdLineAccessors} /> @@ -237,6 +251,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { { x: Math.log2(foldChangeGate), y: yMax }, ]} dataKey="foldChangeLineHigh" + enableEvents={false} {...thresholdLineStyles} {...thresholdLineAccessors} /> @@ -245,7 +260,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { console.log(series); return ( { @@ -254,6 +269,34 @@ function VolcanoPlot(props: VolcanoPlotProps) { /> ); })} + { + console.log(tooltipData); + const isThresholdLine = + tooltipData?.nearestDatum?.key.includes('Line'); + return ( +
+
+ {tooltipData?.nearestDatum?.key} +
+ {isThresholdLine ? ( + 'thresholdline!' + ) : ( +
+ {dataAccessors.xAccessor(tooltipData?.nearestDatum?.datum)} + {', '} + {dataAccessors.yAccessor(tooltipData?.nearestDatum?.datum)} +
+ )} +
+ ); + }} + />
); From f921d453b811184f8affe6ca9a3f416f76889158 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 31 Mar 2023 09:52:43 -0400 Subject: [PATCH 14/28] use annotation lines for threshold lines --- .../libs/components/src/plots/VolcanoPlot.tsx | 120 +++++++++++------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 15601e398b..3849cf76ac 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, useContext } from 'react'; import { makePlotlyPlotComponent, PlotProps } from './PlotlyPlot'; // truncation @@ -22,6 +22,9 @@ import { Grid, GlyphSeries, LineSeries, + DataContext, + Annotation, + AnnotationLineSubject, } from '@visx/xychart'; import { useTooltip } from '@visx/tooltip'; import { max, min } from 'lodash'; @@ -31,7 +34,7 @@ import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-trun import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; import { tickSettings } from '../utils/tick-settings'; import * as ColorMath from 'color-math'; -import { rgb } from 'd3'; +import { scaleOrdinal } from 'd3'; export interface VolcanoPlotProps extends PlotProps, @@ -89,6 +92,13 @@ function VolcanoPlot(props: VolcanoPlotProps) { let xMax: number | undefined; let yMin: number | undefined; let yMax: number | undefined; + let seriesPoints: { + foldChange: string; + pValue: string; + adjustedPValue: string; + pointId: string; + colorNum: number; + }[] = []; data.data.forEach((series, index: number) => { if (index == 0) { @@ -114,6 +124,15 @@ function VolcanoPlot(props: VolcanoPlotProps) { max(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))), ]); } + series.foldChange.forEach((v: string, ind: number) => { + seriesPoints.push({ + foldChange: series.foldChange[ind], + pValue: series.pValue[ind], + adjustedPValue: series.adjustedPValue[ind], + pointId: series.pointId[ind], + colorNum: index, + }); + }); }); // Add a little margin for axes @@ -132,6 +151,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { yMax = 0; } + // const colorScale = scaleOrdinal({ + // domain: ['0', '1', '2'], + // range: ['#00ee00', '#888811', '#88800'] + // }); + // process the data. unzip and zip function formatData(series: VolcanoPlotDataSeries) { // assume at least foldChange is there (should be type error if not!) @@ -202,6 +226,10 @@ function VolcanoPlot(props: VolcanoPlotProps) { console.log('tooltipdata'); console.log(tooltipData); + // data context? + const { theme, width, height } = useContext(DataContext); + console.log(width); + return ( // From docs " For correct tooltip positioning, it is important to wrap your // component in an element (e.g., div) with relative positioning." @@ -221,54 +249,56 @@ function VolcanoPlot(props: VolcanoPlotProps) { {/* Draw threshold lines below data points */} {adjustedPValueGate && ( - - )} - {foldChangeGate && ( - + > + + )} {foldChangeGate && ( - + <> + + + + + + + )} - {formattedData.map((series: any, index: any) => { + {/* */} + {/* {formattedData.map((series: any, index: any) => { console.log(series); - return ( - { - return volcanoColors[index]; - }} - /> - ); - })} + return ( */} + { + return volcanoColors[d.colorNum]; + }} + /> + {/* ); + })} */} + {/* */} Date: Fri, 31 Mar 2023 10:19:30 -0400 Subject: [PATCH 15/28] a little volcano cleanup --- .../libs/components/src/plots/VolcanoPlot.tsx | 28 ++- .../src/stories/plots/VolcanoPlot.stories.tsx | 207 ++---------------- .../src/stories/plots/VolcanoVisx.stories.tsx | 141 ------------ 3 files changed, 39 insertions(+), 337 deletions(-) delete mode 100755 packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 3849cf76ac..8ca198231f 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -26,6 +26,7 @@ import { Annotation, AnnotationLineSubject, } from '@visx/xychart'; +import { Group } from '@visx/group'; import { useTooltip } from '@visx/tooltip'; import { max, min } from 'lodash'; @@ -284,21 +285,24 @@ function VolcanoPlot(props: VolcanoPlotProps) { )} - {/* */} - {/* {formattedData.map((series: any, index: any) => { + {/* Didn't find a nice way to pass opacity to the GlyphSeries, so + instead just wrapping the series in a group and taking care of it there. */} + + {/* {formattedData.map((series: any, index: any) => { console.log(series); return ( */} - { - return volcanoColors[d.colorNum]; - }} - /> - {/* ); + { + return volcanoColors[d.colorNum]; + }} + // {...{'opacity': '0.2'}} didn't work :( + /> + {/* ); })} */} - {/* */} +
= (args) => { - console.log(args.data); - const { dataSetProcess: datasetProcess } = processVolcanoData( - args.data, - highMedLowColors - ); - console.log(datasetProcess); - // Better to break into a high and low prop? Would be more clear - const foldChangeGates = [-1, 1]; - const adjustedPValueGate = 1.6; + const foldChangeGates = [-1.5, 1.5]; const comparisonLabels = ['group a', 'group b']; - /** - * Volcano knows - * x and y label (always fold change and pvalue) - */ - - /** - * datasetProcess has three or fewer series - * has columms for foldChange, adjustedPValue, pointId, significanceDirection (naming help!!) - * - */ - - const independentAxisRange = { - min: -5, - max: 5, + const volcanoPlotProps: VolcanoPlotProps = { + data: dataSetVolcano.volcanoplot, + adjustedPValueGate: 0.1, + foldChangeGate: 2, + markerBodyOpacity: args.markerBodyOpacity, }; - // Determined by the data and symmetric around 0 by default? - const dependentAxisRange = { - min: -1, - max: 8, - }; // By default max determined by data and min at 0 - return ( -
- -
- ); + return ; }; -export const Default = Template.bind({}); -Default.args = { +export const Basic = Template.bind({}); +Basic.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, }; +// export const Truncation = Template.bind({}) +// Truncation.args = { +// data: dataSetVolcano, +// independentAxisRange: [] +// } + // this process input data function is similar to scatter's but not the same. // would probably be worth revisiting what is in common and factoring accordingly -function processVolcanoData( - dataSet: VEuPathDBVolcanoPlotData, - colors: string[] -): { - dataSetProcess: ScatterPlotData; - xAxisRange: NumberRange; - yAxisRange: NumberRange; -} { - // set variables for x- and yaxis ranges - let xMin: number = 0; - let xMax: number = 0; - let yMin: number = 0; - let yMax: number = 0; - - let processedDataSeries: any = []; - dataSet.volcanoplot.data.forEach(function (el: any, index: number) { - // initialize variables: setting with union type for future, but this causes typescript issue in the current version - let xSeries = []; - let ySeries = []; - - // set rgbValue here per dataset with a default color - // Add check for len(colors) = number of series - let rgbValue: number[] = hexToRgb(colors[index]); - - let scatterPointColor: string = ''; - - // series is for scatter plot - if (el) { - // check the number of x = number of y - if (el.foldChange.length !== el.adjustedPValue.length) { - console.log( - 'x length=', - el.foldChange.length, - ' y length=', - el.adjustedPValue.length - ); - alert('The number of X data is not equal to the number of Y data'); - throw new Error( - 'The number of X data is not equal to the number of Y data' - ); - } - - /* - * set variables for x-/y-axes ranges including x,y data points: considering Date data for X as well - * This is for finding global min/max values among data arrays for better display of the plot(s) - */ - - xSeries = el.foldChange.map((fc: number) => Math.log2(fc)); - ySeries = el.adjustedPValue.map((apv: number) => -Math.log10(apv)); - - xMin = - xMin < Math.min(...(xSeries as number[])) - ? xMin - : Math.min(...(xSeries as number[])); - xMax = - xMax > Math.max(...(xSeries as number[])) - ? xMax - : Math.max(...(xSeries as number[])); - - // check if this Y array consists of numbers & add type assertion - if (index == 0) { - yMin = Math.min(...ySeries); - yMax = Math.max(...ySeries); - } else { - yMin = yMin < Math.min(...ySeries) ? yMin : Math.min(...ySeries); - yMax = yMax > Math.max(...ySeries) ? yMax : Math.max(...ySeries); - } - - // use global opacity for coloring - scatterPointColor = - 'rgba(' + - rgbValue[0] + - ',' + - rgbValue[1] + - ',' + - rgbValue[2] + - ',' + - 0.8 + - ')'; - - // add scatter data considering input options - processedDataSeries.push({ - x: xSeries, - y: ySeries, - name: el.label, - mode: 'markers', - // type: 'scattergl', - type: 'scatter', - marker: { color: scatterPointColor, size: 12 }, - }); - } - - // make some margin for y-axis range (5% of range for now) - if (typeof yMin == 'number' && typeof yMax == 'number') { - yMin = yMin - (yMax - yMin) * 0.05; - yMax = yMax + (yMax - yMin) * 0.05; - } - }); - - return { - dataSetProcess: { series: processedDataSeries }, - xAxisRange: { min: xMin, max: xMax }, - yAxisRange: { min: yMin, max: yMax }, - }; -} - -// change HTML hex code to rgb array -const hexToRgb = (hex?: string): [number, number, number] => { - if (!hex) return [0, 0, 0]; - const fullHex = hex.replace( - /^#?([a-f\d])([a-f\d])([a-f\d])$/i, - (m: string, r: string, g: string, b: string): string => - '#' + r + r + g + g + b + b - ); - const hexDigits = fullHex.substring(1); - const matches = hexDigits.match(/.{2}/g); - if (matches == null) return [0, 0, 0]; - return matches.map((x: string) => parseInt(x, 16)) as [ - number, - number, - number - ]; -}; diff --git a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx deleted file mode 100755 index bcbb743831..0000000000 --- a/packages/libs/components/src/stories/plots/VolcanoVisx.stories.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useState } from 'react'; -import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; -import { - XYChart, - Tooltip, - Axis, - Grid, - GlyphSeries, - LineSeries, -} from '@visx/xychart'; -// import { min, max, lte, gte } from 'lodash'; -// import { dataSetProcess, xAxisRange, yAxisRange } from './ScatterPlot.storyData'; -import { Story, Meta } from '@storybook/react/types-6-0'; -// test to use RadioButtonGroup directly instead of ScatterPlotControls -import { NumberRange } from '../../types/general'; - -import { ScatterPlotData } from '../../types/plots'; -import { AxisBottom } from '@visx/visx'; -import { scaleLinear } from '@visx/scale'; -import ControlsHeader from '../../../lib/components/typography/ControlsHeader'; -import { Line } from '@visx/shape'; -import { Group } from '@visx/group'; - -export default { - title: 'Plots/VolcanoPlotVisx', - component: VolcanoPlot, -} as Meta; - -interface VEuPathDBVolcanoPlotData { - volcanoplot: { - data: Array<{ - foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointId: string[]; - overlayValue: string; - }>; - }; -} - -// Let's make some fake data! -const dataSetVolcano: VEuPathDBVolcanoPlotData = { - volcanoplot: { - data: [ - { - foldChange: ['2', '3'], - pValue: ['0.001', '0.0001'], - adjustedPValue: ['0.01', '0.001'], - pointId: ['a', 'b'], - overlayValue: 'positive', - }, - { - foldChange: ['0.5', '0.8', '1', '0.5', '0.1', '4', '0.2'], - pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], - adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], - pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], - overlayValue: 'none', - }, - { - foldChange: ['0.01', '0.02', '0.03'], - pValue: ['0.001', '0.0001', '0.002'], - adjustedPValue: ['0.01', '0.001', '0.02'], - pointId: ['j', 'k', 'l'], - overlayValue: 'negative', - }, - ], - }, -}; - -// These can go into addons eventually. I'd expect other vizs that involve significance to use these as well -// These are NOT the final proposed colors -const highMedLowColors = ['#dd1111', '#bbbbbb', '#1111dd']; - -const plotTitle = 'Volcano erupt!'; - -interface TemplateProps { - data: VEuPathDBVolcanoPlotData; - markerBodyOpacity: number; - // foldChangeHighGate: number; - // foldChangeLowGate: number; // we can't have gates unless we mimic the backend updating the data format when we change gates - adjustedPValueGate: number; -} - -const Template: Story = (args) => { - // Better to break into a high and low prop? Would be more clear - const foldChangeGates = [-1.5, 1.5]; - - const comparisonLabels = ['group a', 'group b']; - - /** - * Volcano knows - * x and y label (always fold change and pvalue) - */ - - /** - * datasetProcess has three or fewer series - * has columms for foldChange, adjustedPValue, pointId, significanceDirection (naming help!!) - * - */ - - const independentAxisRange = { - min: -5, - max: 5, - }; - // Determined by the data and symmetric around 0 by default? - const dependentAxisRange = { - min: 0, - max: 0.03, - }; // By default max determined by data and min at 0 - - const accessors = { - xAccessor: (d: any) => { - return d.x; - }, - yAccessor: (d: any) => { - return d.y; - }, - }; - - const bottomScale = scaleLinear({ - domain: [-4, 4], - range: [-1, 8], - nice: true, - }); - const volcanoPlotProps: VolcanoPlotProps = { - data: dataSetVolcano.volcanoplot, - adjustedPValueGate: 0.1, - foldChangeGate: 2, - }; - - return ; -}; - -export const Default2 = Template.bind({}); -Default2.args = { - data: dataSetVolcano, - markerBodyOpacity: 0.8, -}; - -// this process input data function is similar to scatter's but not the same. -// would probably be worth revisiting what is in common and factoring accordingly From 27c96a7b1512440a7162eaf205603066f67c1e03 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 6 Apr 2023 07:32:36 -0400 Subject: [PATCH 16/28] volcano cleanup and reorganize how data gets formatted --- .../libs/components/src/plots/VolcanoPlot.tsx | 321 +++++++----------- .../src/stories/plots/ScatterVisx.stories.tsx | 71 ---- .../src/stories/plots/VolcanoPlot.stories.tsx | 14 +- .../libs/components/src/types/plots/addOns.ts | 3 + .../components/src/types/plots/volcanoplot.ts | 6 +- 5 files changed, 135 insertions(+), 280 deletions(-) delete mode 100644 packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 8ca198231f..5354572132 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -1,19 +1,10 @@ -import { useMemo, useCallback, useContext } from 'react'; -import { makePlotlyPlotComponent, PlotProps } from './PlotlyPlot'; +import { PlotProps } from './PlotlyPlot'; -// truncation -import { - OrientationAddon, - OrientationDefault, - AxisTruncationAddon, - independentAxisLogScaleAddon, - DependentAxisLogScaleAddon, -} from '../types/plots'; +import { significanceColors } from '../types/plots'; import { VolcanoPlotData, VolcanoPlotDataSeries, } from '../types/plots/volcanoplot'; -// add Shape for truncation import { NumberRange } from '../types/general'; import { XYChart, @@ -21,166 +12,153 @@ import { Axis, Grid, GlyphSeries, - LineSeries, - DataContext, Annotation, AnnotationLineSubject, } from '@visx/xychart'; import { Group } from '@visx/group'; -import { useTooltip } from '@visx/tooltip'; import { max, min } from 'lodash'; -// import truncation util functions -import { extendAxisRangeForTruncations } from '../utils/extended-axis-range-truncations'; -import { truncationLayoutShapes } from '../utils/truncation-layout-shapes'; -import { tickSettings } from '../utils/tick-settings'; -import * as ColorMath from 'color-math'; -import { scaleOrdinal } from 'd3'; - -export interface VolcanoPlotProps - extends PlotProps, - // truncation - OrientationAddon, - independentAxisLogScaleAddon, - DependentAxisLogScaleAddon, - AxisTruncationAddon { +export interface VolcanoPlotProps extends PlotProps { /** x-axis range: */ independentAxisRange?: NumberRange; /** y-axis range: */ dependentAxisRange?: NumberRange; - foldChangeGate?: number; + /** + * Used to set the fold change thresholds. Will + * set two thresholds at +/- this number + */ + foldChangeThreshold?: number; + /** Set the threshold for significance. */ + significanceThreshold?: number; + /** + * 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; - adjustedPValueGate?: number; + /** What is this plot's name? */ plotTitle?: string; - /** marker color opacity: range from 0 to 1 */ markerBodyOpacity?: number; } const EmptyVolcanoPlotData: VolcanoPlotData = { - data: [], + series: [], }; /** - * This component handles several plots such as marker, line, confidence interval, - * density, and combinations of plots like marker + line + confidence interval + * The Volcano Plot displays points on a (magnitude change) by (significance) xy axis. */ function VolcanoPlot(props: VolcanoPlotProps) { const { data = EmptyVolcanoPlotData, independentAxisRange, dependentAxisRange, - // independentAxisLabel, - // dependentAxisLabel, - // independentValueType, - // dependentValueType, - // truncation - orientation = OrientationDefault, - axisTruncationConfig, - independentAxisLogScale = false, - dependentAxisLogScale = false, markerBodyOpacity, - adjustedPValueGate, - foldChangeGate, + significanceThreshold, + foldChangeThreshold, ...restProps } = props; - // add truncation + /** + * Find mins, maxes, and format data while we're at it. + * These are all lumped together so that we only have to go + * through the data once. */ + function formatData(data: VolcanoPlotData) { + // Prep + let dataXMin: number | undefined; + let dataXMax: number | undefined; + let dataYMin: number | undefined; + let dataYMax: number | undefined; - // Axis ranges - // Let's do something dumb for now... - let xMin: number | undefined; - let xMax: number | undefined; - let yMin: number | undefined; - let yMax: number | undefined; - let seriesPoints: { - foldChange: string; - pValue: string; - adjustedPValue: string; - pointId: string; - colorNum: number; - }[] = []; + // Loop through the data and format. While we're here, might + // as well also get the data mins and maxes for the axes. + const formattedData = data.series.map((series, index: number) => { + let seriesPoints: { + foldChange: string; + pValue: string; + adjustedPValue: string; + pointId: string; + colorNum: number; + }[] = []; - data.data.forEach((series, index: number) => { - if (index == 0) { - xMin = min(series.foldChange.map((fc) => Math.log2(Number(fc)))); - xMax = max(series.foldChange.map((fc) => Math.log2(Number(fc)))); - yMin = min(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))); - yMax = max(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))); - } else { - xMin = min([ - xMin, - min(series.foldChange.map((fc) => Math.log2(Number(fc)))), - ]); - xMax = max([ - xMax, - max(series.foldChange.map((fc) => Math.log2(Number(fc)))), - ]); - yMin = min([ - yMin, - min(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))), - ]); - yMax = max([ - yMax, - max(series.adjustedPValue.map((apv) => -Math.log10(Number(apv)))), - ]); - } - series.foldChange.forEach((v: string, ind: number) => { - seriesPoints.push({ - foldChange: series.foldChange[ind], - pValue: series.pValue[ind], - adjustedPValue: series.adjustedPValue[ind], - pointId: series.pointId[ind], - colorNum: index, + if (index == 0) { + dataXMin = min(series.foldChange.map((fc) => Number(fc))); + dataXMax = max(series.foldChange.map((fc) => Number(fc))); + dataYMin = min(series.adjustedPValue.map((apv) => Number(apv))); + dataYMax = max(series.adjustedPValue.map((apv) => Number(apv))); + } else { + dataXMin = min([ + dataXMin, + min(series.foldChange.map((fc) => Number(fc))), + ]); + dataXMax = max([ + dataXMax, + max(series.foldChange.map((fc) => Number(fc))), + ]); + dataYMin = min([ + dataYMin, + min(series.adjustedPValue.map((apv) => Number(apv))), + ]); + dataYMax = max([ + dataYMax, + max(series.adjustedPValue.map((apv) => Number(apv))), + ]); + } + series.foldChange.forEach((v: string, ind: number) => { + seriesPoints.push({ + foldChange: series.foldChange[ind], + pValue: series.pValue[ind], + adjustedPValue: series.adjustedPValue[ind], + pointId: series.pointId[ind], + colorNum: index, + }); }); + + return seriesPoints; }); - }); - // Add a little margin for axes - if (xMin && xMax) { - xMin = xMin - (xMax - xMin) * 0.05; - xMax = xMax + (xMax - xMin) * 0.05; + return { formattedData, dataXMin, dataXMax, dataYMin, dataYMax }; + } + + const { formattedData, dataXMin, dataXMax, dataYMin, dataYMax } = + formatData(data); + console.log(formattedData); + + /** + * 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. + */ + + 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); + xMin = xMin - (xMin - xMin) * 0.05; + xMax = Math.log2(dataXMax); + xMax = xMax + (xMax - xMax) * 0.05; } else { xMin = 0; xMax = 0; } - if (yMin && yMax) { - yMin = yMin - (yMax - yMin) * 0.05; - yMax = yMax + (yMax - yMin) * 0.05; + if (dataYMin && dataYMax) { + yMin = -Math.log10(dataYMax); + yMax = -Math.log10(dataYMin); + yMin = yMin - (yMin - yMin) * 0.05; + yMax = yMax + (yMax - yMax) * 0.05; } else { yMin = 0; yMax = 0; } - // const colorScale = scaleOrdinal({ - // domain: ['0', '1', '2'], - // range: ['#00ee00', '#888811', '#88800'] - // }); - - // process the data. unzip and zip - function formatData(series: VolcanoPlotDataSeries) { - // assume at least foldChange is there (should be type error if not!) - let seriesPoints: { - foldChange: string; - pValue: string; - adjustedPValue: string; - pointId: string; - }[] = []; - - series.foldChange.forEach((value: string, index: number) => { - // Find axis ranges - seriesPoints.push({ - foldChange: value, - pValue: series.pValue[index], - adjustedPValue: series.adjustedPValue[index], - pointId: series.pointId[index], - }); - }); - - return seriesPoints; - } - - const formattedData = data.data.map((series) => formatData(series)); + /** + * Accessors + */ const dataAccessors = { xAccessor: (d: any) => { @@ -200,37 +178,20 @@ function VolcanoPlot(props: VolcanoPlotProps) { }, }; + /** + * 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, }; - // move the following to addOns? maybe leave here until needed in another plot - const volcanoColors = ['#fa1122', '#cccccc', '#2211fa']; - - // tooltip?? - const { - tooltipData, - tooltipLeft, - tooltipTop, - tooltipOpen, - showTooltip, - hideTooltip, - } = useTooltip(); - - console.log('tooltipdata'); - console.log(tooltipData); - - // data context? - const { theme, width, height } = useContext(DataContext); - console.log(width); - return ( // From docs " For correct tooltip positioning, it is important to wrap your // component in an element (e.g., div) with relative positioning." @@ -249,11 +210,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { {/* Draw threshold lines below data points */} - {adjustedPValueGate && ( + {significanceThreshold && ( @@ -263,11 +224,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { /> )} - {foldChangeGate && ( + {foldChangeThreshold && ( <> )} - {/* Didn't find a nice way to pass opacity to the GlyphSeries, so - instead just wrapping the series in a group and taking care of it there. */} - {/* {formattedData.map((series: any, index: any) => { - console.log(series); - return ( */} - { - return volcanoColors[d.colorNum]; - }} - // {...{'opacity': '0.2'}} didn't work :( - /> - {/* ); - })} */} - - { - console.log(tooltipData); - const isThresholdLine = - tooltipData?.nearestDatum?.key.includes('Line'); + {formattedData.map((series: any, index: any) => { return ( -
-
- {tooltipData?.nearestDatum?.key} -
- {isThresholdLine ? ( - 'thresholdline!' - ) : ( -
- {dataAccessors.xAccessor(tooltipData?.nearestDatum?.datum)} - {', '} - {dataAccessors.yAccessor(tooltipData?.nearestDatum?.datum)} -
- )} -
+ { + return significanceColors[d.colorNum]; + }} + /> ); - }} - /> + })} +
); diff --git a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx b/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx deleted file mode 100644 index 3c3f54a760..0000000000 --- a/packages/libs/components/src/stories/plots/ScatterVisx.stories.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { XYChart, Tooltip, Axis, Grid, GlyphSeries } from '@visx/xychart'; -import ScatterPlot from '../../../lib/plots/ScatterPlot'; -import { Story, Meta } from '@storybook/react/types-6-0'; -import { DivIcon } from 'leaflet'; -import { AxisBottom } from '@visx/axis'; -import { scaleLinear } from '@visx/scale'; - -export default { - title: 'Plots/VisxScatter', - component: ScatterPlot, -} as Meta; - -const data1 = [ - { x: 10, y: 50 }, - { x: 20, y: 10 }, - { x: 5, y: 20 }, -]; - -const data2 = [ - { x: 3, y: 30 }, - { x: 40, y: 40 }, - { x: 1, y: 80 }, -]; - -interface TemplateProps {} - -const Template: Story = (args) => { - const accessors = { - xAccessor: (d: any) => d.x, - yAccessor: (d: any) => d.y, - }; - - const bottomScale = scaleLinear({ - domain: [-4, 4], - range: [-1, 8], - nice: true, - }); - - return ( - - - {/* */} - - - - - ( -
-
- {tooltipData!.nearestDatum!.key} -
- {accessors.xAccessor(tooltipData!.nearestDatum!.datum)} - {', '} - {accessors.yAccessor(tooltipData!.nearestDatum!.datum)} -
- )} - /> -
- ); -}; - -export const Default = Template.bind({}); diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 259280d03b..605d6e2506 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -10,7 +10,7 @@ export default { interface VEuPathDBVolcanoPlotData { volcanoplot: { - data: Array<{ + series: Array<{ foldChange: string[]; pValue: string[]; adjustedPValue: string[]; @@ -23,7 +23,7 @@ interface VEuPathDBVolcanoPlotData { // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - data: [ + series: [ { foldChange: ['2', '3'], pValue: ['0.001', '0.0001'], @@ -60,16 +60,14 @@ interface TemplateProps { } const Template: Story = (args) => { - // Better to break into a high and low prop? Would be more clear - const foldChangeGates = [-1.5, 1.5]; - - const comparisonLabels = ['group a', 'group b']; + const comparisonLabels = ['up in group a', 'up in group b']; const volcanoPlotProps: VolcanoPlotProps = { data: dataSetVolcano.volcanoplot, - adjustedPValueGate: 0.1, - foldChangeGate: 2, + significanceThreshold: 0.1, + foldChangeThreshold: 2, markerBodyOpacity: args.markerBodyOpacity, + comparisonLabels: comparisonLabels, }; return ; diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 66ecb46639..1afd0e11d5 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -283,6 +283,9 @@ export const gradientConvergingColorscaleMap = scaleLinear() .range(ConvergingGradientColorscale) .interpolate(interpolateLab); +// Significance colors +export const significanceColors = ['#AC3B4E', '#B5B8B4', '#0E8FAB']; + /** truncated axis flags */ export type AxisTruncationAddon = { /** truncation config (flags) to show truncated axis (true) or not (false) */ diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index cd8dd2e44b..fb3ba989e2 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,15 +1,11 @@ export type VolcanoPlotDataSeries = { - /** x/y data */ foldChange: string[]; pValue: string[]; adjustedPValue: string[]; pointId: string[]; - overlayValue: string; - /** opacity of points? */ - opacity?: number; }; export type VolcanoPlotData = { /** an array of data series (aka traces) */ - data: VolcanoPlotDataSeries[]; + series: VolcanoPlotDataSeries[]; }; From 3645576dc70a33a6babcc71494cb1fc53fb6a50f Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 6 Apr 2023 07:38:50 -0400 Subject: [PATCH 17/28] document volcano --- .../libs/components/src/plots/VolcanoPlot.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 5354572132..38d13f6c6c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -191,6 +191,10 @@ function VolcanoPlot(props: VolcanoPlotProps) { stroke: '#bbbbbb', strokeWidth: 1, }; + const gridStyles = { + stroke: '#dddddd', + strokeWidth: 0.5, + }; return ( // From docs " For correct tooltip positioning, it is important to wrap your @@ -198,22 +202,19 @@ function VolcanoPlot(props: VolcanoPlotProps) {
- + - {/* Draw threshold lines below data points */} + {/* Draw threshold lines as annotations below the data points */} {significanceThreshold && ( @@ -238,7 +239,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { @@ -246,6 +247,8 @@ function VolcanoPlot(props: VolcanoPlotProps) { )} + + {/* The data itself */} {formattedData.map((series: any, index: any) => { return ( From 8c4b45872e115aa63d0e213e75c5081ec6fd4366 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 6 Apr 2023 14:26:36 -0400 Subject: [PATCH 18/28] refactor so volcano data is only one series --- .../libs/components/src/plots/VolcanoPlot.tsx | 136 +++++++----------- .../src/stories/plots/VolcanoPlot.stories.tsx | 92 +++++++----- .../components/src/types/plots/volcanoplot.ts | 7 +- 3 files changed, 111 insertions(+), 124 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 38d13f6c6c..fa2d72bcc1 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -1,10 +1,7 @@ import { PlotProps } from './PlotlyPlot'; import { significanceColors } from '../types/plots'; -import { - VolcanoPlotData, - VolcanoPlotDataSeries, -} from '../types/plots/volcanoplot'; +import { VolcanoPlotData } from '../types/plots/volcanoplot'; import { NumberRange } from '../types/general'; import { XYChart, @@ -43,11 +40,15 @@ export interface VolcanoPlotProps extends PlotProps { } const EmptyVolcanoPlotData: VolcanoPlotData = { - series: [], + foldChange: [], + pValue: [], + adjustedPValue: [], + pointId: [], }; /** * 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 { @@ -61,75 +62,18 @@ function VolcanoPlot(props: VolcanoPlotProps) { } = props; /** - * Find mins, maxes, and format data while we're at it. - * These are all lumped together so that we only have to go - * through the data once. */ - function formatData(data: VolcanoPlotData) { - // Prep - let dataXMin: number | undefined; - let dataXMax: number | undefined; - let dataYMin: number | undefined; - let dataYMax: number | undefined; - - // Loop through the data and format. While we're here, might - // as well also get the data mins and maxes for the axes. - const formattedData = data.series.map((series, index: number) => { - let seriesPoints: { - foldChange: string; - pValue: string; - adjustedPValue: string; - pointId: string; - colorNum: number; - }[] = []; - - if (index == 0) { - dataXMin = min(series.foldChange.map((fc) => Number(fc))); - dataXMax = max(series.foldChange.map((fc) => Number(fc))); - dataYMin = min(series.adjustedPValue.map((apv) => Number(apv))); - dataYMax = max(series.adjustedPValue.map((apv) => Number(apv))); - } else { - dataXMin = min([ - dataXMin, - min(series.foldChange.map((fc) => Number(fc))), - ]); - dataXMax = max([ - dataXMax, - max(series.foldChange.map((fc) => Number(fc))), - ]); - dataYMin = min([ - dataYMin, - min(series.adjustedPValue.map((apv) => Number(apv))), - ]); - dataYMax = max([ - dataYMax, - max(series.adjustedPValue.map((apv) => Number(apv))), - ]); - } - series.foldChange.forEach((v: string, ind: number) => { - seriesPoints.push({ - foldChange: series.foldChange[ind], - pValue: series.pValue[ind], - adjustedPValue: series.adjustedPValue[ind], - pointId: series.pointId[ind], - colorNum: index, - }); - }); - - return seriesPoints; - }); - - return { formattedData, dataXMin, dataXMax, dataYMin, dataYMax }; - } + * Find mins and maxes of the data and for the plot + */ - const { formattedData, dataXMin, dataXMax, dataYMin, dataYMax } = - formatData(data); - console.log(formattedData); + 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. - */ + // 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. + // let xMin: number; let xMax: number; @@ -157,7 +101,31 @@ function VolcanoPlot(props: VolcanoPlotProps) { } /** - * Accessors + * Turn the data (array of arrays) into data points (array of points) + */ + + let dataPoints: { + foldChange: string; + pValue: string; + adjustedPValue: string; + pointId: string; + colorNum: number; + }[] = []; + + // 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], + colorNum: 1, // ANN NEED TO FIX HERE + }); + }); + + /** + * Accessors - tell visx which value of each points we should use and where. */ const dataAccessors = { @@ -165,7 +133,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { return Math.log2(d?.foldChange); }, yAccessor: (d: any) => { - return -Math.log10(d?.adjustedPValue); + return -Math.log10(d?.pValue); }, }; @@ -250,18 +218,14 @@ function VolcanoPlot(props: VolcanoPlotProps) { {/* The data itself */} - {formattedData.map((series: any, index: any) => { - return ( - { - return significanceColors[d.colorNum]; - }} - /> - ); - })} + { + return significanceColors[d.colorNum]; + }} + />
diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 605d6e2506..96472bad32 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; import { scaleLinear } from '@visx/scale'; +import { range } from 'lodash'; +import { AreaSeries } from '@visx/xychart'; export default { title: 'Plots/VolcanoPlot', @@ -10,42 +12,64 @@ export default { interface VEuPathDBVolcanoPlotData { volcanoplot: { - series: Array<{ + series: { foldChange: string[]; pValue: string[]; adjustedPValue: string[]; pointId: string[]; - overlayValue: string; - }>; + }; }; } // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - series: [ - { - foldChange: ['2', '3'], - pValue: ['0.001', '0.0001'], - adjustedPValue: ['0.01', '0.001'], - pointId: ['a', 'b'], - overlayValue: 'positive', - }, - { - foldChange: ['0.5', '0.8', '1', '0.5', '0.1', '4', '0.2'], - pValue: ['0.001', '0.0001', '0.2', '0.1', '0.7', '0.1', '0.4'], - adjustedPValue: ['0.01', '0.001', '2', '1', '7', '1', '4'], - pointId: ['c', 'd', 'e', 'f', 'g', 'h', 'i'], - overlayValue: 'none', - }, - { - foldChange: ['0.01', '0.02', '0.03'], - pValue: ['0.001', '0.0001', '0.002'], - adjustedPValue: ['0.01', '0.001', '0.02'], - pointId: ['j', 'k', 'l'], - overlayValue: 'negative', - }, - ], + series: { + foldChange: [ + '2', + '3', + '0.5', + '0.8', + '1', + '0.5', + '0.1', + '4', + '0.2', + '0.01', + '0.02', + '0.03', + ], + pValue: [ + '0.001', + '0.0001', + '0.01', + '0.001', + '2', + '1', + '7', + '1', + '4', + '0.001', + '0.0001', + '0.002', + ], + adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02'], + pointId: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'], + }, + }, +}; + +const nPoints = 300; +const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { + volcanoplot: { + series: { + foldChange: range(1, nPoints).map((p) => String(6 * Math.random() + 3)), + pValue: range(1, nPoints).map((p) => String(Math.random())), + adjustedPValue: range(1, nPoints).map((p) => + String(nPoints * Math.random()) + ), + pointId: range(1, nPoints).map((p) => String(p)), + }, }, }; @@ -63,7 +87,7 @@ const Template: Story = (args) => { const comparisonLabels = ['up in group a', 'up in group b']; const volcanoPlotProps: VolcanoPlotProps = { - data: dataSetVolcano.volcanoplot, + data: args.data.volcanoplot.series, significanceThreshold: 0.1, foldChangeThreshold: 2, markerBodyOpacity: args.markerBodyOpacity, @@ -73,17 +97,21 @@ const Template: Story = (args) => { return ; }; -export const Basic = Template.bind({}); -Basic.args = { +export const Simple = Template.bind({}); +Simple.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, }; +export const ManyPoints = Template.bind({}); +ManyPoints.args = { + data: dataSetVolcanoManyPoints, + markerBodyOpacity: 0.5, +}; + +// Add story for truncation // export const Truncation = Template.bind({}) // Truncation.args = { // data: dataSetVolcano, // independentAxisRange: [] // } - -// this process input data function is similar to scatter's but not the same. -// would probably be worth revisiting what is in common and factoring accordingly diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index fb3ba989e2..121071fbf8 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,11 +1,6 @@ -export type VolcanoPlotDataSeries = { +export type VolcanoPlotData = { foldChange: string[]; pValue: string[]; adjustedPValue: string[]; pointId: string[]; }; - -export type VolcanoPlotData = { - /** an array of data series (aka traces) */ - series: VolcanoPlotDataSeries[]; -}; From ff6b5d87600a98d9ad2a399a541fcfadbb6a911b Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 7 Apr 2023 07:25:40 -0400 Subject: [PATCH 19/28] volcano colors points based on input thresholds --- .../libs/components/src/plots/VolcanoPlot.tsx | 79 ++++++++++++++----- .../src/stories/plots/VolcanoPlot.stories.tsx | 21 +++-- .../libs/components/src/types/plots/addOns.ts | 2 +- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index fa2d72bcc1..ba11e8f84c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -16,17 +16,17 @@ import { Group } from '@visx/group'; import { max, min } from 'lodash'; export interface VolcanoPlotProps extends PlotProps { - /** x-axis range: */ - independentAxisRange?: NumberRange; - /** y-axis range: */ - dependentAxisRange?: NumberRange; /** * Used to set the fold change thresholds. Will - * set two thresholds at +/- this number + * set two thresholds at +/- this number. */ - foldChangeThreshold?: number; + log2FoldChangeThreshold: number; /** Set the threshold for significance. */ - significanceThreshold?: number; + 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 @@ -46,6 +46,14 @@ const EmptyVolcanoPlotData: VolcanoPlotData = { 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. @@ -55,9 +63,9 @@ function VolcanoPlot(props: VolcanoPlotProps) { data = EmptyVolcanoPlotData, independentAxisRange, dependentAxisRange, - markerBodyOpacity, significanceThreshold, - foldChangeThreshold, + log2FoldChangeThreshold, + markerBodyOpacity, ...restProps } = props; @@ -104,13 +112,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { * Turn the data (array of arrays) into data points (array of points) */ - let dataPoints: { - foldChange: string; - pValue: string; - adjustedPValue: string; - pointId: string; - colorNum: number; - }[] = []; + let dataPoints: DataPoint[] = []; // Loop through the data and return points. Doesn't really matter // which var of the data we map over. @@ -120,7 +122,13 @@ function VolcanoPlot(props: VolcanoPlotProps) { pValue: data.pValue[ind], adjustedPValue: data.adjustedPValue[ind], pointId: data.pointId[ind], - colorNum: 1, // ANN NEED TO FIX HERE + color: assignSignificanceColor( + Math.log2(Number(fc)), + Number(data.pValue[ind]), + significanceThreshold, + log2FoldChangeThreshold, + significanceColors + ), }); }); @@ -193,11 +201,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { /> )} - {foldChangeThreshold && ( + {log2FoldChangeThreshold && ( <> { - return significanceColors[d.colorNum]; + return d.color; }} /> @@ -232,4 +240,33 @@ function VolcanoPlot(props: VolcanoPlotProps) { ); } +/** + * 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] +) { + // 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; diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 96472bad32..5e14cdd10d 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -8,6 +8,14 @@ import { AreaSeries } from '@visx/xychart'; export default { title: 'Plots/VolcanoPlot', component: VolcanoPlot, + argTypes: { + log2FoldChangeThreshold: { + control: { type: 'range', min: 0.5, max: 10, step: 0.5 }, + }, + significanceThreshold: { + control: { type: 'range', min: 0.001, max: 0.1, step: 0.001 }, + }, + }, } as Meta; interface VEuPathDBVolcanoPlotData { @@ -78,18 +86,18 @@ const plotTitle = 'Volcano erupt!'; interface TemplateProps { data: VEuPathDBVolcanoPlotData; markerBodyOpacity: number; - // foldChangeHighGate: number; - // foldChangeLowGate: number; // we can't have gates unless we mimic the backend updating the data format when we change gates + log2FoldChangeThreshold: number; + significanceThreshold: number; adjustedPValueGate: number; } const Template: Story = (args) => { - const comparisonLabels = ['up in group a', 'up in group b']; + const comparisonLabels = ['up in group a', 'up in group b']; // not yet used const volcanoPlotProps: VolcanoPlotProps = { data: args.data.volcanoplot.series, - significanceThreshold: 0.1, - foldChangeThreshold: 2, + significanceThreshold: args.significanceThreshold, + log2FoldChangeThreshold: args.log2FoldChangeThreshold, markerBodyOpacity: args.markerBodyOpacity, comparisonLabels: comparisonLabels, }; @@ -97,10 +105,13 @@ const Template: Story = (args) => { return ; }; +// Stories! export const Simple = Template.bind({}); Simple.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, + log2FoldChangeThreshold: 1, + significanceThreshold: 0.01, }; export const ManyPoints = Template.bind({}); diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 1afd0e11d5..359b2422ad 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -284,7 +284,7 @@ export const gradientConvergingColorscaleMap = scaleLinear() .interpolate(interpolateLab); // Significance colors -export const significanceColors = ['#AC3B4E', '#B5B8B4', '#0E8FAB']; +export const significanceColors = ['#AC3B4E', '#0E8FAB', '#B5B8B4']; /** truncated axis flags */ export type AxisTruncationAddon = { From 0a3109593eeddafc7a089c0b88adadd4d14f81ea Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 7 Apr 2023 07:42:06 -0400 Subject: [PATCH 20/28] fix volcano story with many points --- .../src/stories/plots/ScatterPlot.storyData.ts | 5 ++++- .../src/stories/plots/VolcanoPlot.stories.tsx | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/stories/plots/ScatterPlot.storyData.ts b/packages/libs/components/src/stories/plots/ScatterPlot.storyData.ts index 4cb89f8787..5c455c7d50 100644 --- a/packages/libs/components/src/stories/plots/ScatterPlot.storyData.ts +++ b/packages/libs/components/src/stories/plots/ScatterPlot.storyData.ts @@ -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; diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 5e14cdd10d..53090b3294 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -1,9 +1,7 @@ -import React, { useState } from 'react'; import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; -import { scaleLinear } from '@visx/scale'; import { range } from 'lodash'; -import { AreaSeries } from '@visx/xychart'; +import { getNormallyDistributedRandomNumber } from './ScatterPlot.storyData'; export default { title: 'Plots/VolcanoPlot', @@ -71,8 +69,10 @@ const nPoints = 300; const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { volcanoplot: { series: { - foldChange: range(1, nPoints).map((p) => String(6 * Math.random() + 3)), - pValue: range(1, nPoints).map((p) => String(Math.random())), + foldChange: range(1, nPoints).map((p) => + String(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()) ), @@ -118,6 +118,8 @@ export const ManyPoints = Template.bind({}); ManyPoints.args = { data: dataSetVolcanoManyPoints, markerBodyOpacity: 0.5, + log2FoldChangeThreshold: 3, + significanceThreshold: 0.01, }; // Add story for truncation From fe8114688d5fd10d982e10ad626ae6ea811f47d6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 7 Apr 2023 07:49:50 -0400 Subject: [PATCH 21/28] fix volcano y axis so that it doesnt always start at 0 --- packages/libs/components/src/plots/VolcanoPlot.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index ba11e8f84c..bd8e8a79d2 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -91,9 +91,9 @@ function VolcanoPlot(props: VolcanoPlotProps) { // Log transform for plotting, and add a little margin for axes if (dataXMin && dataXMax) { xMin = Math.log2(dataXMin); - xMin = xMin - (xMin - xMin) * 0.05; xMax = Math.log2(dataXMax); - xMax = xMax + (xMax - xMax) * 0.05; + xMin = xMin - (xMax - xMin) * 0.05; + xMax = xMax + (xMax - xMin) * 0.05; } else { xMin = 0; xMax = 0; @@ -101,12 +101,13 @@ function VolcanoPlot(props: VolcanoPlotProps) { if (dataYMin && dataYMax) { yMin = -Math.log10(dataYMax); yMax = -Math.log10(dataYMin); - yMin = yMin - (yMin - yMin) * 0.05; - yMax = yMax + (yMax - yMax) * 0.05; + yMin = yMin - (yMax - yMin) * 0.05; + yMax = yMax + (yMax - yMin) * 0.05; } else { yMin = 0; yMax = 0; } + console.log(yMin); /** * Turn the data (array of arrays) into data points (array of points) @@ -171,6 +172,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { stroke: '#dddddd', strokeWidth: 0.5, }; + console.log(yMin); return ( // From docs " For correct tooltip positioning, it is important to wrap your @@ -179,7 +181,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { From 9b628917fe20e8d5d46cf96b880ff5a761ff6bb6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 10 Apr 2023 05:37:23 -0400 Subject: [PATCH 22/28] remove series from volcano --- .../libs/components/src/plots/VolcanoPlot.tsx | 2 - .../src/stories/plots/VolcanoPlot.stories.tsx | 92 ++-- yarn.lock | 442 +++++++++++++++++- 3 files changed, 482 insertions(+), 54 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index bd8e8a79d2..95afa39327 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -107,7 +107,6 @@ function VolcanoPlot(props: VolcanoPlotProps) { yMin = 0; yMax = 0; } - console.log(yMin); /** * Turn the data (array of arrays) into data points (array of points) @@ -172,7 +171,6 @@ function VolcanoPlot(props: VolcanoPlotProps) { stroke: '#dddddd', strokeWidth: 0.5, }; - console.log(yMin); return ( // From docs " For correct tooltip positioning, it is important to wrap your diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 53090b3294..fb6a74f8a7 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -18,66 +18,60 @@ export default { interface VEuPathDBVolcanoPlotData { volcanoplot: { - series: { - foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointId: string[]; - }; + foldChange: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointId: string[]; }; } // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - series: { - foldChange: [ - '2', - '3', - '0.5', - '0.8', - '1', - '0.5', - '0.1', - '4', - '0.2', - '0.01', - '0.02', - '0.03', - ], - pValue: [ - '0.001', - '0.0001', - '0.01', - '0.001', - '2', - '1', - '7', - '1', - '4', - '0.001', - '0.0001', - '0.002', - ], - adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02'], - pointId: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'], - }, + foldChange: [ + '2', + '3', + '0.5', + '0.8', + '1', + '0.5', + '0.1', + '4', + '0.2', + '0.01', + '0.02', + '0.03', + ], + pValue: [ + '0.001', + '0.0001', + '0.01', + '0.001', + '2', + '1', + '7', + '1', + '4', + '0.001', + '0.0001', + '0.002', + ], + adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02'], + pointId: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'], }, }; const nPoints = 300; const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { volcanoplot: { - series: { - foldChange: range(1, nPoints).map((p) => - String(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)), - }, + foldChange: range(1, nPoints).map((p) => + String(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)), }, }; @@ -95,7 +89,7 @@ const Template: Story = (args) => { const comparisonLabels = ['up in group a', 'up in group b']; // not yet used const volcanoPlotProps: VolcanoPlotProps = { - data: args.data.volcanoplot.series, + data: args.data.volcanoplot, significanceThreshold: args.significanceThreshold, log2FoldChangeThreshold: args.log2FoldChangeThreshold, markerBodyOpacity: args.markerBodyOpacity, diff --git a/yarn.lock b/yarn.lock index 630ea1e0b6..230da7597c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3689,6 +3689,137 @@ __metadata: languageName: node linkType: hard +"@react-spring/animated@npm:~9.7.2": + version: 9.7.2 + resolution: "@react-spring/animated@npm:9.7.2" + dependencies: + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 08f2f1e54468776b2abf76e3c3504129b8d965875781bc1cfd40adddff8f310869ecfb6d18e937df8ed63a01b3d0cb2335fb531be03ddf25f6f58583a91e08e7 + languageName: node + linkType: hard + +"@react-spring/core@npm:~9.7.1, @react-spring/core@npm:~9.7.2": + version: 9.7.2 + resolution: "@react-spring/core@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/rafz": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 7d14369d6bc3cb4f51bce946a0499484bd03a06b2602edd1f0478806759fd2a5f0800892cdf8d38a3bd4ef949f3c61de65933c46784683ea757f16db710a04fd + languageName: node + linkType: hard + +"@react-spring/konva@npm:~9.7.1": + version: 9.7.2 + resolution: "@react-spring/konva@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/core": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + konva: ">=2.6" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-konva: ^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0 + checksum: de1819722d578355a51b7e70509adde747650c22df7540a81b989a423292110b606e5675d41e70cce332b528bfe62e9b56571ed70acf72896014f8c852b2a777 + languageName: node + linkType: hard + +"@react-spring/native@npm:~9.7.1": + version: 9.7.2 + resolution: "@react-spring/native@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/core": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || >=17.0.0 || >=18.0.0 + react-native: ">=0.58" + checksum: 11963f7c9993f1c34bd32f82a1425f9e50d83c86f28ff53940aed87c677732d11e379e596fd2728861b5d8978255496eb63769d54018f943f15f6d80628e17a0 + languageName: node + linkType: hard + +"@react-spring/rafz@npm:~9.7.2": + version: 9.7.2 + resolution: "@react-spring/rafz@npm:9.7.2" + checksum: 88ad6275ed172745c7cd309e6a06bb52b76b6f391510afcf8ad12d1fc50950e74f902f96bcaae0895ab75e47a1d809d36fe843ce8d3d0d2ea2d6700ff9e03d81 + languageName: node + linkType: hard + +"@react-spring/shared@npm:~9.7.2": + version: 9.7.2 + resolution: "@react-spring/shared@npm:9.7.2" + dependencies: + "@react-spring/rafz": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f3041a537dddad63c64e68890f3276b72c4e2d5828621a09dbd722edb7a12edc0026555118f8305b969253f44e409ddd411dd5b99ec4e04edea32bee5e5f57fd + languageName: node + linkType: hard + +"@react-spring/three@npm:~9.7.1": + version: 9.7.2 + resolution: "@react-spring/three@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/core": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + "@react-three/fiber": ">=6.0" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: ">=0.126" + checksum: 970efd0fb7eee29edc0f468fde3ab590905cd9136e6f1a017dc40b9b94dc1bf1befe399ac08848994b1359f83339be93b09eb5b30f9f2ff97f9f0477bb4de45c + languageName: node + linkType: hard + +"@react-spring/types@npm:~9.7.2": + version: 9.7.2 + resolution: "@react-spring/types@npm:9.7.2" + checksum: 145a79d2f40dfc9f0a4b54db17890c6471b4e50563c01951ca13022977b0d8009865c1839cf07a762fd33ae0b83f2d6972e2d0fe50bd3105942b9cea5253612e + languageName: node + linkType: hard + +"@react-spring/web@npm:~9.7.1": + version: 9.7.2 + resolution: "@react-spring/web@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/core": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f872dfd9bbbfc27f32e4406076729a36169e2da3785709fbf981b7d3a77a1a430d7f295b300a9acc8cb4805d0965e6d9a54914a4263abfd96806b7ae53ba69cd + languageName: node + linkType: hard + +"@react-spring/zdog@npm:~9.7.1": + version: 9.7.2 + resolution: "@react-spring/zdog@npm:9.7.2" + dependencies: + "@react-spring/animated": ~9.7.2 + "@react-spring/core": ~9.7.2 + "@react-spring/shared": ~9.7.2 + "@react-spring/types": ~9.7.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-zdog: ">=1.0" + zdog: ">=1.0" + checksum: 550edcb6a3632c97a3511d35b91bc8d9ef09542846572be40017ceeea404f4a6579e34b2888ae39abf8c00f97cee2152a8266d2fe5f60176c51ab680bdad93e1 + languageName: node + linkType: hard + "@restart/context@npm:^2.1.4": version: 2.1.4 resolution: "@restart/context@npm:2.1.4" @@ -6221,7 +6352,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-interpolate@npm:*": +"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "@types/d3-interpolate@npm:3.0.1" dependencies: @@ -6288,7 +6419,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-scale@npm:*": +"@types/d3-scale@npm:*, @types/d3-scale@npm:^4.0.2": version: 4.0.3 resolution: "@types/d3-scale@npm:4.0.3" dependencies: @@ -7858,6 +7989,7 @@ __metadata: "@veupathdb/coreui": "workspace:^" "@veupathdb/eslint-config": "workspace:^" "@veupathdb/tsconfig": "workspace:^" + "@visx/axis": ^3.1.0 "@visx/gradient": ^1.0.0 "@visx/group": ^1.0.0 "@visx/hierarchy": ^1.0.0 @@ -7865,6 +7997,7 @@ __metadata: "@visx/text": ^1.3.0 "@visx/tooltip": ^1.3.0 "@visx/visx": ^1.1.0 + "@visx/xychart": ^3.1.0 babel-loader: ^8.1.0 bootstrap: ^4.5.2 color-math: ^1.1.3 @@ -7892,6 +8025,8 @@ __metadata: 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 stats-lite: ^2.2.0 storybook: ^6.5.14 @@ -8740,6 +8875,23 @@ __metadata: languageName: node linkType: hard +"@visx/annotation@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/annotation@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/drag": 3.0.1 + "@visx/group": 3.0.0 + "@visx/text": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.5.10 + react-use-measure: ^2.0.4 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: e1a3016b8b00c843b56a41c0b07c5509ea195948408c856f09fd2db3268fbc4641f1e6adf0022e60a042cd25d7f0334e7ac6128a367c92a27e42770573eef5f6 + languageName: node + linkType: hard + "@visx/axis@npm:1.17.1": version: 1.17.1 resolution: "@visx/axis@npm:1.17.1" @@ -8758,6 +8910,24 @@ __metadata: languageName: node linkType: hard +"@visx/axis@npm:3.1.0, @visx/axis@npm:^3.1.0": + version: 3.1.0 + resolution: "@visx/axis@npm:3.1.0" + dependencies: + "@types/react": "*" + "@visx/group": 3.0.0 + "@visx/point": 3.0.1 + "@visx/scale": 3.0.0 + "@visx/shape": 3.0.0 + "@visx/text": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.6.0 + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 9f69e37823bba06ffddf2126b93eb3b3468fa88a82e40c7a1a0678dfdb7950a2e430db06055d6e746504e2092107012e5a9d3815c4de16f87cc3cc3bbdba574b + languageName: node + linkType: hard + "@visx/bounds@npm:1.7.0": version: 1.7.0 resolution: "@visx/bounds@npm:1.7.0" @@ -8772,6 +8942,20 @@ __metadata: languageName: node linkType: hard +"@visx/bounds@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/bounds@npm:3.0.0" + dependencies: + "@types/react": "*" + "@types/react-dom": "*" + prop-types: ^15.5.10 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + react-dom: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 3c50dad1c78b526bebe20a690360ae5ad3dd2a31c86150553071671e49309a9fcf3bca3303eab7f7ac2e28be8d7987ac9ce5daa28b8df32c647b0aa97f118fa9 + languageName: node + linkType: hard + "@visx/brush@npm:1.18.1": version: 1.18.1 resolution: "@visx/brush@npm:1.18.1" @@ -8810,6 +8994,16 @@ __metadata: languageName: node linkType: hard +"@visx/curve@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/curve@npm:3.0.0" + dependencies: + "@types/d3-shape": ^1.3.1 + d3-shape: ^1.0.6 + checksum: 1707b2b0e002d35e77cec3c5fbf8a05e70f10881ffb123edf7e04e9ccfb05262e1cbbc0dfd45067e1c3ae5bc653325ef728fe880fecc7be9620ae267631f4149 + languageName: node + linkType: hard + "@visx/drag@npm:1.18.1": version: 1.18.1 resolution: "@visx/drag@npm:1.18.1" @@ -8823,6 +9017,20 @@ __metadata: languageName: node linkType: hard +"@visx/drag@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/drag@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/event": 3.0.1 + "@visx/point": 3.0.1 + prop-types: ^15.5.10 + peerDependencies: + react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 256e4a7b649e29b2112f77a57a657cd3aedb76b605ebc8b49e6c3d4b152fea5d81de7820060dc569dda0c81a0e7b6668ce9fe071536198b26cf2ebd655a6e6a9 + languageName: node + linkType: hard + "@visx/event@npm:1.7.0": version: 1.7.0 resolution: "@visx/event@npm:1.7.0" @@ -8833,6 +9041,16 @@ __metadata: languageName: node linkType: hard +"@visx/event@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/event@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/point": 3.0.1 + checksum: 0cb4dd578bbe54bd428a0cfae9e64db141b039c2c6c1412d1cd1d04e1613d193212b031124948ca3b7eed877cdea87d161cc3a008b973c6f217e21a8a2dd27b0 + languageName: node + linkType: hard + "@visx/geo@npm:1.17.1": version: 1.17.1 resolution: "@visx/geo@npm:1.17.1" @@ -8866,6 +9084,22 @@ __metadata: languageName: node linkType: hard +"@visx/glyph@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/glyph@npm:3.0.0" + dependencies: + "@types/d3-shape": ^1.3.1 + "@types/react": "*" + "@visx/group": 3.0.0 + classnames: ^2.3.1 + d3-shape: ^1.2.0 + prop-types: ^15.6.2 + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 0a944f2594c519e66e66dbc806e9ff2e53bbc64bfd8c2106420d97f0c47824d22b6e733f7bed3591dd94fb00b32ead68c34fd3d342fc42968182d511ac9996c5 + languageName: node + linkType: hard + "@visx/gradient@npm:1.7.0, @visx/gradient@npm:^1.0.0": version: 1.7.0 resolution: "@visx/gradient@npm:1.7.0" @@ -8896,6 +9130,24 @@ __metadata: languageName: node linkType: hard +"@visx/grid@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/grid@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/curve": 3.0.0 + "@visx/group": 3.0.0 + "@visx/point": 3.0.1 + "@visx/scale": 3.0.0 + "@visx/shape": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.6.2 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 2ff960b180f0ac1960170f3628a49667f3da58751242638af7844eb49a30273e52bba380103c14bb5b4829ca4bef0c072a8a78696abf2b41d4a49fcdbc02a263 + languageName: node + linkType: hard + "@visx/group@npm:1.17.1, @visx/group@npm:^1.0.0": version: 1.17.1 resolution: "@visx/group@npm:1.17.1" @@ -8909,6 +9161,19 @@ __metadata: languageName: node linkType: hard +"@visx/group@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/group@npm:3.0.0" + dependencies: + "@types/react": "*" + classnames: ^2.3.1 + prop-types: ^15.6.2 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: f52b946fb39cfcd4798bd5c77b729ceb41b2041e0464edeef2f15955c4022d51ed9f7626f2a7ac331f8430c84037254be5dd38f8282881a63d1680b27f41d04d + languageName: node + linkType: hard + "@visx/heatmap@npm:1.17.1": version: 1.17.1 resolution: "@visx/heatmap@npm:1.17.1" @@ -9013,6 +9278,13 @@ __metadata: languageName: node linkType: hard +"@visx/point@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/point@npm:3.0.1" + checksum: 727f314826eb85a608b4088a9e2fedb7e6996a5f0fdddaeb40702a51aa04da07fdbc5fdcf2d595d914dfa083389c644f769fc69b3a247b4da274936dd58c7bb8 + languageName: node + linkType: hard + "@visx/react-spring@npm:1.17.1": version: 1.17.1 resolution: "@visx/react-spring@npm:1.17.1" @@ -9031,6 +9303,24 @@ __metadata: languageName: node linkType: hard +"@visx/react-spring@npm:3.1.0": + version: 3.1.0 + resolution: "@visx/react-spring@npm:3.1.0" + dependencies: + "@types/react": "*" + "@visx/axis": 3.1.0 + "@visx/grid": 3.0.1 + "@visx/scale": 3.0.0 + "@visx/text": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.6.2 + peerDependencies: + "@react-spring/web": ^9.4.5 + react: ^16.3.0-0 || ^17.0.0 || ^18.0.0 + checksum: 5d727dd8053c101d6473580feba5b8c05d70fa6311a0dc2f9c1d420c6fce4ce497f7f191b89bb10c53eedd986aa3006cd7a7f389035c02a2a847f8c3c38fcafb + languageName: node + linkType: hard + "@visx/responsive@npm:1.10.1": version: 1.10.1 resolution: "@visx/responsive@npm:1.10.1" @@ -9046,6 +9336,20 @@ __metadata: languageName: node linkType: hard +"@visx/responsive@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/responsive@npm:3.0.0" + dependencies: + "@types/lodash": ^4.14.172 + "@types/react": "*" + lodash: ^4.17.21 + prop-types: ^15.6.1 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 0a547c7cec9f2f61931d1de6ee8955b76904ea0bcc1c55d18d991067f63579c6ca1d1b976a0ada21d3b3818551b3d5749d8b11ab50dc472f38bd94f21b56b8ec + languageName: node + linkType: hard + "@visx/scale@npm:1.14.0": version: 1.14.0 resolution: "@visx/scale@npm:1.14.0" @@ -9060,6 +9364,20 @@ __metadata: languageName: node linkType: hard +"@visx/scale@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/scale@npm:3.0.0" + dependencies: + "@types/d3-interpolate": ^3.0.1 + "@types/d3-scale": ^4.0.2 + "@types/d3-time": ^2.0.0 + d3-interpolate: ^3.0.1 + d3-scale: ^4.0.2 + d3-time: ^2.1.1 + checksum: 3740fd0f6844b2297847f48f34afa9772e35d8b6faf26fb7b18c78fcd83634de57a44cd4aa9263dfb3a62bec609cf24b36db26f6db8ba36908dbdb83f359d9ed + languageName: node + linkType: hard + "@visx/shape@npm:1.17.1, @visx/shape@npm:^1.4.0": version: 1.17.1 resolution: "@visx/shape@npm:1.17.1" @@ -9082,6 +9400,28 @@ __metadata: languageName: node linkType: hard +"@visx/shape@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/shape@npm:3.0.0" + dependencies: + "@types/d3-path": ^1.0.8 + "@types/d3-shape": ^1.3.1 + "@types/lodash": ^4.14.172 + "@types/react": "*" + "@visx/curve": 3.0.0 + "@visx/group": 3.0.0 + "@visx/scale": 3.0.0 + classnames: ^2.3.1 + d3-path: ^1.0.5 + d3-shape: ^1.2.0 + lodash: ^4.17.21 + prop-types: ^15.5.10 + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: d6e71e0c5dd90957a38b1404e8357cd2192f741f8c7b4f73fac3c007e4b0af398f5cf648f5f1ecc5ca72759e729cfa58fff4f8a409f95a344219309881e58b37 + languageName: node + linkType: hard + "@visx/text@npm:1.17.1, @visx/text@npm:^1.3.0": version: 1.17.1 resolution: "@visx/text@npm:1.17.1" @@ -9098,6 +9438,22 @@ __metadata: languageName: node linkType: hard +"@visx/text@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/text@npm:3.0.0" + dependencies: + "@types/lodash": ^4.14.172 + "@types/react": "*" + classnames: ^2.3.1 + lodash: ^4.17.21 + prop-types: ^15.7.2 + reduce-css-calc: ^1.3.0 + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: a8456fceb785db17487103ee3c2c5acb5988f0ec883735ecaf60fa734d8876c0d72eab6862dc8e272cf87644694bc21aff5c9cb9eccf4e948bd0899926513efd + languageName: node + linkType: hard + "@visx/tooltip@npm:1.17.1, @visx/tooltip@npm:^1.3.0": version: 1.17.1 resolution: "@visx/tooltip@npm:1.17.1" @@ -9114,6 +9470,22 @@ __metadata: languageName: node linkType: hard +"@visx/tooltip@npm:3.1.2": + version: 3.1.2 + resolution: "@visx/tooltip@npm:3.1.2" + dependencies: + "@types/react": "*" + "@visx/bounds": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.5.10 + react-use-measure: ^2.0.4 + peerDependencies: + react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 + react-dom: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 4c2127068f22766e984ed3ed1452dcece919bacb0297b17ba862a6815bb7f04c4fc462a100d3934e0cb56a9c7962b82f7c4184748ef4b2dbef3b4b0d5b717fdd + languageName: node + linkType: hard + "@visx/visx@npm:^1.1.0": version: 1.18.1 resolution: "@visx/visx@npm:1.18.1" @@ -9168,6 +9540,21 @@ __metadata: languageName: node linkType: hard +"@visx/voronoi@npm:3.0.0": + version: 3.0.0 + resolution: "@visx/voronoi@npm:3.0.0" + dependencies: + "@types/d3-voronoi": ^1.1.9 + "@types/react": "*" + classnames: ^2.3.1 + d3-voronoi: ^1.1.2 + prop-types: ^15.6.1 + peerDependencies: + react: ^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 7c0f4ff846401bae337bf3e961e3807a1ae7b6e50da3817dd7bfba0c8b40017079ef81baa6145bab61b29368d27d2852b5fcf17ee52b372ab17ff2d81a087b9a + languageName: node + linkType: hard + "@visx/xychart@npm:1.18.1": version: 1.18.1 resolution: "@visx/xychart@npm:1.18.1" @@ -9200,6 +9587,38 @@ __metadata: languageName: node linkType: hard +"@visx/xychart@npm:^3.1.0": + version: 3.1.2 + resolution: "@visx/xychart@npm:3.1.2" + dependencies: + "@types/lodash": ^4.14.172 + "@types/react": "*" + "@visx/annotation": 3.0.1 + "@visx/axis": 3.1.0 + "@visx/event": 3.0.1 + "@visx/glyph": 3.0.0 + "@visx/grid": 3.0.1 + "@visx/react-spring": 3.1.0 + "@visx/responsive": 3.0.0 + "@visx/scale": 3.0.0 + "@visx/shape": 3.0.0 + "@visx/text": 3.0.0 + "@visx/tooltip": 3.1.2 + "@visx/voronoi": 3.0.0 + classnames: ^2.3.1 + d3-array: ^2.6.0 + d3-interpolate-path: 2.2.1 + d3-shape: ^2.0.0 + lodash: ^4.17.21 + mitt: ^2.1.0 + prop-types: ^15.6.2 + peerDependencies: + "@react-spring/web": ^9.4.5 + react: ^16.8.0 || ^17.0.0 || ^ 18.0.0 + checksum: f6776a4da8000811c7f71e2285610dfe26b159f43904ba8e124225219a6791a89b9ded2f71c4f04f38da03f10b59e5f0d63a6b2b7f8c688aebc9107be4ec2e57 + languageName: node + linkType: hard + "@visx/zoom@npm:1.14.1": version: 1.14.1 resolution: "@visx/zoom@npm:1.14.1" @@ -15022,7 +15441,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:4": +"d3-scale@npm:4, d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -30597,6 +31016,23 @@ __metadata: languageName: node linkType: hard +"react-spring@npm:^9.7.1": + version: 9.7.1 + resolution: "react-spring@npm:9.7.1" + dependencies: + "@react-spring/core": ~9.7.1 + "@react-spring/konva": ~9.7.1 + "@react-spring/native": ~9.7.1 + "@react-spring/three": ~9.7.1 + "@react-spring/web": ~9.7.1 + "@react-spring/zdog": ~9.7.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 75f31601d9a8e94e93e66ccfed2ced8cc71399835cf4d509eae6b373315f182f3b5ca0d96da7cd132f01faa6b1cf89dcf087ca43bf40e51c3202c6713f730c28 + languageName: node + linkType: hard + "react-table@npm:^7.7.0": version: 7.8.0 resolution: "react-table@npm:7.8.0" From 9469695340828b3b12a58177c248d836851f1066 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 13 Apr 2023 14:14:31 -0400 Subject: [PATCH 23/28] synchronous PR review feedback --- .../libs/components/src/plots/VolcanoPlot.tsx | 26 +++++++++++++++---- .../components/src/types/plots/volcanoplot.ts | 8 ++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 95afa39327..c25ac0c7e2 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -92,15 +92,19 @@ function VolcanoPlot(props: VolcanoPlotProps) { if (dataXMin && dataXMax) { xMin = Math.log2(dataXMin); xMax = Math.log2(dataXMax); - xMin = xMin - (xMax - xMin) * 0.05; - xMax = xMax + (xMax - xMin) * 0.05; + // Adding the extra buffer + // Could extract into a function? Shared with scatterplot + xMin = xMin - (xMax - xMin) * 0.05; //ANN add descriptive comment + xMax = xMax + (xMax - xMin) * 0.05; // Mention the problem it solves (preventing data points from being clipped) } else { xMin = 0; xMax = 0; } if (dataYMin && dataYMax) { + // note that negative log because it's standard practice! yMin = -Math.log10(dataYMax); yMax = -Math.log10(dataYMin); + // add more commments here yMin = yMin - (yMax - yMin) * 0.05; yMax = yMax + (yMax - yMin) * 0.05; } else { @@ -110,12 +114,16 @@ function VolcanoPlot(props: VolcanoPlotProps) { /** * Turn the data (array of arrays) into data points (array of points) + * NOT TRUE ANYMORE! */ let dataPoints: DataPoint[] = []; // Loop through the data and return points. Doesn't really matter // which var of the data we map over. + // const dataPoints = data.foldChange.map(...) or .transform + // return the datapoint object + // note that this also signals that we're not going to update dataPoints data.foldChange.forEach((fc, ind: number) => { dataPoints.push({ foldChange: fc, @@ -137,8 +145,11 @@ function VolcanoPlot(props: VolcanoPlotProps) { */ const dataAccessors = { - xAccessor: (d: any) => { - return Math.log2(d?.foldChange); + // Can we make the type better??? + // Can we annotate the type of data accessor? Use some generic type here?? + xAccessor: (d: DataPoint) => { + // ANN improve types for all these accessors + return Math.log2(Number(d?.foldChange)); }, yAccessor: (d: any) => { return -Math.log10(d?.pValue); @@ -175,6 +186,9 @@ function VolcanoPlot(props: VolcanoPlotProps) { return ( // From docs " For correct tooltip positioning, it is important to wrap your // component in an element (e.g., div) with relative positioning." + // ANN add comments about why i put things in particular places or + // any magic i learned. Describing how i did things and why (since there's not + // a lot of docs on that)
= significanceThreshold) { return significanceColors[2]; diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index 121071fbf8..99248709be 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -4,3 +4,11 @@ export type VolcanoPlotData = { adjustedPValue: string[]; pointId: string[]; }; + +// would be more natural to have an array of objects, like an array of DataPoints +// (this is even more general, so not visx specific yay!) +// wouldn't have to worry about arrays having the same length +// can plot.data return that type of structure? +// Would be able to remove the whole data processing part +// Think of visualizatoin component as an adapter to the specific application +// Could the visualization do the processing? From b71d80989ba712af0635b33de3bb1329a3fde019 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 14 Apr 2023 06:21:24 -0400 Subject: [PATCH 24/28] volcano data now array of objects and add docs --- .../libs/components/src/plots/VolcanoPlot.tsx | 236 +++++++++--------- .../src/stories/plots/VolcanoPlot.stories.tsx | 66 +++-- .../components/src/types/plots/volcanoplot.ts | 23 +- 3 files changed, 171 insertions(+), 154 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index c25ac0c7e2..f008870a41 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -1,11 +1,11 @@ -import { PlotProps } from './PlotlyPlot'; - import { significanceColors } from '../types/plots'; -import { VolcanoPlotData } from '../types/plots/volcanoplot'; +import { + VolcanoPlotData, + VolcanoPlotDataPoint, +} from '../types/plots/volcanoplot'; import { NumberRange } from '../types/general'; import { XYChart, - Tooltip, Axis, Grid, GlyphSeries, @@ -15,17 +15,19 @@ import { import { Group } from '@visx/group'; import { max, min } from 'lodash'; -export interface VolcanoPlotProps extends PlotProps { +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. + * set two thresholds at +/- this number. Affects point colors */ log2FoldChangeThreshold: number; - /** Set the threshold for significance. */ + /** Set the threshold for significance. Affects point colors */ significanceThreshold: number; - /** x-axis range: */ + /** x-axis range */ independentAxisRange?: NumberRange; - /** y-axis range: */ + /** y-axis range */ dependentAxisRange?: NumberRange; /** * Array of size 2 that contains a label for the left and right side @@ -33,26 +35,40 @@ export interface VolcanoPlotProps extends PlotProps { * on the type of data we're using (genes vs taxa vs etc.) */ comparisonLabels?: Array; - /** What is this plot's name? */ + /** Title of the plot */ plotTitle?: string; - /** marker color opacity: range from 0 to 1 */ + /** marker fill opacity: range from 0 to 1 */ markerBodyOpacity?: number; + /** Height of plot */ + height?: number; + /** Width of plot */ + width?: number; } -const EmptyVolcanoPlotData: VolcanoPlotData = { - foldChange: [], - pValue: [], - adjustedPValue: [], - pointId: [], +/** moving to visx standard file... */ +type VisxPoint = { + x?: number; + y?: number; +}; +/** + * 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, }; -interface DataPoint { - foldChange: string; - pValue: string; - adjustedPValue: string; - pointId: string; - color: string; -} +const EmptyVolcanoPlotData: VolcanoPlotData = []; /** * The Volcano Plot displays points on a (magnitude change) by (significance) xy axis. @@ -66,141 +82,106 @@ function VolcanoPlot(props: VolcanoPlotProps) { significanceThreshold, log2FoldChangeThreshold, markerBodyOpacity, - ...restProps + height, + width, } = props; /** - * Find mins and maxes of the data and for the plot + * 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. */ - 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)); + // 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. - // - + // 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; - // Log transform for plotting, and add a little margin for axes + // X axis if (dataXMin && dataXMax) { - xMin = Math.log2(dataXMin); - xMax = Math.log2(dataXMax); - // Adding the extra buffer - // Could extract into a function? Shared with scatterplot - xMin = xMin - (xMax - xMin) * 0.05; //ANN add descriptive comment - xMax = xMax + (xMax - xMin) * 0.05; // Mention the problem it solves (preventing data points from being clipped) + // 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) { - // note that negative log because it's standard practice! + // Standard volcano plots have -log10(raw p value) as the y axis yMin = -Math.log10(dataYMax); yMax = -Math.log10(dataYMin); - // add more commments here - yMin = yMin - (yMax - yMin) * 0.05; - yMax = yMax + (yMax - yMin) * 0.05; + // 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; } /** - * Turn the data (array of arrays) into data points (array of points) - * NOT TRUE ANYMORE! - */ - - let dataPoints: DataPoint[] = []; - - // Loop through the data and return points. Doesn't really matter - // which var of the data we map over. - // const dataPoints = data.foldChange.map(...) or .transform - // return the datapoint object - // note that this also signals that we're not going to update dataPoints - 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. + * Accessors - tell visx which value of the data point we should use and where. */ const dataAccessors = { - // Can we make the type better??? - // Can we annotate the type of data accessor? Use some generic type here?? - xAccessor: (d: DataPoint) => { - // ANN improve types for all these accessors - return Math.log2(Number(d?.foldChange)); + xAccessor: (d: VolcanoPlotDataPoint) => { + return Number(d?.log2foldChange); }, - yAccessor: (d: any) => { - return -Math.log10(d?.pValue); + yAccessor: (d: VolcanoPlotDataPoint) => { + return -Math.log10(Number(d?.pValue)); }, }; const thresholdLineAccessors = { - xAccessor: (d: any) => { + xAccessor: (d: VisxPoint) => { return d?.x; }, - yAccessor: (d: any) => { + yAccessor: (d: VisxPoint) => { 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, - }; - return ( - // From docs " For correct tooltip positioning, it is important to wrap your - // component in an element (e.g., div) with relative positioning." - // ANN add comments about why i put things in particular places or - // any magic i learned. Describing how i did things and why (since there's not - // a lot of docs on that) + // Relative positioning so that tooltips are positioned correctly (they are positioned absolutely)
+ {/* 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). */} + {/* Set up the axes and grid lines. XYChart magically lays them out correctly */} - {/* Draw threshold lines as annotations below the data points */} + {/* 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. */} + + {/* Horizontal significance threshold */} {significanceThreshold && ( )} + {/* Both vertical log2 fold change threshold lines */} {log2FoldChangeThreshold && ( <> so there should be a way to pass opacity + down to those elements, but I haven't found it yet */} { - return d.color; + return assignSignificanceColor( + Number(d.log2foldChange), + Number(d.pValue), + significanceThreshold, + log2FoldChangeThreshold, + significanceColors + ); }} /> @@ -258,31 +249,34 @@ function VolcanoPlot(props: VolcanoPlotProps) { * 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 + log2foldChange: number, + pValue: number, significanceThreshold: number, log2FoldChangeThreshold: number, - significanceColors: string[] // Assuming the order is [high (up regulated), low (down regulated), not significant] + significanceColors: string[] // Assuming the order is [high (up regulated), low (down regulated), insignificant] ) { - // Look at Sam's comment for improving readability + // Name indices of the significanceColors array for easier accessing. + const HIGH = 0; + const LOW = 1; + const INSIGNIFICANT = 2; // Test 1. If the y value is higher than the significance threshold, just return not significant - if (yValue >= significanceThreshold) { - return significanceColors[2]; + if (pValue >= significanceThreshold) { + return significanceColors[INSIGNIFICANT]; } // Test 2. So the y is significant. Is the x larger than the positive foldChange threshold? - if (xValue >= log2FoldChangeThreshold) { - return significanceColors[0]; + if (log2foldChange >= log2FoldChangeThreshold) { + return significanceColors[HIGH]; } // Test 3. Is the x value lower than the negative foldChange threshold? - if (xValue <= -log2FoldChangeThreshold) { - return significanceColors[1]; + if (log2foldChange <= -log2FoldChangeThreshold) { + return significanceColors[LOW]; } // If we're still here, it must be a non significant point. - return significanceColors[2]; + return significanceColors[INSIGNIFICANT]; } export default VolcanoPlot; diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index fb6a74f8a7..afa48c9023 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -2,23 +2,28 @@ import VolcanoPlot, { VolcanoPlotProps } from '../../plots/VolcanoPlot'; import { Story, Meta } from '@storybook/react/types-6-0'; import { range } from 'lodash'; import { getNormallyDistributedRandomNumber } from './ScatterPlot.storyData'; +import { VolcanoPlotData } from '../../types/plots/volcanoplot'; export default { title: 'Plots/VolcanoPlot', component: VolcanoPlot, argTypes: { log2FoldChangeThreshold: { - control: { type: 'range', min: 0.5, max: 10, step: 0.5 }, + control: { type: 'range', min: 0.5, max: 10, step: 0.01 }, }, significanceThreshold: { - control: { type: 'range', min: 0.001, max: 0.1, step: 0.001 }, + control: { type: 'range', min: 0.0001, max: 0.2, step: 0.001 }, }, }, } as Meta; +// Currently going to assume that the backend will send us data like this. Then +// the volcano visualization will do some processing. Will discuss +// with Danielle if we can have the backend send us an array of objects +// instead of this object of arrays... interface VEuPathDBVolcanoPlotData { volcanoplot: { - foldChange: string[]; + log2foldChange: string[]; pValue: string[]; adjustedPValue: string[]; pointId: string[]; @@ -28,30 +33,30 @@ interface VEuPathDBVolcanoPlotData { // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - foldChange: [ + log2foldChange: [ '2', '3', '0.5', - '0.8', + '-0.1', '1', - '0.5', - '0.1', + '-0.5', + '-1.2', '4', '0.2', - '0.01', - '0.02', - '0.03', + '-8', + '-4', + '-3', ], pValue: [ '0.001', '0.0001', '0.01', '0.001', - '2', + '0.98', '1', - '7', + '0.8', '1', - '4', + '0.6', '0.001', '0.0001', '0.002', @@ -61,11 +66,12 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { }, }; +// Make a fake dataset with lots of points! const nPoints = 300; const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { volcanoplot: { - foldChange: range(1, nPoints).map((p) => - String(Math.abs(getNormallyDistributedRandomNumber(0, 5))) + 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) => @@ -75,8 +81,6 @@ const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { }, }; -const plotTitle = 'Volcano erupt!'; - interface TemplateProps { data: VEuPathDBVolcanoPlotData; markerBodyOpacity: number; @@ -86,20 +90,37 @@ interface TemplateProps { } const Template: Story = (args) => { - const comparisonLabels = ['up in group a', 'up in group b']; // not yet used + // Eventually should be a Template prop. Not yet implemented in the component. + const comparisonLabels = ['up in group a', 'up in group b']; + + // Process input data. Take the object of arrays and turn it into + // an array of data points + const volcanoDataPoints: VolcanoPlotData = + 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], + }; + }); const volcanoPlotProps: VolcanoPlotProps = { - data: args.data.volcanoplot, + data: volcanoDataPoints, significanceThreshold: args.significanceThreshold, log2FoldChangeThreshold: args.log2FoldChangeThreshold, markerBodyOpacity: args.markerBodyOpacity, - comparisonLabels: comparisonLabels, + comparisonLabels: comparisonLabels, // currently does nothing. not yet implemented. }; return ; }; -// Stories! +/** + * Stories + */ + +// A small volcano plot. Proof of concept export const Simple = Template.bind({}); Simple.args = { data: dataSetVolcano, @@ -108,6 +129,9 @@ Simple.args = { significanceThreshold: 0.01, }; +// Most volcano plots will have thousands of points, since each point +// represents a gene or taxa. Make a volcano plot with +// a lot of points. export const ManyPoints = Template.bind({}); ManyPoints.args = { data: dataSetVolcanoManyPoints, diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index 99248709be..01b1a0bd58 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,14 +1,13 @@ -export type VolcanoPlotData = { - foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointId: string[]; +export type VolcanoPlotDataPoint = { + // log2foldChange becomes the x axis. Also used for coloring points + log2foldChange: 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; + // Used for thresholding and tooltip + adjustedPValue: string; + // Used for tooltip + pointId: string; }; -// would be more natural to have an array of objects, like an array of DataPoints -// (this is even more general, so not visx specific yay!) -// wouldn't have to worry about arrays having the same length -// can plot.data return that type of structure? -// Would be able to remove the whole data processing part -// Think of visualizatoin component as an adapter to the specific application -// Could the visualization do the processing? +export type VolcanoPlotData = Array; From 07b2575e1f84fdb0a71b8b6466b844f914cfc261 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 14 Apr 2023 06:34:27 -0400 Subject: [PATCH 25/28] move visx styles and types to own file --- .../libs/components/src/plots/VolcanoPlot.tsx | 29 ++++--------------- .../components/src/plots/visxVEuPathDB.tsx | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 packages/libs/components/src/plots/visxVEuPathDB.tsx diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index f008870a41..ab15bb686c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -14,6 +14,12 @@ import { } 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 */ @@ -45,29 +51,6 @@ export interface VolcanoPlotProps { width?: number; } -/** moving to visx standard file... */ -type VisxPoint = { - x?: number; - y?: number; -}; -/** - * 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, -}; - const EmptyVolcanoPlotData: VolcanoPlotData = []; /** diff --git a/packages/libs/components/src/plots/visxVEuPathDB.tsx b/packages/libs/components/src/plots/visxVEuPathDB.tsx new file mode 100644 index 0000000000..cbe03bdd86 --- /dev/null +++ b/packages/libs/components/src/plots/visxVEuPathDB.tsx @@ -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, +}; From 94d7d411de7f8aa40873570b6a03111dc5598893 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 14 Apr 2023 06:43:08 -0400 Subject: [PATCH 26/28] documentation --- .../libs/components/src/plots/VolcanoPlot.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index ab15bb686c..9f17eb2774 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -55,13 +55,15 @@ const EmptyVolcanoPlotData: VolcanoPlotData = []; /** * 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. + * 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, - dependentAxisRange, + 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, @@ -141,7 +143,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { }; return ( - // Relative positioning so that tooltips are positioned correctly (they are positioned absolutely) + // Relative positioning so that tooltips are positioned correctly (tooltips are positioned absolutely)
{/* 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, @@ -164,7 +166,7 @@ function VolcanoPlot(props: VolcanoPlotProps) { Another option would be to make Line with LineSeries, but the default hover response is on the points instead of the line connecting them. */} - {/* Horizontal significance threshold */} + {/* Draw horizontal significance threshold */} {significanceThreshold && ( )} - {/* Both vertical log2 fold change threshold lines */} + {/* Draw both vertical log2 fold change threshold lines */} {log2FoldChangeThreshold && ( <> so there should be a way to pass opacity down to those elements, but I haven't found it yet */} From 26fba4d1f43847e9ea81b0d547486c963c2de584 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 17 Apr 2023 05:55:37 -0400 Subject: [PATCH 27/28] remove unnecessary storybook plugin --- packages/libs/components/.storybook/main.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/libs/components/.storybook/main.js b/packages/libs/components/.storybook/main.js index 3b7dfc1d28..ef29acc36f 100644 --- a/packages/libs/components/.storybook/main.js +++ b/packages/libs/components/.storybook/main.js @@ -25,10 +25,7 @@ module.exports = { test: /\.(js|jsx)$/, loader: require.resolve('babel-loader'), options: { - plugins: [ - '@babel/plugin-proposal-nullish-coalescing-operator', - '@babel/plugin-syntax-class-properties', - ], + plugins: ['@babel/plugin-proposal-nullish-coalescing-operator'], presets: ['@babel/preset-env', '@babel/preset-react'], }, }); From 6d62e39e7c7d8be3e22f3b159484fdd9ef48c92f Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 17 Apr 2023 06:34:36 -0400 Subject: [PATCH 28/28] swap significance_colors indices --- packages/libs/components/src/plots/VolcanoPlot.tsx | 8 ++++---- packages/libs/components/src/types/plots/addOns.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 9f17eb2774..ca2a6953c5 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -238,12 +238,12 @@ function assignSignificanceColor( pValue: number, significanceThreshold: number, log2FoldChangeThreshold: number, - significanceColors: string[] // Assuming the order is [high (up regulated), low (down regulated), insignificant] + significanceColors: string[] // Assuming the order is [insignificant, high (up regulated), low (down regulated)] ) { // Name indices of the significanceColors array for easier accessing. - const HIGH = 0; - const LOW = 1; - const INSIGNIFICANT = 2; + 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) { diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 359b2422ad..4c40d47ac0 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -283,8 +283,8 @@ export const gradientConvergingColorscaleMap = scaleLinear() .range(ConvergingGradientColorscale) .interpolate(interpolateLab); -// Significance colors -export const significanceColors = ['#AC3B4E', '#0E8FAB', '#B5B8B4']; +// Significance colors (not significant, high, low) +export const significanceColors = ['#B5B8B4', '#AC3B4E', '#0E8FAB']; /** truncated axis flags */ export type AxisTruncationAddon = {