From fa8588991cdcb938e13e18ba1a9910453152e7ef Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 14 May 2024 07:01:51 -0400 Subject: [PATCH 1/8] add self-correlations plugin --- .../components/computations/plugins/index.ts | 2 + .../computations/plugins/selfCorrelation.tsx | 340 ++++++++++++++++++ packages/libs/eda/src/lib/core/types/apps.ts | 12 + 3 files changed, 354 insertions(+) create mode 100644 packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx 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 c03060b752..58053638d3 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 @@ -9,6 +9,7 @@ import { plugin as differentialabundance } from './differentialabundance'; import { plugin as correlationassaymetadata } from './correlationAssayMetadata'; // mbio import { plugin as correlationassayassay } from './correlationAssayAssay'; // mbio import { plugin as correlation } from './correlation'; // genomics (- vb) +import { plugin as selfcorrelation } from './selfCorrelation'; import { plugin as xyrelationships } from './xyRelationships'; export const plugins: Record = { abundance, @@ -18,6 +19,7 @@ export const plugins: Record = { correlationassaymetadata, correlationassayassay, correlation, + selfcorrelation, countsandproportions, distributions, pass, diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx new file mode 100644 index 0000000000..99f8f98819 --- /dev/null +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -0,0 +1,340 @@ +import { useMemo } from 'react'; +import { VariableTreeNode, useFindEntityAndVariableCollection } from '../../..'; +import { ComputationConfigProps, ComputationPlugin } from '../Types'; +import { partial } from 'lodash'; +import { + useConfigChangeHandler, + assertComputationWithConfig, + isNotAbsoluteAbundanceVariableCollection, +} from '../Utils'; +import { Computation } from '../../../types/visualization'; +import { ComputationStepContainer } from '../ComputationStepContainer'; +import './Plugins.scss'; +import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { H6 } from '@veupathdb/coreui'; +import { bipartiteNetworkVisualization } from '../../visualizations/implementations/BipartiteNetworkVisualization'; +import { VariableCollectionSelectList } from '../../variableSelectors/VariableCollectionSingleSelect'; +import SingleSelect, { + ItemGroup, +} from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; +import { + entityTreeToArray, + findEntityAndVariableCollection, + isVariableCollectionDescriptor, +} from '../../../utils/study-metadata'; +import { IsEnabledInPickerParams } from '../../visualizations/VisualizationTypes'; +import { ancestorEntitiesForEntityId } from '../../../utils/data-element-constraints'; +import { NumberInput } from '@veupathdb/components/lib/components/widgets/NumberAndDateInputs'; +import ExpandablePanel from '@veupathdb/coreui/lib/components/containers/ExpandablePanel'; +import { variableCollectionsAreUnique } from '../../../utils/visualization'; +import PluginError from '../../visualizations/PluginError'; +import { + CompleteSelfCorrelationConfig, + SelfCorrelationConfig, +} from '../../../types/apps'; + +const cx = makeClassNameHelper('AppStepConfigurationContainer'); + +/** + * Self-Correlation + * + * The Correlation app takes all collections and visualizes the correlation between a collection and itself. + * For example, if the collection is a set of genes, the app will show the correlation between every pair of genes in the collection. + * + * As of 05/14/24, this app will only be available for mbio assay data. + */ + +export const plugin: ComputationPlugin = { + configurationComponent: SelfCorrelationConfiguration, + configurationDescriptionComponent: SelfCorrelationConfigDescriptionComponent, + createDefaultConfiguration: () => ({ + prefilterThresholds: { + proportionNonZero: DEFAULT_PROPORTION_NON_ZERO_THRESHOLD, + variance: DEFAULT_VARIANCE_THRESHOLD, + standardDeviation: DEFAULT_STANDARD_DEVIATION_THRESHOLD, + }, + }), + isConfigurationComplete: (configuration) => { + // First, the configuration must be complete + // ANN CLEAN + if (!CompleteSelfCorrelationConfig.is(configuration)) return false; + return true; + }, + visualizationPlugins: { + bipartitenetwork: bipartiteNetworkVisualization.withOptions({ + getLegendTitle(config) { + if (SelfCorrelationConfig.is(config)) { + return ['absolute correlation coefficient', 'correlation direction']; + } else { + return []; + } + }, + // makeGetNodeMenuActions(studyMetadata) { + // const entities = entityTreeToArray(studyMetadata.rootEntity); + // const variables = entities.flatMap((e) => e.variables); + // const collections = entities.flatMap( + // (entity) => entity.collections ?? [] + // ); + // const hostCollection = collections.find( + // (c) => c.id === 'EUPATH_0005050' + // ); + // const parasiteCollection = collections.find( + // (c) => c.id === 'EUPATH_0005051' + // ); + // return function getNodeActions(nodeId: string) { + // const [, variableId] = nodeId.split('.'); + // const variable = variables.find((v) => v.id === variableId); + // if (variable == null) return []; + + // // E.g., "qa." + // const urlPrefix = window.location.host.replace( + // /(plasmodb|hostdb)\.org/, + // '' + // ); + + // const href = parasiteCollection?.memberVariableIds.includes( + // variable.id + // ) + // ? `//${urlPrefix}plasmodb.org/plasmo/app/search/transcript/GenesByRNASeqpfal3D7_Lee_Gambian_ebi_rnaSeq_RSRCWGCNAModules?param.wgcnaParam=${variable.displayName.toLowerCase()}&autoRun=1` + // : hostCollection?.memberVariableIds.includes(variable.id) + // ? `//${urlPrefix}hostdb.org/hostdb/app/search/transcript/GenesByRNASeqhsapREF_Lee_Gambian_ebi_rnaSeq_RSRCWGCNAModules?param.wgcnaParam=${variable.displayName.toLowerCase()}&autoRun=1` + // : undefined; + // if (href == null) return []; + // return [ + // { + // label: 'See list of genes', + // href, + // }, + // ]; + // }; + // }, + // getParitionNames(studyMetadata, config) { + // if (CorrelationConfig.is(config)) { + // const entities = entityTreeToArray(studyMetadata.rootEntity); + // const partition1Name = findEntityAndVariableCollection( + // entities, + // config.data1?.collectionSpec + // )?.variableCollection.displayName; + // const partition2Name = + // config.data2?.dataType === 'collection' + // ? findEntityAndVariableCollection( + // entities, + // config.data2?.collectionSpec + // )?.variableCollection.displayName + // : 'Continuous metadata variables'; + // return { partition1Name, partition2Name }; + // } + // }, + }), // Must match name in data service and in visualization.tsx + }, + isEnabledInPicker: isEnabledInPicker, + studyRequirements: + 'These visualizations are only available for studies with compatible metadata.', +}; + +// Renders on the thumbnail page to give a summary of the app instance +function SelfCorrelationConfigDescriptionComponent({ + computation, +}: { + computation: Computation; +}) { + const findEntityAndVariableCollection = useFindEntityAndVariableCollection(); + assertComputationWithConfig(computation, SelfCorrelationConfig); + + const { data1, correlationMethod } = computation.descriptor.configuration; + + const entityAndCollectionVariableTreeNode = + findEntityAndVariableCollection(data1); + + const correlationMethodDisplayName = correlationMethod + ? CORRELATION_METHODS.find((method) => method.value === correlationMethod) + ?.displayName + : undefined; + + return ( +
+

