Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature 556 connect correlation backend #560

Merged
merged 20 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading