From da12b844962598dc7c77d3b6d30323c98b9cd458 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 22 Aug 2023 17:20:38 -0400 Subject: [PATCH 01/43] Resolve linting errors --- .../MarkerConfiguration/BarPlotMarkerConfigurationMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BarPlotMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BarPlotMarkerConfigurationMenu.tsx index ae67024c10..fb03f6d96f 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BarPlotMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BarPlotMarkerConfigurationMenu.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useCallback } from 'react'; import { InputVariables, Props as InputVariablesProps, @@ -133,6 +133,7 @@ export function BarPlotMarkerConfigurationMenu({ color: '#333', }; }, [ + studyId, overlayVariable, overlayConfiguration?.overlayType, subsettingClient, From fb75085cdc9ee2b6b7accf73d1d241f59da74888 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 22 Aug 2023 17:22:31 -0400 Subject: [PATCH 02/43] Resolve marker preview log scale issue via dependentAxisRange props --- .../libs/components/src/map/ChartMarker.tsx | 10 ++++++++++ .../eda/src/lib/map/analysis/MapAnalysis.tsx | 18 ++++++++++++------ .../CategoricalMarkerPreview.tsx | 11 ++++++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/libs/components/src/map/ChartMarker.tsx b/packages/libs/components/src/map/ChartMarker.tsx index 0a333fa471..691e0cb187 100755 --- a/packages/libs/components/src/map/ChartMarker.tsx +++ b/packages/libs/components/src/map/ChartMarker.tsx @@ -315,3 +315,13 @@ function chartMarkerSVGIcon(props: ChartMarkerStandaloneProps): { sumValuesString, }; } + +export function getChartMarkerDependentAxisRange( + data: ChartMarkerProps['data'], + isLogScale: boolean +) { + return { + min: isLogScale ? 0.1 : 0, // matches what we pass into map markers + max: Math.max(...data.map((d) => d.value)), + }; +} diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index c750a27a1a..453785447f 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -103,6 +103,7 @@ import DonutMarkerComponent, { import ChartMarkerComponent, { ChartMarkerProps, ChartMarkerStandalone, + getChartMarkerDependentAxisRange, } from '@veupathdb/components/lib/map/ChartMarker'; import { sharedStandaloneMarkerProperties } from './MarkerConfiguration/CategoricalMarkerPreview'; import { mFormatter, kFormatter } from '../../core/utils/big-number-formatters'; @@ -512,16 +513,21 @@ function MapAnalysisImpl(props: ImplProps) { /> ); } else { + const dependentAxisLogScale = + activeMarkerConfiguration && + 'dependentAxisLogScale' in activeMarkerConfiguration + ? activeMarkerConfiguration.dependentAxisLogScale + : false; return ( p + c.value, 0))} - dependentAxisLogScale={ - activeMarkerConfiguration && - 'dependentAxisLogScale' in activeMarkerConfiguration - ? activeMarkerConfiguration.dependentAxisLogScale - : false - } + dependentAxisLogScale={dependentAxisLogScale} + // pass in an axis range to mimic map markers, especially in log scale + dependentAxisRange={getChartMarkerDependentAxisRange( + finalData, + dependentAxisLogScale + )} {...sharedStandaloneMarkerProperties} /> ); diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerPreview.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerPreview.tsx index 4b1f0e2f96..bee5f142a3 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerPreview.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/CategoricalMarkerPreview.tsx @@ -1,6 +1,9 @@ import { AllValuesDefinition, OverlayConfig } from '../../../core'; import { ColorPaletteDefault } from '@veupathdb/components/lib/types/plots'; -import { ChartMarkerStandalone } from '@veupathdb/components/lib/map/ChartMarker'; +import { + ChartMarkerStandalone, + getChartMarkerDependentAxisRange, +} from '@veupathdb/components/lib/map/ChartMarker'; import { DonutMarkerStandalone } from '@veupathdb/components/lib/map/DonutMarker'; import { UNSELECTED_TOKEN } from '../..'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; @@ -68,6 +71,10 @@ export function CategoricalMarkerPreview({ 0, })); if (mapType === 'barplot') { + const dependentAxisRange = getChartMarkerDependentAxisRange( + plotData, + isDependentAxisLogScaleActive + ); return (
p + c.value, 0))} dependentAxisLogScale={isDependentAxisLogScaleActive} + // pass in an axis range to mimic map markers, especially in log scale + dependentAxisRange={dependentAxisRange} {...sharedStandaloneMarkerProperties} />
From f9e5f54cf0c53b5382b90d8a3f817a81afd59d3f Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 23 Aug 2023 15:57:16 -0400 Subject: [PATCH 03/43] Fix bug with proportion mode in continuous marker preview --- .../libs/components/src/map/ChartMarker.tsx | 1 + .../libs/components/src/map/DonutMarker.tsx | 1 + .../eda/src/lib/map/analysis/MapAnalysis.tsx | 37 ++++++++++++++++--- .../analysis/hooks/standaloneMapMarkers.tsx | 6 ++- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/libs/components/src/map/ChartMarker.tsx b/packages/libs/components/src/map/ChartMarker.tsx index 691e0cb187..7c1e83cce2 100755 --- a/packages/libs/components/src/map/ChartMarker.tsx +++ b/packages/libs/components/src/map/ChartMarker.tsx @@ -23,6 +23,7 @@ export interface ChartMarkerProps borderWidth?: number; data: { value: number; + count?: number; label: string; color?: string; }[]; diff --git a/packages/libs/components/src/map/DonutMarker.tsx b/packages/libs/components/src/map/DonutMarker.tsx index 590d63e0b0..3e217692e3 100755 --- a/packages/libs/components/src/map/DonutMarker.tsx +++ b/packages/libs/components/src/map/DonutMarker.tsx @@ -18,6 +18,7 @@ export interface DonutMarkerProps MarkerScaleAddon { data: { value: number; + count?: number; label: string; color?: string; }[]; diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 453785447f..3e2adfea9d 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -485,17 +485,26 @@ function MapAnalysisImpl(props: ImplProps) { const initialDataObject = previewMarkerData[0].data.map((data) => ({ label: data.label, value: 0, + count: 0, ...(data.color ? { color: data.color } : {}), })); const typedData = markerType === 'pie' ? ([...previewMarkerData] as DonutMarkerProps[]) : ([...previewMarkerData] as ChartMarkerProps[]); - const finalData = typedData.reduce( + /** + * This version of data can be used in markers that either don't have proportion mode + * or proportion mode is false. In proportion mode, the values are pre-calculated proportion + * values, so adding them together is inaccurate. By reducing the counts data alongside the + * proportion value, we can later use the aggregated counts to recalculate a final proportion + * (see below) + */ + const finalNonProportionModeData = typedData.reduce( (prevData, currData) => currData.data.map((data, index) => ({ label: data.label, value: data.value + prevData[index].value, + count: (data.count ?? 0) + prevData[index].count, ...('color' in prevData[index] ? { color: prevData[index].color } : 'color' in data @@ -504,11 +513,15 @@ function MapAnalysisImpl(props: ImplProps) { })), initialDataObject ); + const totalCount = finalNonProportionModeData.reduce( + (p, c) => p + c.count, + 0 + ); if (markerType === 'pie') { return ( p + c.value, 0))} + data={finalNonProportionModeData} + markerLabel={kFormatter(totalCount)} {...sharedStandaloneMarkerProperties} /> ); @@ -518,14 +531,26 @@ function MapAnalysisImpl(props: ImplProps) { 'dependentAxisLogScale' in activeMarkerConfiguration ? activeMarkerConfiguration.dependentAxisLogScale : false; + const isPlotModeProportion = + activeMarkerConfiguration && + 'selectedPlotMode' in activeMarkerConfiguration && + activeMarkerConfiguration.selectedPlotMode === 'proportion'; + const finalChartMarkerData = isPlotModeProportion + ? // If proportion mode is true, we need to recalculate the proportion value using + // the aggregated counts property + finalNonProportionModeData.map((d) => ({ + ...d, + value: d.count / totalCount, + })) + : finalNonProportionModeData; return ( p + c.value, 0))} + data={finalChartMarkerData} + markerLabel={mFormatter(totalCount)} dependentAxisLogScale={dependentAxisLogScale} // pass in an axis range to mimic map markers, especially in log scale dependentAxisRange={getChartMarkerDependentAxisRange( - finalData, + finalChartMarkerData, dependentAxisLogScale )} {...sharedStandaloneMarkerProperties} diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx index 935b6160a1..bda5e6d890 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneMapMarkers.tsx @@ -600,9 +600,10 @@ const processRawMarkersData = ( const donutData = vocabulary && overlayValues && overlayValues.length - ? overlayValues.map(({ binLabel, value }) => ({ + ? overlayValues.map(({ binLabel, value, count }) => ({ label: binLabel, - value: value, + value, + count, color: overlayType === 'categorical' ? ColorPaletteDefault[vocabulary.indexOf(binLabel)] @@ -626,6 +627,7 @@ const processRawMarkersData = ( donutData.find(({ label }) => label === overlayLabel) ?? { label: fixLabelForOtherValues(overlayLabel), value: 0, + count: 0, } ) : // however, if there is no overlay data From 203dd15e581cd36f73aac427cc698291768950dc Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 12 Sep 2023 14:01:05 -0400 Subject: [PATCH 04/43] Extract data property's type into BaseMarkerData --- packages/libs/components/src/map/ChartMarker.tsx | 13 +++++++------ packages/libs/components/src/map/DonutMarker.tsx | 8 ++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/libs/components/src/map/ChartMarker.tsx b/packages/libs/components/src/map/ChartMarker.tsx index 7c1e83cce2..d33a50c1c8 100755 --- a/packages/libs/components/src/map/ChartMarker.tsx +++ b/packages/libs/components/src/map/ChartMarker.tsx @@ -15,18 +15,19 @@ import { MarkerScaleDefault, } from '../types/plots'; +export type BaseMarkerData = { + value: number; + label: string; + color?: string; +}; + export interface ChartMarkerProps extends BoundsDriftMarkerProps, MarkerScaleAddon, DependentAxisLogScaleAddon { borderColor?: string; borderWidth?: number; - data: { - value: number; - count?: number; - label: string; - color?: string; - }[]; + data: BaseMarkerData[]; isAtomic?: boolean; // add a special thumbtack icon if this is true (it's a marker that won't disaggregate if zoomed in further) // changed to dependentAxisRange dependentAxisRange?: NumberRange | null; // y-axis range for setting global max diff --git a/packages/libs/components/src/map/DonutMarker.tsx b/packages/libs/components/src/map/DonutMarker.tsx index 3e217692e3..4359695b32 100755 --- a/packages/libs/components/src/map/DonutMarker.tsx +++ b/packages/libs/components/src/map/DonutMarker.tsx @@ -11,17 +11,13 @@ import { } from '../types/plots'; import { last } from 'lodash'; +import { BaseMarkerData } from './ChartMarker'; // ts definition for HistogramMarkerSVGProps: need some adjustment but for now, just use Donut marker one export interface DonutMarkerProps extends BoundsDriftMarkerProps, MarkerScaleAddon { - data: { - value: number; - count?: number; - label: string; - color?: string; - }[]; + data: BaseMarkerData[]; // isAtomic: add a special thumbtack icon if this is true isAtomic?: boolean; onClick?: (event: L.LeafletMouseEvent) => void | undefined; From b59e43042eecf7ed722fc485399082a73dc74edd Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 12 Sep 2023 14:03:03 -0400 Subject: [PATCH 05/43] Clean up types and logic for continuous var marker previews --- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 61 +++++++++---------- .../analysis/hooks/standaloneMapMarkers.tsx | 25 +++++++- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index f85cc7a9c8..60ce32ef60 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -57,7 +57,11 @@ import { useToggleStarredVariable } from '../../core/hooks/starredVariables'; import { filtersFromBoundingBox } from '../../core/utils/visualization'; import { EditLocation, InfoOutlined, Notes, Share } from '@material-ui/icons'; import { ComputationAppOverview } from '../../core/types/visualization'; -import { useStandaloneMapMarkers } from './hooks/standaloneMapMarkers'; +import { + ChartMarkerPropsWithCounts, + DonutMarkerPropsWithCounts, + useStandaloneMapMarkers, +} from './hooks/standaloneMapMarkers'; import { useStandaloneVizPlugins } from './hooks/standaloneVizPlugins'; import geohashAnimation from '@veupathdb/components/lib/map/animation_functions/geohash'; import { defaultAnimationDuration } from '@veupathdb/components/lib/map/config/map'; @@ -542,29 +546,29 @@ function MapAnalysisImpl(props: ImplProps) { !Array.isArray(previewMarkerData[0].data) ) return; - const initialDataObject = previewMarkerData[0].data.map((data) => ({ + const typedData = + markerType === 'pie' + ? (previewMarkerData as DonutMarkerPropsWithCounts[]) + : (previewMarkerData as ChartMarkerPropsWithCounts[]); + const initialDataObject = typedData[0].data.map((data) => ({ label: data.label, value: 0, count: 0, ...(data.color ? { color: data.color } : {}), })); - const typedData = - markerType === 'pie' - ? ([...previewMarkerData] as DonutMarkerProps[]) - : ([...previewMarkerData] as ChartMarkerProps[]); /** - * This version of data can be used in markers that either don't have proportion mode - * or proportion mode is false. In proportion mode, the values are pre-calculated proportion - * values, so adding them together is inaccurate. By reducing the counts data alongside the - * proportion value, we can later use the aggregated counts to recalculate a final proportion - * (see below) + * In the chart marker's proportion mode, the values are pre-calculated proportion values so summing them for totalCount is inaccurate. + * Instead, we'll reduce the counts and use them later when determining totalCount and when determining the data for + * ChartMarkerStandalone. + * + * NOTE: the donut preview doesn't care about the counts, but it's receiving them anyway. */ - const finalNonProportionModeData = typedData.reduce( + const dataWithCounts = typedData.reduce( (prevData, currData) => currData.data.map((data, index) => ({ label: data.label, value: data.value + prevData[index].value, - count: (data.count ?? 0) + prevData[index].count, + count: data.count + prevData[index].count, ...('color' in prevData[index] ? { color: prevData[index].color } : 'color' in data @@ -573,14 +577,11 @@ function MapAnalysisImpl(props: ImplProps) { })), initialDataObject ); - const totalCount = finalNonProportionModeData.reduce( - (p, c) => p + c.count, - 0 - ); + const totalCount = dataWithCounts.reduce((p, c) => p + c.count, 0); if (markerType === 'pie') { return ( @@ -591,26 +592,22 @@ function MapAnalysisImpl(props: ImplProps) { 'dependentAxisLogScale' in activeMarkerConfiguration ? activeMarkerConfiguration.dependentAxisLogScale : false; - const isPlotModeProportion = - activeMarkerConfiguration && - 'selectedPlotMode' in activeMarkerConfiguration && - activeMarkerConfiguration.selectedPlotMode === 'proportion'; - const finalChartMarkerData = isPlotModeProportion - ? // If proportion mode is true, we need to recalculate the proportion value using - // the aggregated counts property - finalNonProportionModeData.map((d) => ({ - ...d, - value: d.count / totalCount, - })) - : finalNonProportionModeData; + /** + * Let's recalculate the value property for the marker preview to be a now-correct proportion value. + * Since no axes/numbers are displayed, we don't need to condition this based on plot mode. + */ + const chartMarkerData = dataWithCounts.map((d) => ({ + ...d, + value: d.count / totalCount, + })); return ( & { + data: DonutMarkerDataWithCounts[]; +}; + +export type ChartMarkerPropsWithCounts = Omit & { + data: ChartMarkerDataWithCounts[]; +}; + // what this hook returns interface MapMarkers { /** the markers */ markersData: - | DonutMarkerProps[] - | ChartMarkerProps[] + | DonutMarkerPropsWithCounts[] + | ChartMarkerPropsWithCounts[] | BubbleMarkerProps[] | undefined; /** `totalVisibleEntityCount` tells you how many entities are visible at a given viewport. But not necessarily with data for the overlay variable. */ From 30b2969b849c1ba2e1a4cfe7637fc29d5917922a Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 15 Sep 2023 13:54:32 -0400 Subject: [PATCH 06/43] Add traceid to FetchClientError message (#506) --- packages/libs/http-utils/src/FetchClient.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/libs/http-utils/src/FetchClient.ts b/packages/libs/http-utils/src/FetchClient.ts index a5c7091558..6e0ff116c8 100644 --- a/packages/libs/http-utils/src/FetchClient.ts +++ b/packages/libs/http-utils/src/FetchClient.ts @@ -104,12 +104,14 @@ export abstract class FetchClient { return await transformResponse(responseBody); } - const fetchError = new FetchClientError( - `${response.status} ${ - response.statusText - }: ${request.method.toUpperCase()} ${request.url} - ${'\n'}${await response.text()}` - ); + const { status, statusText } = response; + const { headers, method, url } = request; + const traceid = headers.get('traceid'); + const fetchError = new FetchClientError(` + ${status} ${statusText}: ${method.toUpperCase()} ${url} + ${traceid != null ? 'Traceid: ' + traceid : ''} + + ${await response.text()}`); this.onNonSuccessResponse?.(fetchError); throw fetchError; } From f9413597e9f16175db1c7f97da4d7809dcf93302 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Fri, 15 Sep 2023 16:40:44 -0400 Subject: [PATCH 07/43] Overwrite all values with counts in continuous marker preview data --- .../libs/components/src/map/ChartMarker.tsx | 7 ++++- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 31 ++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/libs/components/src/map/ChartMarker.tsx b/packages/libs/components/src/map/ChartMarker.tsx index d33a50c1c8..6c8f915e39 100755 --- a/packages/libs/components/src/map/ChartMarker.tsx +++ b/packages/libs/components/src/map/ChartMarker.tsx @@ -323,7 +323,12 @@ export function getChartMarkerDependentAxisRange( isLogScale: boolean ) { return { - min: isLogScale ? 0.1 : 0, // matches what we pass into map markers + min: isLogScale + ? Math.min( + 0.1, + ...data.filter(({ value }) => value > 0).map(({ value }) => value) + ) + : 0, max: Math.max(...data.map((d) => d.value)), }; } diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 8193c0c0b4..b7daf32ceb 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -557,17 +557,19 @@ function MapAnalysisImpl(props: ImplProps) { ...(data.color ? { color: data.color } : {}), })); /** - * In the chart marker's proportion mode, the values are pre-calculated proportion values so summing them for totalCount is inaccurate. - * Instead, we'll reduce the counts and use them later when determining totalCount and when determining the data for - * ChartMarkerStandalone. + * In the chart marker's proportion mode, the values are pre-calculated proportion values. Using these pre-calculated proportion values results + * in an erroneous totalCount summation and some off visualizations in the marker previews. Since no axes/numbers are displayed in the marker + * previews, let's just overwrite the value property with the count property. * - * NOTE: the donut preview doesn't care about the counts, but it's receiving them anyway. + * NOTE: the donut preview doesn't have proportion mode and was working just fine, but now it's going to receive count data that it neither + * needs nor consumes. */ - const dataWithCounts = typedData.reduce( + const dataWithCountsOnly = typedData.reduce( (prevData, currData) => currData.data.map((data, index) => ({ label: data.label, - value: data.value + prevData[index].value, + // here's the overwrite mentioned in the above comment + value: data.count + prevData[index].count, count: data.count + prevData[index].count, ...('color' in prevData[index] ? { color: prevData[index].color } @@ -577,11 +579,12 @@ function MapAnalysisImpl(props: ImplProps) { })), initialDataObject ); - const totalCount = dataWithCounts.reduce((p, c) => p + c.count, 0); + // NOTE: we could just as well reduce using c.value since we overwrite the value prop with the count data + const totalCount = dataWithCountsOnly.reduce((p, c) => p + c.count, 0); if (markerType === 'pie') { return ( @@ -592,22 +595,14 @@ function MapAnalysisImpl(props: ImplProps) { 'dependentAxisLogScale' in activeMarkerConfiguration ? activeMarkerConfiguration.dependentAxisLogScale : false; - /** - * Let's recalculate the value property for the marker preview to be a now-correct proportion value. - * Since no axes/numbers are displayed, we don't need to condition this based on plot mode. - */ - const chartMarkerData = dataWithCounts.map((d) => ({ - ...d, - value: d.count / totalCount, - })); return ( Date: Mon, 18 Sep 2023 05:36:39 -0400 Subject: [PATCH 08/43] turn on diff abund --- .../eda/src/lib/core/components/computations/plugins/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts index 10ab8888e5..a337e37ae1 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts @@ -12,7 +12,7 @@ export const plugins: Record = { abundance, alphadiv, betadiv, - // differentialabundance, + differentialabundance, countsandproportions, distributions, pass, From 1a5f6068ba3ec23c2e13e8e053666eff57c7be35 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 19 Sep 2023 09:49:57 -0400 Subject: [PATCH 09/43] Tweak some verbiage and convert let to const --- .../reporters/BedGeneReporterForm.jsx | 67 +++++++++------ .../reporters/SequenceGeneReporterForm.jsx | 83 +++++++++++-------- 2 files changed, 92 insertions(+), 58 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx index 751013f875..18e90aaff3 100644 --- a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx +++ b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx @@ -17,38 +17,52 @@ import createSequenceForm from './SequenceFormFactory'; * Adapted from SequenceGeneReporterForm * (no protein options) */ -let util = Object.assign({}, ComponentUtils, ReporterUtils); +const util = Object.assign({}, ComponentUtils, ReporterUtils); -let splicedGenomicOptions = [ +const splicedGenomicOptions = [ { value: 'cds', display: 'Coding Sequence' }, { value: 'transcript', display: 'Transcript' }, ]; -let dnaComponentOptions = [ +const dnaComponentOptions = [ { value: 'exon', display: 'Exon' }, { value: 'intron', display: 'Intron' }, ]; -let transcriptComponentOptions = [ +const transcriptComponentOptions = [ { value: 'five_prime_utr', display: "5' UTR" }, { value: 'cds', display: 'CDS' }, { value: 'three_prime_utr', display: "3' UTR" }, ]; -let genomicAnchorValues = [ +const genomicAnchorValues = [ { value: 'Start', display: 'Transcription Start***' }, { value: 'CodeStart', display: 'Translation Start (ATG)' }, { value: 'CodeEnd', display: 'Translation Stop Codon' }, { value: 'End', display: 'Transcription Stop***' }, ]; -let signs = [ +const signs = [ { value: 'plus', display: '+' }, { value: 'minus', display: '-' }, ]; -let SequenceRegionRange = (props) => { - let { label, anchor, sign, offset, formState, getUpdateHandler } = props; +const formSequenceTypeOptions = [ + { value: 'genomic', display: 'Unspliced Genomic Region' }, + { + value: 'spliced_genomic', + display: ( + <> + Spliced Genomic Region (i.e. transcribed sequences) + + ), + }, + { value: 'dna_component', display: 'DNA Component' }, + { value: 'transcript_component', display: 'Transcript Component' }, +]; + +const SequenceRegionRange = (props) => { + const { label, anchor, sign, offset, formState, getUpdateHandler } = props; return ( {label} @@ -78,8 +92,8 @@ let SequenceRegionRange = (props) => { ); }; -let GenomicSequenceRegionInputs = (props) => { - let { formState, getUpdateHandler } = props; +const GenomicSequenceRegionInputs = (props) => { + const { formState, getUpdateHandler } = props; return (
{ }; /** @type import('./Types').ReporterFormComponent */ -let formBeforeCommonOptions = (props) => { - let { formState, updateFormState, onSubmit, includeSubmit } = props; - let getUpdateHandler = (fieldName) => +const formBeforeCommonOptions = (props) => { + const { formState, updateFormState, onSubmit, includeSubmit } = props; + const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); - let typeUpdateHandler = function (newTypeValue) { + const typeUpdateHandler = function (newTypeValue) { updateFormState(Object.assign({}, formState, { type: newTypeValue })); }; - let getTypeSpecificParams = () => { + const getTypeSpecificParams = () => { switch (formState.type) { case 'genomic': return ( @@ -161,26 +175,29 @@ let formBeforeCommonOptions = (props) => { }; return ( -

Choose the type of result:

+

Choose the type of sequence:

-

Configure details:

+

+ Configure details for{' '} + { + formSequenceTypeOptions.find( + (item) => item.value === formState.type + ).display + } + : +

{getTypeSpecificParams()}
); }; -let formAfterSubmitButton = (props) => { +const formAfterSubmitButton = (props) => { return (
@@ -197,7 +214,7 @@ let formAfterSubmitButton = (props) => { ); }; -let getFormInitialState = () => ({ +const getFormInitialState = () => ({ type: 'genomic', reverseAndComplement: false, diff --git a/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx b/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx index 77bb9c3a66..e77462ff3b 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx @@ -17,50 +17,66 @@ import createSequenceForm from './SequenceFormFactory'; * Similar to SequenceGeneReporterForm * (but with protein options) */ -let util = Object.assign({}, ComponentUtils, ReporterUtils); +const util = Object.assign({}, ComponentUtils, ReporterUtils); -let splicedGenomicOptions = [ +const splicedGenomicOptions = [ { value: 'cds', display: 'Coding Sequence' }, { value: 'transcript', display: 'Transcript' }, ]; -let proteinFeatureOptions = [ +const proteinFeatureOptions = [ { value: 'interpro', display: 'InterPro' }, { value: 'signalp', display: 'SignalP' }, { value: 'tmhmm', display: 'Transmembrane Domains' }, { value: 'low_complexity', display: 'Low Complexity Regions' }, ]; -let dnaComponentOptions = [ +const dnaComponentOptions = [ { value: 'exon', display: 'Exon' }, { value: 'intron', display: 'Intron' }, ]; -let transcriptComponentOptions = [ +const transcriptComponentOptions = [ { value: 'five_prime_utr', display: "5' UTR" }, { value: 'cds', display: 'CDS' }, { value: 'three_prime_utr', display: "3' UTR" }, ]; -let genomicAnchorValues = [ +const genomicAnchorValues = [ { value: 'Start', display: 'Transcription Start***' }, { value: 'CodeStart', display: 'Translation Start (ATG)' }, { value: 'CodeEnd', display: 'Translation Stop Codon' }, { value: 'End', display: 'Transcription Stop***' }, ]; -let proteinAnchorValues = [ +const proteinAnchorValues = [ { value: 'DownstreamFromStart', display: 'Downstream from Start' }, { value: 'UpstreamFromEnd', display: 'Upstream from End' }, ]; -let signs = [ +const signs = [ { value: 'plus', display: '+' }, { value: 'minus', display: '-' }, ]; -let SequenceRegionRange = (props) => { - let { label, anchor, sign, offset, formState, getUpdateHandler } = props; +const formSequenceTypeOptions = [ + { value: 'genomic', display: 'Unspliced Genomic Sequence' }, + { + value: 'spliced_genomic', + display: ( + <> + Spliced Genomic Region (i.e. transcribed sequences) + + ), + }, + { value: 'dna_component', display: 'DNA Component' }, + { value: 'transcript_component', display: 'Transcript Component' }, + { value: 'protein', display: 'Protein Sequence' }, + { value: 'protein_features', display: 'Protein Features' }, +]; + +const SequenceRegionRange = (props) => { + const { label, anchor, sign, offset, formState, getUpdateHandler } = props; return ( {label} @@ -90,8 +106,8 @@ let SequenceRegionRange = (props) => { ); }; -let ProteinRegionRange = (props) => { - let { label, anchor, offset, formState, getUpdateHandler } = props; +const ProteinRegionRange = (props) => { + const { label, anchor, offset, formState, getUpdateHandler } = props; return ( {label} @@ -115,8 +131,8 @@ let ProteinRegionRange = (props) => { ); }; -let GenomicSequenceRegionInputs = (props) => { - let { formState, getUpdateHandler } = props; +const GenomicSequenceRegionInputs = (props) => { + const { formState, getUpdateHandler } = props; return (
@@ -157,8 +173,8 @@ let GenomicSequenceRegionInputs = (props) => {
); }; -let ProteinSequenceRegionInputs = (props) => { - let { formState, getUpdateHandler } = props; +const ProteinSequenceRegionInputs = (props) => { + const { formState, getUpdateHandler } = props; return (
{ }; /** @type import('./Types').ReporterFormComponent */ -let formBeforeCommonOptions = (props) => { - let { formState, updateFormState, onSubmit, includeSubmit } = props; - let getUpdateHandler = (fieldName) => +const formBeforeCommonOptions = (props) => { + const { formState, updateFormState, onSubmit, includeSubmit } = props; + const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); - let typeUpdateHandler = function (newTypeValue) { + const typeUpdateHandler = function (newTypeValue) { updateFormState(Object.assign({}, formState, { type: newTypeValue })); }; - let getTypeSpecificParams = () => { + const getTypeSpecificParams = () => { switch (formState.type) { case 'genomic': return ( @@ -254,28 +270,29 @@ let formBeforeCommonOptions = (props) => { }; return ( -

Choose the type of result:

+

Choose the type of sequence:

-

Configure details:

+

+ Configure details for{' '} + { + formSequenceTypeOptions.find( + (item) => item.value === formState.type + ).display + } + : +

{getTypeSpecificParams()}
); }; -let formAfterSubmitButton = (props) => { +const formAfterSubmitButton = (props) => { return (
@@ -297,7 +314,7 @@ let formAfterSubmitButton = (props) => { ); }; -let getFormInitialState = () => ({ +const getFormInitialState = () => ({ type: 'genomic', // sequence region inputs for 'genomic' From a6109113bfecea1be8c591a3840ed03061325c56 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 19 Sep 2023 09:51:02 -0400 Subject: [PATCH 10/43] Adjust alignment and layout of sequence format section and minor code cleanup --- .../components/reporters/ReporterForms.scss | 5 +++ .../reporters/SequenceFormFactory.jsx | 37 +++++++++++-------- 2 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 packages/libs/web-common/src/components/reporters/ReporterForms.scss diff --git a/packages/libs/web-common/src/components/reporters/ReporterForms.scss b/packages/libs/web-common/src/components/reporters/ReporterForms.scss new file mode 100644 index 0000000000..53cea0edc4 --- /dev/null +++ b/packages/libs/web-common/src/components/reporters/ReporterForms.scss @@ -0,0 +1,5 @@ +.ebrc-FixedWidth-detail { + .wdk-NumberSelector { + margin: 0 0.25em; + } +} diff --git a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx index 74f6e78447..04b9bb9637 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx @@ -10,10 +10,11 @@ import { import { FeaturesList, ComponentsList } from './SequenceFormElements'; import * as ComponentUtils from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import * as ReporterUtils from '@veupathdb/wdk-client/lib/Views/ReporterForm/reporterUtils'; +import './ReporterForms.scss'; -let util = Object.assign({}, ComponentUtils, ReporterUtils); +const util = Object.assign({}, ComponentUtils, ReporterUtils); -let deflineFieldOptions = [ +const deflineFieldOptions = [ { value: 'organism', display: 'Organism' }, { value: 'description', display: 'Description' }, { value: 'position', display: 'Location' }, @@ -21,9 +22,9 @@ let deflineFieldOptions = [ { value: 'segment_length', display: 'Segment Length' }, ]; -let sequenceOptions = (props) => { - let { formState, updateFormState, onSubmit, includeSubmit } = props; - let getUpdateHandler = (fieldName) => +const sequenceOptions = (props) => { + const { formState, updateFormState, onSubmit, includeSubmit } = props; + const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); return ( @@ -48,19 +49,24 @@ let sequenceOptions = (props) => { )}

Sequence format:

-
+
- {formState.sequenceFormat === 'single_line' ? null : ( - - Bases Per Line: + {formState.sequenceFormat === 'fixed_width' && ( +
{ onChange={getUpdateHandler('basesPerLine')} size="6" /> - + bases per line +
)}
); }; -let createSequenceForm = ( +const createSequenceForm = ( formBeforeCommonOptions, formAfterSubmitButton, getFormInitialState, reportType ) => { - let Form = (props) => { - let { formState, updateFormState, onSubmit, includeSubmit } = props; - let getUpdateHandler = (fieldName) => + const Form = (props) => { + const { formState, updateFormState, onSubmit, includeSubmit } = props; + const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); return (
From 04b97685007de0baba35125213114160b9839970 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 10:37:06 -0400 Subject: [PATCH 11/43] Remove unused imports and add note to sequence type entry --- .../components/reporters/SequenceGeneReporterForm.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx b/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx index e77462ff3b..4ff448fc94 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceGeneReporterForm.jsx @@ -1,9 +1,7 @@ import React from 'react'; import { RadioList, - CheckboxList, SingleSelect, - TextBox, Checkbox, NumberSelector, } from '@veupathdb/wdk-client/lib/Components'; @@ -72,7 +70,14 @@ const formSequenceTypeOptions = [ { value: 'dna_component', display: 'DNA Component' }, { value: 'transcript_component', display: 'Transcript Component' }, { value: 'protein', display: 'Protein Sequence' }, - { value: 'protein_features', display: 'Protein Features' }, + { + value: 'protein_features', + display: ( + <> + Protein Features (one per line) + + ), + }, ]; const SequenceRegionRange = (props) => { From a59a4fc04a21c7535c035801e40b67ea580a7caf Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 10:37:30 -0400 Subject: [PATCH 12/43] Remove unused imports --- .../src/components/reporters/BedGeneReporterForm.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx index 18e90aaff3..4166be7327 100644 --- a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx +++ b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx @@ -1,10 +1,7 @@ import React from 'react'; import { RadioList, - CheckboxList, SingleSelect, - TextBox, - Checkbox, NumberSelector, } from '@veupathdb/wdk-client/lib/Components'; import { FeaturesList } from './SequenceFormElements'; From 1ffc19e7babda4e338efcab586ee02af00ea7008 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 10:38:28 -0400 Subject: [PATCH 13/43] Use CoreUI's CheckboxList in place of WDK's --- .../src/components/reporters/SequenceFormElements.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/libs/web-common/src/components/reporters/SequenceFormElements.jsx b/packages/libs/web-common/src/components/reporters/SequenceFormElements.jsx index 4fb1fc584d..86f72a5beb 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceFormElements.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceFormElements.jsx @@ -1,5 +1,6 @@ import React from 'react'; -import { RadioList, CheckboxList } from '@veupathdb/wdk-client/lib/Components'; +import { RadioList } from '@veupathdb/wdk-client/lib/Components'; +import CheckboxList from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxList'; let FeaturesList = (props) => { let { features, field, formState, getUpdateHandler } = props; @@ -37,6 +38,7 @@ let ComponentsList = (props) => { onChange={getUpdateHandler(field)} items={features} linksPosition={null} + disabledCheckboxTooltipContent="Required field" />
From 71ee6636426e2433227e0fa90d246afe9297cadf Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 10:39:19 -0400 Subject: [PATCH 14/43] Refactor/flatten defline config menu --- .../reporters/SequenceFormFactory.jsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx index 04b9bb9637..075aed01a8 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx @@ -1,13 +1,9 @@ import React from 'react'; import { RadioList, - CheckboxList, - SingleSelect, - TextBox, - Checkbox, NumberSelector, } from '@veupathdb/wdk-client/lib/Components'; -import { FeaturesList, ComponentsList } from './SequenceFormElements'; +import { ComponentsList } from './SequenceFormElements'; import * as ComponentUtils from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import * as ReporterUtils from '@veupathdb/wdk-client/lib/Views/ReporterForm/reporterUtils'; import './ReporterForms.scss'; @@ -15,6 +11,7 @@ import './ReporterForms.scss'; const util = Object.assign({}, ComponentUtils, ReporterUtils); const deflineFieldOptions = [ + { value: 'gene_id', display: 'Gene ID', disabled: true }, { value: 'organism', display: 'Organism' }, { value: 'description', display: 'Description' }, { value: 'position', display: 'Location' }, @@ -23,23 +20,14 @@ const deflineFieldOptions = [ ]; const sequenceOptions = (props) => { - const { formState, updateFormState, onSubmit, includeSubmit } = props; + const { formState, updateFormState } = props; const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); return (

Fasta defline:

-
- - {formState.deflineType === 'short' ? null : ( +
+ {formState.deflineType === 'full' && ( ({ formState: { attachmentType: 'plain', - deflineType: 'short', - deflineFields: deflineFieldOptions.map((x) => x.value), + deflineType: 'full', + // QUESTION: should I remove this from formState when form is submitted or should backend expect this field? + deflineFields: ['gene_id'], sequenceFormat: 'fixed_width', basesPerLine: 60, ...getFormInitialState(), From 6371ee4121d026a42a7b71476a05ff0d90ad49ea Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 13:04:10 -0400 Subject: [PATCH 15/43] Resolve type mismatch with NumberSelector's size prop --- .../BedAndSequenceGenomicSequenceReporterForms.jsx | 2 +- .../src/components/reporters/BedGeneReporterForm.jsx | 2 +- .../src/components/reporters/SequenceFormFactory.jsx | 4 ++-- .../src/components/reporters/SequenceGeneReporterForm.jsx | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/BedAndSequenceGenomicSequenceReporterForms.jsx b/packages/libs/web-common/src/components/reporters/BedAndSequenceGenomicSequenceReporterForms.jsx index 041bb02694..b5fef0aaa2 100644 --- a/packages/libs/web-common/src/components/reporters/BedAndSequenceGenomicSequenceReporterForms.jsx +++ b/packages/libs/web-common/src/components/reporters/BedAndSequenceGenomicSequenceReporterForms.jsx @@ -41,7 +41,7 @@ let SequenceRegionRange = (props) => { end={10000} step={1} onChange={getUpdateHandler(offset)} - size="6" + size={6} /> nucleotides diff --git a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx index 4166be7327..c90a01818f 100644 --- a/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx +++ b/packages/libs/web-common/src/components/reporters/BedGeneReporterForm.jsx @@ -82,7 +82,7 @@ const SequenceRegionRange = (props) => { end={10000} step={1} onChange={getUpdateHandler(offset)} - size="6" + size={6} /> nucleotides diff --git a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx index 075aed01a8..d97862f74f 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx @@ -62,7 +62,7 @@ const sequenceOptions = (props) => { value={formState['basesPerLine']} step={1} onChange={getUpdateHandler('basesPerLine')} - size="6" + size={6} /> bases per line
@@ -85,7 +85,7 @@ const createSequenceForm = ( return (
{formBeforeCommonOptions(props)} -

Download Type:

+

Download type:

{ end={10000} step={1} onChange={getUpdateHandler(offset)} - size="6" + size={6} /> nucleotides @@ -129,7 +129,7 @@ const ProteinRegionRange = (props) => { end={10000} step={1} onChange={getUpdateHandler(offset)} - size="6" + size={6} /> amino acids @@ -213,7 +213,7 @@ const ProteinSequenceRegionInputs = (props) => { /** @type import('./Types').ReporterFormComponent */ const formBeforeCommonOptions = (props) => { - const { formState, updateFormState, onSubmit, includeSubmit } = props; + const { formState, updateFormState, viewFilters } = props; const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); const typeUpdateHandler = function (newTypeValue) { From d760592d7a3122e77b2f5032236f8d3ac30dbd7f Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 21 Sep 2023 14:24:57 -0400 Subject: [PATCH 16/43] bipartite network appears in story --- .../components/src/plots/BipartiteNetwork.tsx | 106 +++++++++++ .../libs/components/src/plots/Network.tsx | 4 +- .../plots/BipartiteNetwork.stories.tsx | 175 ++++++++++++++++++ .../components/src/types/plots/network.ts | 13 +- 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100755 packages/libs/components/src/plots/BipartiteNetwork.tsx create mode 100755 packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx new file mode 100755 index 0000000000..c9e2702f18 --- /dev/null +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -0,0 +1,106 @@ +import { DefaultNode } from '@visx/network'; +import { Text } from '@visx/text'; +import { LinkData, NodeData } from '../types/plots/network'; + +interface NodeWithLabelProps { + /** Network node */ + node: NodeData; + /** Function to run when a user clicks either the node or label */ + onClick?: () => void; + /** Should the label be drawn to the left or right of the node? */ + labelPosition?: 'right' | 'left'; + /** Font size for the label. Ex. "1em" */ + fontSize?: string; + /** Font weight for the label */ + fontWeight?: number; + /** Color for the label */ + labelColor?: string; +} + +// NodeWithLabel draws one node and an optional label for the node. Both the node and +// label can be styled. +export function NodeWithLabel(props: NodeWithLabelProps) { + const DEFAULT_NODE_RADIUS = 4; + const DEFAULT_NODE_COLOR = '#aaa'; + const DEFAULT_STROKE_WIDTH = 1; + + const { + node, + onClick, + labelPosition = 'right', + fontSize = '1em', + fontWeight = 200, + labelColor = '#000', + } = props; + + const { color, label, stroke, strokeWidth } = node; + + const nodeRadius = node.r ?? DEFAULT_NODE_RADIUS; + + // Calculate where the label should be posiitoned based on + // total size of the node. + let textXOffset: number; + let textAnchor: 'start' | 'end'; + + if (labelPosition === 'right') { + textXOffset = 4 + nodeRadius; + if (strokeWidth) textXOffset = textXOffset + strokeWidth; + textAnchor = 'start'; + } else { + textXOffset = -4 - nodeRadius; + if (strokeWidth) textXOffset = textXOffset - strokeWidth; + textAnchor = 'end'; + } + + return ( + <> + + {/* Note that Text becomes a tspan */} + + {label} + + + ); +} + +export interface LinkProps { + link: LinkData; + // onClick?: () => void; To add in the future, maybe also some hover action +} + +// Link component draws a linear edge between two nodes. +// Eventually can grow into drawing directed edges (edges with arrows) when the time comes. +export function Link(props: LinkProps) { + const DEFAULT_LINK_WIDTH = 1; + const DEFAULT_COLOR = '#222'; + const DEFAULT_OPACITY = 0.95; + + const { link } = props; + + return ( + + ); +} diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx index c9e2702f18..eb5ebea41c 100755 --- a/packages/libs/components/src/plots/Network.tsx +++ b/packages/libs/components/src/plots/Network.tsx @@ -2,13 +2,15 @@ import { DefaultNode } from '@visx/network'; import { Text } from '@visx/text'; import { LinkData, NodeData } from '../types/plots/network'; +export type LabelPosition = 'right' | 'left'; + interface NodeWithLabelProps { /** Network node */ node: NodeData; /** Function to run when a user clicks either the node or label */ onClick?: () => void; /** Should the label be drawn to the left or right of the node? */ - labelPosition?: 'right' | 'left'; + labelPosition?: LabelPosition; /** Font size for the label. Ex. "1em" */ fontSize?: string; /** Font weight for the label */ diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx new file mode 100755 index 0000000000..74c9d8b9c4 --- /dev/null +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -0,0 +1,175 @@ +import { Story, Meta } from '@storybook/react/types-6-0'; +import { Graph } from '@visx/network'; +import { + NodeData, + LinkData, + BipartiteNetworkData, +} from '../../types/plots/network'; +import { LabelPosition, Link, NodeWithLabel } from '../../plots/Network'; + +export default { + title: 'Plots/BipartiteNetwork', + component: NodeWithLabel, +} as Meta; + +// For simplicity, make square svgs with the following height and width +const DEFAULT_PLOT_SIZE = 500; + +interface TemplateProps { + data: BipartiteNetworkData; +} + +// This template is a simple network that highlights our NodeWithLabel and Link components. +const Template: Story = (args) => { + // BIPARTITE network should position nodes!!! + + // The backend can't do it because we eventually want to click nodes and have them reposition. + const allNodes = args.data.nodes; + const nodes = allNodes.map((node) => { + const columnNumber = args.data.column1NodeIDs.includes(node.id) ? 0 : 1; + + type ColumnName = keyof typeof args.data; + const columnName = ('column' + + (columnNumber + 1) + + 'NodeIDs') as ColumnName; + const indexInColumn = args.data[columnName].findIndex( + (id) => id === node.id + ); + + return { + x: 90 + columnNumber * 100, + y: 30 + 30 * indexInColumn, + labelPosition: columnNumber === 0 ? 'left' : ('right' as LabelPosition), + ...node, + }; + }); + + const links = args.data.links.map((link) => { + const sourceNode = nodes.find((node) => node.id === link.source.id); + const targetNode = nodes.find((node) => node.id === link.target.id); + return { + ...link, + source: { + x: sourceNode?.x, + y: sourceNode?.y, + ...link.source, + }, + target: { + x: targetNode?.x, + y: targetNode?.y, + ...link.target, + }, + color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + }; + }); + + // also bpnet should set the label left/right appropriatey + return ( + + } + // The node components are already transformed using x and y. + // So inside the node component all coords should be relative to this + // initial transform. + nodeComponent={({ node }) => { + const nodeWithLabelProps = { + node: node, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + + ); +}; + +/** + * Stories + */ + +// A simple network with node labels +const simpleData = genBipartiteNetwork( + 20, + 10, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const Simple = Template.bind({}); +Simple.args = { + data: simpleData, +}; + +// A network with lots and lots of points! +const manyPointsData = genBipartiteNetwork( + 1000, + 100, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const ManyPoints = Template.bind({}); +ManyPoints.args = { + data: manyPointsData, +}; + +/** NetworkData is the same format accepted by visx's Graph component. */ +// export type NetworkData = { +// nodes: NodeData[]; +// links: LinkData[]; +// }; + +// /** Bipartite network data is a regular network with addiitonal declarations of +// * nodes in each of the two columns. IDs in columnXNodeIDs must match node ids exactly. +// */ +// export type BipartiteNetworkData = { +// column1NodeIDs: string[]; +// column2NodeIDs: string[]; +// } & NetworkData; + +// Gerenate a network with a given number of nodes and random edges +function genBipartiteNetwork( + column1nNodes: number, + column2nNodes: number, + height: number, + width: number +) { + // Create the first column of nodes + const column1Nodes: NodeData[] = [...Array(column1nNodes).keys()].map((i) => { + return { + id: String(i), + label: 'Node ' + String(i), + }; + }); + + // Create the second column of nodes + const column2Nodes: NodeData[] = [...Array(column2nNodes).keys()].map((i) => { + return { + id: String(i + column1nNodes), + label: 'Node ' + String(i), + }; + }); + + // Create links + // @ANN come back and mmake this more tunable + const links: LinkData[] = [...Array(column1nNodes * 2).keys()].map(() => { + return { + source: column1Nodes[Math.floor(Math.random() * column1nNodes)], + target: column2Nodes[Math.floor(Math.random() * column2nNodes)], + strokeWidth: Math.random() * 2, + color: Math.random() > 0.5 ? 'positive' : 'negative', + }; + }); + + const nodes = column1Nodes.concat(column2Nodes); + const column1NodeIDs = column1Nodes.map((node) => node.id); + const column2NodeIDs = column2Nodes.map((node) => node.id); + + return { + nodes, + links, + column1NodeIDs, + column2NodeIDs, + } as BipartiteNetworkData; +} diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index fadf279582..81ccbc1314 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -1,20 +1,19 @@ // Types required for creating networks export type NodeData = { - /** For now x and y are required. Eventually the network should have a default layout so that - * these become unnecessary in certain situations. - */ - /** The x coordinate of the node */ - x: number; - /** The y coordinate of the node */ - y: number; /** Node ID. Must be unique in the network! */ id: string; + /** The x coordinate of the node */ + x?: number; + /** The y coordinate of the node */ + y?: number; /** Node color */ color?: string; /** Node radius */ r?: number; /** User-friendly node label */ label?: string; + /** Draw the label on the right or left of the node */ + // labelPosition?: 'right' | 'left' | undefined; /** Color for the stroke of the node */ stroke?: string; /** Width of node stroke */ From 6ca856637669bf48e0266ae57bd91341eea2ab49 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 14:47:35 -0400 Subject: [PATCH 17/43] functional BipartiteNetwork component --- .../components/src/plots/BipartiteNetwork.tsx | 176 +++++++++--------- .../plots/BipartiteNetwork.stories.tsx | 73 +------- 2 files changed, 94 insertions(+), 155 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index c9e2702f18..d63ca5766d 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -1,106 +1,100 @@ -import { DefaultNode } from '@visx/network'; -import { Text } from '@visx/text'; -import { LinkData, NodeData } from '../types/plots/network'; +import { BipartiteNetworkData, NodeData } from '../types/plots/network'; +import { partition } from 'lodash'; +import { LabelPosition, Link, NodeWithLabel } from './Network'; +import { Graph } from '@visx/network'; -interface NodeWithLabelProps { - /** Network node */ - node: NodeData; - /** Function to run when a user clicks either the node or label */ - onClick?: () => void; - /** Should the label be drawn to the left or right of the node? */ - labelPosition?: 'right' | 'left'; - /** Font size for the label. Ex. "1em" */ - fontSize?: string; - /** Font weight for the label */ - fontWeight?: number; - /** Color for the label */ - labelColor?: string; +interface BipartiteNetworkProps { + /** Bipartite network data */ + data: BipartiteNetworkData; } // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. -export function NodeWithLabel(props: NodeWithLabelProps) { - const DEFAULT_NODE_RADIUS = 4; - const DEFAULT_NODE_COLOR = '#aaa'; - const DEFAULT_STROKE_WIDTH = 1; +export function BipartiteNetwork(props: BipartiteNetworkProps) { + const { data } = props; - const { - node, - onClick, - labelPosition = 'right', - fontSize = '1em', - fontWeight = 200, - labelColor = '#000', - } = props; + // BIPARTITE network should position nodes!!! - const { color, label, stroke, strokeWidth } = node; + // The backend can't do it because we eventually want to click nodes and have them reposition. + const nodesByColumn: NodeData[][] = partition(data.nodes, (node) => { + return data.column1NodeIDs.includes(node.id); + }); - const nodeRadius = node.r ?? DEFAULT_NODE_RADIUS; + const nodesByColumnWithCoordinates = nodesByColumn.map( + (column, columnIndex) => { + const columnWithCoordinates = column.map((node) => { + type ColumnName = keyof typeof data; + const columnName = ('column' + + (columnIndex + 1) + + 'NodeIDs') as ColumnName; + const indexInColumn = data[columnName].findIndex( + (id) => id === node.id + ); - // Calculate where the label should be posiitoned based on - // total size of the node. - let textXOffset: number; - let textAnchor: 'start' | 'end'; - - if (labelPosition === 'right') { - textXOffset = 4 + nodeRadius; - if (strokeWidth) textXOffset = textXOffset + strokeWidth; - textAnchor = 'start'; - } else { - textXOffset = -4 - nodeRadius; - if (strokeWidth) textXOffset = textXOffset - strokeWidth; - textAnchor = 'end'; - } - - return ( - <> - - {/* Note that Text becomes a tspan */} - - {label} - - + return { + x: 90 + (columnIndex + 1) * 100, + y: 30 + 30 * indexInColumn, + labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), + ...node, + }; + }); + return columnWithCoordinates; + } ); -} -export interface LinkProps { - link: LinkData; - // onClick?: () => void; To add in the future, maybe also some hover action -} - -// Link component draws a linear edge between two nodes. -// Eventually can grow into drawing directed edges (edges with arrows) when the time comes. -export function Link(props: LinkProps) { - const DEFAULT_LINK_WIDTH = 1; - const DEFAULT_COLOR = '#222'; - const DEFAULT_OPACITY = 0.95; - - const { link } = props; + const links = data.links.map((link) => { + const sourceNode = nodesByColumnWithCoordinates[0].find( + (node) => node.id === link.source.id + ); + const targetNode = nodesByColumnWithCoordinates[1].find( + (node) => node.id === link.target.id + ); + return { + ...link, + source: { + x: sourceNode?.x, + y: sourceNode?.y, + ...link.source, + }, + target: { + x: targetNode?.x, + y: targetNode?.y, + ...link.target, + }, + color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + }; + }); + // also bpnet should set the label left/right appropriatey return ( - + + } + // The node components are already transformed using x and y. + // So inside the node component all coords should be relative to this + // initial transform. + nodeComponent={({ node }) => { + const nodeWithLabelProps = { + node: node, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + ); } diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 74c9d8b9c4..9483510d1f 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -1,11 +1,15 @@ import { Story, Meta } from '@storybook/react/types-6-0'; -import { Graph } from '@visx/network'; import { NodeData, LinkData, BipartiteNetworkData, } from '../../types/plots/network'; import { LabelPosition, Link, NodeWithLabel } from '../../plots/Network'; +import { partition } from 'lodash'; +import { + BipartiteNetwork, + BipartiteNetworkProps, +} from '../../plots/BipartiteNetwork'; export default { title: 'Plots/BipartiteNetwork', @@ -21,69 +25,10 @@ interface TemplateProps { // This template is a simple network that highlights our NodeWithLabel and Link components. const Template: Story = (args) => { - // BIPARTITE network should position nodes!!! - - // The backend can't do it because we eventually want to click nodes and have them reposition. - const allNodes = args.data.nodes; - const nodes = allNodes.map((node) => { - const columnNumber = args.data.column1NodeIDs.includes(node.id) ? 0 : 1; - - type ColumnName = keyof typeof args.data; - const columnName = ('column' + - (columnNumber + 1) + - 'NodeIDs') as ColumnName; - const indexInColumn = args.data[columnName].findIndex( - (id) => id === node.id - ); - - return { - x: 90 + columnNumber * 100, - y: 30 + 30 * indexInColumn, - labelPosition: columnNumber === 0 ? 'left' : ('right' as LabelPosition), - ...node, - }; - }); - - const links = args.data.links.map((link) => { - const sourceNode = nodes.find((node) => node.id === link.source.id); - const targetNode = nodes.find((node) => node.id === link.target.id); - return { - ...link, - source: { - x: sourceNode?.x, - y: sourceNode?.y, - ...link.source, - }, - target: { - x: targetNode?.x, - y: targetNode?.y, - ...link.target, - }, - color: link.color === 'positive' ? '#116699' : '#994411', //fake colors - }; - }); - - // also bpnet should set the label left/right appropriatey - return ( - - } - // The node components are already transformed using x and y. - // So inside the node component all coords should be relative to this - // initial transform. - nodeComponent={({ node }) => { - const nodeWithLabelProps = { - node: node, - labelPosition: node.labelPosition, - }; - return ; - }} - /> - - ); + const bipartiteNetworkProps: BipartiteNetworkProps = { + data: args.data, + }; + return ; }; /** From 9ee85113d189895b0d2071a5acd183c9353181b0 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:11:31 -0400 Subject: [PATCH 18/43] add column name for bipartite network --- .../components/src/plots/BipartiteNetwork.tsx | 23 ++++++++++--- .../plots/BipartiteNetwork.stories.tsx | 34 ++++++++----------- .../stories/plots/NodeWithLabel.stories.tsx | 2 +- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index d63ca5766d..946b47d8dd 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -2,16 +2,21 @@ import { BipartiteNetworkData, NodeData } from '../types/plots/network'; import { partition } from 'lodash'; import { LabelPosition, Link, NodeWithLabel } from './Network'; import { Graph } from '@visx/network'; +import { Text } from '@visx/text'; -interface BipartiteNetworkProps { +export interface BipartiteNetworkProps { /** Bipartite network data */ data: BipartiteNetworkData; + /** Name of column 1 */ + column1Name?: string; + /** Name of column 2 */ + column2Name?: string; } // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. export function BipartiteNetwork(props: BipartiteNetworkProps) { - const { data } = props; + const { data, column1Name, column2Name } = props; // BIPARTITE network should position nodes!!! @@ -33,7 +38,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { return { x: 90 + (columnIndex + 1) * 100, - y: 30 + 30 * indexInColumn, + y: 40 + 30 * indexInColumn, labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), ...node, }; @@ -65,7 +70,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { }; }); - // also bpnet should set the label left/right appropriatey return ( + {/* Draw names of node colums if they exist */} + {column1Name && ( + + {column1Name} + + )} + {column2Name && ( + + {column2Name} + + )} = (args) => { const bipartiteNetworkProps: BipartiteNetworkProps = { data: args.data, + column1Name: args.column1Name, + column2Name: args.column2Name, }; return ; }; @@ -59,19 +61,13 @@ ManyPoints.args = { data: manyPointsData, }; -/** NetworkData is the same format accepted by visx's Graph component. */ -// export type NetworkData = { -// nodes: NodeData[]; -// links: LinkData[]; -// }; - -// /** Bipartite network data is a regular network with addiitonal declarations of -// * nodes in each of the two columns. IDs in columnXNodeIDs must match node ids exactly. -// */ -// export type BipartiteNetworkData = { -// column1NodeIDs: string[]; -// column2NodeIDs: string[]; -// } & NetworkData; +// With column names +export const WithColumnNames = Template.bind({}); +WithColumnNames.args = { + data: simpleData, + column1Name: 'Column 1', + column2Name: 'Column 2', +}; // Gerenate a network with a given number of nodes and random edges function genBipartiteNetwork( @@ -92,7 +88,7 @@ function genBipartiteNetwork( const column2Nodes: NodeData[] = [...Array(column2nNodes).keys()].map((i) => { return { id: String(i + column1nNodes), - label: 'Node ' + String(i), + label: 'Node ' + String(i + column1nNodes), }; }); diff --git a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx index f69368b550..5581465b64 100755 --- a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx +++ b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx @@ -4,7 +4,7 @@ import { NodeWithLabel } from '../../plots/Network'; import { Group } from '@visx/group'; export default { - title: 'Plots/Network', + title: 'Plots/Network/NodeWithLabel', component: NodeWithLabel, } as Meta; From 31cce71256fff72c321da1cf26bed57ec7321208 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:26:07 -0400 Subject: [PATCH 19/43] some small styling tweaks --- .../components/src/plots/BipartiteNetwork.tsx | 109 +++++++++++------- .../plots/BipartiteNetwork.stories.tsx | 11 ++ 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 946b47d8dd..4a7c0dd4a5 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -3,6 +3,9 @@ import { partition } from 'lodash'; import { LabelPosition, Link, NodeWithLabel } from './Network'; import { Graph } from '@visx/network'; import { Text } from '@visx/text'; +import { CSSProperties } from 'react'; +import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; +import Spinner from '../components/Spinner'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -11,12 +14,25 @@ export interface BipartiteNetworkProps { column1Name?: string; /** Name of column 2 */ column2Name?: string; + /** styling for the plot's container */ + containerStyles?: CSSProperties; + /** container name */ + containerClass?: string; + /** shall we show the loading spinner? */ + showSpinner?: boolean; } // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. export function BipartiteNetwork(props: BipartiteNetworkProps) { - const { data, column1Name, column2Name } = props; + const { + data, + column1Name, + column2Name, + containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, + containerClass = 'web-components-plot', + showSpinner = false, + } = props; // BIPARTITE network should position nodes!!! @@ -71,45 +87,58 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { }); return ( - - {/* Draw names of node colums if they exist */} - {column1Name && ( - - {column1Name} - - )} - {column2Name && ( - - {column2Name} - - )} - } - // The node components are already transformed using x and y. - // So inside the node component all coords should be relative to this - // initial transform. - nodeComponent={({ node }) => { - const nodeWithLabelProps = { - node: node, - labelPosition: node.labelPosition, - }; - return ; - }} - /> - + + {/* Draw names of node colums if they exist */} + {column1Name && ( + + {column1Name} + + )} + {column2Name && ( + + {column2Name} + + )} + } + // The node components are already transformed using x and y. + // So inside the node component all coords should be relative to this + // initial transform. + nodeComponent={({ node }) => { + const nodeWithLabelProps = { + node: { + ...node, + stroke: '#111', + strokeWidth: 1, + color: '#fff', + r: 6, + }, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + + {showSpinner && } +
); } diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 11fae90d5f..328a756548 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -21,6 +21,7 @@ interface TemplateProps { data: BipartiteNetworkData; column1Name?: string; column2Name?: string; + loading?: boolean; } // This template is a simple network that highlights our BipartiteNetwork component. @@ -29,6 +30,7 @@ const Template: Story = (args) => { data: args.data, column1Name: args.column1Name, column2Name: args.column2Name, + showSpinner: args.loading, }; return ; }; @@ -69,6 +71,15 @@ WithColumnNames.args = { column2Name: 'Column 2', }; +// Loading with a spinner +export const Loading = Template.bind({}); +Loading.args = { + data: simpleData, + column1Name: 'Column 1', + column2Name: 'Column 2', + loading: true, +}; + // Gerenate a network with a given number of nodes and random edges function genBipartiteNetwork( column1nNodes: number, From a8807ca29f37989d53e487c9cef61c07c8564e9b Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:41:19 -0400 Subject: [PATCH 20/43] set default constants for layout --- .../components/src/plots/BipartiteNetwork.tsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 4a7c0dd4a5..128c67c2ab 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -22,8 +22,7 @@ export interface BipartiteNetworkProps { showSpinner?: boolean; } -// NodeWithLabel draws one node and an optional label for the node. Both the node and -// label can be styled. +// TODO Document export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -34,9 +33,15 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { showSpinner = false, } = props; - // BIPARTITE network should position nodes!!! + // Defaults + const DEFAULT_COLUMN1_X = 100; + const DEFAULT_COLUMN2_X = 300; + const DEFAULT_NODE_VERTICAL_SPACE = 30; + const DEFAULT_TOP_PADDING = 40; - // The backend can't do it because we eventually want to click nodes and have them reposition. + // In order to assign coordinates to each node, we'll separate the + // nodes based on their column, then will use their order in the column + // (given by columnXNodeIDs) to finally assign the coordinates. const nodesByColumn: NodeData[][] = partition(data.nodes, (node) => { return data.column1NodeIDs.includes(node.id); }); @@ -44,6 +49,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { const nodesByColumnWithCoordinates = nodesByColumn.map( (column, columnIndex) => { const columnWithCoordinates = column.map((node) => { + // Find the index of the node in the column type ColumnName = keyof typeof data; const columnName = ('column' + (columnIndex + 1) + @@ -53,8 +59,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { ); return { - x: 90 + (columnIndex + 1) * 100, - y: 40 + 30 * indexInColumn, + x: columnIndex ? DEFAULT_COLUMN2_X : DEFAULT_COLUMN1_X, + y: DEFAULT_TOP_PADDING + DEFAULT_NODE_VERTICAL_SPACE * indexInColumn, labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), ...node, }; @@ -95,18 +101,26 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { width={400} height={ Math.max(data.column1NodeIDs.length, data.column2NodeIDs.length) * - 30 + - 50 + DEFAULT_NODE_VERTICAL_SPACE + + DEFAULT_TOP_PADDING } > {/* Draw names of node colums if they exist */} {column1Name && ( - + {column1Name} )} {column2Name && ( - + {column2Name} )} From 641fbfd0c7c7fabf54b509abf62ffce6368af7a3 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:55:12 -0400 Subject: [PATCH 21/43] add twoColorPalette --- .../libs/components/src/plots/BipartiteNetwork.tsx | 12 ++++++++---- packages/libs/components/src/types/plots/addOns.ts | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 128c67c2ab..7d57e012d7 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -6,6 +6,7 @@ import { Text } from '@visx/text'; import { CSSProperties } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; +import { twoColorPalette } from '../types/plots'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -22,7 +23,8 @@ export interface BipartiteNetworkProps { showSpinner?: boolean; } -// TODO Document +// The BipartiteNetwork function draws a two-column network using visx. This component handles +// the positioning of each column, and consequently the positioning of nodes. export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -69,7 +71,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { } ); - const links = data.links.map((link) => { + // Assign coordinates to links based on the newly created node coordinates + const linksWithCoordinates = data.links.map((link) => { const sourceNode = nodesByColumnWithCoordinates[0].find( (node) => node.id === link.source.id ); @@ -88,7 +91,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { y: targetNode?.y, ...link.target, }, - color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + color: + link.color === 'positive' ? twoColorPalette[0] : twoColorPalette[1], }; }); @@ -129,7 +133,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { nodes: nodesByColumnWithCoordinates[0].concat( nodesByColumnWithCoordinates[1] ), - links, + links: linksWithCoordinates, }} // Our Link component has nice defaults and in the future can // carry more complex events. diff --git a/packages/libs/components/src/types/plots/addOns.ts b/packages/libs/components/src/types/plots/addOns.ts index 00480ff4bb..27b97bdb3f 100644 --- a/packages/libs/components/src/types/plots/addOns.ts +++ b/packages/libs/components/src/types/plots/addOns.ts @@ -339,6 +339,9 @@ export const significanceColors: SignificanceColors = { low: '#007F5C', }; +// Color palette optimized for two colors +export const twoColorPalette: string[] = ['#0EADA5', '#AD3C00']; + /** truncated axis flags */ export type AxisTruncationAddon = { /** truncation config (flags) to show truncated axis (true) or not (false) */ From 42985d9eb6e2d686a4e99a154963acd6a59640f6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 16:16:33 -0400 Subject: [PATCH 22/43] set node style defaults and fix link color logic --- .../components/src/plots/BipartiteNetwork.tsx | 21 ++++++---------- .../libs/components/src/plots/Network.tsx | 7 +++--- .../plots/BipartiteNetwork.stories.tsx | 24 ++++--------------- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 7d57e012d7..22ea4a4574 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -21,6 +21,8 @@ export interface BipartiteNetworkProps { containerClass?: string; /** shall we show the loading spinner? */ showSpinner?: boolean; + /** Array of colors to assign to links */ + linkPalette?: string[]; } // The BipartiteNetwork function draws a two-column network using visx. This component handles @@ -33,6 +35,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, + linkPalette, } = props; // Defaults @@ -91,8 +94,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { y: targetNode?.y, ...link.target, }, - color: - link.color === 'positive' ? twoColorPalette[0] : twoColorPalette[1], }; }); @@ -128,6 +129,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { {column2Name} )} + } - // The node components are already transformed using x and y. - // So inside the node component all coords should be relative to this - // initial transform. nodeComponent={({ node }) => { const nodeWithLabelProps = { - node: { - ...node, - stroke: '#111', - strokeWidth: 1, - color: '#fff', - r: 6, - }, + node: node, labelPosition: node.labelPosition, }; return ; diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx index eb5ebea41c..66243bb1cf 100755 --- a/packages/libs/components/src/plots/Network.tsx +++ b/packages/libs/components/src/plots/Network.tsx @@ -22,9 +22,10 @@ interface NodeWithLabelProps { // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. export function NodeWithLabel(props: NodeWithLabelProps) { - const DEFAULT_NODE_RADIUS = 4; - const DEFAULT_NODE_COLOR = '#aaa'; + const DEFAULT_NODE_RADIUS = 6; + const DEFAULT_NODE_COLOR = '#fff'; const DEFAULT_STROKE_WIDTH = 1; + const DEFAULT_STROKE = '#111'; const { node, @@ -60,7 +61,7 @@ export function NodeWithLabel(props: NodeWithLabelProps) { r={nodeRadius} fill={color ?? DEFAULT_NODE_COLOR} onClick={onClick} - stroke={stroke} + stroke={stroke ?? DEFAULT_STROKE} strokeWidth={strokeWidth ?? DEFAULT_STROKE_WIDTH} /> {/* Note that Text becomes a tspan */} diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 328a756548..67b3c705ca 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -8,6 +8,7 @@ import { BipartiteNetwork, BipartiteNetworkProps, } from '../../plots/BipartiteNetwork'; +import { twoColorPalette } from '../../types/plots'; export default { title: 'Plots/Network/BipartiteNetwork', @@ -40,24 +41,14 @@ const Template: Story = (args) => { */ // A simple network with node labels -const simpleData = genBipartiteNetwork( - 20, - 10, - DEFAULT_PLOT_SIZE, - DEFAULT_PLOT_SIZE -); +const simpleData = genBipartiteNetwork(20, 10); export const Simple = Template.bind({}); Simple.args = { data: simpleData, }; // A network with lots and lots of points! -const manyPointsData = genBipartiteNetwork( - 1000, - 100, - DEFAULT_PLOT_SIZE, - DEFAULT_PLOT_SIZE -); +const manyPointsData = genBipartiteNetwork(1000, 100); export const ManyPoints = Template.bind({}); ManyPoints.args = { data: manyPointsData, @@ -81,12 +72,7 @@ Loading.args = { }; // Gerenate a network with a given number of nodes and random edges -function genBipartiteNetwork( - column1nNodes: number, - column2nNodes: number, - height: number, - width: number -) { +function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { // Create the first column of nodes const column1Nodes: NodeData[] = [...Array(column1nNodes).keys()].map((i) => { return { @@ -110,7 +96,7 @@ function genBipartiteNetwork( source: column1Nodes[Math.floor(Math.random() * column1nNodes)], target: column2Nodes[Math.floor(Math.random() * column2nNodes)], strokeWidth: Math.random() * 2, - color: Math.random() > 0.5 ? 'positive' : 'negative', + color: Math.random() > 0.5 ? twoColorPalette[0] : twoColorPalette[1], }; }); From a120e2cc13077b9cc61f17f7fb6cf836ee627b09 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 25 Sep 2023 06:21:22 -0400 Subject: [PATCH 23/43] cleanup --- .../components/src/plots/BipartiteNetwork.tsx | 6 +----- .../plots/BipartiteNetwork.stories.tsx | 20 ++++++++++--------- .../components/src/types/plots/network.ts | 2 -- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 22ea4a4574..053e634ffb 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -6,7 +6,6 @@ import { Text } from '@visx/text'; import { CSSProperties } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; -import { twoColorPalette } from '../types/plots'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -21,12 +20,10 @@ export interface BipartiteNetworkProps { containerClass?: string; /** shall we show the loading spinner? */ showSpinner?: boolean; - /** Array of colors to assign to links */ - linkPalette?: string[]; } // The BipartiteNetwork function draws a two-column network using visx. This component handles -// the positioning of each column, and consequently the positioning of nodes. +// the positioning of each column, and consequently the positioning of nodes and links. export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -35,7 +32,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, - linkPalette, } = props; // Defaults diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 67b3c705ca..0c253dcaa9 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -15,9 +15,6 @@ export default { component: BipartiteNetwork, } as Meta; -// For simplicity, make square svgs with the following height and width -const DEFAULT_PLOT_SIZE = 500; - interface TemplateProps { data: BipartiteNetworkData; column1Name?: string; @@ -25,7 +22,7 @@ interface TemplateProps { loading?: boolean; } -// This template is a simple network that highlights our BipartiteNetwork component. +// Template for showcasing our BipartiteNetwork component. const Template: Story = (args) => { const bipartiteNetworkProps: BipartiteNetworkProps = { data: args.data, @@ -40,7 +37,7 @@ const Template: Story = (args) => { * Stories */ -// A simple network with node labels +// A basic bipartite network const simpleData = genBipartiteNetwork(20, 10); export const Simple = Template.bind({}); Simple.args = { @@ -71,8 +68,11 @@ Loading.args = { loading: true, }; -// Gerenate a network with a given number of nodes and random edges -function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { +// Gerenate a bipartite network with a given number of nodes and random edges +function genBipartiteNetwork( + column1nNodes: number, + column2nNodes: number +): BipartiteNetworkData { // Create the first column of nodes const column1Nodes: NodeData[] = [...Array(column1nNodes).keys()].map((i) => { return { @@ -90,7 +90,9 @@ function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { }); // Create links - // @ANN come back and mmake this more tunable + // Not worried about exactly how many edges we're adding just yet since this is + // used for stories only. Adding color here to mimic what the visualization + // will do. const links: LinkData[] = [...Array(column1nNodes * 2).keys()].map(() => { return { source: column1Nodes[Math.floor(Math.random() * column1nNodes)], @@ -109,5 +111,5 @@ function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { links, column1NodeIDs, column2NodeIDs, - } as BipartiteNetworkData; + }; } diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 81ccbc1314..a8cfb8a5c4 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -12,8 +12,6 @@ export type NodeData = { r?: number; /** User-friendly node label */ label?: string; - /** Draw the label on the right or left of the node */ - // labelPosition?: 'right' | 'left' | undefined; /** Color for the stroke of the node */ stroke?: string; /** Width of node stroke */ From 276b8dce0e3bc2b81cc5c587329eb8254d10b53d Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 25 Sep 2023 10:51:25 -0400 Subject: [PATCH 24/43] added correlation plugin --- .../plugins/correlationAssayMetadata.tsx | 193 ++++++ .../components/computations/plugins/index.ts | 3 +- .../BipartiteNetworkVisualization.tsx | 638 ++++++++++++++++++ 3 files changed, 833 insertions(+), 1 deletion(-) create mode 100644 packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx create mode 100755 packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx new file mode 100644 index 0000000000..a606209350 --- /dev/null +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx @@ -0,0 +1,193 @@ +import { useCollectionVariables, useStudyMetadata } from '../../..'; +import { VariableDescriptor } from '../../../types/variable'; +import { ComputationConfigProps, ComputationPlugin } from '../Types'; +import { isEqual, partial } from 'lodash'; +import { useConfigChangeHandler, assertComputationWithConfig } from '../Utils'; +import * as t from 'io-ts'; +import { Computation } from '../../../types/visualization'; +import SingleSelect from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; +import { useMemo } from 'react'; +import { ComputationStepContainer } from '../ComputationStepContainer'; +import { Filter } from '../../..'; +import './Plugins.scss'; +import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { H6 } from '@veupathdb/coreui'; +import { bipartiteNetworkVisualization } from '../../visualizations/implementations/BipartiteNetworkVisualization'; + +const cx = makeClassNameHelper('AppStepConfigurationContainer'); + +/** + * Correlation + * + * info... + */ + +export type CorrelationAssayMetadataConfig = t.TypeOf< + typeof CorrelationAssayMetadataConfig +>; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const CorrelationAssayMetadataConfig = t.type({ + collectionVariable: VariableDescriptor, + correlationMethod: t.string, +}); + +export const plugin: ComputationPlugin = { + configurationComponent: CorrelationAssayMetadataConfiguration, + configurationDescriptionComponent: + CorrelationAssayMetadataConfigDescriptionComponent, + createDefaultConfiguration: () => undefined, + isConfigurationValid: CorrelationAssayMetadataConfig.is, + visualizationPlugins: { + bipartitenetwork: bipartiteNetworkVisualization, // Must match name in data service and in visualization.tsx + }, +}; + +// Yikes what a name +function CorrelationAssayMetadataConfigDescriptionComponent({ + computation, +}: { + computation: Computation; +}) { + const studyMetadata = useStudyMetadata(); + const collections = useCollectionVariables(studyMetadata.rootEntity); + assertComputationWithConfig( + computation, + Computation + ); + + const { configuration } = computation.descriptor; + const collectionVariable = + 'collectionVariable' in configuration + ? configuration.collectionVariable + : undefined; + const correlationMethod = + 'correlationMethod' in configuration + ? configuration.correlationMethod + : undefined; + + const updatedCollectionVariable = collections.find((collectionVar) => + isEqual( + { + variableId: collectionVar.id, + entityId: collectionVar.entityId, + }, + collectionVariable + ) + ); + + return ( +
+

+ Data:{' '} + + {updatedCollectionVariable ? ( + `${updatedCollectionVariable?.entityDisplayName} > ${updatedCollectionVariable?.displayName}` + ) : ( + Not selected + )} + +

+

+ Method:{' '} + + {correlationMethod ? ( + correlationMethod[0].toUpperCase() + correlationMethod.slice(1) + ) : ( + Not selected + )} + +

+
+ ); +} + +export function CorrelationAssayMetadataConfiguration( + props: ComputationConfigProps +) { + const { + computationAppOverview, + computation, + analysisState, + visualizationId, + } = props; + + const configuration = computation.descriptor + .configuration as CorrelationAssayMetadataConfig; + const studyMetadata = useStudyMetadata(); + + // For now, set the method to 'spearman'. When we add the next method, we can just add it here (no api change!) + if (configuration) configuration.correlationMethod = 'spearman'; + + // Include known collection variables in this array. + const collections = useCollectionVariables(studyMetadata.rootEntity); + if (collections.length === 0) + throw new Error('Could not find any collections for this app.'); + + assertComputationWithConfig( + computation, + Computation + ); + + const changeConfigHandler = + useConfigChangeHandler( + analysisState, + computation, + visualizationId + ); + + const collectionVarItems = useMemo(() => { + return collections.map((collectionVar) => ({ + value: { + variableId: collectionVar.id, + entityId: collectionVar.entityId, + }, + display: + collectionVar.entityDisplayName + ' > ' + collectionVar.displayName, + })); + }, [collections]); + + const selectedCollectionVar = useMemo(() => { + if (configuration && 'collectionVariable' in configuration) { + const selectedItem = collectionVarItems.find((item) => + isEqual(item.value, { + variableId: configuration.collectionVariable.variableId, + entityId: configuration.collectionVariable.entityId, + }) + ); + return selectedItem; + } + }, [collectionVarItems, configuration]); + + return ( + +
+
+
Input Data
+
+ Data + +
+
+
+
+ ); +} diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts index a337e37ae1..518615f2bf 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/index.ts @@ -7,12 +7,13 @@ import { plugin as countsandproportions } from './countsAndProportions'; import { plugin as abundance } from './abundance'; import { plugin as differentialabundance } from './differentialabundance'; import { plugin as xyrelationships } from './xyRelationships'; - +import { plugin as correlationassaymetadata } from './correlationAssayMetadata'; export const plugins: Record = { abundance, alphadiv, betadiv, differentialabundance, + correlationassaymetadata, countsandproportions, distributions, pass, diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx new file mode 100755 index 0000000000..cb4b10f149 --- /dev/null +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -0,0 +1,638 @@ +// load scatter plot component +import VolcanoPlot, { + VolcanoPlotProps, + assignSignificanceColor, + RawDataMinMaxValues, +} from '@veupathdb/components/lib/plots/VolcanoPlot'; + +import * as t from 'io-ts'; +import { useCallback, useMemo } from 'react'; + +import { usePromise } from '../../../hooks/promise'; +import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; +import { + useDataClient, + useStudyEntities, + useStudyMetadata, +} from '../../../hooks/workspace'; +import { PlotLayout } from '../../layouts/PlotLayout'; + +import { VisualizationProps } from '../VisualizationTypes'; + +// concerning axis range control +import { useVizConfig } from '../../../hooks/visualizations'; +import { createVisualizationPlugin } from '../VisualizationPlugin'; +import LabelledGroup from '@veupathdb/components/lib/components/widgets/LabelledGroup'; +import { NumberInput } from '@veupathdb/components/lib/components/widgets/NumberAndDateInputs'; + +import { LayoutOptions } from '../../layouts/types'; +import { RequestOptions } from '../options/types'; + +// Volcano plot imports +import DataClient, { + VolcanoPlotRequestParams, + VolcanoPlotResponse, +} from '../../../api/DataClient'; +import { + VolcanoPlotData, + VolcanoPlotDataPoint, +} from '@veupathdb/components/lib/types/plots/volcanoplot'; +import VolcanoSVG from './selectorIcons/VolcanoSVG'; +import { NumberOrDate } from '@veupathdb/components/lib/types/general'; +import { DifferentialAbundanceConfig } from '../../computations/plugins/differentialabundance'; +import { yellow } from '@material-ui/core/colors'; +import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; +import { significanceColors } from '@veupathdb/components/lib/types/plots'; +import { NumberOrDateRange, NumberRange } from '../../../types/general'; +import { max, min } from 'lodash'; + +// plot controls +import SliderWidget, { + plotsSliderOpacityGradientColorSpec, +} from '@veupathdb/components/lib/components/widgets/Slider'; +import { ResetButtonCoreUI } from '../../ResetButton'; +import AxisRangeControl from '@veupathdb/components/lib/components/plotControls/AxisRangeControl'; +import { fixVarIdLabel } from '../../../utils/visualization'; +// end imports + +const DEFAULT_SIG_THRESHOLD = 0.05; +const DEFAULT_FC_THRESHOLD = 2; +const DEFAULT_MARKER_OPACITY = 0.8; +/** + * The padding ensures we don't clip off part of the glyphs that represent the most extreme points. + * We could have also used d3.scale.nice but then we dont have precise control of where the extremes + * are, which is important for user-defined ranges and truncation bars. + */ +const AXIS_PADDING_FACTOR = 0.05; +const EMPTY_VIZ_AXIS_RANGES = { + independentAxisRange: { min: -9, max: 9 }, + dependentAxisRange: { min: -1, max: 9 }, +}; + +const plotContainerStyles = { + width: 750, + height: 450, + marginLeft: '0.75rem', + border: '1px solid #dedede', + boxShadow: '1px 1px 4px #00000066', +}; + +export const bipartiteNetworkVisualization = createVisualizationPlugin({ + selectorIcon: VolcanoSVG, + fullscreenComponent: VolcanoPlotViz, + createDefaultConfig: createDefaultConfig, +}); + +function createDefaultConfig(): VolcanoPlotConfig { + return { + log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, + significanceThreshold: DEFAULT_SIG_THRESHOLD, + markerBodyOpacity: DEFAULT_MARKER_OPACITY, + independentAxisRange: undefined, + dependentAxisRange: undefined, + }; +} + +export type VolcanoPlotConfig = t.TypeOf; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const VolcanoPlotConfig = t.partial({ + log2FoldChangeThreshold: t.number, + significanceThreshold: t.number, + markerBodyOpacity: t.number, + independentAxisRange: NumberRange, + dependentAxisRange: NumberRange, +}); + +interface Options + extends LayoutOptions, + RequestOptions {} + +// Volcano Plot Visualization +// The volcano plot visualization takes no input variables. The received data populates all parts of the plot. +// The user can control the threshold lines, which affect the marker colors. Additional controls +// include axis ranges and marker opacity slider. +function VolcanoPlotViz(props: VisualizationProps) { + const { + options, + computation, + visualization, + updateConfiguration, + updateThumbnail, + filters, + dataElementConstraints, + dataElementDependencyOrder, + filteredCounts, + computeJobStatus, + } = props; + + const studyMetadata = useStudyMetadata(); + const { id: studyId } = studyMetadata; + const entities = useStudyEntities(filters); + const dataClient: DataClient = useDataClient(); + const computationConfiguration: DifferentialAbundanceConfig = computation + .descriptor.configuration as DifferentialAbundanceConfig; + + const [vizConfig, updateVizConfig] = useVizConfig( + visualization.descriptor.configuration, + VolcanoPlotConfig, + createDefaultConfig, + updateConfiguration + ); + + // Get the volcano plot data! + const data = usePromise( + useCallback(async (): Promise => { + // Only need to check compute job status and filter status, since there are no + // viz input variables. + if (computeJobStatus !== 'complete') return undefined; + if (filteredCounts.pending || filteredCounts.value == null) + return undefined; + + // There are _no_ viz request params for the volcano plot (config: {}). + // The data service streams the volcano data directly from the compute service. + const params = { + studyId, + filters, + config: {}, + computeConfig: computationConfiguration, + }; + const response = await dataClient.getVisualizationData( + computation.descriptor.type, + visualization.descriptor.type, + params, + VolcanoPlotResponse + ); + + return response; + }, [ + computeJobStatus, + filteredCounts.pending, + filteredCounts.value, + filters, + studyId, + computationConfiguration, + computation.descriptor.type, + dataClient, + visualization.descriptor.type, + ]) + ); + + /** + * Find mins and maxes of the data and for the plot. + * The standard x axis is the log2 fold change. The standard + * y axis is -log10 raw p value. + */ + + // Find maxes and mins of the data itself + const rawDataMinMaxValues: RawDataMinMaxValues = useMemo(() => { + if (!data.value) + return { + x: { min: 0, max: 0 }, + y: { min: 1, max: 1 }, + }; + const dataXMin = min(data.value.map((d) => Number(d.log2foldChange))) ?? 0; + const dataXMax = max(data.value.map((d) => Number(d.log2foldChange))) ?? 0; + const dataYMin = min(data.value.map((d) => Number(d.pValue))) ?? 0; + const dataYMax = max(data.value.map((d) => Number(d.pValue))) ?? 0; + return { + x: { min: dataXMin, max: dataXMax }, + y: { min: dataYMin, max: dataYMax }, + }; + }, [data.value]); + + // Determine mins, maxes of axes in the plot. These are different than the data mins/maxes because + // of the log transform and the little bit of padding, or because axis ranges are supplied. + const independentAxisRange = useMemo(() => { + if (!data.value) return undefined; + if (vizConfig.independentAxisRange) { + return vizConfig.independentAxisRange; + } else { + const { + x: { min: dataXMin, max: dataXMax }, + } = rawDataMinMaxValues; + // We can use the dataMin and dataMax here because we don't have a further transform + // Add a little padding to prevent clipping the glyph representing the extreme points + return { + min: Math.floor(dataXMin - (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), + max: Math.ceil(dataXMax + (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), + }; + } + }, [data.value, vizConfig.independentAxisRange, rawDataMinMaxValues]); + + const dependentAxisRange = useMemo(() => { + if (!data.value) return undefined; + if (vizConfig.dependentAxisRange) { + return vizConfig.dependentAxisRange; + } else { + const { + y: { min: dataYMin, max: dataYMax }, + } = rawDataMinMaxValues; + // Standard volcano plots have -log10(raw p value) as the y axis + const yAxisMin = -Math.log10(dataYMax); + const yAxisMax = -Math.log10(dataYMin); + + // Add a little padding to prevent clipping the glyph representing the extreme points + return { + min: Math.floor(yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), + max: Math.ceil(yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), + }; + } + }, [data.value, vizConfig.dependentAxisRange, rawDataMinMaxValues]); + + const significanceThreshold = + vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD; + const log2FoldChangeThreshold = + vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD; + + /** + * This version of the data will get passed to the VolcanoPlot component + */ + const finalData = useMemo(() => { + if (data.value && independentAxisRange && dependentAxisRange) { + const cleanedData = data.value + // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot. + .filter((d) => { + const log2foldChange = Number(d?.log2foldChange); + const transformedPValue = -Math.log10(Number(d?.pValue)); + return ( + log2foldChange <= independentAxisRange.max && + log2foldChange >= independentAxisRange.min && + transformedPValue <= dependentAxisRange.max && + transformedPValue >= dependentAxisRange.min + ); + }) + /** + * Okay, this map function is doing a number of things. + * 1. We're going to remove the pointID property and replace it with a pointIDs property that is an array of strings. + * Some data share coordinates but correspond to a different pointID. By converting pointID to pointIDs, we can + * later aggregate data that share coordinates and then render one tooltip that lists all pointIDs corresponding + * to the point on the plot + * 2. We also add a significanceColor property that is assigned a value that gets used in VolcanoPlot when rendering + * the data point and the data point's tooltip. The property is also used in the countsData logic. + */ + .map((d) => { + const { pointID, ...remainingProperties } = d; + // Try to find a user-friendly label for the point. Note that pointIDs are in entityID.variableID format. + const displayLabel = + pointID && + fixVarIdLabel( + pointID.split('.')[1], + pointID.split('.')[0], + entities + ); + return { + ...remainingProperties, + pointIDs: pointID ? [pointID] : undefined, + displayLabels: displayLabel ? [displayLabel] : undefined, + significanceColor: assignSignificanceColor( + Number(d.log2foldChange), + Number(d.pValue), + significanceThreshold, + log2FoldChangeThreshold, + significanceColors + ), + }; + }) + // Sort data in ascending order for tooltips to work most effectively + .sort((a, b) => Number(a.log2foldChange) - Number(b.log2foldChange)); + + // Here we're going to loop through the cleanedData to aggregate any data with shared coordinates. + // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: + // Yes? => update the matched aggregatedData element's pointID array to include the pointID of the matching entry + // No? => just push the entry onto the aggregatedData array since no match was found + const aggregatedData: VolcanoPlotData = []; + for (const entry of cleanedData) { + const foundIndex = aggregatedData.findIndex( + (d: VolcanoPlotDataPoint) => + d.log2foldChange === entry.log2foldChange && + d.pValue === entry.pValue + ); + if (foundIndex === -1) { + aggregatedData.push(entry); + } else { + const { pointIDs, displayLabels } = aggregatedData[foundIndex]; + if (pointIDs) { + aggregatedData[foundIndex] = { + ...aggregatedData[foundIndex], + pointIDs: [ + ...pointIDs, + ...(entry.pointIDs ? entry.pointIDs : []), + ], + displayLabels: displayLabels && [ + ...displayLabels, + ...(entry.displayLabels ? entry.displayLabels : []), + ], + }; + } else { + aggregatedData[foundIndex] = { + ...aggregatedData[foundIndex], + pointIDs: entry.pointIDs, + displayLabels: entry.displayLabels, + }; + } + } + } + return aggregatedData; + } + }, [ + data.value, + independentAxisRange, + dependentAxisRange, + significanceThreshold, + log2FoldChangeThreshold, + entities, + ]); + + // For the legend, we need the counts of the data + const countsData = useMemo(() => { + if (!finalData) return; + const counts = { + [significanceColors['inconclusive']]: 0, + [significanceColors['high']]: 0, + [significanceColors['low']]: 0, + }; + for (const entry of finalData) { + if (entry.significanceColor) { + // Recall that finalData combines data with shared coords into one point in order to display a + // single tooltip that lists all the pointIDs for that shared point. This means we need to use + // the length of the pointID array to accurately reflect the counts of unique data (not unique coords). + const addend = entry.pointIDs?.length ?? 1; + counts[entry.significanceColor] = + addend + counts[entry.significanceColor]; + } + } + return counts; + }, [finalData]); + + const plotRef = useUpdateThumbnailEffect( + updateThumbnail, + plotContainerStyles, + [ + finalData, + // vizConfig.checkedLegendItems, TODO + vizConfig.independentAxisRange, + vizConfig.dependentAxisRange, + vizConfig.markerBodyOpacity, + ] + ); + + // Add labels to the extremes of the x axis. These may change in the future based on the type + // of data. For example, for genes we may want to say Up regulated in... + const comparisonLabels = + computationConfiguration && + computationConfiguration.comparator?.groupA && + computationConfiguration.comparator?.groupB + ? [ + 'Up in ' + + computationConfiguration.comparator.groupA + .map((entry) => entry.label) + .join(','), + 'Up in ' + + computationConfiguration.comparator.groupB + .map((entry) => entry.label) + .join(','), + ] + : []; + + const volcanoPlotProps: VolcanoPlotProps = { + /** + * VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined. + * In order to display an empty viz, EmptyVolcanoPlotData is defined as: + * const EmptyVolcanoPlotData: VolcanoPlotData = [{log2foldChange: '0', pValue: '1'}]; + */ + data: finalData ? Object.values(finalData) : undefined, + significanceThreshold, + log2FoldChangeThreshold, + /** + * Since we are rendering a single point in order to display an empty viz, let's hide the data point + * by setting the marker opacity to 0 when data.value doesn't exist + */ + markerBodyOpacity: data.value + ? vizConfig.markerBodyOpacity ?? DEFAULT_MARKER_OPACITY + : 0, + containerStyles: plotContainerStyles, + /** + * Let's not display comparisonLabels before we have data for the viz. This prevents what may be + * confusing behavior where selecting group values displays on the empty viz placeholder. + */ + comparisonLabels: data.value ? comparisonLabels : [], + showSpinner: data.pending, + truncationBarFill: yellow[300], + independentAxisRange, + dependentAxisRange, + rawDataMinMaxValues, + /** + * As sophisticated aesthetes, let's specify axis ranges for the empty viz placeholder + */ + ...(data.value ? {} : EMPTY_VIZ_AXIS_RANGES), + }; + + // @ts-ignore + const plotNode = ; + + const controlsNode = ( +
+ + { + updateVizConfig({ markerBodyOpacity: newValue }); + }} + containerStyles={{ width: '20em', marginTop: '0.5em' }} + showLimits={true} + label={'Marker opacity'} + colorSpec={plotsSliderOpacityGradientColorSpec} + /> + +
+
+
+ } + containerStyles={{ + marginRight: 0, + paddingLeft: 0, + }} + /> + + updateVizConfig({ independentAxisRange: undefined }) + } + /> +
+ { + const typeCheckedNewRange = + typeof newRange?.min === 'number' && + typeof newRange?.max === 'number' + ? { + min: newRange.min, + max: newRange.max, + } + : undefined; + updateVizConfig({ + independentAxisRange: typeCheckedNewRange, + }); + }} + step={0.01} + /> +
+ {/** vertical line to separate x from y range controls */} +
+
+
+ } + containerStyles={{ + marginRight: 0, + paddingLeft: 0, + }} + /> + updateVizConfig({ dependentAxisRange: undefined })} + /> +
+ { + const typeCheckedNewRange = + typeof newRange?.min === 'number' && + typeof newRange?.max === 'number' + ? { + min: newRange.min, + max: newRange.max, + } + : undefined; + updateVizConfig({ + dependentAxisRange: typeCheckedNewRange, + }); + }} + step={0.01} + /> +
+
+
+ ); + + const legendNode = finalData && countsData && ( + entry.label) + .join(',')} (${countsData[significanceColors['high']]})`, + marker: 'circle', + hasData: true, + markerColor: significanceColors['high'], + }, + { + label: `Up regulated in ${computationConfiguration.comparator.groupA + ?.map((entry) => entry.label) + .join(',')} (${countsData[significanceColors['low']]})`, + marker: 'circle', + hasData: true, + markerColor: significanceColors['low'], + }, + ]} + showCheckbox={false} + /> + ); + + // TODO + const tableGroupNode = <> ; + + const LayoutComponent = options?.layoutComponent ?? PlotLayout; + + return ( +
+ + + updateVizConfig({ log2FoldChangeThreshold: Number(newValue) }) + } + label="log2(Fold Change)" + minValue={0} + value={vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD} + containerStyles={{ marginRight: 10 }} + /> + + + updateVizConfig({ significanceThreshold: Number(newValue) }) + } + minValue={0} + value={vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD} + containerStyles={{ marginLeft: 10 }} + step={0.001} + /> + + + {/* This should be populated with info from the colections var. So like "Showing 1000 taxa blah". Waiting on collections annotations. */} + {/* */} + +
+ ); +} From 81b3dae3a2170c67e58d936457e4da66160475a1 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Mon, 25 Sep 2023 11:12:11 -0400 Subject: [PATCH 25/43] Remove initial new line from error message (#520) --- packages/libs/http-utils/src/FetchClient.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/libs/http-utils/src/FetchClient.ts b/packages/libs/http-utils/src/FetchClient.ts index 6e0ff116c8..5e4b455128 100644 --- a/packages/libs/http-utils/src/FetchClient.ts +++ b/packages/libs/http-utils/src/FetchClient.ts @@ -107,11 +107,14 @@ export abstract class FetchClient { const { status, statusText } = response; const { headers, method, url } = request; const traceid = headers.get('traceid'); - const fetchError = new FetchClientError(` - ${status} ${statusText}: ${method.toUpperCase()} ${url} - ${traceid != null ? 'Traceid: ' + traceid : ''} + const fetchError = new FetchClientError( + [ + `${status} ${statusText}: ${method.toUpperCase()} ${url}`, + traceid != null ? 'Traceid: ' + traceid + '\n' : '', + await response.text(), + ].join('\n') + ); - ${await response.text()}`); this.onNonSuccessResponse?.(fetchError); throw fetchError; } From cd4c3e9a0e6a8d7069c696a644c76fae602c7268 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Wed, 20 Sep 2023 14:48:07 -0400 Subject: [PATCH 26/43] Wire up a viewFilter for SRT --- .../reporters/SequenceFormFactory.jsx | 68 +++++++++++++++---- ...iptRecordClasses.TranscriptRecordClass.jsx | 4 +- .../js/client/util/transcriptFilters.js | 2 +- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx index d97862f74f..966a68325c 100644 --- a/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx +++ b/packages/libs/web-common/src/components/reporters/SequenceFormFactory.jsx @@ -2,12 +2,18 @@ import React from 'react'; import { RadioList, NumberSelector, + Checkbox, } from '@veupathdb/wdk-client/lib/Components'; import { ComponentsList } from './SequenceFormElements'; import * as ComponentUtils from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import * as ReporterUtils from '@veupathdb/wdk-client/lib/Views/ReporterForm/reporterUtils'; import './ReporterForms.scss'; +const SINGLE_TRANSCRIPT_VIEW_FILTER_VALUE = { + name: 'representativeTranscriptOnly', + value: {}, +}; + const util = Object.assign({}, ComponentUtils, ReporterUtils); const deflineFieldOptions = [ @@ -79,9 +85,27 @@ const createSequenceForm = ( reportType ) => { const Form = (props) => { - const { formState, updateFormState, onSubmit, includeSubmit } = props; + const { + formState, + updateFormState, + onSubmit, + includeSubmit, + viewFilters, + updateViewFilters, + } = props; const getUpdateHandler = (fieldName) => util.getChangeHandler(fieldName, updateFormState, formState); + const transcriptPerGeneChangeHandler = (isChecked) => { + const nextViewFilters = + viewFilters?.filter( + (filterValue) => + filterValue.name !== SINGLE_TRANSCRIPT_VIEW_FILTER_VALUE.name + ) ?? []; + if (isChecked) { + nextViewFilters.push(SINGLE_TRANSCRIPT_VIEW_FILTER_VALUE); + } + updateViewFilters(nextViewFilters); + }; return (
{formBeforeCommonOptions(props)} @@ -95,6 +119,22 @@ const createSequenceForm = ( />
{reportType === 'Sequences' && sequenceOptions(props)} +

Additional options:

+
+ +
{includeSubmit && (
); From 7cfeaa54db90e4ca458cb0ea4223e66fe488ae76 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 26 Sep 2023 06:38:41 -0400 Subject: [PATCH 28/43] add bipartite network viz --- .../components/src/plots/BipartiteNetwork.tsx | 7 +- .../plots/BipartiteNetwork.stories.tsx | 2 +- .../plugins/correlationAssayMetadata.tsx | 3 + .../BipartiteNetworkVisualization.tsx | 564 +++--------------- 4 files changed, 77 insertions(+), 499 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 053e634ffb..0b66de2535 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -1,4 +1,8 @@ -import { BipartiteNetworkData, NodeData } from '../types/plots/network'; +import { + BipartiteNetworkData, + LinkData, + NodeData, +} from '../types/plots/network'; import { partition } from 'lodash'; import { LabelPosition, Link, NodeWithLabel } from './Network'; import { Graph } from '@visx/network'; @@ -6,6 +10,7 @@ import { Text } from '@visx/text'; import { CSSProperties } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; +import { twoColorPalette } from '../types/plots/addOns'; export interface BipartiteNetworkProps { /** Bipartite network data */ diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 0c253dcaa9..a12729ce14 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -8,7 +8,7 @@ import { BipartiteNetwork, BipartiteNetworkProps, } from '../../plots/BipartiteNetwork'; -import { twoColorPalette } from '../../types/plots'; +import { twoColorPalette } from '../../types/plots/addOns'; export default { title: 'Plots/Network/BipartiteNetwork', diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx index a606209350..2a48d6e879 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx @@ -112,6 +112,9 @@ export function CorrelationAssayMetadataConfiguration( visualizationId, } = props; + console.log(computation); + console.log(computationAppOverview); + const configuration = computation.descriptor .configuration as CorrelationAssayMetadataConfig; const studyMetadata = useStudyMetadata(); diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index cb4b10f149..4a670f55bf 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -4,6 +4,10 @@ import VolcanoPlot, { assignSignificanceColor, RawDataMinMaxValues, } from '@veupathdb/components/lib/plots/VolcanoPlot'; +import { + BipartiteNetwork, + BipartiteNetworkProps, +} from '@veupathdb/components/lib/plots/BipartiteNetwork'; import * as t from 'io-ts'; import { useCallback, useMemo } from 'react'; @@ -22,37 +26,22 @@ import { VisualizationProps } from '../VisualizationTypes'; // concerning axis range control import { useVizConfig } from '../../../hooks/visualizations'; import { createVisualizationPlugin } from '../VisualizationPlugin'; -import LabelledGroup from '@veupathdb/components/lib/components/widgets/LabelledGroup'; -import { NumberInput } from '@veupathdb/components/lib/components/widgets/NumberAndDateInputs'; import { LayoutOptions } from '../../layouts/types'; import { RequestOptions } from '../options/types'; -// Volcano plot imports -import DataClient, { - VolcanoPlotRequestParams, - VolcanoPlotResponse, -} from '../../../api/DataClient'; -import { - VolcanoPlotData, - VolcanoPlotDataPoint, -} from '@veupathdb/components/lib/types/plots/volcanoplot'; +// Bipartite network imports import VolcanoSVG from './selectorIcons/VolcanoSVG'; -import { NumberOrDate } from '@veupathdb/components/lib/types/general'; import { DifferentialAbundanceConfig } from '../../computations/plugins/differentialabundance'; -import { yellow } from '@material-ui/core/colors'; -import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; -import { significanceColors } from '@veupathdb/components/lib/types/plots'; -import { NumberOrDateRange, NumberRange } from '../../../types/general'; -import { max, min } from 'lodash'; - -// plot controls -import SliderWidget, { - plotsSliderOpacityGradientColorSpec, -} from '@veupathdb/components/lib/components/widgets/Slider'; -import { ResetButtonCoreUI } from '../../ResetButton'; -import AxisRangeControl from '@veupathdb/components/lib/components/plotControls/AxisRangeControl'; -import { fixVarIdLabel } from '../../../utils/visualization'; +import { NumberRange } from '../../../types/general'; +import { VolcanoPlotRequestParams } from '../../../api/DataClient/types'; +import { + BipartiteNetworkData, + LinkData, + NodeData, +} from '@veupathdb/components/lib/types/plots/network'; +import DataClient from '../../../api/DataClient'; +import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; // end imports const DEFAULT_SIG_THRESHOLD = 0.05; @@ -119,8 +108,6 @@ function VolcanoPlotViz(props: VisualizationProps) { updateConfiguration, updateThumbnail, filters, - dataElementConstraints, - dataElementDependencyOrder, filteredCounts, computeJobStatus, } = props; @@ -139,492 +126,29 @@ function VolcanoPlotViz(props: VisualizationProps) { updateConfiguration ); - // Get the volcano plot data! - const data = usePromise( - useCallback(async (): Promise => { - // Only need to check compute job status and filter status, since there are no - // viz input variables. - if (computeJobStatus !== 'complete') return undefined; - if (filteredCounts.pending || filteredCounts.value == null) - return undefined; - - // There are _no_ viz request params for the volcano plot (config: {}). - // The data service streams the volcano data directly from the compute service. - const params = { - studyId, - filters, - config: {}, - computeConfig: computationConfiguration, - }; - const response = await dataClient.getVisualizationData( - computation.descriptor.type, - visualization.descriptor.type, - params, - VolcanoPlotResponse - ); - - return response; - }, [ - computeJobStatus, - filteredCounts.pending, - filteredCounts.value, - filters, - studyId, - computationConfiguration, - computation.descriptor.type, - dataClient, - visualization.descriptor.type, - ]) - ); - - /** - * Find mins and maxes of the data and for the plot. - * The standard x axis is the log2 fold change. The standard - * y axis is -log10 raw p value. - */ - - // Find maxes and mins of the data itself - const rawDataMinMaxValues: RawDataMinMaxValues = useMemo(() => { - if (!data.value) - return { - x: { min: 0, max: 0 }, - y: { min: 1, max: 1 }, - }; - const dataXMin = min(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataXMax = max(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataYMin = min(data.value.map((d) => Number(d.pValue))) ?? 0; - const dataYMax = max(data.value.map((d) => Number(d.pValue))) ?? 0; - return { - x: { min: dataXMin, max: dataXMax }, - y: { min: dataYMin, max: dataYMax }, - }; - }, [data.value]); - - // Determine mins, maxes of axes in the plot. These are different than the data mins/maxes because - // of the log transform and the little bit of padding, or because axis ranges are supplied. - const independentAxisRange = useMemo(() => { - if (!data.value) return undefined; - if (vizConfig.independentAxisRange) { - return vizConfig.independentAxisRange; - } else { - const { - x: { min: dataXMin, max: dataXMax }, - } = rawDataMinMaxValues; - // We can use the dataMin and dataMax here because we don't have a further transform - // Add a little padding to prevent clipping the glyph representing the extreme points - return { - min: Math.floor(dataXMin - (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), - max: Math.ceil(dataXMax + (dataXMax - dataXMin) * AXIS_PADDING_FACTOR), - }; - } - }, [data.value, vizConfig.independentAxisRange, rawDataMinMaxValues]); - - const dependentAxisRange = useMemo(() => { - if (!data.value) return undefined; - if (vizConfig.dependentAxisRange) { - return vizConfig.dependentAxisRange; - } else { - const { - y: { min: dataYMin, max: dataYMax }, - } = rawDataMinMaxValues; - // Standard volcano plots have -log10(raw p value) as the y axis - const yAxisMin = -Math.log10(dataYMax); - const yAxisMax = -Math.log10(dataYMin); + // Fake data + const data: BipartiteNetworkData = genBipartiteNetwork(100, 10); - // Add a little padding to prevent clipping the glyph representing the extreme points - return { - min: Math.floor(yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), - max: Math.ceil(yAxisMax + (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), - }; - } - }, [data.value, vizConfig.dependentAxisRange, rawDataMinMaxValues]); - - const significanceThreshold = - vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD; - const log2FoldChangeThreshold = - vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD; - - /** - * This version of the data will get passed to the VolcanoPlot component - */ - const finalData = useMemo(() => { - if (data.value && independentAxisRange && dependentAxisRange) { - const cleanedData = data.value - // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot. - .filter((d) => { - const log2foldChange = Number(d?.log2foldChange); - const transformedPValue = -Math.log10(Number(d?.pValue)); - return ( - log2foldChange <= independentAxisRange.max && - log2foldChange >= independentAxisRange.min && - transformedPValue <= dependentAxisRange.max && - transformedPValue >= dependentAxisRange.min - ); - }) - /** - * Okay, this map function is doing a number of things. - * 1. We're going to remove the pointID property and replace it with a pointIDs property that is an array of strings. - * Some data share coordinates but correspond to a different pointID. By converting pointID to pointIDs, we can - * later aggregate data that share coordinates and then render one tooltip that lists all pointIDs corresponding - * to the point on the plot - * 2. We also add a significanceColor property that is assigned a value that gets used in VolcanoPlot when rendering - * the data point and the data point's tooltip. The property is also used in the countsData logic. - */ - .map((d) => { - const { pointID, ...remainingProperties } = d; - // Try to find a user-friendly label for the point. Note that pointIDs are in entityID.variableID format. - const displayLabel = - pointID && - fixVarIdLabel( - pointID.split('.')[1], - pointID.split('.')[0], - entities - ); - return { - ...remainingProperties, - pointIDs: pointID ? [pointID] : undefined, - displayLabels: displayLabel ? [displayLabel] : undefined, - significanceColor: assignSignificanceColor( - Number(d.log2foldChange), - Number(d.pValue), - significanceThreshold, - log2FoldChangeThreshold, - significanceColors - ), - }; - }) - // Sort data in ascending order for tooltips to work most effectively - .sort((a, b) => Number(a.log2foldChange) - Number(b.log2foldChange)); - - // Here we're going to loop through the cleanedData to aggregate any data with shared coordinates. - // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: - // Yes? => update the matched aggregatedData element's pointID array to include the pointID of the matching entry - // No? => just push the entry onto the aggregatedData array since no match was found - const aggregatedData: VolcanoPlotData = []; - for (const entry of cleanedData) { - const foundIndex = aggregatedData.findIndex( - (d: VolcanoPlotDataPoint) => - d.log2foldChange === entry.log2foldChange && - d.pValue === entry.pValue - ); - if (foundIndex === -1) { - aggregatedData.push(entry); - } else { - const { pointIDs, displayLabels } = aggregatedData[foundIndex]; - if (pointIDs) { - aggregatedData[foundIndex] = { - ...aggregatedData[foundIndex], - pointIDs: [ - ...pointIDs, - ...(entry.pointIDs ? entry.pointIDs : []), - ], - displayLabels: displayLabels && [ - ...displayLabels, - ...(entry.displayLabels ? entry.displayLabels : []), - ], - }; - } else { - aggregatedData[foundIndex] = { - ...aggregatedData[foundIndex], - pointIDs: entry.pointIDs, - displayLabels: entry.displayLabels, - }; - } - } - } - return aggregatedData; - } - }, [ - data.value, - independentAxisRange, - dependentAxisRange, - significanceThreshold, - log2FoldChangeThreshold, - entities, - ]); - - // For the legend, we need the counts of the data - const countsData = useMemo(() => { - if (!finalData) return; - const counts = { - [significanceColors['inconclusive']]: 0, - [significanceColors['high']]: 0, - [significanceColors['low']]: 0, - }; - for (const entry of finalData) { - if (entry.significanceColor) { - // Recall that finalData combines data with shared coords into one point in order to display a - // single tooltip that lists all the pointIDs for that shared point. This means we need to use - // the length of the pointID array to accurately reflect the counts of unique data (not unique coords). - const addend = entry.pointIDs?.length ?? 1; - counts[entry.significanceColor] = - addend + counts[entry.significanceColor]; - } - } - return counts; - }, [finalData]); - - const plotRef = useUpdateThumbnailEffect( - updateThumbnail, - plotContainerStyles, - [ - finalData, - // vizConfig.checkedLegendItems, TODO - vizConfig.independentAxisRange, - vizConfig.dependentAxisRange, - vizConfig.markerBodyOpacity, - ] - ); - - // Add labels to the extremes of the x axis. These may change in the future based on the type - // of data. For example, for genes we may want to say Up regulated in... - const comparisonLabels = - computationConfiguration && - computationConfiguration.comparator?.groupA && - computationConfiguration.comparator?.groupB - ? [ - 'Up in ' + - computationConfiguration.comparator.groupA - .map((entry) => entry.label) - .join(','), - 'Up in ' + - computationConfiguration.comparator.groupB - .map((entry) => entry.label) - .join(','), - ] - : []; - - const volcanoPlotProps: VolcanoPlotProps = { + const bipartiteNetworkProps: BipartiteNetworkProps = { /** * VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined. * In order to display an empty viz, EmptyVolcanoPlotData is defined as: * const EmptyVolcanoPlotData: VolcanoPlotData = [{log2foldChange: '0', pValue: '1'}]; */ - data: finalData ? Object.values(finalData) : undefined, - significanceThreshold, - log2FoldChangeThreshold, - /** - * Since we are rendering a single point in order to display an empty viz, let's hide the data point - * by setting the marker opacity to 0 when data.value doesn't exist - */ - markerBodyOpacity: data.value - ? vizConfig.markerBodyOpacity ?? DEFAULT_MARKER_OPACITY - : 0, - containerStyles: plotContainerStyles, - /** - * Let's not display comparisonLabels before we have data for the viz. This prevents what may be - * confusing behavior where selecting group values displays on the empty viz placeholder. - */ - comparisonLabels: data.value ? comparisonLabels : [], - showSpinner: data.pending, - truncationBarFill: yellow[300], - independentAxisRange, - dependentAxisRange, - rawDataMinMaxValues, - /** - * As sophisticated aesthetes, let's specify axis ranges for the empty viz placeholder - */ - ...(data.value ? {} : EMPTY_VIZ_AXIS_RANGES), + data: data, }; // @ts-ignore - const plotNode = ; - - const controlsNode = ( -
- - { - updateVizConfig({ markerBodyOpacity: newValue }); - }} - containerStyles={{ width: '20em', marginTop: '0.5em' }} - showLimits={true} - label={'Marker opacity'} - colorSpec={plotsSliderOpacityGradientColorSpec} - /> - -
-
-
- } - containerStyles={{ - marginRight: 0, - paddingLeft: 0, - }} - /> - - updateVizConfig({ independentAxisRange: undefined }) - } - /> -
- { - const typeCheckedNewRange = - typeof newRange?.min === 'number' && - typeof newRange?.max === 'number' - ? { - min: newRange.min, - max: newRange.max, - } - : undefined; - updateVizConfig({ - independentAxisRange: typeCheckedNewRange, - }); - }} - step={0.01} - /> -
- {/** vertical line to separate x from y range controls */} -
-
-
- } - containerStyles={{ - marginRight: 0, - paddingLeft: 0, - }} - /> - updateVizConfig({ dependentAxisRange: undefined })} - /> -
- { - const typeCheckedNewRange = - typeof newRange?.min === 'number' && - typeof newRange?.max === 'number' - ? { - min: newRange.min, - max: newRange.max, - } - : undefined; - updateVizConfig({ - dependentAxisRange: typeCheckedNewRange, - }); - }} - step={0.01} - /> -
-
-
- ); + const plotNode = ; - const legendNode = finalData && countsData && ( - entry.label) - .join(',')} (${countsData[significanceColors['high']]})`, - marker: 'circle', - hasData: true, - markerColor: significanceColors['high'], - }, - { - label: `Up regulated in ${computationConfiguration.comparator.groupA - ?.map((entry) => entry.label) - .join(',')} (${countsData[significanceColors['low']]})`, - marker: 'circle', - hasData: true, - markerColor: significanceColors['low'], - }, - ]} - showCheckbox={false} - /> - ); - - // TODO + const controlsNode = <> ; + const legendNode = <> ; const tableGroupNode = <> ; const LayoutComponent = options?.layoutComponent ?? PlotLayout; return (
- - - updateVizConfig({ log2FoldChangeThreshold: Number(newValue) }) - } - label="log2(Fold Change)" - minValue={0} - value={vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD} - containerStyles={{ marginRight: 10 }} - /> - - - updateVizConfig({ significanceThreshold: Number(newValue) }) - } - minValue={0} - value={vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD} - containerStyles={{ marginLeft: 10 }} - step={0.001} - /> - - - {/* This should be populated with info from the colections var. So like "Showing 1000 taxa blah". Waiting on collections annotations. */} - {/* */} ) {
); } + +// Gerenate a bipartite network with a given number of nodes and random edges +function genBipartiteNetwork( + column1nNodes: number, + column2nNodes: number +): BipartiteNetworkData { + // Create the first column of nodes + const column1Nodes: NodeData[] = [...Array(column1nNodes).keys()].map((i) => { + return { + id: String(i), + label: 'Node ' + String(i), + }; + }); + + // Create the second column of nodes + const column2Nodes: NodeData[] = [...Array(column2nNodes).keys()].map((i) => { + return { + id: String(i + column1nNodes), + label: 'Node ' + String(i + column1nNodes), + }; + }); + + // Create links + // Not worried about exactly how many edges we're adding just yet since this is + // used for stories only. Adding color here to mimic what the visualization + // will do. + const links: LinkData[] = [...Array(column1nNodes * 2).keys()].map(() => { + return { + source: column1Nodes[Math.floor(Math.random() * column1nNodes)], + target: column2Nodes[Math.floor(Math.random() * column2nNodes)], + strokeWidth: Math.random() * 2, + color: Math.random() > 0.5 ? twoColorPalette[0] : twoColorPalette[1], + }; + }); + + const nodes = column1Nodes.concat(column2Nodes); + const column1NodeIDs = column1Nodes.map((node) => node.id); + const column2NodeIDs = column2Nodes.map((node) => node.id); + + return { + nodes, + links, + column1NodeIDs, + column2NodeIDs, + }; +} From 56ad38df74ef197fabc89e9a3ebe1e672fd044a3 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 26 Sep 2023 06:55:14 -0400 Subject: [PATCH 29/43] add optional width prop and clarify logic --- .../components/src/plots/BipartiteNetwork.tsx | 17 ++++++++++++----- .../stories/plots/BipartiteNetwork.stories.tsx | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 053e634ffb..577099871d 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -20,6 +20,8 @@ export interface BipartiteNetworkProps { containerClass?: string; /** shall we show the loading spinner? */ showSpinner?: boolean; + /** plot width */ + width?: number; } // The BipartiteNetwork function draws a two-column network using visx. This component handles @@ -32,13 +34,16 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, + width, } = props; // Defaults - const DEFAULT_COLUMN1_X = 100; - const DEFAULT_COLUMN2_X = 300; + // Many of the below can get optional props in the future as we figure out optimal layouts + const DEFAULT_WIDTH = 400; const DEFAULT_NODE_VERTICAL_SPACE = 30; const DEFAULT_TOP_PADDING = 40; + const DEFAULT_COLUMN1_X = 100; + const DEFAULT_COLUMN2_X = (width ?? DEFAULT_WIDTH) - DEFAULT_COLUMN1_X; // In order to assign coordinates to each node, we'll separate the // nodes based on their column, then will use their order in the column @@ -60,9 +65,11 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { ); return { - x: columnIndex ? DEFAULT_COLUMN2_X : DEFAULT_COLUMN1_X, + // columnIndex of 0 refers to the left-column nodes whereas 1 refers to right-column nodes + x: columnIndex === 0 ? DEFAULT_COLUMN1_X : DEFAULT_COLUMN2_X, y: DEFAULT_TOP_PADDING + DEFAULT_NODE_VERTICAL_SPACE * indexInColumn, - labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), + labelPosition: + columnIndex === 0 ? 'left' : ('right' as LabelPosition), ...node, }; }); @@ -99,7 +106,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { style={{ ...containerStyles, position: 'relative' }} > = (args) => { column1Name: args.column1Name, column2Name: args.column2Name, showSpinner: args.loading, + width: 500, }; return ; }; From b7b25c51abbc4dff8f76cd896bd5cd327f4e2df6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 27 Sep 2023 07:04:17 -0400 Subject: [PATCH 30/43] add bipartite network thumbnail --- .../components/src/plots/BipartiteNetwork.tsx | 127 +++++++++++------- .../plots/BipartiteNetwork.stories.tsx | 39 +++++- .../BipartiteNetworkVisualization.tsx | 60 +++------ 3 files changed, 136 insertions(+), 90 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 0b66de2535..9291ae2f71 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -7,10 +7,18 @@ import { partition } from 'lodash'; import { LabelPosition, Link, NodeWithLabel } from './Network'; import { Graph } from '@visx/network'; import { Text } from '@visx/text'; -import { CSSProperties } from 'react'; +import { + CSSProperties, + Ref, + forwardRef, + useImperativeHandle, + useRef, +} from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; import { twoColorPalette } from '../types/plots/addOns'; +import { ToImgopts } from 'plotly.js'; +import domToImage from 'dom-to-image'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -29,7 +37,10 @@ export interface BipartiteNetworkProps { // The BipartiteNetwork function draws a two-column network using visx. This component handles // the positioning of each column, and consequently the positioning of nodes and links. -export function BipartiteNetwork(props: BipartiteNetworkProps) { +function BipartiteNetwork( + props: BipartiteNetworkProps, + ref: Ref +) { const { data, column1Name, @@ -45,6 +56,20 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { const DEFAULT_NODE_VERTICAL_SPACE = 30; const DEFAULT_TOP_PADDING = 40; + // Use ref forwarding to enable screenshotting of the plot for thumbnail versions. + const plotRef = useRef(null); + useImperativeHandle( + ref, + () => ({ + // The thumbnail generator makePlotThumbnailUrl expects to call a toImage function + toImage: async (imageOpts: ToImgopts) => { + if (!plotRef.current) throw new Error('Plot not ready'); + return domToImage.toPng(plotRef.current, imageOpts); + }, + }), + [] + ); + // In order to assign coordinates to each node, we'll separate the // nodes based on their column, then will use their order in the column // (given by columnXNodeIDs) to finally assign the coordinates. @@ -103,54 +128,58 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { className={containerClass} style={{ ...containerStyles, position: 'relative' }} > - - {/* Draw names of node colums if they exist */} - {column1Name && ( - - {column1Name} - - )} - {column2Name && ( - - {column2Name} - - )} +
+ + {/* Draw names of node colums if they exist */} + {column1Name && ( + + {column1Name} + + )} + {column2Name && ( + + {column2Name} + + )} - } - nodeComponent={({ node }) => { - const nodeWithLabelProps = { - node: node, - labelPosition: node.labelPosition, - }; - return ; - }} - /> - - {showSpinner && } + } + nodeComponent={({ node }) => { + const nodeWithLabelProps = { + node: node, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + + {showSpinner && } +
); } + +export default forwardRef(BipartiteNetwork); diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index a12729ce14..a9c60c9356 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -1,11 +1,11 @@ +import { useState, useEffect, useRef } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { NodeData, LinkData, BipartiteNetworkData, } from '../../types/plots/network'; -import { - BipartiteNetwork, +import BipartiteNetwork, { BipartiteNetworkProps, } from '../../plots/BipartiteNetwork'; import { twoColorPalette } from '../../types/plots/addOns'; @@ -20,17 +20,41 @@ interface TemplateProps { column1Name?: string; column2Name?: string; loading?: boolean; + showThumbnail?: boolean; } // Template for showcasing our BipartiteNetwork component. const Template: Story = (args) => { + // Generate a jpeg version of the network (svg). + // Mimicks the makePlotThumbnailUrl process in web-eda. + const ref = useRef(null); + const [img, setImg] = useState(''); + useEffect(() => { + setTimeout(() => { + ref.current + ?.toImage({ format: 'jpeg', height: 400, width: 600 }) + .then((src: string) => setImg(src)); + }, 2000); + }, []); + const bipartiteNetworkProps: BipartiteNetworkProps = { data: args.data, column1Name: args.column1Name, column2Name: args.column2Name, showSpinner: args.loading, }; - return ; + return ( + <> + + {args.showThumbnail && ( + <> +

+

A snapshot of the plot will appear below after two sconds...

+ + + )} + + ); }; /** @@ -68,6 +92,15 @@ Loading.args = { loading: true, }; +// Show thumbnail +export const Thumbnail = Template.bind({}); +Thumbnail.args = { + data: genBipartiteNetwork(10, 10), + column1Name: 'Column 1', + column2Name: 'Column 2', + showThumbnail: true, +}; + // Gerenate a bipartite network with a given number of nodes and random edges function genBipartiteNetwork( column1nNodes: number, diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index 4a670f55bf..9bc9cd4c1b 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -44,19 +44,8 @@ import DataClient from '../../../api/DataClient'; import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; // end imports -const DEFAULT_SIG_THRESHOLD = 0.05; -const DEFAULT_FC_THRESHOLD = 2; -const DEFAULT_MARKER_OPACITY = 0.8; -/** - * The padding ensures we don't clip off part of the glyphs that represent the most extreme points. - * We could have also used d3.scale.nice but then we dont have precise control of where the extremes - * are, which is important for user-defined ranges and truncation bars. - */ -const AXIS_PADDING_FACTOR = 0.05; -const EMPTY_VIZ_AXIS_RANGES = { - independentAxisRange: { min: -9, max: 9 }, - dependentAxisRange: { min: -1, max: 9 }, -}; +// Defaults +const DEFAULT_EDGE_THRESHOLD = 0.9; const plotContainerStyles = { width: 750, @@ -67,40 +56,32 @@ const plotContainerStyles = { }; export const bipartiteNetworkVisualization = createVisualizationPlugin({ - selectorIcon: VolcanoSVG, - fullscreenComponent: VolcanoPlotViz, + selectorIcon: VolcanoSVG, // TEMP + fullscreenComponent: BipartiteNetworkViz, createDefaultConfig: createDefaultConfig, }); -function createDefaultConfig(): VolcanoPlotConfig { +function createDefaultConfig(): BipartiteNetworkConfig { return { - log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, - significanceThreshold: DEFAULT_SIG_THRESHOLD, - markerBodyOpacity: DEFAULT_MARKER_OPACITY, - independentAxisRange: undefined, - dependentAxisRange: undefined, + edgeThreshold: DEFAULT_EDGE_THRESHOLD, }; } -export type VolcanoPlotConfig = t.TypeOf; +export type BipartiteNetworkConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare -export const VolcanoPlotConfig = t.partial({ - log2FoldChangeThreshold: t.number, - significanceThreshold: t.number, - markerBodyOpacity: t.number, - independentAxisRange: NumberRange, - dependentAxisRange: NumberRange, +export const BipartiteNetworkConfig = t.partial({ + edgeThreshold: t.number, }); interface Options extends LayoutOptions, - RequestOptions {} + RequestOptions {} // Volcano Plot Visualization // The volcano plot visualization takes no input variables. The received data populates all parts of the plot. // The user can control the threshold lines, which affect the marker colors. Additional controls // include axis ranges and marker opacity slider. -function VolcanoPlotViz(props: VisualizationProps) { +function BipartiteNetworkViz(props: VisualizationProps) { const { options, computation, @@ -121,7 +102,7 @@ function VolcanoPlotViz(props: VisualizationProps) { const [vizConfig, updateVizConfig] = useVizConfig( visualization.descriptor.configuration, - VolcanoPlotConfig, + BipartiteNetworkConfig, createDefaultConfig, updateConfiguration ); @@ -129,17 +110,20 @@ function VolcanoPlotViz(props: VisualizationProps) { // Fake data const data: BipartiteNetworkData = genBipartiteNetwork(100, 10); + const plotRef = useUpdateThumbnailEffect( + updateThumbnail, + plotContainerStyles, + [data] + ); + const bipartiteNetworkProps: BipartiteNetworkProps = { - /** - * VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined. - * In order to display an empty viz, EmptyVolcanoPlotData is defined as: - * const EmptyVolcanoPlotData: VolcanoPlotData = [{log2foldChange: '0', pValue: '1'}]; - */ data: data, }; // @ts-ignore - const plotNode = ; + const plotNode = ( + + ); const controlsNode = <> ; const legendNode = <> ; @@ -161,7 +145,7 @@ function VolcanoPlotViz(props: VisualizationProps) { ); } -// Gerenate a bipartite network with a given number of nodes and random edges +// TEMP: Gerenate a bipartite network with a given number of nodes and random edges function genBipartiteNetwork( column1nNodes: number, column2nNodes: number From beb2018e3dea1dff789fd9537d82e32f54bd974b Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 27 Sep 2023 07:28:21 -0400 Subject: [PATCH 31/43] define api types network --- .../eda/src/lib/core/api/DataClient/types.ts | 31 +++++++++++++++++++ .../BipartiteNetworkVisualization.tsx | 27 +++++++--------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index c9bae7c184..bc08cf1e3c 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -25,6 +25,7 @@ import { } from '../../types/general'; import { VariableDescriptor, StringVariableValue } from '../../types/variable'; import { ComputationAppOverview } from '../../types/visualization'; +import { BipartiteNetworkData } from '@veupathdb/components/lib/types/plots/network'; export const AppsResponse = type({ apps: array(ComputationAppOverview), @@ -373,6 +374,36 @@ export interface VolcanoPlotRequestParams { config: {}; // Empty viz config because there are no viz input vars } +// Bipartite network +export type BipartiteNetworkResponse = TypeOf; + +const NodeData = type({ + id: string, +}); + +export const BipartiteNetworkResponse = type({ + column1NodeIDs: array(string), + column2NodeIDs: array(string), + nodes: array(NodeData), + links: array( + type({ + source: NodeData, + target: NodeData, + strokeWidth: number, + color: string, + }) + ), +}); + +export interface BipartiteNetworkRequestParams { + studyId: string; + filters: Filter[]; + config: { + correlationCoefThreshold?: number; + significanceThreshold?: number; + }; +} + //////////////// // Table Data // //////////////// diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index 9bc9cd4c1b..906091a2d5 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -1,9 +1,3 @@ -// load scatter plot component -import VolcanoPlot, { - VolcanoPlotProps, - assignSignificanceColor, - RawDataMinMaxValues, -} from '@veupathdb/components/lib/plots/VolcanoPlot'; import { BipartiteNetwork, BipartiteNetworkProps, @@ -33,8 +27,7 @@ import { RequestOptions } from '../options/types'; // Bipartite network imports import VolcanoSVG from './selectorIcons/VolcanoSVG'; import { DifferentialAbundanceConfig } from '../../computations/plugins/differentialabundance'; -import { NumberRange } from '../../../types/general'; -import { VolcanoPlotRequestParams } from '../../../api/DataClient/types'; +import { BipartiteNetworkRequestParams } from '../../../api/DataClient/types'; import { BipartiteNetworkData, LinkData, @@ -45,7 +38,8 @@ import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; // end imports // Defaults -const DEFAULT_EDGE_THRESHOLD = 0.9; +const DEFAULT_CORRELATION_COEF_THRESHOLD = 0.9; +const DEFAULT_SIGNIFICANCE_THRESHOLD = 0.05; const plotContainerStyles = { width: 750, @@ -63,24 +57,25 @@ export const bipartiteNetworkVisualization = createVisualizationPlugin({ function createDefaultConfig(): BipartiteNetworkConfig { return { - edgeThreshold: DEFAULT_EDGE_THRESHOLD, + correlationCoefThreshold: DEFAULT_CORRELATION_COEF_THRESHOLD, + significanceThreshold: DEFAULT_SIGNIFICANCE_THRESHOLD, }; } export type BipartiteNetworkConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const BipartiteNetworkConfig = t.partial({ - edgeThreshold: t.number, + correlationCoefThreshold: t.number, + significanceThreshold: t.number, }); interface Options extends LayoutOptions, - RequestOptions {} + RequestOptions {} -// Volcano Plot Visualization -// The volcano plot visualization takes no input variables. The received data populates all parts of the plot. -// The user can control the threshold lines, which affect the marker colors. Additional controls -// include axis ranges and marker opacity slider. +// Bipartite Network Visualization +// The bipartite network takes no input variables, because the received data will complete the plot. +// Eventually the user will be able to control the significance and correlation coefficient values. function BipartiteNetworkViz(props: VisualizationProps) { const { options, From e6634ce5f49856d4db8283f84d3748112cc8d5b5 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Wed, 27 Sep 2023 20:22:01 -0400 Subject: [PATCH 32/43] draft api changes --- .../libs/components/src/plots/VolcanoPlot.tsx | 48 +++++++++++-------- .../components/src/types/plots/volcanoplot.ts | 9 +++- .../eda/src/lib/core/api/DataClient/types.ts | 10 ++-- .../computations/plugins/abundance.tsx | 6 +-- .../computations/plugins/alphaDiv.tsx | 6 +-- .../computations/plugins/betadiv.tsx | 6 +-- .../plugins/differentialabundance.tsx | 38 +++++++++++++-- .../ScatterplotVisualization.tsx | 9 +++- .../visualizations/options/types.ts | 9 +++- .../libs/eda/src/lib/core/types/variable.ts | 10 ++++ 10 files changed, 109 insertions(+), 42 deletions(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 1347097f46..509ec1368e 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -8,6 +8,7 @@ import { import { VolcanoPlotData, VolcanoPlotDataPoint, + VolcanoPlotStats, } from '../types/plots/volcanoplot'; import { NumberRange } from '../types/general'; import { SignificanceColors, significanceColors } from '../types/plots'; @@ -46,13 +47,13 @@ export interface RawDataMinMaxValues { } export interface VolcanoPlotProps { - /** Data for the plot. An array of VolcanoPlotDataPoints */ + /** Data for the plot. An effectSizeLabel and an array of VolcanoPlotDataPoints */ data: VolcanoPlotData | undefined; /** * Used to set the fold change thresholds. Will * set two thresholds at +/- this number. Affects point colors */ - log2FoldChangeThreshold: number; + effectSizeThreshold: number; /** Set the threshold for significance. Affects point colors */ significanceThreshold: number; /** x-axis range */ @@ -83,10 +84,15 @@ export interface VolcanoPlotProps { minPValueCap?: number; } -const EmptyVolcanoPlotData: VolcanoPlotData = [ - { log2foldChange: '0', pValue: '1' }, +const EmptyVolcanoPlotStats: VolcanoPlotStats = [ + { effectSize: '0', pValue: '1' }, ]; +const EmptyVolcanoPlotData: VolcanoPlotData = { + effectSizeLabel: 'log2(FoldChange)', + statistics: EmptyVolcanoPlotStats, +}; + const MARGIN_DEFAULT = 50; interface TruncationRectangleProps { @@ -130,7 +136,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { independentAxisRange, dependentAxisRange, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, markerBodyOpacity, containerClass = 'web-components-plot', containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, @@ -155,6 +161,8 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { [] ); + const effectSizeLabel = data?.effectSizeLabel; + // Set maxes and mins of the data itself from rawDataMinMaxValues prop const { min: dataXMin, max: dataXMax } = rawDataMinMaxValues.x; const { min: dataYMin, max: dataYMax } = rawDataMinMaxValues.y; @@ -193,10 +201,8 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { * Check whether each threshold line is within the graph's axis ranges so we can * prevent the line from rendering outside the graph. */ - const showNegativeFoldChangeThresholdLine = - -log2FoldChangeThreshold > xAxisMin; - const showPositiveFoldChangeThresholdLine = - log2FoldChangeThreshold < xAxisMax; + const showNegativeFoldChangeThresholdLine = -effectSizeThreshold > xAxisMin; + const showPositiveFoldChangeThresholdLine = effectSizeThreshold < xAxisMax; const showSignificanceThresholdLine = -Math.log10(Number(significanceThreshold)) > yAxisMin && -Math.log10(Number(significanceThreshold)) < yAxisMax; @@ -207,7 +213,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { // For the actual volcano plot data. Y axis points are capped at -Math.log10(minPValueCap) const dataAccessors = { - xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.log2foldChange), + xAccessor: (d: VolcanoPlotDataPoint) => Number(d?.effectSize), yAccessor: (d: VolcanoPlotDataPoint) => d.pValue === '0' ? -Math.log10(minPValueCap) @@ -268,7 +274,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { {/* Set up the axes and grid lines. XYChart magically lays them out correctly */} - + {/* X axis annotations */} {comparisonLabels && @@ -316,13 +322,13 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { /> )} - {/* Draw both vertical log2 fold change threshold lines */} - {log2FoldChangeThreshold && ( + {/* Draw both vertical effect size threshold lines */} + {effectSizeThreshold && ( <> {showNegativeFoldChangeThresholdLine && ( ) { {showPositiveFoldChangeThresholdLine && ( ) { d.significanceColor} findNearestDatumOverride={findNearestDatumXY} @@ -432,7 +438,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { >
  • - log2 Fold Change: {data?.log2foldChange} + {effectSizeLabel}: {data?.effectSize}
  • P Value: {data?.pValue} @@ -507,10 +513,10 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { * Assign color to point based on significance and magnitude change thresholds */ export function assignSignificanceColor( - log2foldChange: number, + effectSize: number, pValue: number, significanceThreshold: number, - log2FoldChangeThreshold: number, + effectSizeThreshold: number, significanceColors: SignificanceColors ) { // Test 1. If the y value is higher than the significance threshold, just return not significant @@ -519,12 +525,12 @@ export function assignSignificanceColor( } // Test 2. So the y is significant. Is the x larger than the positive foldChange threshold? - if (log2foldChange >= log2FoldChangeThreshold) { + if (effectSize >= effectSizeThreshold) { return significanceColors['high']; } // Test 3. Is the x value lower than the negative foldChange threshold? - if (log2foldChange <= -log2FoldChangeThreshold) { + if (effectSize <= -effectSizeThreshold) { return significanceColors['low']; } diff --git a/packages/libs/components/src/types/plots/volcanoplot.ts b/packages/libs/components/src/types/plots/volcanoplot.ts index 51a9fe163a..74b179a435 100755 --- a/packages/libs/components/src/types/plots/volcanoplot.ts +++ b/packages/libs/components/src/types/plots/volcanoplot.ts @@ -1,7 +1,7 @@ // Can remove the | undefined from most of these after some other backend work is merged export type VolcanoPlotDataPoint = { // log2foldChange becomes the x axis. Also used for coloring points - log2foldChange?: string; + effectSize?: 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; @@ -15,4 +15,9 @@ export type VolcanoPlotDataPoint = { displayLabels?: string[]; }; -export type VolcanoPlotData = Array; +export type VolcanoPlotStats = Array; + +export type VolcanoPlotData = { + effectSizeLabel: string; + statistics: VolcanoPlotStats; +}; diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index c9bae7c184..6eeeebada2 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -357,16 +357,20 @@ export const ScatterplotResponse = intersection([ // The volcano plot response type MUST be the same as the VolcanoPlotData type defined in the components package export type VolcanoPlotResponse = TypeOf; -// TEMP - Many of these can be simplified after some backend work is merged (microbiomeComputations #37) -export const VolcanoPlotResponse = array( +export const VolcanoPlotStatistics = array( partial({ - log2foldChange: string, + effectSize: string, pValue: string, adjustedPValue: string, pointID: string, }) ); +export const VolcanoPlotResponse = type({ + effectSizeLabel: string, + statistics: VolcanoPlotStatistics, +}); + export interface VolcanoPlotRequestParams { studyId: string; filters: Filter[]; diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx index e5ac58d28e..b6b059ba51 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx @@ -1,6 +1,6 @@ import { useStudyMetadata } from '../../..'; import { useCollectionVariables } from '../../../hooks/workspace'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { boxplotVisualization } from '../../visualizations/implementations/BoxplotVisualization'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; @@ -19,7 +19,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type AbundanceConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const AbundanceConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, rankingMethod: t.string, }); @@ -170,7 +170,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx index eac9b0c18d..76101e76bf 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx @@ -1,5 +1,5 @@ import { useCollectionVariables, useStudyMetadata } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { boxplotVisualization } from '../../visualizations/implementations/BoxplotVisualization'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; @@ -18,7 +18,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type AlphaDivConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const AlphaDivConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, alphaDivMethod: t.string, }); @@ -153,7 +153,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx index 67022b1381..fe8846c1c6 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx @@ -1,5 +1,5 @@ import { useCollectionVariables, useStudyMetadata } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { VariableCollectionDescriptor } from '../../../types/variable'; import { scatterplotVisualization } from '../../visualizations/implementations/ScatterplotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; import { isEqual, partial } from 'lodash'; @@ -18,7 +18,7 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); export type BetaDivConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const BetaDivConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, betaDivDissimilarityMethod: t.string, }); @@ -156,7 +156,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index d3b8a91438..8e59e89c11 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -5,7 +5,10 @@ import { usePromise, useStudyMetadata, } from '../../..'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { volcanoPlotVisualization } from '../../visualizations/implementations/VolcanoPlotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; import { isEqual, partial } from 'lodash'; @@ -67,7 +70,7 @@ const Comparator = t.intersection([ // eslint-disable-next-line @typescript-eslint/no-redeclare export const DifferentialAbundanceConfig = t.type({ - collectionVariable: VariableDescriptor, + collectionVariable: VariableCollectionDescriptor, comparator: Comparator, differentialAbundanceMethod: t.string, }); @@ -151,6 +154,10 @@ function DifferentialAbundanceConfigDescriptionComponent({ ); } +// Include available methods in this array. +// TODO do we need the display names different to these internal strings? +const DIFFERENTIAL_ABUNDANCE_METHODS = ['DESeq', 'Maaslin']; + export function DifferentialAbundanceConfiguration( props: ComputationConfigProps ) { @@ -208,11 +215,12 @@ export function DifferentialAbundanceConfiguration( })); }, [collections]); + // TODO presumably to keep the saved analyses from breaking, we need to maintain support for a variableId const selectedCollectionVar = useMemo(() => { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.variableId, + variableId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); @@ -280,6 +288,12 @@ export function DifferentialAbundanceConfiguration( } ); + const differentialAbundanceMethod = useMemo(() => { + if (configuration && 'differentialAbundanceMethod' in configuration) { + return configuration.differentialAbundanceMethod; + } + }, [configuration]); + return (
+ +
+ Method + ({ + value: method, + display: method, + }))} + /> +
); diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index f45617e6f9..0e51d8b8c6 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -20,7 +20,10 @@ import { } from '../../../hooks/workspace'; import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; import { BirdsEyeView } from '../../BirdsEyeView'; @@ -243,7 +246,9 @@ interface Options getComputedYAxisDetails?( config: unknown ): ComputedVariableDetails | undefined; - getComputedOverlayVariable?(config: unknown): VariableDescriptor | undefined; + getComputedOverlayVariable?( + config: unknown + ): VariableDescriptor | VariableCollectionDescriptor | undefined; hideTrendlines?: boolean; hideLogScale?: boolean; } diff --git a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts index e8cc10e6ad..7032ede4d4 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts +++ b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts @@ -1,11 +1,16 @@ import { ReactNode } from 'react'; import { OverlayConfig } from '../../../api/DataClient'; import { Filter } from '../../../types/filter'; -import { VariableDescriptor } from '../../../types/variable'; +import { + VariableDescriptor, + VariableCollectionDescriptor, +} from '../../../types/variable'; import { Computation } from '../../../types/visualization'; export interface XAxisOptions { - getXAxisVariable?: (computeConfig: unknown) => VariableDescriptor | undefined; + getXAxisVariable?: ( + computeConfig: unknown + ) => VariableDescriptor | VariableCollectionDescriptor | undefined; } export interface OverlayOptions { diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index c1afd30d77..a7d83b6758 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -16,3 +16,13 @@ export const StringVariableValue = t.intersection([ value: t.string, }), ]); + +const _VariableCollectionBase = t.type({ + entityId: t.string, + collectionId: t.string, +}); + +export type VariableCollectionDescriptor = t.TypeOf< + typeof VariableCollectionDescriptor +>; +export const VariableCollectionDescriptor = _VariableCollectionBase; From 0e5a8e20d55b671b230a7ed1d3ee8a0c533b1522 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 28 Sep 2023 14:25:54 -0400 Subject: [PATCH 33/43] bipartite viz handles link colors --- .../BipartiteNetworkVisualization.tsx | 86 ++++++++----------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index 906091a2d5..7abbc9b4b5 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -1,45 +1,32 @@ -import { - BipartiteNetwork, - BipartiteNetworkProps, -} from '@veupathdb/components/lib/plots/BipartiteNetwork'; - import * as t from 'io-ts'; -import { useCallback, useMemo } from 'react'; - -import { usePromise } from '../../../hooks/promise'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; -import { - useDataClient, - useStudyEntities, - useStudyMetadata, -} from '../../../hooks/workspace'; import { PlotLayout } from '../../layouts/PlotLayout'; - import { VisualizationProps } from '../VisualizationTypes'; - -// concerning axis range control -import { useVizConfig } from '../../../hooks/visualizations'; import { createVisualizationPlugin } from '../VisualizationPlugin'; - import { LayoutOptions } from '../../layouts/types'; import { RequestOptions } from '../options/types'; // Bipartite network imports -import VolcanoSVG from './selectorIcons/VolcanoSVG'; -import { DifferentialAbundanceConfig } from '../../computations/plugins/differentialabundance'; +import BipartiteNetwork, { + BipartiteNetworkProps, +} from '@veupathdb/components/lib/plots/BipartiteNetwork'; +import VolcanoSVG from './selectorIcons/VolcanoSVG'; // TEMP import { BipartiteNetworkRequestParams } from '../../../api/DataClient/types'; import { BipartiteNetworkData, LinkData, NodeData, } from '@veupathdb/components/lib/types/plots/network'; -import DataClient from '../../../api/DataClient'; import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; +import { useMemo } from 'react'; +import { scaleOrdinal } from 'd3-scale'; +import { uniq } from 'lodash'; // end imports // Defaults const DEFAULT_CORRELATION_COEF_THRESHOLD = 0.9; const DEFAULT_SIGNIFICANCE_THRESHOLD = 0.05; +const DEFAULT_LINK_COLOR_DATA = '0'; const plotContainerStyles = { width: 750, @@ -77,45 +64,44 @@ interface Options // The bipartite network takes no input variables, because the received data will complete the plot. // Eventually the user will be able to control the significance and correlation coefficient values. function BipartiteNetworkViz(props: VisualizationProps) { - const { - options, - computation, - visualization, - updateConfiguration, - updateThumbnail, - filters, - filteredCounts, - computeJobStatus, - } = props; - - const studyMetadata = useStudyMetadata(); - const { id: studyId } = studyMetadata; - const entities = useStudyEntities(filters); - const dataClient: DataClient = useDataClient(); - const computationConfiguration: DifferentialAbundanceConfig = computation - .descriptor.configuration as DifferentialAbundanceConfig; - - const [vizConfig, updateVizConfig] = useVizConfig( - visualization.descriptor.configuration, - BipartiteNetworkConfig, - createDefaultConfig, - updateConfiguration - ); + const { options, updateThumbnail } = props; // Fake data - const data: BipartiteNetworkData = genBipartiteNetwork(100, 10); + const data: BipartiteNetworkData = useMemo( + () => genBipartiteNetwork(100, 10), + [] + ); + + // Assign color to links. + // Color palettes live here in the frontend, but the backend knows that the edges should be two colors. + // So we'll make it generalizable by mapping the values of the links.color prop to the palette. + const uniqueLinkColors = uniq( + data.links.map((link) => link.color ?? DEFAULT_LINK_COLOR_DATA) + ); + const linkColorScale = scaleOrdinal() + .domain(uniqueLinkColors) + .range(twoColorPalette); // the output palette may change if this visualization is reused in other contexts. + const cleanedData: BipartiteNetworkData = { + ...data, + links: data.links.map((link) => { + return { + ...link, + color: linkColorScale(link.color ?? DEFAULT_LINK_COLOR_DATA), + }; + }), + }; const plotRef = useUpdateThumbnailEffect( updateThumbnail, plotContainerStyles, - [data] + [cleanedData] ); const bipartiteNetworkProps: BipartiteNetworkProps = { - data: data, + data: cleanedData, }; - // @ts-ignore + //@ts-ignore const plotNode = ( ); @@ -170,7 +156,7 @@ function genBipartiteNetwork( source: column1Nodes[Math.floor(Math.random() * column1nNodes)], target: column2Nodes[Math.floor(Math.random() * column2nNodes)], strokeWidth: Math.random() * 2, - color: Math.random() > 0.5 ? twoColorPalette[0] : twoColorPalette[1], + color: Math.random() > 0.5 ? '0' : '1', }; }); From 2828137da9504860f8c3e8a6ca82aec026243672 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 28 Sep 2023 14:30:33 -0400 Subject: [PATCH 34/43] remove unused vars --- packages/libs/components/src/plots/BipartiteNetwork.tsx | 7 +------ packages/libs/eda/src/lib/core/api/DataClient/types.ts | 1 - .../computations/plugins/correlationAssayMetadata.tsx | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 8776eff4e3..fba1b4d747 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -1,8 +1,4 @@ -import { - BipartiteNetworkData, - LinkData, - NodeData, -} from '../types/plots/network'; +import { BipartiteNetworkData, NodeData } from '../types/plots/network'; import { partition } from 'lodash'; import { LabelPosition, Link, NodeWithLabel } from './Network'; import { Graph } from '@visx/network'; @@ -16,7 +12,6 @@ import { } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; -import { twoColorPalette } from '../types/plots/addOns'; import { ToImgopts } from 'plotly.js'; import domToImage from 'dom-to-image'; diff --git a/packages/libs/eda/src/lib/core/api/DataClient/types.ts b/packages/libs/eda/src/lib/core/api/DataClient/types.ts index bc08cf1e3c..ca54125898 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -25,7 +25,6 @@ import { } from '../../types/general'; import { VariableDescriptor, StringVariableValue } from '../../types/variable'; import { ComputationAppOverview } from '../../types/visualization'; -import { BipartiteNetworkData } from '@veupathdb/components/lib/types/plots/network'; export const AppsResponse = type({ apps: array(ComputationAppOverview), diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx index 2a48d6e879..489a168b99 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx @@ -8,7 +8,6 @@ import { Computation } from '../../../types/visualization'; import SingleSelect from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; import { useMemo } from 'react'; import { ComputationStepContainer } from '../ComputationStepContainer'; -import { Filter } from '../../..'; import './Plugins.scss'; import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import { H6 } from '@veupathdb/coreui'; From 7f6e8771f4650ca1f3fb49fc02fc1ca75402b471 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 28 Sep 2023 14:43:23 -0400 Subject: [PATCH 35/43] clean --- .../plugins/correlationAssayMetadata.tsx | 14 +++++++++----- .../BipartiteNetworkVisualization.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx index 489a168b99..d90ad47e96 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/correlationAssayMetadata.tsx @@ -18,7 +18,13 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); /** * Correlation * - * info... + * The Correlation Assay vs Metadata app takes in a user-selected collection (ex. Species) and + * runs a correlation of that data against all appropriate metadata in the study. The result is + * a correlation coefficient and significance value for each (assay member, metadata variable) pair. + * + * Importantly, this is the first of a few correlation-type apps that are coming along in the near future. + * There will also be an Assay vs Assay app and a Metadata vs Metadata correlation app. It's possible that + * when those roll out we'll be able to do a little refactoring to make the code a bit nicer. */ export type CorrelationAssayMetadataConfig = t.TypeOf< @@ -42,7 +48,7 @@ export const plugin: ComputationPlugin = { }, }; -// Yikes what a name +// Renders on the thumbnail page to give a summary of the app instance function CorrelationAssayMetadataConfigDescriptionComponent({ computation, }: { @@ -101,6 +107,7 @@ function CorrelationAssayMetadataConfigDescriptionComponent({ ); } +// Shows as Step 1 in the full screen visualization page export function CorrelationAssayMetadataConfiguration( props: ComputationConfigProps ) { @@ -111,9 +118,6 @@ export function CorrelationAssayMetadataConfiguration( visualizationId, } = props; - console.log(computation); - console.log(computationAppOverview); - const configuration = computation.descriptor .configuration as CorrelationAssayMetadataConfig; const studyMetadata = useStudyMetadata(); diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index 7abbc9b4b5..789133c0f7 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -101,8 +101,8 @@ function BipartiteNetworkViz(props: VisualizationProps) { data: cleanedData, }; - //@ts-ignore const plotNode = ( + //@ts-ignore ); From 32e708eec943c4343fafe1195d666fc894cc2343 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Thu, 28 Sep 2023 22:06:50 -0400 Subject: [PATCH 36/43] wip --- .../computations/plugins/abundance.tsx | 4 +- .../computations/plugins/alphaDiv.tsx | 4 +- .../computations/plugins/betadiv.tsx | 4 +- .../plugins/differentialabundance.tsx | 4 +- .../implementations/BoxplotVisualization.tsx | 22 ++++-- .../ScatterplotVisualization.tsx | 14 +++- .../VolcanoPlotVisualization.tsx | 27 +++---- packages/libs/eda/src/lib/core/types/study.ts | 1 + .../libs/eda/src/lib/core/types/variable.ts | 12 ++++ .../eda/src/lib/core/utils/study-metadata.ts | 71 ++++++++++++++++++- 10 files changed, 133 insertions(+), 30 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx index b6b059ba51..1728f6c7e3 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/abundance.tsx @@ -158,7 +158,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -170,7 +170,7 @@ export function AbundanceConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx index 76101e76bf..d12ed5a99a 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/alphaDiv.tsx @@ -141,7 +141,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -153,7 +153,7 @@ export function AlphaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx index fe8846c1c6..392be442c9 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/betadiv.tsx @@ -144,7 +144,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -156,7 +156,7 @@ export function BetaDivConfiguration(props: ComputationConfigProps) { if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 8e59e89c11..017aa258fc 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -207,7 +207,7 @@ export function DifferentialAbundanceConfiguration( }) .map((collectionVar) => ({ value: { - variableId: collectionVar.id, + collectionId: collectionVar.id, entityId: collectionVar.entityId, }, display: @@ -220,7 +220,7 @@ export function DifferentialAbundanceConfiguration( if (configuration && 'collectionVariable' in configuration) { const selectedItem = collectionVarItems.find((item) => isEqual(item.value, { - variableId: configuration.collectionVariable.collectionId, + collectionId: configuration.collectionVariable.collectionId, entityId: configuration.collectionVariable.entityId, }) ); diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx index 516c48e8ba..0503f7fdac 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx @@ -15,7 +15,10 @@ import { } from '../../../hooks/workspace'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; import { useDataClient, useStudyMetadata } from '../../../hooks/workspace'; -import { VariableDescriptor } from '../../../types/variable'; +import { + isVariableDescriptor, + VariableDescriptor, +} from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -90,7 +93,10 @@ import { truncationConfig } from '../../../utils/truncation-config-utils'; import Notification from '@veupathdb/components/lib/components/widgets//Notification'; import { useDefaultAxisRange } from '../../../hooks/computeDefaultAxisRange'; // alphadiv abundance this should be used for collection variable -import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; +import { + findEntityAndDynamicData, + getTreeNode, +} from '../../../utils/study-metadata'; // type of computedVariableMetadata for computation apps such as alphadiv and abundance import { BoxplotRequestParams, @@ -652,12 +658,11 @@ function BoxplotViz(props: VisualizationProps) { // alphadiv abundance findEntityAndVariable does not work properly for collection variable const independentAxisEntityAndVariable = useMemo( - () => - findCollectionVariableEntityAndVariable(entities, providedXAxisVariable), + () => findEntityAndDynamicData(entities, providedXAxisVariable), [entities, providedXAxisVariable] ); const independentAxisLabel = - independentAxisEntityAndVariable?.variable.displayName ?? + getTreeNode(independentAxisEntityAndVariable)?.displayName ?? variableDisplayWithUnit(xAxisVariable) ?? 'X-axis'; @@ -752,6 +757,7 @@ function BoxplotViz(props: VisualizationProps) { /> ); + // TODO understand how we know this is a collection without checking isCollection? // List variables in a collection one by one in the variable coverage table. Create these extra rows // here and then append to the variable coverage table rows array. const collectionVariableMetadata = data.value?.computedVariableMetadata?.find( @@ -797,7 +803,11 @@ function BoxplotViz(props: VisualizationProps) { role: 'X-axis', required: true, display: independentAxisLabel, - variable: providedXAxisVariable ?? vizConfig.xAxisVariable, + variable: + isVariableDescriptor(providedOverlayVariable) && + providedOverlayVariable != null + ? providedOverlayVariable + : vizConfig.xAxisVariable, }, ...additionalVariableCoverageTableRows, { diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index 0e51d8b8c6..b3da60c46a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -23,6 +23,7 @@ import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from import { VariableDescriptor, VariableCollectionDescriptor, + isVariableDescriptor, } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -852,7 +853,9 @@ function ScatterplotViz(props: VisualizationProps) { if (computedOverlayVariableDescriptor) { return findCollectionVariableEntityAndVariable( entities, - computedOverlayVariableDescriptor + isVariableDescriptor(computedOverlayVariableDescriptor) + ? computedOverlayVariableDescriptor + : undefined )?.variable.displayName; } return variableDisplayWithUnit(overlayVariable); @@ -1912,7 +1915,9 @@ function ScatterplotViz(props: VisualizationProps) { }, { role: 'Y-axis', - required: !computedOverlayVariableDescriptor?.variableId, + required: isVariableDescriptor(computedOverlayVariableDescriptor) + ? !computedOverlayVariableDescriptor?.variableId + : false, display: dependentAxisLabel, variable: !computedOverlayVariableDescriptor && computedYAxisDescriptor @@ -1924,7 +1929,10 @@ function ScatterplotViz(props: VisualizationProps) { required: !!computedOverlayVariableDescriptor, display: legendTitle, variable: - computedOverlayVariableDescriptor ?? vizConfig.overlayVariable, + isVariableDescriptor(computedOverlayVariableDescriptor) && + computedOverlayVariableDescriptor != null + ? computedOverlayVariableDescriptor + : vizConfig.overlayVariable, }, ...additionalVariableCoverageTableRows, { diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 686add6c79..0bf64c75d9 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -190,10 +190,14 @@ function VolcanoPlotViz(props: VisualizationProps) { x: { min: 0, max: 0 }, y: { min: 1, max: 1 }, }; - const dataXMin = min(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataXMax = max(data.value.map((d) => Number(d.log2foldChange))) ?? 0; - const dataYMin = min(data.value.map((d) => Number(d.pValue))) ?? 0; - const dataYMax = max(data.value.map((d) => Number(d.pValue))) ?? 0; + const dataXMin = + min(data.value.statistics.map((d) => Number(d.effectSize))) ?? 0; + const dataXMax = + max(data.value.statistics.map((d) => Number(d.effectSize))) ?? 0; + const dataYMin = + min(data.value.statistics.map((d) => Number(d.pValue))) ?? 0; + const dataYMax = + max(data.value.statistics.map((d) => Number(d.pValue))) ?? 0; return { x: { min: dataXMin, max: dataXMax }, y: { min: dataYMin, max: dataYMax }, @@ -249,14 +253,14 @@ function VolcanoPlotViz(props: VisualizationProps) { */ const finalData = useMemo(() => { if (data.value && independentAxisRange && dependentAxisRange) { - const cleanedData = data.value + const cleanedData = data.value.statistics // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot. .filter((d) => { - const log2foldChange = Number(d?.log2foldChange); + const effectSize = Number(d?.effectSize); const transformedPValue = -Math.log10(Number(d?.pValue)); return ( - log2foldChange <= independentAxisRange.max && - log2foldChange >= independentAxisRange.min && + effectSize <= independentAxisRange.max && + effectSize >= independentAxisRange.min && transformedPValue <= dependentAxisRange.max && transformedPValue >= dependentAxisRange.min ); @@ -285,7 +289,7 @@ function VolcanoPlotViz(props: VisualizationProps) { pointIDs: pointID ? [pointID] : undefined, displayLabels: displayLabel ? [displayLabel] : undefined, significanceColor: assignSignificanceColor( - Number(d.log2foldChange), + Number(d.effectSize), Number(d.pValue), significanceThreshold, log2FoldChangeThreshold, @@ -294,7 +298,7 @@ function VolcanoPlotViz(props: VisualizationProps) { }; }) // Sort data in ascending order for tooltips to work most effectively - .sort((a, b) => Number(a.log2foldChange) - Number(b.log2foldChange)); + .sort((a, b) => Number(a.effectSize) - Number(b.effectSize)); // Here we're going to loop through the cleanedData to aggregate any data with shared coordinates. // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: @@ -304,8 +308,7 @@ function VolcanoPlotViz(props: VisualizationProps) { for (const entry of cleanedData) { const foundIndex = aggregatedData.findIndex( (d: VolcanoPlotDataPoint) => - d.log2foldChange === entry.log2foldChange && - d.pValue === entry.pValue + d.log2foldChange === entry.effectSize && d.pValue === entry.pValue ); if (foundIndex === -1) { aggregatedData.push(entry); diff --git a/packages/libs/eda/src/lib/core/types/study.ts b/packages/libs/eda/src/lib/core/types/study.ts index fecdf8125c..ef83c4f8d4 100644 --- a/packages/libs/eda/src/lib/core/types/study.ts +++ b/packages/libs/eda/src/lib/core/types/study.ts @@ -202,6 +202,7 @@ export const Variable = t.union([ export type VariableTreeNode = t.TypeOf; export const VariableTreeNode = t.union([Variable, VariableCategory]); +// TODO change to VariableCollectionTreeNode export type CollectionVariableTreeNode = t.TypeOf< typeof CollectionVariableTreeNode >; diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index a7d83b6758..033b777239 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -26,3 +26,15 @@ export type VariableCollectionDescriptor = t.TypeOf< typeof VariableCollectionDescriptor >; export const VariableCollectionDescriptor = _VariableCollectionBase; + +export function isVariableDescriptor( + object: any +): object is VariableDescriptor { + return 'entityId' in object && 'variableId' in object; +} + +export function isVariableCollectionDescriptor( + object: any +): object is VariableCollectionDescriptor { + return 'entityId' in object && 'collectionId' in object; +} diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index e4d770658d..6145ac7f1d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -1,11 +1,17 @@ import { keyBy } from 'lodash'; import { find } from '@veupathdb/wdk-client/lib/Utils/IterableUtils'; import { + CollectionVariableTreeNode, MultiFilterVariable, StudyEntity, VariableTreeNode, } from '../types/study'; -import { VariableDescriptor } from '../types/variable'; +import { + VariableCollectionDescriptor, + VariableDescriptor, + isVariableCollectionDescriptor, + isVariableDescriptor, +} from '../types/variable'; import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; export function entityTreeToArray(rootEntity: StudyEntity) { @@ -17,6 +23,35 @@ export interface EntityAndVariable { variable: VariableTreeNode; } +export interface EntityAndVariableCollection { + entity: StudyEntity; + variableCollection: CollectionVariableTreeNode; +} + +export function isEntityAndVariable(object: any): object is EntityAndVariable { + return 'entity' in object && 'variable' in object; +} + +export function isEntityAndVariableCollection( + object: any +): object is EntityAndVariableCollection { + return 'entity' in object && 'variableCollection' in object; +} + +export function getTreeNode( + entityAndDynamicData: + | EntityAndVariable + | EntityAndVariableCollection + | undefined +): VariableTreeNode | CollectionVariableTreeNode | undefined { + if (entityAndDynamicData == null) return undefined; + if (isEntityAndVariable(entityAndDynamicData)) { + return entityAndDynamicData.variable; + } else if (isEntityAndVariableCollection(entityAndDynamicData)) { + return entityAndDynamicData.variableCollection; + } +} + export function findEntityAndVariable( entities: Iterable, variableDescriptor?: VariableDescriptor @@ -36,6 +71,40 @@ export function findEntityAndVariable( return { entity, variable }; } +export function findEntityAndVariableCollection( + entities: Iterable, + variableCollectionDescriptor?: VariableCollectionDescriptor +): EntityAndVariableCollection | undefined { + if (variableCollectionDescriptor == null) return undefined; + const entity = find( + (entity) => entity.id === variableCollectionDescriptor.entityId, + entities + ); + const variableCollection = + entity && + find( + (variableCollection) => + variableCollection.id === variableCollectionDescriptor.collectionId, + entity.collections ?? [] + ); + if (entity == null || variableCollection == null) return undefined; + return { entity, variableCollection }; +} + +export function findEntityAndDynamicData( + entities: Iterable, + dynamicDataDescriptor?: VariableDescriptor | VariableCollectionDescriptor +): EntityAndVariable | EntityAndVariableCollection | undefined { + if (dynamicDataDescriptor == null) return undefined; + if (isVariableDescriptor(dynamicDataDescriptor)) { + return findEntityAndVariable(entities, dynamicDataDescriptor); + } else if (isVariableCollectionDescriptor(dynamicDataDescriptor)) { + return findEntityAndVariableCollection(entities, dynamicDataDescriptor); + } else { + return undefined; + } +} + export function makeEntityDisplayName(entity: StudyEntity, isPlural: boolean) { return !isPlural ? entity.displayName From 2fe70af48092feb84953d9c1ec55017eac04ed6d Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Fri, 29 Sep 2023 12:38:25 -0400 Subject: [PATCH 37/43] check for undefined when testing obj type --- .../computations/plugins/differentialabundance.tsx | 14 ++++++++++++++ packages/libs/eda/src/lib/core/types/variable.ts | 6 ++++++ .../libs/eda/src/lib/core/utils/study-metadata.ts | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 017aa258fc..c9df5e2788 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -117,6 +117,10 @@ function DifferentialAbundanceConfigDescriptionComponent({ 'comparator' in configuration ? findEntityAndVariable(configuration.comparator.variable) : undefined; + const differentialAbundanceMethod = + 'differentialAbundanceMethod' in configuration + ? configuration.differentialAbundanceMethod + : undefined; const updatedCollectionVariable = collections.find((collectionVar) => isEqual( @@ -150,6 +154,16 @@ function DifferentialAbundanceConfigDescriptionComponent({ )} +

+ Method:{' '} + + {differentialAbundanceMethod ? ( + differentialAbundanceMethod + ) : ( + Not selected + )} + +

); } diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index 033b777239..7f13cda8a9 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -30,11 +30,17 @@ export const VariableCollectionDescriptor = _VariableCollectionBase; export function isVariableDescriptor( object: any ): object is VariableDescriptor { + if (!object) { + return false; + } return 'entityId' in object && 'variableId' in object; } export function isVariableCollectionDescriptor( object: any ): object is VariableCollectionDescriptor { + if (!object) { + return false; + } return 'entityId' in object && 'collectionId' in object; } diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index 6145ac7f1d..9c966b171d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -29,12 +29,18 @@ export interface EntityAndVariableCollection { } export function isEntityAndVariable(object: any): object is EntityAndVariable { + if (!object) { + return false; + } return 'entity' in object && 'variable' in object; } export function isEntityAndVariableCollection( object: any ): object is EntityAndVariableCollection { + if (!object) { + return false; + } return 'entity' in object && 'variableCollection' in object; } From 9af1635c1d95bb8e05b300cce1819a795e41f50e Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Fri, 29 Sep 2023 12:42:44 -0400 Subject: [PATCH 38/43] remove hardcoded deseq method --- .../components/computations/plugins/differentialabundance.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index c9df5e2788..9fb81295e8 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -190,9 +190,6 @@ export function DifferentialAbundanceConfiguration( const filters = analysisState.analysis?.descriptor.subset.descriptor; const findEntityAndVariable = useFindEntityAndVariable(filters); - // For now, set the method to DESeq2. When we add the next method, we can just add it here (no api change!) - if (configuration) configuration.differentialAbundanceMethod = 'DESeq'; - // Include known collection variables in this array. const collections = useCollectionVariables(studyMetadata.rootEntity); if (collections.length === 0) From f8d02b0c2c22d86bf7889afe7afaae2f340be9a3 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Sun, 1 Oct 2023 23:03:06 -0400 Subject: [PATCH 39/43] update stories --- .../src/stories/plots/VolcanoPlot.stories.tsx | 196 ++++++++++-------- .../stories/plots/VolcanoPlotRef.stories.tsx | 96 +++++---- .../plugins/differentialabundance.tsx | 9 +- 3 files changed, 163 insertions(+), 138 deletions(-) diff --git a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx index 06d1188a5f..5010f1eb8c 100755 --- a/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlot.stories.tsx @@ -12,7 +12,7 @@ export default { title: 'Plots/VolcanoPlot', component: VolcanoPlot, argTypes: { - log2FoldChangeThreshold: { + effectSizeThreshold: { control: { type: 'range', min: 0.5, max: 10, step: 0.01 }, }, significanceThreshold: { @@ -26,65 +26,71 @@ export default { // of objects for actual use :) interface VEuPathDBVolcanoPlotData { volcanoplot: { - log2foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointID: string[]; + effectSizeLabel: string; + statistics: { + effectSize: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointID: string[]; + }; }; } // Let's make some fake data! const dataSetVolcano: VEuPathDBVolcanoPlotData = { volcanoplot: { - log2foldChange: [ - '2', - '3', - '0.5', - '-0.1', - '1', - '-0.5', - '-1.2', - '4', - '0.2', - '-8', - '-4', - '-3', - '-8.2', - '7', - ], - pValue: [ - '0.001', - '0.0001', - '0.01', - '0.001', - '0.98', - '1', - '0.8', - '1', - '0.6', - '0.001', - '0.0001', - '0.002', - '0', - '0', - ], - adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02', '0', '0'], - pointID: [ - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'buzz', - 'lightyear', - ], + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: [ + '2', + '3', + '0.5', + '-0.1', + '1', + '-0.5', + '-1.2', + '4', + '0.2', + '-8', + '-4', + '-3', + '-8.2', + '7', + ], + pValue: [ + '0.001', + '0.0001', + '0.01', + '0.001', + '0.98', + '1', + '0.8', + '1', + '0.6', + '0.001', + '0.0001', + '0.002', + '0', + '0', + ], + adjustedPValue: ['0.01', '0.001', '0.01', '0.001', '0.02', '0', '0'], + pointID: [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'buzz', + 'lightyear', + ], + }, }, }; @@ -92,21 +98,24 @@ const dataSetVolcano: VEuPathDBVolcanoPlotData = { const nPoints = 300; const dataSetVolcanoManyPoints: VEuPathDBVolcanoPlotData = { volcanoplot: { - 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) => - String(nPoints * Math.random()) - ), - pointID: range(1, nPoints).map((p) => String(p)), + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: 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) => + String(nPoints * Math.random()) + ), + pointID: range(1, nPoints).map((p) => String(p)), + }, }, }; interface TemplateProps { data: VEuPathDBVolcanoPlotData | undefined; markerBodyOpacity: number; - log2FoldChangeThreshold: number; + effectSizeThreshold: number; significanceThreshold: number; adjustedPValueGate: number; independentAxisRange?: NumberRange; @@ -119,51 +128,54 @@ interface TemplateProps { const Template: Story = (args) => { // Process input data. Take the object of arrays and turn it into // an array of data points. Note the backend will do this for us! - const volcanoDataPoints: VolcanoPlotData | undefined = - args.data?.volcanoplot.log2foldChange - .map((l2fc, index) => { + const volcanoDataPoints: VolcanoPlotData | undefined = { + effectSizeLabel: args.data?.volcanoplot.effectSizeLabel ?? '', + statistics: + args.data?.volcanoplot.statistics.effectSize.map((effectSize, index) => { return { - log2foldChange: l2fc, - pValue: args.data?.volcanoplot.pValue[index], - adjustedPValue: args.data?.volcanoplot.adjustedPValue[index], - pointID: args.data?.volcanoplot.pointID[index], + effectSize: effectSize, + pValue: args.data?.volcanoplot.statistics.pValue[index], + adjustedPValue: + args.data?.volcanoplot.statistics.adjustedPValue[index], + pointID: args.data?.volcanoplot.statistics.pointID[index], + significanceColor: assignSignificanceColor( + Number(effectSize), + Number(args.data?.volcanoplot.statistics.pValue[index]), + args.significanceThreshold, + args.effectSizeThreshold, + significanceColors + ), }; - }) - .map((d) => ({ - ...d, - pointIDs: d.pointID ? [d.pointID] : undefined, - significanceColor: assignSignificanceColor( - Number(d.log2foldChange), - Number(d.pValue), - args.significanceThreshold, - args.log2FoldChangeThreshold, - significanceColors - ), - })); + }) ?? [], + }; const rawDataMinMaxValues = { x: { min: (volcanoDataPoints && Math.min( - ...volcanoDataPoints.map((d) => Number(d.log2foldChange)) + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) )) ?? 0, max: (volcanoDataPoints && Math.max( - ...volcanoDataPoints.map((d) => Number(d.log2foldChange)) + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) )) ?? 0, }, y: { min: (volcanoDataPoints && - Math.min(...volcanoDataPoints.map((d) => Number(d.pValue)))) ?? + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + )) ?? 1, max: (volcanoDataPoints && - Math.max(...volcanoDataPoints.map((d) => Number(d.pValue)))) ?? + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + )) ?? 1, }, }; @@ -171,7 +183,7 @@ const Template: Story = (args) => { const volcanoPlotProps: VolcanoPlotProps = { data: volcanoDataPoints, significanceThreshold: args.significanceThreshold, - log2FoldChangeThreshold: args.log2FoldChangeThreshold, + effectSizeThreshold: args.effectSizeThreshold, markerBodyOpacity: args.markerBodyOpacity, comparisonLabels: args.comparisonLabels, independentAxisRange: args.independentAxisRange, @@ -197,7 +209,7 @@ export const Simple = Template.bind({}); Simple.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 1, + effectSizeThreshold: 1, significanceThreshold: 0.01, comparisonLabels: ['up in group a', 'up in group b'], independentAxisRange: { min: -9, max: 9 }, @@ -211,7 +223,7 @@ export const ManyPoints = Template.bind({}); ManyPoints.args = { data: dataSetVolcanoManyPoints, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 3, + effectSizeThreshold: 3, significanceThreshold: 0.01, independentAxisRange: { min: -9, max: 9 }, dependentAxisRange: { min: 0, max: 9 }, @@ -226,7 +238,7 @@ export const Truncation = Template.bind({}); Truncation.args = { data: dataSetVolcano, markerBodyOpacity: 0.5, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, significanceThreshold: 0.01, independentAxisRange: { min: -3, max: 3 }, dependentAxisRange: { min: 1, max: 3 }, @@ -238,7 +250,7 @@ export const Spinner = Template.bind({}); Spinner.args = { data: dataSetVolcano, markerBodyOpacity: 0.8, - log2FoldChangeThreshold: 1, + effectSizeThreshold: 1, significanceThreshold: 0.01, comparisonLabels: ['up in group a', 'up in group b'], independentAxisRange: { min: -8, max: 9 }, @@ -251,7 +263,7 @@ export const Empty = Template.bind({}); Empty.args = { data: undefined, markerBodyOpacity: 0, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, significanceThreshold: 0.05, independentAxisRange: { min: -9, max: 9 }, dependentAxisRange: { min: -1, max: 9 }, diff --git a/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx b/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx index 65bd5e9de8..70b1d10a43 100644 --- a/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx +++ b/packages/libs/components/src/stories/plots/VolcanoPlotRef.stories.tsx @@ -17,7 +17,7 @@ export default { interface TemplateProps { data: VEuPathDBVolcanoPlotData; markerBodyOpacity: number; - log2FoldChangeThreshold: number; + effectSizeThreshold: number; significanceThreshold: number; adjustedPValueGate: number; comparisonLabels?: string[]; @@ -27,23 +27,29 @@ interface TemplateProps { // Generate fake data interface VEuPathDBVolcanoPlotData { volcanoplot: { - log2foldChange: string[]; - pValue: string[]; - adjustedPValue: string[]; - pointID: string[]; + effectSizeLabel: string; + statistics: { + effectSize: string[]; + pValue: string[]; + adjustedPValue: string[]; + pointID: string[]; + }; }; } const nPoints = 20; const data: VEuPathDBVolcanoPlotData = { volcanoplot: { - 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) => - String(nPoints * Math.random()) - ), - pointID: range(1, nPoints).map((p) => String(p)), + effectSizeLabel: 'log2FoldChange', + statistics: { + effectSize: 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) => + String(nPoints * Math.random()) + ), + pointID: range(1, nPoints).map((p) => String(p)), + }, }, }; @@ -65,46 +71,54 @@ const Template: Story = (args) => { }, []); // Wrangle data to get it into the nice form for plot component. - const volcanoDataPoints: VolcanoPlotData = data.volcanoplot.log2foldChange - .map((l2fc, index) => { - return { - log2foldChange: l2fc, - pValue: data.volcanoplot.pValue[index], - adjustedPValue: data.volcanoplot.adjustedPValue[index], - pointID: data.volcanoplot.pointID[index], - }; - }) - .map((d) => ({ - ...d, - pointID: d.pointID ? [d.pointID] : undefined, - significanceColor: assignSignificanceColor( - Number(d.log2foldChange), - Number(d.pValue), - args.significanceThreshold, - args.log2FoldChangeThreshold, - significanceColors - ), - })); + const volcanoDataPoints: VolcanoPlotData = { + effectSizeLabel: data.volcanoplot.effectSizeLabel, + statistics: data.volcanoplot.statistics.effectSize.map( + (effectSize, index) => { + return { + effectSize: effectSize, + pValue: data.volcanoplot.statistics.pValue[index], + adjustedPValue: data.volcanoplot.statistics.adjustedPValue[index], + pointID: data.volcanoplot.statistics.pointID[index], + significanceColor: assignSignificanceColor( + Number(effectSize), + Number(data.volcanoplot.statistics.pValue[index]), + args.significanceThreshold, + args.effectSizeThreshold, + significanceColors + ), + }; + } + ), + }; const rawDataMinMaxValues = { x: { min: - Math.min(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ?? - 0, + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) + ) ?? 0, max: - Math.max(...volcanoDataPoints.map((d) => Number(d.log2foldChange))) ?? - 0, + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.effectSize)) + ) ?? 0, }, y: { - min: Math.min(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0, - max: Math.max(...volcanoDataPoints.map((d) => Number(d.pValue))) ?? 0, + min: + Math.min( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + ) ?? 0, + max: + Math.max( + ...volcanoDataPoints.statistics.map((d) => Number(d.pValue)) + ) ?? 0, }, }; const volcanoPlotProps: VolcanoPlotProps = { data: volcanoDataPoints, significanceThreshold: args.significanceThreshold, - log2FoldChangeThreshold: args.log2FoldChangeThreshold, + effectSizeThreshold: args.effectSizeThreshold, markerBodyOpacity: args.markerBodyOpacity, comparisonLabels: args.comparisonLabels, rawDataMinMaxValues, @@ -128,7 +142,7 @@ export const ToImage = Template.bind({}); ToImage.args = { data: data, significanceThreshold: 0.05, - log2FoldChangeThreshold: 2, + effectSizeThreshold: 2, markerBodyOpacity: 0.9, comparisonLabels: ['a', 'b'], }; diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 9fb81295e8..bcd1e6a418 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -169,7 +169,6 @@ function DifferentialAbundanceConfigDescriptionComponent({ } // Include available methods in this array. -// TODO do we need the display names different to these internal strings? const DIFFERENTIAL_ABUNDANCE_METHODS = ['DESeq', 'Maaslin']; export function DifferentialAbundanceConfiguration( @@ -210,10 +209,10 @@ export function DifferentialAbundanceConfiguration( const collectionVarItems = useMemo(() => { return collections .filter((collectionVar) => { - return collectionVar.normalizationMethod - ? !collectionVar.isProportion && - collectionVar.normalizationMethod === 'NULL' && - !collectionVar.displayName?.includes('pathway') + return collectionVar.normalizationMethod // i guess diy stuff doesnt have this prop? + ? // !collectionVar.isProportion && + // collectionVar.normalizationMethod === 'NULL' && + !collectionVar.displayName?.includes('pathway') : true; }) .map((collectionVar) => ({ From e72fd2dc850575369afb933710d86846e42a3cc9 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Mon, 2 Oct 2023 10:30:42 -0400 Subject: [PATCH 40/43] fix up volcano viz --- .../VolcanoPlotVisualization.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 0bf64c75d9..344b45bd9a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -36,6 +36,7 @@ import DataClient, { import { VolcanoPlotData, VolcanoPlotDataPoint, + VolcanoPlotStats, } from '@veupathdb/components/lib/types/plots/volcanoplot'; import VolcanoSVG from './selectorIcons/VolcanoSVG'; import { NumberOrDate } from '@veupathdb/components/lib/types/general'; @@ -85,7 +86,7 @@ export const volcanoPlotVisualization = createVisualizationPlugin({ function createDefaultConfig(): VolcanoPlotConfig { return { - log2FoldChangeThreshold: DEFAULT_FC_THRESHOLD, + effectSizeThreshold: DEFAULT_FC_THRESHOLD, significanceThreshold: DEFAULT_SIG_THRESHOLD, markerBodyOpacity: DEFAULT_MARKER_OPACITY, independentAxisRange: undefined, @@ -96,7 +97,7 @@ function createDefaultConfig(): VolcanoPlotConfig { export type VolcanoPlotConfig = t.TypeOf; // eslint-disable-next-line @typescript-eslint/no-redeclare export const VolcanoPlotConfig = t.partial({ - log2FoldChangeThreshold: t.number, + effectSizeThreshold: t.number, significanceThreshold: t.number, markerBodyOpacity: t.number, independentAxisRange: NumberRange, @@ -245,8 +246,8 @@ function VolcanoPlotViz(props: VisualizationProps) { const significanceThreshold = vizConfig.significanceThreshold ?? DEFAULT_SIG_THRESHOLD; - const log2FoldChangeThreshold = - vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD; + const effectSizeThreshold = + vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD; /** * This version of the data will get passed to the VolcanoPlot component @@ -292,7 +293,7 @@ function VolcanoPlotViz(props: VisualizationProps) { Number(d.effectSize), Number(d.pValue), significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, significanceColors ), }; @@ -304,11 +305,11 @@ function VolcanoPlotViz(props: VisualizationProps) { // For each entry, we'll check if our aggregatedData includes an item with the same coordinates: // Yes? => update the matched aggregatedData element's pointID array to include the pointID of the matching entry // No? => just push the entry onto the aggregatedData array since no match was found - const aggregatedData: VolcanoPlotData = []; + const aggregatedData: VolcanoPlotStats = []; for (const entry of cleanedData) { const foundIndex = aggregatedData.findIndex( (d: VolcanoPlotDataPoint) => - d.log2foldChange === entry.effectSize && d.pValue === entry.pValue + d.effectSize === entry.effectSize && d.pValue === entry.pValue ); if (foundIndex === -1) { aggregatedData.push(entry); @@ -335,14 +336,17 @@ function VolcanoPlotViz(props: VisualizationProps) { } } } - return aggregatedData; + return { + effectSizeLabel: data.value.effectSizeLabel, + statistics: Object.values(aggregatedData), + }; } }, [ data.value, independentAxisRange, dependentAxisRange, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, entities, ]); @@ -354,7 +358,7 @@ function VolcanoPlotViz(props: VisualizationProps) { [significanceColors['high']]: 0, [significanceColors['low']]: 0, }; - for (const entry of finalData) { + for (const entry of finalData.statistics) { if (entry.significanceColor) { // Recall that finalData combines data with shared coords into one point in order to display a // single tooltip that lists all the pointIDs for that shared point. This means we need to use @@ -401,11 +405,11 @@ function VolcanoPlotViz(props: VisualizationProps) { /** * VolcanoPlot defines an EmptyVolcanoPlotData variable that will be assigned when data is undefined. * In order to display an empty viz, EmptyVolcanoPlotData is defined as: - * const EmptyVolcanoPlotData: VolcanoPlotData = [{log2foldChange: '0', pValue: '1'}]; + * const EmptyVolcanoPlotData: VolcanoPlotData = [{effectSize: '0', pValue: '1'}]; */ - data: finalData ? Object.values(finalData) : undefined, + data: finalData ?? undefined, significanceThreshold, - log2FoldChangeThreshold, + effectSizeThreshold, /** * Since we are rendering a single point in order to display an empty viz, let's hide the data point * by setting the marker opacity to 0 when data.value doesn't exist @@ -602,11 +606,11 @@ function VolcanoPlotViz(props: VisualizationProps) { - updateVizConfig({ log2FoldChangeThreshold: Number(newValue) }) + updateVizConfig({ effectSizeThreshold: Number(newValue) }) } label="log2(Fold Change)" minValue={0} - value={vizConfig.log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD} + value={vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD} containerStyles={{ marginRight: 10 }} /> From a55bb987cc01ea9bac63092e9d3f48c94715a06f Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:18:55 -0400 Subject: [PATCH 41/43] address some review feedback --- packages/libs/components/src/plots/VolcanoPlot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/components/src/plots/VolcanoPlot.tsx b/packages/libs/components/src/plots/VolcanoPlot.tsx index 509ec1368e..2cf096f82c 100755 --- a/packages/libs/components/src/plots/VolcanoPlot.tsx +++ b/packages/libs/components/src/plots/VolcanoPlot.tsx @@ -161,7 +161,7 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref) { [] ); - const effectSizeLabel = data?.effectSizeLabel; + const effectSizeLabel = data.effectSizeLabel; // Set maxes and mins of the data itself from rawDataMinMaxValues prop const { min: dataXMin, max: dataXMax } = rawDataMinMaxValues.x; From 7af0c41ec13caf1c23f087942563d9417e9d2181 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:24:44 -0400 Subject: [PATCH 42/43] effect size threshold label --- .../implementations/VolcanoPlotVisualization.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx index 344b45bd9a..a093726e0b 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/VolcanoPlotVisualization.tsx @@ -157,6 +157,7 @@ function VolcanoPlotViz(props: VisualizationProps) { config: {}, computeConfig: computationConfiguration, }; + const response = await dataClient.getVisualizationData( computation.descriptor.type, visualization.descriptor.type, @@ -608,7 +609,7 @@ function VolcanoPlotViz(props: VisualizationProps) { onValueChange={(newValue?: NumberOrDate) => updateVizConfig({ effectSizeThreshold: Number(newValue) }) } - label="log2(Fold Change)" + label={finalData?.effectSizeLabel ?? 'Effect Size'} minValue={0} value={vizConfig.effectSizeThreshold ?? DEFAULT_FC_THRESHOLD} containerStyles={{ marginRight: 10 }} From 28d59f1b62a22893ee0668d5a7a704aaee5925b7 Mon Sep 17 00:00:00 2001 From: Danielle Callan Date: Tue, 3 Oct 2023 09:35:58 -0400 Subject: [PATCH 43/43] move some helpers to study-metadata --- .../implementations/BoxplotVisualization.tsx | 6 ++---- .../ScatterplotVisualization.tsx | 21 ++++++++++++------- .../libs/eda/src/lib/core/types/variable.ts | 18 ---------------- .../eda/src/lib/core/utils/study-metadata.ts | 20 ++++++++++++++++-- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx index 0503f7fdac..bcfafdff7a 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx @@ -15,10 +15,7 @@ import { } from '../../../hooks/workspace'; import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; import { useDataClient, useStudyMetadata } from '../../../hooks/workspace'; -import { - isVariableDescriptor, - VariableDescriptor, -} from '../../../types/variable'; +import { VariableDescriptor } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -96,6 +93,7 @@ import { useDefaultAxisRange } from '../../../hooks/computeDefaultAxisRange'; import { findEntityAndDynamicData, getTreeNode, + isVariableDescriptor, } from '../../../utils/study-metadata'; // type of computedVariableMetadata for computation apps such as alphadiv and abundance import { diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx index b3da60c46a..2354c4795b 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ScatterplotVisualization.tsx @@ -18,12 +18,15 @@ import { useStudyEntities, useStudyMetadata, } from '../../../hooks/workspace'; -import { findEntityAndVariable as findCollectionVariableEntityAndVariable } from '../../../utils/study-metadata'; +import { + findEntityAndDynamicData, + getTreeNode, + isVariableDescriptor, +} from '../../../utils/study-metadata'; import { VariableDescriptor, VariableCollectionDescriptor, - isVariableDescriptor, } from '../../../types/variable'; import { VariableCoverageTable } from '../../VariableCoverageTable'; @@ -851,12 +854,14 @@ function ScatterplotViz(props: VisualizationProps) { const legendTitle = useMemo(() => { if (computedOverlayVariableDescriptor) { - return findCollectionVariableEntityAndVariable( - entities, - isVariableDescriptor(computedOverlayVariableDescriptor) - ? computedOverlayVariableDescriptor - : undefined - )?.variable.displayName; + return getTreeNode( + findEntityAndDynamicData( + entities, + isVariableDescriptor(computedOverlayVariableDescriptor) + ? computedOverlayVariableDescriptor + : undefined + ) + )?.displayName; } return variableDisplayWithUnit(overlayVariable); }, [entities, overlayVariable, computedOverlayVariableDescriptor]); diff --git a/packages/libs/eda/src/lib/core/types/variable.ts b/packages/libs/eda/src/lib/core/types/variable.ts index 7f13cda8a9..a7d83b6758 100644 --- a/packages/libs/eda/src/lib/core/types/variable.ts +++ b/packages/libs/eda/src/lib/core/types/variable.ts @@ -26,21 +26,3 @@ export type VariableCollectionDescriptor = t.TypeOf< typeof VariableCollectionDescriptor >; export const VariableCollectionDescriptor = _VariableCollectionBase; - -export function isVariableDescriptor( - object: any -): object is VariableDescriptor { - if (!object) { - return false; - } - return 'entityId' in object && 'variableId' in object; -} - -export function isVariableCollectionDescriptor( - object: any -): object is VariableCollectionDescriptor { - if (!object) { - return false; - } - return 'entityId' in object && 'collectionId' in object; -} diff --git a/packages/libs/eda/src/lib/core/utils/study-metadata.ts b/packages/libs/eda/src/lib/core/utils/study-metadata.ts index 9c966b171d..5bf8c9a57d 100644 --- a/packages/libs/eda/src/lib/core/utils/study-metadata.ts +++ b/packages/libs/eda/src/lib/core/utils/study-metadata.ts @@ -9,8 +9,6 @@ import { import { VariableCollectionDescriptor, VariableDescriptor, - isVariableCollectionDescriptor, - isVariableDescriptor, } from '../types/variable'; import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; @@ -44,6 +42,24 @@ export function isEntityAndVariableCollection( return 'entity' in object && 'variableCollection' in object; } +export function isVariableDescriptor( + object: any +): object is VariableDescriptor { + if (!object) { + return false; + } + return 'entityId' in object && 'variableId' in object; +} + +export function isVariableCollectionDescriptor( + object: any +): object is VariableCollectionDescriptor { + if (!object) { + return false; + } + return 'entityId' in object && 'collectionId' in object; +} + export function getTreeNode( entityAndDynamicData: | EntityAndVariable