+ Data 1:{' '} + + {entityAndCollectionVariableTreeNode ? ( + `${entityAndCollectionVariableTreeNode.entity.displayName} > ${entityAndCollectionVariableTreeNode.variableCollection.displayName}` + ) : ( + Not selected + )} + +

+ {/* The method should be disabled unti lthe data is chosen */} +

+ Method:{' '} + + {correlationMethod ? ( + correlationMethodDisplayName + ) : ( + Not selected + )} + +

+
+ ); +} + +const CORRELATION_METHODS = [ + { value: 'spearman', displayName: 'Spearman' }, + { value: 'pearson', displayName: 'Pearson' }, + { value: 'sparcc', displayName: 'SparCC' }, +]; +const DEFAULT_PROPORTION_NON_ZERO_THRESHOLD = 0.05; +const DEFAULT_VARIANCE_THRESHOLD = 0; +const DEFAULT_STANDARD_DEVIATION_THRESHOLD = 0; + +// Shows as Step 1 in the full screen visualization page +export function SelfCorrelationConfiguration(props: ComputationConfigProps) { + const { + computationAppOverview, + computation, + analysisState, + visualizationId, + } = props; + + const configuration = computation.descriptor + .configuration as SelfCorrelationConfig; + + assertComputationWithConfig(computation, SelfCorrelationConfig); + + const changeConfigHandler = useConfigChangeHandler( + analysisState, + computation, + visualizationId + ); + + // Content for the expandable help section + // Note the text is dependent on the context, for example in genomics we'll use different + // language than in mbio. + const helpContent = ( +
+
What is correlation?
+

+ The correlation between two variables (genes, sample metadata, etc.) + describes the degree to which their presence in samples co-fluctuate. + For example, the Age and Shoe Size of children are correlated since as a + child ages, their feet grow. +

