Skip to content

Commit

Permalink
Merge pull request #560 from VEuPathDB/feature-556-connect-correlatio…
Browse files Browse the repository at this point in the history
…n-backend

Feature 556 connect correlation backend
  • Loading branch information
asizemore authored Nov 20, 2023
2 parents cbb6356 + 142587c commit 1257cb3
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 123 deletions.
4 changes: 4 additions & 0 deletions packages/libs/components/src/plots/BipartiteNetwork.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.BipartiteNetworkColumnTitle {
font-size: 1em;
font-weight: 500;
}
78 changes: 53 additions & 25 deletions packages/libs/components/src/plots/BipartiteNetwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,66 @@ import {
useImperativeHandle,
useRef,
} from 'react';
import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot';
import Spinner from '../components/Spinner';
import { ToImgopts } from 'plotly.js';
import domToImage from 'dom-to-image';
import { gray } from '@veupathdb/coreui/lib/definitions/colors';
import './BipartiteNetwork.css';

export interface BipartiteNetworkSVGStyles {
width?: number; // svg width
topPadding?: number; // space between the top of the svg and the top-most node
nodeSpacing?: number; // space between vertically adjacent nodes
columnPadding?: number; // space between the left of the svg and the left column, also the right of the svg and the right column.
}

export interface BipartiteNetworkProps {
/** Bipartite network data */
data: BipartiteNetworkData;
data: BipartiteNetworkData | undefined;
/** Name of column 1 */
column1Name?: string;
/** Name of column 2 */
column2Name?: string;
/** styling for the plot's container */
containerStyles?: CSSProperties;
/** bipartite network-specific styling for the svg itself. These
* properties will override any adaptation the network may try to do based on the container styles.
*/
svgStyleOverrides?: BipartiteNetworkSVGStyles;
/** container name */
containerClass?: string;
/** shall we show the loading spinner? */
showSpinner?: boolean;
/** plot width */
width?: number;
}

// Show a few gray nodes when there is no real data.
const EmptyBipartiteNetworkData: BipartiteNetworkData = {
column1NodeIDs: ['0', '1', '2', '3', '4', '5'],
column2NodeIDs: ['6', '7', '8'],
nodes: [...Array(9).keys()].map((item) => ({
id: item.toString(),
color: gray[100],
stroke: gray[300],
})),
links: [],
};

// 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.
function BipartiteNetwork(
props: BipartiteNetworkProps,
ref: Ref<HTMLDivElement>
) {
const {
data,
data = EmptyBipartiteNetworkData,
column1Name,
column2Name,
containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT },
containerStyles,
svgStyleOverrides,
containerClass = 'web-components-plot',
showSpinner = false,
width,
} = props;

// Defaults
// 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;

