diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx
new file mode 100755
index 0000000000..577099871d
--- /dev/null
+++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx
@@ -0,0 +1,158 @@
+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';
+import { CSSProperties } from 'react';
+import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot';
+import Spinner from '../components/Spinner';
+
+export interface BipartiteNetworkProps {
+ /** Bipartite network data */
+ data: BipartiteNetworkData;
+ /** Name of column 1 */
+ 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;
+ /** plot width */
+ width?: number;
+}
+
+// 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) {
+ const {
+ data,
+ column1Name,
+ column2Name,
+ containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT },
+ 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;
+
+ // 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);
+ });
+
+ 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) +
+ 'NodeIDs') as ColumnName;
+ const indexInColumn = data[columnName].findIndex(
+ (id) => id === node.id
+ );
+
+ 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,
+ labelPosition:
+ columnIndex === 0 ? 'left' : ('right' as LabelPosition),
+ ...node,
+ };
+ });
+ return columnWithCoordinates;
+ }
+ );
+
+ // 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
+ );
+ 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,
+ },
+ };
+ });
+
+ return (
+
+
+ {showSpinner && }
+
+ );
+}
diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx
index c9e2702f18..66243bb1cf 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 */
@@ -20,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,
@@ -58,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
new file mode 100755
index 0000000000..2fdbb6afe6
--- /dev/null
+++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx
@@ -0,0 +1,116 @@
+import { Story, Meta } from '@storybook/react/types-6-0';
+import {
+ NodeData,
+ LinkData,
+ BipartiteNetworkData,
+} from '../../types/plots/network';
+import {
+ BipartiteNetwork,
+ BipartiteNetworkProps,
+} from '../../plots/BipartiteNetwork';
+import { twoColorPalette } from '../../types/plots';
+
+export default {
+ title: 'Plots/Network/BipartiteNetwork',
+ component: BipartiteNetwork,
+} as Meta;
+
+interface TemplateProps {
+ data: BipartiteNetworkData;
+ column1Name?: string;
+ column2Name?: string;
+ loading?: boolean;
+}
+
+// Template for showcasing our BipartiteNetwork component.
+const Template: Story = (args) => {
+ const bipartiteNetworkProps: BipartiteNetworkProps = {
+ data: args.data,
+ column1Name: args.column1Name,
+ column2Name: args.column2Name,
+ showSpinner: args.loading,
+ width: 500,
+ };
+ return ;
+};
+
+/**
+ * Stories
+ */
+
+// A basic bipartite network
+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);
+export const ManyPoints = Template.bind({});
+ManyPoints.args = {
+ data: manyPointsData,
+};
+
+// With column names
+export const WithColumnNames = Template.bind({});
+WithColumnNames.args = {
+ data: simpleData,
+ column1Name: 'Column 1',
+ 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 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,
+ };
+}
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;
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) */
diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts
index fadf279582..a8cfb8a5c4 100755
--- a/packages/libs/components/src/types/plots/network.ts
+++ b/packages/libs/components/src/types/plots/network.ts
@@ -1,14 +1,11 @@
// 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 */