+ {/* ANN FILL IN */} +
+ ); + + const correlationMethodSelectorText = useMemo(() => { + if (configuration.correlationMethod) { + return ( + CORRELATION_METHODS.find( + (method) => method.value === configuration.correlationMethod + )?.displayName ?? 'Select a method' + ); + } else { + return 'Select a method'; + } + }, [configuration.correlationMethod]); + + return ( + +
+
+
+
Input Data
+
+ Data 1 + +
+
+
+
Correlation Method
+
+ Method + ({ + value: method.value, + display: method.displayName, + }))} + onSelect={partial(changeConfigHandler, 'correlationMethod')} + /> +
+
+
+
Prefilter Data
+
+ Prevalence: + + Keep if abundance is non-zero in at least{' '} + + { + changeConfigHandler('prefilterThresholds', { + proportionNonZero: + // save as decimal point, not % + newValue != null + ? Number((newValue as number) / 100) + : DEFAULT_PROPORTION_NON_ZERO_THRESHOLD, + variance: + configuration.prefilterThresholds?.variance ?? + DEFAULT_VARIANCE_THRESHOLD, + standardDeviation: + configuration.prefilterThresholds?.standardDeviation ?? + DEFAULT_STANDARD_DEVIATION_THRESHOLD, + }); + }} + containerStyles={{ width: '5.5em' }} + /> + % of samples +
+
+
+
+ {/* PluginError here if the method doesn't agree with the data */} +
+ +
+
+ ); +} + +// The self-correlation app is only available for studies that have at least one collection. +function isEnabledInPicker({ + studyMetadata, +}: IsEnabledInPickerParams): boolean { + if (!studyMetadata) return false; + + const entities = entityTreeToArray(studyMetadata.rootEntity); + // Ensure there are collections in this study. Otherwise, disable app + const studyHasCollections = entities.some( + (entity) => !!entity.collections?.length + ); + + return studyHasCollections; +} diff --git a/packages/libs/eda/src/lib/core/types/apps.ts b/packages/libs/eda/src/lib/core/types/apps.ts index b25b44c5c1..b950e89e88 100644 --- a/packages/libs/eda/src/lib/core/types/apps.ts +++ b/packages/libs/eda/src/lib/core/types/apps.ts @@ -25,3 +25,15 @@ export const CorrelationConfig = t.partial({ export const CompleteCorrelationConfig = partialToCompleteCodec(CorrelationConfig); + +export type SelfCorrelationConfig = t.TypeOf; + +export const SelfCorrelationConfig = t.partial({ + data1: VariableCollectionDescriptor, + correlationMethod: t.string, + prefilterThresholds: FeaturePrefilterThresholds, +}); + +export const CompleteSelfCorrelationConfig = partialToCompleteCodec( + SelfCorrelationConfig +); From 8e0e953542c2d4311de105f27f44b4d5fc97d812 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 14 May 2024 08:10:51 -0400 Subject: [PATCH 2/8] succesful network response --- .../eda/src/lib/core/api/DataClient/types.ts | 58 ++- .../computations/plugins/selfCorrelation.tsx | 4 +- .../implementations/NetworkVisualization.tsx | 468 ++++++++++++++++++ 3 files changed, 526 insertions(+), 4 deletions(-) create mode 100755 packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx 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 4547887010..4afb613dc4 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -15,6 +15,7 @@ import { keyof, boolean, literal, + any, } from 'io-ts'; import { Filter } from '../../types/filter'; import { @@ -394,10 +395,48 @@ export const NodeIdList = type({ // Bipartite network export type BipartiteNetworkResponse = TypeOf; -const NodeData = type({ - id: string, +const NodeData = intersection([ + type({ + id: string, + }), + partial({ + x: number, + y: number, + degree: number, + }), +]); + +export const NetworkData = type({ + nodes: array(NodeData), + links: array( + intersection([ + type({ + source: string, + target: string, + weight: number, + }), + partial({ + color: number, + isDirected: boolean, + }), + ]) + ), }); +// @ANN clean your types! +const NetworkConfig = partial({ + variables: any, + correlationCoefThreshold: number, + significanceThreshold: number, +}); +export const NetworkResponse = type({ + network: type({ + data: NetworkData, + config: NetworkConfig, + }), +}); + +// @ANN clean your types! export const BipartiteNetworkData = type({ partitions: array(NodeIdList), nodes: array(NodeData), @@ -447,6 +486,21 @@ export interface BipartiteNetworkRequestParams { significanceThreshold?: number; }; } +// @ANN can also maybe clean types here +// Correlation Network +// a specific flavor of the network that also includes correlationCoefThreshold and significanceThreshold +export type CorrelationNetworkResponse = TypeOf< + typeof CorrelationNetworkResponse +>; +export const CorrelationNetworkResponse = NetworkResponse; +export interface NetworkRequestParams { + studyId: string; + filters: Filter[]; + config: { + correlationCoefThreshold?: number; + significanceThreshold?: number; + }; +} export type FeaturePrefilterThresholds = TypeOf< typeof FeaturePrefilterThresholds diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index 99f8f98819..c2550236f1 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -12,7 +12,7 @@ import { ComputationStepContainer } from '../ComputationStepContainer'; import './Plugins.scss'; import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import { H6 } from '@veupathdb/coreui'; -import { bipartiteNetworkVisualization } from '../../visualizations/implementations/BipartiteNetworkVisualization'; +import { networkVisualization } from '../../visualizations/implementations/NetworkVisualization'; import { VariableCollectionSelectList } from '../../variableSelectors/VariableCollectionSingleSelect'; import SingleSelect, { ItemGroup, @@ -61,7 +61,7 @@ export const plugin: ComputationPlugin = { return true; }, visualizationPlugins: { - bipartitenetwork: bipartiteNetworkVisualization.withOptions({ + unipartitenetwork: networkVisualization.withOptions({ getLegendTitle(config) { if (SelfCorrelationConfig.is(config)) { return ['absolute correlation coefficient', 'correlation direction']; diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx new file mode 100755 index 0000000000..63ca3d3b6d --- /dev/null +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx @@ -0,0 +1,468 @@ +import * as t from 'io-ts'; +import { useUpdateThumbnailEffect } from '../../../hooks/thumbnails'; +import { VisualizationProps } from '../VisualizationTypes'; +import { createVisualizationPlugin } from '../VisualizationPlugin'; +import { + LayoutOptions, + TitleOptions, + LegendOptions, +} from '../../layouts/types'; +import { RequestOptions } from '../options/types'; + +// Network imports +import NetworkPlot, { + NetworkPlotProps, +} from '@veupathdb/components/lib/plots/NetworkPlot'; +import BipartiteNetworkSVG from './selectorIcons/BipartiteNetworkSVG'; +import { + CorrelationNetworkResponse, + NetworkRequestParams, + NetworkResponse, +} from '../../../api/DataClient/types'; +import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; +import { useCallback, useMemo } from 'react'; +import { scaleOrdinal } from 'd3-scale'; +import { uniq } from 'lodash'; +import { usePromise } from '../../../hooks/promise'; +import { + useDataClient, + useStudyEntities, + useStudyMetadata, +} from '../../../hooks/workspace'; +import { fixVarIdLabel } from '../../../utils/visualization'; +import DataClient from '../../../api/DataClient'; +import { OutputEntityTitle } from '../OutputEntityTitle'; +import { scaleLinear } from 'd3'; +import PlotLegend from '@veupathdb/components/lib/components/plotControls/PlotLegend'; +import { LegendItemsProps } from '@veupathdb/components/lib/components/plotControls/PlotListLegend'; +import { gray } from '@veupathdb/coreui/lib/definitions/colors'; +import '../Visualizations.scss'; +import LabelledGroup from '@veupathdb/components/lib/components/widgets/LabelledGroup'; +import { NumberInput } from '@veupathdb/components/lib/components/widgets/NumberAndDateInputs'; +import { NumberOrDate } from '@veupathdb/components/lib/types/general'; +import { useVizConfig } from '../../../hooks/visualizations'; +import { FacetedPlotLayout } from '../../layouts/FacetedPlotLayout'; +import { H6 } from '@veupathdb/coreui'; +import { CorrelationConfig } from '../../../types/apps'; +import { StudyMetadata } from '../../..'; +import { NodeMenuAction } from '@veupathdb/components/lib/types/plots/network'; +import { LabelPosition } from '@veupathdb/components/lib/plots/Node'; +// end imports + +// Defaults +const DEFAULT_CORRELATION_COEF_THRESHOLD = 0.5; // Ability for user to change this value not yet implemented. +const DEFAULT_SIGNIFICANCE_THRESHOLD = 0.05; // Ability for user to change this value not yet implemented. +const DEFAULT_LINK_COLOR_DATA = '0'; +const MIN_STROKE_WIDTH = 0.5; // Minimum stroke width for links in the network. Will represent the smallest link weight. +const MAX_STROKE_WIDTH = 6; // Maximum stroke width for links in the network. Will represent the largest link weight. +const DEFAULT_NUMBER_OF_LINE_LEGEND_ITEMS = 4; + +const plotContainerStyles = { + width: 800, + height: 600, + marginLeft: '0.75rem', + border: '1px solid #dedede', + boxShadow: '1px 1px 4px #00000066', +}; + +export const networkVisualization = createVisualizationPlugin({ + selectorIcon: BipartiteNetworkSVG, + fullscreenComponent: NetworkViz, + createDefaultConfig: createDefaultConfig, +}); + +function createDefaultConfig(): NetworkConfig { + return { + correlationCoefThreshold: DEFAULT_CORRELATION_COEF_THRESHOLD, + significanceThreshold: DEFAULT_SIGNIFICANCE_THRESHOLD, + }; +} + +export type NetworkConfig = t.TypeOf; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const NetworkConfig = t.partial({ + correlationCoefThreshold: t.number, + significanceThreshold: t.number, +}); + +interface Options + extends LayoutOptions, + TitleOptions, + LegendOptions, + RequestOptions {} + +// 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 threshold values. +function NetworkViz(props: VisualizationProps) { + const { + options, + computation, + visualization, + updateConfiguration, + updateThumbnail, + computeJobStatus, + filteredCounts, + filters, + hideInputsAndControls, + plotContainerStyleOverrides, + } = props; + + const studyMetadata = useStudyMetadata(); + const { id: studyId } = studyMetadata; + const entities = useStudyEntities(filters); + const dataClient: DataClient = useDataClient(); + + const computationConfiguration: CorrelationConfig = computation.descriptor + .configuration as CorrelationConfig; + + const [vizConfig, updateVizConfig] = useVizConfig( + visualization.descriptor.configuration, + NetworkConfig, + createDefaultConfig, + updateConfiguration + ); + + // Get data from the compute job + 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; + + const params = { + studyId, + filters, + config: { + correlationCoefThreshold: vizConfig.correlationCoefThreshold, + significanceThreshold: vizConfig.significanceThreshold, + }, + computeConfig: computationConfiguration, + }; + + const response = await dataClient.getVisualizationData( + computation.descriptor.type, + visualization.descriptor.type, + params, + CorrelationNetworkResponse + ); + + return response; + }, [ + computeJobStatus, + filteredCounts.pending, + filteredCounts.value, + filters, + studyId, + computationConfiguration, + computation.descriptor.type, + dataClient, + visualization.descriptor.type, + vizConfig.correlationCoefThreshold, + vizConfig.significanceThreshold, + ]) + ); + + // Determine min and max stroke widths. For use in scaling the strokes (weightToStrokeWidthMap) and the legend. + const dataWeights = + data.value?.network.data.links.map( + (link) => Number(link.weight) // link.weight will always be a number if defined, because it represents the continuous data associated with that link. + ) ?? []; + // Use Set to dedupe the array of dataWeights + const uniqueDataWeights = Array.from(new Set(dataWeights)); + const minDataWeight = Math.min(...uniqueDataWeights); + const maxDataWeight = Math.max(...uniqueDataWeights); + + // Determine min and max x and y positions for nodes. For use in scaling the nodes. + const dataXPositions = + data.value?.network.data.nodes.map((node) => Number(node.x)) ?? []; + const dataYPositions = + data.value?.network.data.nodes.map((node) => Number(node.y)) ?? []; + const minXPosition = Math.min(...dataXPositions); + const maxXPosition = Math.max(...dataXPositions); + const minYPosition = Math.min(...dataYPositions); + const maxYPosition = Math.max(...dataYPositions); + + // Clean and finalize data format. Specifically, assign link colors, add display labels + const cleanedData = useMemo(() => { + if (!data.value) return undefined; + + const scaleX = scaleLinear() + .domain([minXPosition, maxXPosition]) + .range([80, 500]); + const scaleY = scaleLinear() + .domain([minYPosition, maxYPosition]) + .range([80, 500]); + + // Create map that will adjust each link's weight to find a stroke width that spans an appropriate range for this viz. + const weightToStrokeWidthMap = scaleLinear() + .domain([minDataWeight, maxDataWeight]) + .range([MIN_STROKE_WIDTH, MAX_STROKE_WIDTH]); + + // Assign color to links. + // Color palettes live here in the frontend, but the backend decides how to color links (ex. by sign of correlation, or avg degree of parent nodes). + // So we'll make assigning colors generalizable by mapping the values of the links.color prop to the palette. As we add + // different ways to color links in the future, we can adapt our checks and error messaging. + const uniqueLinkColors = uniq( + data.value?.network.data.links.map( + (link) => link.color?.toString() ?? DEFAULT_LINK_COLOR_DATA + ) + ); + if (uniqueLinkColors.length > twoColorPalette.length) { + throw new Error( + `Found ${uniqueLinkColors.length} link colors but expected only ${twoColorPalette.length}.` + ); + } + // The link color sent from the backend should be either '-1' or '1', but we'll allow any two unique values. Assigning the domain + // in the following way preserves "1" getting mapped to the second color in the palette, even if it's the only + // unique value in uniqueLinkColors. + const linkColorScaleDomain = uniqueLinkColors.every((val) => + ['-1', '1'].includes(val) + ) + ? ['-1', '1'] + : uniqueLinkColors; + const linkColorScale = scaleOrdinal() + .domain(linkColorScaleDomain) + .range(twoColorPalette); // the output palette may change if this visualization is reused in other contexts (ex. not a correlation app). + + // Find display labels + const nodesWithLabels = data.value.network.data.nodes.map((node) => { + // node.id is the entityId.variableId + const displayLabel = fixVarIdLabel( + node.id.split('.')[1], + node.id.split('.')[0], + entities + ); + + return { + ...node, + x: scaleX(Number(node.x)), + y: scaleY(Number(node.y)), + id: node.id, + label: displayLabel, + labelPosition: + scaleX(Number(node.x)) > 200 ? 'right' : ('left' as LabelPosition), + }; + }); + + return { + ...data.value.network.data, + nodes: nodesWithLabels, + links: data.value.network.data.links.map((link) => { + return { + source: { id: link.source }, + target: { id: link.target }, + strokeWidth: weightToStrokeWidthMap(Number(link.weight)), + color: link.color ? linkColorScale(link.color.toString()) : '#000000', + }; + }), + }; + }, [data.value, entities, minDataWeight, maxDataWeight]); + + // plot subtitle + const plotSubtitle = ( +
+

+ {`Showing links with an absolute correlation coefficient above ${vizConfig.correlationCoefThreshold?.toString()} and a p-value below ${vizConfig.significanceThreshold?.toString()}`} +

+

Click on a node to highlight its edges.

+
+ ); + + const finalPlotContainerStyles = useMemo( + () => ({ + ...plotContainerStyles, + ...plotContainerStyleOverrides, + }), + [plotContainerStyleOverrides] + ); + + const plotRef = useUpdateThumbnailEffect( + updateThumbnail, + { + ...finalPlotContainerStyles, + height: 400, // no reason for the thumbnail to be as tall as the network (which could be very, very tall!) + }, + [cleanedData] + ); + + // Have the bpnet component say "No nodes" or whatev and have an extra + // prop called errorMessage or something that displays when there are no nodes. + // that error message can say "your thresholds of blah and blah are too high, change them" + const emptyNetworkContent = ( +
+
No correlation results pass the configured thresholds.
+
+
+ Adjust the correlation coefficient and p-value thresholds to continue. +
+ ); + + console.log('cleanedData', cleanedData); + const networkPlotProps: NetworkPlotProps = { + nodes: cleanedData ? cleanedData.nodes : undefined, + links: cleanedData ? cleanedData.links : undefined, + showSpinner: data.pending, + containerStyles: finalPlotContainerStyles, + labelTruncationLength: 40, + emptyNetworkContent, + }; + + const plotNode = ( + //@ts-ignore + + ); + + const controlsNode = <> ; + + // Create legend for (1) Line/link thickness and (2) Link color. + // For (1), we'll do the following: + // - create a base array that is conditioned on the length of uniqueDataWeights since uniqueDataWeights is a deduped map of ALL data.links.weight + // -- if uniqueDataWeights.length is less than or equal to 4, let's use uniqueDataWeights as our base array sorted from greatest to least + // -- if uniqueDataWeights.length is greater than 4, create an array of a default length filled with 'undefined' + // - create lineLegendItems by mapping over lineLegendItemsBaseArray + // -- if the element (weight) is truthy, then we know we're dealing with a copy of the uniqueDataWeights array and can use this value for weightLabel + // -- if the element is falsy, fall back to previous calculation for weightLabel + const lineLegendItemsBaseArray = + uniqueDataWeights.length <= 4 + ? [...uniqueDataWeights].sort((a, b) => b - a) + : Array(DEFAULT_NUMBER_OF_LINE_LEGEND_ITEMS).fill(undefined); + const lineLegendItems: LegendItemsProps[] = lineLegendItemsBaseArray.map( + (weight, index) => { + const weightLabel = + weight ?? + maxDataWeight - + ((maxDataWeight - minDataWeight) / + (lineLegendItemsBaseArray.length - 1)) * + index; + return { + label: String(weightLabel.toFixed(4)), + marker: 'line', + markerColor: gray[900], + hasData: true, + lineThickness: + String( + MAX_STROKE_WIDTH - + ((MAX_STROKE_WIDTH - MIN_STROKE_WIDTH) / + (lineLegendItemsBaseArray.length - 1)) * + index + ) + 'px', + }; + } + ); + + const lineLegendTitle = options?.getLegendTitle?.( + computation.descriptor.configuration + ) + ? 'Link width (' + + options.getLegendTitle(computation.descriptor.configuration)[0] + + ')' + : 'Link width'; + + const colorLegendTitle = options?.getLegendTitle?.( + computation.descriptor.configuration + ) + ? 'Link color (' + + options.getLegendTitle(computation.descriptor.configuration)[1] + + ')' + : 'Link color'; + + const colorLegendItems: LegendItemsProps[] = [ + { + label: 'Positive', + marker: 'line', + markerColor: twoColorPalette[1], + hasData: true, + lineThickness: '3px', + }, + { + label: 'Negative', + marker: 'line', + markerColor: twoColorPalette[0], + hasData: true, + lineThickness: '3px', + }, + ]; + + const legendNode = cleanedData && cleanedData.nodes.length > 0 && ( +
+ + +
+ ); + const tableGroupNode = <> ; + + // The bipartite network uses FacetedPlotLayout in order to position the legends + // atop the plot. The bipartite network plots are often so tall and so wide that + // with the normal PlotLayout component the legends are forced way, way down the screen + // below the plot. + const LayoutComponent = options?.layoutComponent ?? FacetedPlotLayout; + + return ( +
+ {!hideInputsAndControls && ( + + + updateVizConfig({ correlationCoefThreshold: Number(newValue) }) + } + label={'Absolute correlation coefficient'} + minValue={0} + maxValue={1} + value={ + vizConfig.correlationCoefThreshold ?? + DEFAULT_CORRELATION_COEF_THRESHOLD + } + step={0.05} + applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} + /> + + + updateVizConfig({ significanceThreshold: Number(newValue) }) + } + minValue={0} + maxValue={1} + value={ + vizConfig.significanceThreshold ?? DEFAULT_SIGNIFICANCE_THRESHOLD + } + containerStyles={{ marginLeft: 10 }} + step={0.001} + applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} + /> + + )} + + +
+ ); +} From 6e94ded7e51bc605306db0a026031f42160cba8c Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 21 Jun 2024 08:16:13 -0400 Subject: [PATCH 3/8] cleanup and address funny sizing issues --- .../libs/components/src/plots/NetworkPlot.css | 2 +- .../components/src/types/plots/network.ts | 2 + .../computations/plugins/selfCorrelation.tsx | 141 ++++++++---------- .../implementations/NetworkVisualization.tsx | 45 +++--- 4 files changed, 93 insertions(+), 97 deletions(-) diff --git a/packages/libs/components/src/plots/NetworkPlot.css b/packages/libs/components/src/plots/NetworkPlot.css index b1e796f578..a8a367a416 100644 --- a/packages/libs/components/src/plots/NetworkPlot.css +++ b/packages/libs/components/src/plots/NetworkPlot.css @@ -5,7 +5,7 @@ .network-plot-container { width: 100%; - height: 500px; + height: 800px; overflow-y: scroll; } diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 25f5a3d8e3..c2c755a3ed 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -43,6 +43,8 @@ export type LinkData = { color?: string; /** Link opacity. Must be between 0 and 1 */ opacity?: number; + /** Boolean determining if the edge is directed */ + isDirected?: boolean; }; /** NetworkData is the same format accepted by visx's Graph component. */ diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index c2550236f1..b4c2a56ca2 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { VariableTreeNode, useFindEntityAndVariableCollection } from '../../..'; +import { useFindEntityAndVariableCollection } from '../../..'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; import { partial } from 'lodash'; import { @@ -14,20 +14,11 @@ import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUt import { H6 } from '@veupathdb/coreui'; import { networkVisualization } from '../../visualizations/implementations/NetworkVisualization'; import { VariableCollectionSelectList } from '../../variableSelectors/VariableCollectionSingleSelect'; -import SingleSelect, { - ItemGroup, -} from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; -import { - entityTreeToArray, - findEntityAndVariableCollection, - isVariableCollectionDescriptor, -} from '../../../utils/study-metadata'; +import SingleSelect from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; +import { entityTreeToArray } from '../../../utils/study-metadata'; import { IsEnabledInPickerParams } from '../../visualizations/VisualizationTypes'; -import { ancestorEntitiesForEntityId } from '../../../utils/data-element-constraints'; import { NumberInput } from '@veupathdb/components/lib/components/widgets/NumberAndDateInputs'; import ExpandablePanel from '@veupathdb/coreui/lib/components/containers/ExpandablePanel'; -import { variableCollectionsAreUnique } from '../../../utils/visualization'; -import PluginError from '../../visualizations/PluginError'; import { CompleteSelfCorrelationConfig, SelfCorrelationConfig, @@ -55,10 +46,7 @@ export const plugin: ComputationPlugin = { }, }), isConfigurationComplete: (configuration) => { - // First, the configuration must be complete - // ANN CLEAN - if (!CompleteSelfCorrelationConfig.is(configuration)) return false; - return true; + return CompleteSelfCorrelationConfig.is(configuration); }, visualizationPlugins: { unipartitenetwork: networkVisualization.withOptions({ @@ -69,67 +57,11 @@ export const plugin: ComputationPlugin = { return []; } }, - // makeGetNodeMenuActions(studyMetadata) { - // const entities = entityTreeToArray(studyMetadata.rootEntity); - // const variables = entities.flatMap((e) => e.variables); - // const collections = entities.flatMap( - // (entity) => entity.collections ?? [] - // ); - // const hostCollection = collections.find( - // (c) => c.id === 'EUPATH_0005050' - // ); - // const parasiteCollection = collections.find( - // (c) => c.id === 'EUPATH_0005051' - // ); - // return function getNodeActions(nodeId: string) { - // const [, variableId] = nodeId.split('.'); - // const variable = variables.find((v) => v.id === variableId); - // if (variable == null) return []; - - // // E.g., "qa." - // const urlPrefix = window.location.host.replace( - // /(plasmodb|hostdb)\.org/, - // '' - // ); - - // const href = parasiteCollection?.memberVariableIds.includes( - // variable.id - // ) - // ? `//${urlPrefix}plasmodb.org/plasmo/app/search/transcript/GenesByRNASeqpfal3D7_Lee_Gambian_ebi_rnaSeq_RSRCWGCNAModules?param.wgcnaParam=${variable.displayName.toLowerCase()}&autoRun=1` - // : hostCollection?.memberVariableIds.includes(variable.id) - // ? `//${urlPrefix}hostdb.org/hostdb/app/search/transcript/GenesByRNASeqhsapREF_Lee_Gambian_ebi_rnaSeq_RSRCWGCNAModules?param.wgcnaParam=${variable.displayName.toLowerCase()}&autoRun=1` - // : undefined; - // if (href == null) return []; - // return [ - // { - // label: 'See list of genes', - // href, - // }, - // ]; - // }; - // }, - // getParitionNames(studyMetadata, config) { - // if (CorrelationConfig.is(config)) { - // const entities = entityTreeToArray(studyMetadata.rootEntity); - // const partition1Name = findEntityAndVariableCollection( - // entities, - // config.data1?.collectionSpec - // )?.variableCollection.displayName; - // const partition2Name = - // config.data2?.dataType === 'collection' - // ? findEntityAndVariableCollection( - // entities, - // config.data2?.collectionSpec - // )?.variableCollection.displayName - // : 'Continuous metadata variables'; - // return { partition1Name, partition2Name }; - // } - // }, }), // Must match name in data service and in visualization.tsx }, isEnabledInPicker: isEnabledInPicker, studyRequirements: - 'These visualizations are only available for studies with compatible metadata.', + 'These visualizations are only available for studies with compatible collections.', }; // Renders on the thumbnail page to give a summary of the app instance @@ -219,7 +151,65 @@ export function SelfCorrelationConfiguration(props: ComputationConfigProps) { For example, the Age and Shoe Size of children are correlated since as a child ages, their feet grow.

- {/* ANN FILL IN */} +

+ Here we look for correlation between the abundance of different taxa at + a given taxonomic level +

+

+
Inputs:
+

+

    +
  • + Taxonomic Level. The taxonomic abundance data to be + used in the calculation. +
  • +
  • + Method. The type of correlation to compute. The + Pearson method looks for linear trends in the data, while the + Spearman method looks for a monotonic relationship. For Spearman and + Pearson correlation, we use the rcorr function from the Hmisc + package. The SparCC method is a compositional correlation method + appropriate for taxonomic abundance data and any other compositional + data. +
  • +
  • + Prevalence Prefilter. Remove variables that do not + have a set percentage of non-zero abundance across samples. Removing + rarely occurring features before calculating correlation can prevent + some spurious results. +
  • +
+

+

+
Outputs:
+

+ For each pair of variables, the correlation computation returns +

    +
  • + Correlation coefficient. A value between [-1, 1] that describes the + similarity of the input variables. Positive values indicate that + both variables rise and fall together, whereas negative values + indicate that as one rises, the other falls. +
  • +
  • + P Value. A measure of the probability of observing the result by + chance. +
  • +
+

+

+
More Questions?
+

+ Check out the{' '} + + correlation function + {' '} + in our{' '} + + microbiomeComputations + {' '} + R package. +

); @@ -308,9 +298,6 @@ export function SelfCorrelationConfiguration(props: ComputationConfigProps) { -
- {/* PluginError here if the method doesn't agree with the data */} -
{} -// 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 threshold values. +// Network Visualization +// The network takes no input variables, because the received data will complete the plot. +// The user can control the significance and correlation coefficient threshold values. function NetworkViz(props: VisualizationProps) { const { options, @@ -175,7 +172,7 @@ function NetworkViz(props: VisualizationProps) { const minDataWeight = Math.min(...uniqueDataWeights); const maxDataWeight = Math.max(...uniqueDataWeights); - // Determine min and max x and y positions for nodes. For use in scaling the nodes. + // Determine min and max x and y positions for nodes. For use later in scaling the node positions to fit. const dataXPositions = data.value?.network.data.nodes.map((node) => Number(node.x)) ?? []; const dataYPositions = @@ -189,12 +186,14 @@ function NetworkViz(props: VisualizationProps) { const cleanedData = useMemo(() => { if (!data.value) return undefined; + // Note that after applying a buffer of 150px/100px to the x/y scales, the plot size should be a sqare! + // For example, if plotContainerStyles.width=900 and plotContainerStyles.height=800, the scaled network will span 600x600. const scaleX = scaleLinear() .domain([minXPosition, maxXPosition]) - .range([80, 500]); + .range([150, plotContainerStyles.width - 150]); // Add a little extra room in the x direction to accound for labels. const scaleY = scaleLinear() .domain([minYPosition, maxYPosition]) - .range([80, 500]); + .range([100, plotContainerStyles.height - 100]); // Create map that will adjust each link's weight to find a stroke width that spans an appropriate range for this viz. const weightToStrokeWidthMap = scaleLinear() @@ -259,13 +258,22 @@ function NetworkViz(props: VisualizationProps) { }; }), }; - }, [data.value, entities, minDataWeight, maxDataWeight]); + }, [ + data.value, + entities, + minDataWeight, + maxDataWeight, + maxXPosition, + minXPosition, + maxYPosition, + minYPosition, + ]); // plot subtitle const plotSubtitle = (

- {`Showing links with an absolute correlation coefficient above ${vizConfig.correlationCoefThreshold?.toString()} and a p-value below ${vizConfig.significanceThreshold?.toString()}`} + {`Showing links with an absolute correlation coefficient above ${vizConfig.correlationCoefThreshold?.toString()} and a p-value below ${vizConfig.significanceThreshold?.toString()}. Network layout computed using the igraph layout_nicely function.`}

Click on a node to highlight its edges.

@@ -283,14 +291,13 @@ function NetworkViz(props: VisualizationProps) { updateThumbnail, { ...finalPlotContainerStyles, - height: 400, // no reason for the thumbnail to be as tall as the network (which could be very, very tall!) + height: 400, }, [cleanedData] ); - // Have the bpnet component say "No nodes" or whatev and have an extra - // prop called errorMessage or something that displays when there are no nodes. - // that error message can say "your thresholds of blah and blah are too high, change them" + // Content for the Network component to display when no nodes + // pass the correlation coeff and significance thresholds const emptyNetworkContent = (
) { ); const tableGroupNode = <> ; - // The bipartite network uses FacetedPlotLayout in order to position the legends - // atop the plot. The bipartite network plots are often so tall and so wide that + // The network uses FacetedPlotLayout in order to position the legends + // atop the plot. The network plots are often so tall and so wide that // with the normal PlotLayout component the legends are forced way, way down the screen // below the plot. const LayoutComponent = options?.layoutComponent ?? FacetedPlotLayout; From 15eb772f49797d68b23676abbb5b93f32a498ddc Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 21 Jun 2024 08:20:54 -0400 Subject: [PATCH 4/8] restrict selfcorrelation to taxa --- .../libs/eda/src/lib/core/components/computations/Utils.ts | 3 ++- .../core/components/computations/plugins/selfCorrelation.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/Utils.ts b/packages/libs/eda/src/lib/core/components/computations/Utils.ts index 9f6fa7fa02..80314db7f0 100644 --- a/packages/libs/eda/src/lib/core/components/computations/Utils.ts +++ b/packages/libs/eda/src/lib/core/components/computations/Utils.ts @@ -95,7 +95,8 @@ export function isTaxonomicVariableCollection( return ( isNotAbsoluteAbundanceVariableCollection(variableCollection) && (variableCollection.member - ? variableCollection.member === 'taxon' + ? variableCollection.member === 'taxon' && + !!variableCollection.isCompositional : variableCollection.normalizationMethod === 'sumToUnity') // if we have a member annotation, use that. Old datasets may not have this annotation, hence the fall back normalizationMethod check. ); } diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index b4c2a56ca2..367cd7c55a 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -6,6 +6,7 @@ import { useConfigChangeHandler, assertComputationWithConfig, isNotAbsoluteAbundanceVariableCollection, + isTaxonomicVariableCollection, } from '../Utils'; import { Computation } from '../../../types/visualization'; import { ComputationStepContainer } from '../ComputationStepContainer'; @@ -111,9 +112,9 @@ function SelfCorrelationConfigDescriptionComponent({ } const CORRELATION_METHODS = [ + { value: 'sparcc', displayName: 'SparCC' }, { value: 'spearman', displayName: 'Spearman' }, { value: 'pearson', displayName: 'Pearson' }, - { value: 'sparcc', displayName: 'SparCC' }, ]; const DEFAULT_PROPORTION_NON_ZERO_THRESHOLD = 0.05; const DEFAULT_VARIANCE_THRESHOLD = 0; @@ -241,7 +242,7 @@ export function SelfCorrelationConfiguration(props: ComputationConfigProps) {
From 3056b215aab8df33b78fd85eb3da009503b8bde2 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 21 Jun 2024 09:12:50 -0400 Subject: [PATCH 5/8] fix empty network alignment --- packages/libs/components/src/plots/NetworkPlot.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index f8dec667a1..74d9e9bc2e 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -50,15 +50,15 @@ export interface NetworkPlotProps { annotations?: ReactNode[]; } -const DEFAULT_PLOT_WIDTH = 500; -const DEFAULT_PLOT_HEIGHT = 500; +const DEFAULT_PLOT_WIDTH = 800; +const DEFAULT_PLOT_HEIGHT = 900; const emptyNodes: NodeData[] = [...Array(9).keys()].map((item, index) => ({ id: item.toString(), color: gray[100], stroke: gray[300], - x: 230 + 200 * Math.cos(2 * Math.PI * (index / 9)), - y: 230 + 200 * Math.sin(2 * Math.PI * (index / 9)), + x: 400 + 200 * Math.cos(2 * Math.PI * (index / 9)), + y: 300 + 200 * Math.sin(2 * Math.PI * (index / 9)), })); const emptyLinks: LinkData[] = []; From 8434e2e3462523778f533a612a67e013810009e1 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 21 Jun 2024 12:03:43 -0400 Subject: [PATCH 6/8] cleanup --- .../libs/components/src/plots/NetworkPlot.css | 2 +- .../libs/components/src/plots/NetworkPlot.tsx | 2 +- .../eda/src/lib/core/api/DataClient/types.ts | 37 +++++++++---------- .../computations/plugins/selfCorrelation.tsx | 6 +-- .../implementations/NetworkVisualization.tsx | 22 +++++++---- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/libs/components/src/plots/NetworkPlot.css b/packages/libs/components/src/plots/NetworkPlot.css index a8a367a416..ea9e806a4b 100644 --- a/packages/libs/components/src/plots/NetworkPlot.css +++ b/packages/libs/components/src/plots/NetworkPlot.css @@ -6,7 +6,7 @@ .network-plot-container { width: 100%; height: 800px; - overflow-y: scroll; + /* overflow-y: scroll; */ } .net-hover-dropdown { diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 74d9e9bc2e..172f6bf3e1 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -73,7 +73,7 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { svgStyleOverrides, containerClass = 'web-components-plot', showSpinner = false, - labelTruncationLength = 20, + labelTruncationLength = 10, emptyNetworkContent, annotations, } = props; 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 4afb613dc4..5438ba64ca 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -392,9 +392,10 @@ export const NodeIdList = type({ nodeIds: array(string), }); -// Bipartite network -export type BipartiteNetworkResponse = TypeOf; - +// Network types (including bipartite network) +// The network types are all built from nodes and links. Currently we've defined specific +// flavors of networks, called "correlation" networks, that have extra information +// about them based on their context. const NodeData = intersection([ type({ id: string, @@ -423,7 +424,6 @@ export const NetworkData = type({ ), }); -// @ANN clean your types! const NetworkConfig = partial({ variables: any, correlationCoefThreshold: number, @@ -436,7 +436,15 @@ export const NetworkResponse = type({ }), }); -// @ANN clean your types! +export interface NetworkRequestParams { + studyId: string; + filters: Filter[]; + config: { + correlationCoefThreshold?: number; + significanceThreshold?: number; + }; +} + export const BipartiteNetworkData = type({ partitions: array(NodeIdList), nodes: array(NodeData), @@ -465,6 +473,8 @@ export const BipartiteNetworkResponse = type({ }), }); +export type BipartiteNetworkResponse = TypeOf; + // Correlation Bipartite Network // a specific flavor of the bipartite network that also includes correlationCoefThreshold and significanceThreshold export type CorrelationBipartiteNetworkResponse = TypeOf< @@ -486,21 +496,8 @@ export interface BipartiteNetworkRequestParams { significanceThreshold?: number; }; } -// @ANN can also maybe clean types here -// Correlation Network -// a specific flavor of the network that also includes correlationCoefThreshold and significanceThreshold -export type CorrelationNetworkResponse = TypeOf< - typeof CorrelationNetworkResponse ->; -export const CorrelationNetworkResponse = NetworkResponse; -export interface NetworkRequestParams { - studyId: string; - filters: Filter[]; - config: { - correlationCoefThreshold?: number; - significanceThreshold?: number; - }; -} + +export type NetworkResponse = TypeOf; export type FeaturePrefilterThresholds = TypeOf< typeof FeaturePrefilterThresholds diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index 367cd7c55a..98833e16cd 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -5,7 +5,6 @@ import { partial } from 'lodash'; import { useConfigChangeHandler, assertComputationWithConfig, - isNotAbsoluteAbundanceVariableCollection, isTaxonomicVariableCollection, } from '../Utils'; import { Computation } from '../../../types/visualization'; @@ -30,10 +29,10 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer'); /** * Self-Correlation * - * The Correlation app takes all collections and visualizes the correlation between a collection and itself. + * The Correlation app takes a collection and visualizes the correlation between the collection and itself. * For example, if the collection is a set of genes, the app will show the correlation between every pair of genes in the collection. * - * As of 05/14/24, this app will only be available for mbio assay data. + * As of 05/14/24, this app will only be available for mbio taxonomic data. */ export const plugin: ComputationPlugin = { @@ -96,7 +95,6 @@ function SelfCorrelationConfigDescriptionComponent({ )} - {/* The method should be disabled unti lthe data is chosen */}

Method:{' '} diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx index e3ecf851e8..a146cdfe5d 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/NetworkVisualization.tsx @@ -15,7 +15,7 @@ import NetworkPlot, { } from '@veupathdb/components/lib/plots/NetworkPlot'; import BipartiteNetworkSVG from './selectorIcons/BipartiteNetworkSVG'; import { - CorrelationNetworkResponse, + NetworkResponse, NetworkRequestParams, } from '../../../api/DataClient/types'; import { twoColorPalette } from '@veupathdb/components/lib/types/plots/addOns'; @@ -63,7 +63,7 @@ const plotContainerStyles = { }; export const networkVisualization = createVisualizationPlugin({ - selectorIcon: BipartiteNetworkSVG, + selectorIcon: BipartiteNetworkSVG, // Placeholder for now until ann has created a new one! fullscreenComponent: NetworkViz, createDefaultConfig: createDefaultConfig, }); @@ -122,7 +122,7 @@ function NetworkViz(props: VisualizationProps) { // Get data from the compute job const data = usePromise( - useCallback(async (): Promise => { + 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; @@ -143,7 +143,7 @@ function NetworkViz(props: VisualizationProps) { computation.descriptor.type, visualization.descriptor.type, params, - CorrelationNetworkResponse + NetworkResponse ); return response; @@ -167,7 +167,7 @@ function NetworkViz(props: VisualizationProps) { data.value?.network.data.links.map( (link) => Number(link.weight) // link.weight will always be a number if defined, because it represents the continuous data associated with that link. ) ?? []; - // Use Set to dedupe the array of dataWeights + // Use Set to deduplicate the array of dataWeights const uniqueDataWeights = Array.from(new Set(dataWeights)); const minDataWeight = Math.min(...uniqueDataWeights); const maxDataWeight = Math.max(...uniqueDataWeights); @@ -241,8 +241,14 @@ function NetworkViz(props: VisualizationProps) { y: scaleY(Number(node.y)), id: node.id, label: displayLabel, + // the following attempts to place the label in a "reasonable" position + // on the plot. So if a node is far on the left side, the label will be on the left side. + // This simple solution attempts to avoid overlapping labels and give us a cheap, clean-ish plot. labelPosition: - scaleX(Number(node.x)) > 200 ? 'right' : ('left' as LabelPosition), + scaleX(Number(node.x)) > + (plotContainerStyleOverrides?.width ?? 900) / 2 + ? 'right' + : ('left' as LabelPosition), }; }); @@ -267,6 +273,7 @@ function NetworkViz(props: VisualizationProps) { minXPosition, maxYPosition, minYPosition, + plotContainerStyleOverrides?.width, ]); // plot subtitle @@ -314,13 +321,12 @@ function NetworkViz(props: VisualizationProps) { ); - console.log('cleanedData', cleanedData); const networkPlotProps: NetworkPlotProps = { nodes: cleanedData ? cleanedData.nodes : undefined, links: cleanedData ? cleanedData.links : undefined, showSpinner: data.pending, containerStyles: finalPlotContainerStyles, - labelTruncationLength: 40, + labelTruncationLength: 30, emptyNetworkContent, }; From 907f550ebcc3ce87b0bbe5de4c5f0ef5fdd74bb1 Mon Sep 17 00:00:00 2001 From: Ann Sizemore Blevins Date: Fri, 28 Jun 2024 06:57:16 -0400 Subject: [PATCH 7/8] Update packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx Co-authored-by: Dave Falke --- .../core/components/computations/plugins/selfCorrelation.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index 98833e16cd..66d3ec6a6c 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -127,11 +127,10 @@ export function SelfCorrelationConfiguration(props: ComputationConfigProps) { visualizationId, } = props; - const configuration = computation.descriptor - .configuration as SelfCorrelationConfig; - assertComputationWithConfig(computation, SelfCorrelationConfig); + const { configuration } = computation.descriptor; + const changeConfigHandler = useConfigChangeHandler( analysisState, computation, From e82f114da2a272b860440f4e278128528b63fa9b Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 28 Jun 2024 07:04:30 -0400 Subject: [PATCH 8/8] comment, clean types, remove unnecessary memo --- .../eda/src/lib/core/api/DataClient/types.ts | 7 ++++++- .../computations/plugins/selfCorrelation.tsx | 16 +++++----------- 2 files changed, 11 insertions(+), 12 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 5438ba64ca..9201806bc6 100755 --- a/packages/libs/eda/src/lib/core/api/DataClient/types.ts +++ b/packages/libs/eda/src/lib/core/api/DataClient/types.ts @@ -396,6 +396,11 @@ export const NodeIdList = type({ // The network types are all built from nodes and links. Currently we've defined specific // flavors of networks, called "correlation" networks, that have extra information // about them based on their context. + +// NOTE tech debt below! The `sourece` and `target` of the NetworkData links should be +// NodeData, but the backend is sending us strings. At this point it is unlikely the backend +// will change for a while to fix this issue. If it does, we can simplify the below and let +// BipartiteNetworkData extend NetworkData. const NodeData = intersection([ type({ id: string, @@ -425,7 +430,7 @@ export const NetworkData = type({ }); const NetworkConfig = partial({ - variables: any, + variables: unknown, correlationCoefThreshold: number, significanceThreshold: number, }); diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx index 98833e16cd..9280642a07 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/selfCorrelation.tsx @@ -212,17 +212,11 @@ export function SelfCorrelationConfiguration(props: ComputationConfigProps) { ); - const correlationMethodSelectorText = useMemo(() => { - if (configuration.correlationMethod) { - return ( - CORRELATION_METHODS.find( - (method) => method.value === configuration.correlationMethod - )?.displayName ?? 'Select a method' - ); - } else { - return 'Select a method'; - } - }, [configuration.correlationMethod]); + const correlationMethodSelectorText = configuration.correlationMethod + ? CORRELATION_METHODS.find( + (method) => method.value === configuration.correlationMethod + )?.displayName ?? 'Select a method' + : 'Select a method'; return (