// Use ref forwarding to enable screenshotting of the plot for thumbnail versions.
const plotRef = useRef<HTMLDivElement>(null);
useImperativeHandle<HTMLDivElement, any>(
Expand All @@ -70,6 +84,18 @@ function BipartiteNetwork(
[]
);

// Set up styles for the bipartite network and incorporate overrides
const svgStyles = {
width: Number(containerStyles?.width) || 400,
topPadding: 40,
nodeSpacing: 30,
columnPadding: 100,
...svgStyleOverrides,
};

const column1Position = svgStyles.columnPadding;
const column2Position = svgStyles.width - svgStyles.columnPadding;

// 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.
Expand All @@ -91,8 +117,8 @@ function BipartiteNetwork(

return {
// 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,
x: columnIndex === 0 ? column1Position : column2Position,
y: svgStyles.topPadding + svgStyles.nodeSpacing * indexInColumn,
labelPosition:
columnIndex === 0 ? 'left' : ('right' as LabelPosition),
...node,
Expand Down Expand Up @@ -128,32 +154,34 @@ function BipartiteNetwork(
return (
<div
className={containerClass}
style={{ ...containerStyles, position: 'relative' }}
style={{ width: '100%', ...containerStyles, position: 'relative' }}
>
<div ref={plotRef} style={{ width: '100%', height: '100%' }}>
<svg
width={width ?? DEFAULT_WIDTH}
width={svgStyles.width}
height={
Math.max(data.column1NodeIDs.length, data.column2NodeIDs.length) *
DEFAULT_NODE_VERTICAL_SPACE +
DEFAULT_TOP_PADDING
svgStyles.nodeSpacing +
svgStyles.topPadding
}
>
{/* Draw names of node colums if they exist */}
{column1Name && (
<Text
x={DEFAULT_COLUMN1_X}
y={DEFAULT_TOP_PADDING / 2}
x={column1Position}
y={svgStyles.topPadding / 2}
textAnchor="end"
className="BipartiteNetworkColumnTitle"
>
{column1Name}
</Text>
)}
{column2Name && (
<Text
x={DEFAULT_COLUMN2_X}
y={DEFAULT_TOP_PADDING / 2}
x={column2Position}
y={svgStyles.topPadding / 2}
textAnchor="start"
className="BipartiteNetworkColumnTitle"
>
{column2Name}
</Text>
Expand Down
2 changes: 1 addition & 1 deletion packages/libs/components/src/plots/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function NodeWithLabel(props: NodeWithLabelProps) {
onClick,
labelPosition = 'right',
fontSize = '1em',
fontWeight = 200,
fontWeight = 400,
labelColor = '#000',
} = props;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, CSSProperties } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import {
NodeData,
Expand All @@ -7,6 +7,7 @@ import {
} from '../../types/plots/network';
import BipartiteNetwork, {
BipartiteNetworkProps,
BipartiteNetworkSVGStyles,
} from '../../plots/BipartiteNetwork';
import { twoColorPalette } from '../../types/plots/addOns';

Expand All @@ -21,6 +22,8 @@ interface TemplateProps {
column2Name?: string;
loading?: boolean;
showThumbnail?: boolean;
containerStyles?: CSSProperties;
svgStyleOverrides?: BipartiteNetworkSVGStyles;
}

// Template for showcasing our BipartiteNetwork component.
Expand All @@ -42,7 +45,8 @@ const Template: Story<TemplateProps> = (args) => {
column1Name: args.column1Name,
column2Name: args.column2Name,
showSpinner: args.loading,
width: 500,
containerStyles: args.containerStyles,
svgStyleOverrides: args.svgStyleOverrides,
};
return (
<>
Expand Down Expand Up @@ -93,6 +97,12 @@ Loading.args = {
loading: true,
};

// Empty bipartite network
export const Empty = Template.bind({});
Empty.args = {
data: undefined,
};

// Show thumbnail
export const Thumbnail = Template.bind({});
Thumbnail.args = {
Expand All @@ -102,6 +112,27 @@ Thumbnail.args = {
showThumbnail: true,
};

// With style
const plotContainerStyles = {
width: 700,
marginLeft: '0.75rem',
border: '1px solid #dedede',
boxShadow: '1px 1px 4px #00000066',
};
const svgStyleOverrides = {
columnPadding: 150,
topPadding: 100,
// width: 300, // should override the plotContainerStyles.width
};
export const WithStyle = Template.bind({});
WithStyle.args = {
data: manyPointsData,
containerStyles: plotContainerStyles,
column1Name: 'Column 1',
column2Name: 'Column 2',
svgStyleOverrides: svgStyleOverrides,
};

// Gerenate a bipartite network with a given number of nodes and random edges
function genBipartiteNetwork(
column1nNodes: number,
Expand Down
30 changes: 23 additions & 7 deletions packages/libs/eda/src/lib/core/api/DataClient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,20 +390,36 @@ const NodeData = type({
id: string,
});

export const BipartiteNetworkResponse = type({
export const BipartiteNetworkData = type({
column1NodeIDs: array(string),
column2NodeIDs: array(string),
nodes: array(NodeData),
links: array(
type({
source: NodeData,
target: NodeData,
strokeWidth: number,
color: string,
})
intersection([
type({
source: NodeData,
target: NodeData,
strokeWidth: string,
}),
partial({
color: string,
}),
])
),
});

const BipartiteNetworkConfig = type({
column1Metadata: string,
column2Metadata: string,
});

export const BipartiteNetworkResponse = type({
bipartitenetwork: type({
data: BipartiteNetworkData,
config: BipartiteNetworkConfig,
}),
});

export interface BipartiteNetworkRequestParams {
studyId: string;
filters: Filter[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCollectionVariables, useStudyMetadata } from '../../..';
import { VariableDescriptor } from '../../../types/variable';
import { VariableCollectionDescriptor } from '../../../types/variable';
import { ComputationConfigProps, ComputationPlugin } from '../Types';
import { isEqual, partial } from 'lodash';
import { useConfigChangeHandler, assertComputationWithConfig } from '../Utils';
Expand All @@ -19,8 +19,8 @@ const cx = makeClassNameHelper('AppStepConfigurationContainer');
* Correlation
*
* 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.
* runs a correlation of that data against all appropriate metadata in the study (found by the backend). The result is
* a correlation coefficient and (soon) a 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
Expand All @@ -33,7 +33,7 @@ export type CorrelationAssayMetadataConfig = t.TypeOf<

// eslint-disable-next-line @typescript-eslint/no-redeclare
export const CorrelationAssayMetadataConfig = t.type({
collectionVariable: VariableDescriptor,
collectionVariable: VariableCollectionDescriptor,
correlationMethod: t.string,
});

Expand All @@ -44,7 +44,13 @@ export const plugin: ComputationPlugin = {
createDefaultConfiguration: () => undefined,
isConfigurationValid: CorrelationAssayMetadataConfig.is,
visualizationPlugins: {
bipartitenetwork: bipartiteNetworkVisualization, // Must match name in data service and in visualization.tsx
bipartitenetwork: bipartiteNetworkVisualization.withOptions({
getPlotSubtitle(config) {
if (CorrelationAssayMetadataConfig.is(config)) {
return 'Showing links with an absolute correlation coefficient above '; // visualization will add in the actual value
}
},
}), // Must match name in data service and in visualization.tsx
},
};

Expand Down Expand Up @@ -145,7 +151,7 @@ export function CorrelationAssayMetadataConfiguration(
const collectionVarItems = useMemo(() => {
return collections.map((collectionVar) => ({
value: {
variableId: collectionVar.id,
collectionId: collectionVar.id,
entityId: collectionVar.entityId,
},
display:
Expand All @@ -157,7 +163,7 @@ export function CorrelationAssayMetadataConfiguration(
if (configuration && 'collectionVariable' in configuration) {
const selectedItem = collectionVarItems.find((item) =>
isEqual(item.value, {
variableId: configuration.collectionVariable.variableId,
collectionId: configuration.collectionVariable.collectionId,
entityId: configuration.collectionVariable.entityId,
})
);
Expand Down
Loading

0 comments on commit 1257cb3

Please sign in to comment.