Skip to content

Commit

Permalink
Consolidate network components (#1052)
Browse files Browse the repository at this point in the history
* draft Network and update stories

* have BipartiteNetwork call NetworkPlot

* rename so that networks in components follow Plot convention

* consolidate network style props

* add back partition names as annotations

* handle empty Network data

* add default layout for network

* Clean up Network stories

* cleanup

* update imports for BipartiteNetworkPlot

* update NodeMenuActions imports

* simplify coordinate logic in bpnet

* remove a loop over the nodes

* update imports
  • Loading branch information
asizemore authored May 9, 2024
1 parent ac9f56f commit 05d427a
Show file tree
Hide file tree
Showing 13 changed files with 542 additions and 432 deletions.
4 changes: 4 additions & 0 deletions packages/libs/components/src/plots/BipartiteNetworkPlot.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.BipartiteNetworkPartitionTitle {
font-size: 1em;
font-weight: 500;
}
145 changes: 145 additions & 0 deletions packages/libs/components/src/plots/BipartiteNetworkPlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { BipartiteNetworkData, NetworkPartition } from '../types/plots/network';
import { LabelPosition } from './Node';
import { Ref, forwardRef, useMemo, SVGAttributes } from 'react';
import { gray } from '@veupathdb/coreui/lib/definitions/colors';
import { Text } from '@visx/text';

import './BipartiteNetworkPlot.css';
import NetworkPlot, { NetworkPlotProps } from './NetworkPlot';

export interface BipartiteNetworkSVGStyles extends SVGAttributes<SVGElement> {
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 BipartiteNetworkPlotProps extends NetworkPlotProps {
/** Partitions. An array of NetworkPartitions (an array of node ids and optional name) that defines the two node groups */
partitions: NetworkPartition[] | undefined;
/** 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;
}

const DEFAULT_TOP_PADDING = 40;
const DEFAULT_NODE_SPACING = 30;
const DEFAULT_SVG_WIDTH = 400;

// Show a few gray nodes when there is no real data.
const EmptyBipartiteNetworkData: BipartiteNetworkData = {
partitions: [
{ nodeIds: ['0', '1', '2', '3', '4', '5'], name: '' },
{ nodeIds: ['6', '7', '8'], name: '' },
],
nodes: [...Array(9).keys()].map((item) => ({
id: item.toString(),
color: gray[100],
stroke: gray[300],
y: item < 6 ? 40 + 30 * item : 40 + 30 * (item - 6),
})),
links: [],
};

// The BipartiteNetworkPlot function takes a network w two partitions of nodes and draws those partitions as columns.
// This component handles the positioning of each column, and consequently the positioning of nodes and links.
// The BipartiteNetworkPlot effectively wraps NetworkPlot by using the 'partitions' argument
// to layout the network and assigning helpful defaults.
function BipartiteNetworkPlot(
props: BipartiteNetworkPlotProps,
ref: Ref<HTMLDivElement>
) {
const {
nodes = EmptyBipartiteNetworkData.nodes,
links = EmptyBipartiteNetworkData.links,
partitions = EmptyBipartiteNetworkData.partitions,
containerStyles,
svgStyleOverrides,
getNodeMenuActions: getNodeActions,
} = props;

// Set up styles for the bipartite network and incorporate overrides
const svgStyles = {
width: Number(containerStyles?.width) || DEFAULT_SVG_WIDTH,
height:
Math.max(partitions[1].nodeIds.length, partitions[0].nodeIds.length) *
DEFAULT_NODE_SPACING +
DEFAULT_TOP_PADDING,
topPadding:
partitions[0].name || partitions[1].name ? 60 : DEFAULT_TOP_PADDING,
nodeSpacing: DEFAULT_NODE_SPACING,
columnPadding: 100,
...svgStyleOverrides,
};

const column1Position = svgStyles.columnPadding;
const column2Position = Number(svgStyles.width) - svgStyles.columnPadding;

// Assign coordinates to each node
// We'll draw the bipartite network in two columns. Nodes in the first partition will
// get drawn in the left column, and nodes in the second partition will get drawn in the right column.
const nodesWithCoordinates = useMemo(
() =>
nodes.map((node) => {
// Determine if the node is in the left or right partition (partitionIndex = 0 or 1, respectively)
const partitionIndex = partitions[0].nodeIds.includes(node.id) ? 0 : 1;
const nodeIndexInPartition = partitions[
partitionIndex
].nodeIds.findIndex((id) => id === node.id);

return {
// Recall partitionIndex = 0 refers to the left-column nodes whereas 1 refers to right-column nodes
x: partitionIndex === 0 ? column1Position : column2Position,
y:
svgStyles.topPadding + svgStyles.nodeSpacing * nodeIndexInPartition,
labelPosition:
partitionIndex === 0 ? 'left' : ('right' as LabelPosition),
...node,
actions: getNodeActions?.(node.id),
};
}),
[
nodes,
partitions,
column1Position,
column2Position,
svgStyles.nodeSpacing,
svgStyles.topPadding,
]
);

// Create column labels if any exist
const leftColumnLabel = partitions[0].name && (
<Text
x={column1Position}
y={svgStyles.topPadding / 2}
textAnchor="end"
className="BipartiteNetworkPartitionTitle"
>
{partitions[0].name}
</Text>
);
const rightColumnLabel = partitions[1].name && (
<Text
x={column2Position}
y={svgStyles.topPadding / 2}
textAnchor="start"
className="BipartiteNetworkPartitionTitle"
>
{partitions[1].name}
</Text>
);

return (
<NetworkPlot
{...props}
nodes={nodesWithCoordinates}
links={links}
annotations={[leftColumnLabel, rightColumnLabel]}
svgStyleOverrides={svgStyles}
ref={ref}
/>
);
}

export default forwardRef(BipartiteNetworkPlot);
28 changes: 28 additions & 0 deletions packages/libs/components/src/plots/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LinkData } from '../types/plots/network';

export interface LinkProps {
link: LinkData;
// onClick?: () => void; To add in the future, maybe also some hover action
}

// Link component draws a linear edge between two nodes.
// Eventually can grow into drawing directed edges (edges with arrows) when the time comes.
export function Link(props: LinkProps) {
const DEFAULT_LINK_WIDTH = 1;
const DEFAULT_COLOR = '#222';
const DEFAULT_OPACITY = 0.95;

const { link } = props;

return (
<line
x1={link.source.x}
y1={link.source.y}
x2={link.target.x}
y2={link.target.y}
strokeWidth={link.strokeWidth ?? DEFAULT_LINK_WIDTH}
stroke={link.color ?? DEFAULT_COLOR}
strokeOpacity={link.opacity ?? DEFAULT_OPACITY}
/>
);
}
3 changes: 0 additions & 3 deletions packages/libs/components/src/plots/Network.css

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
.BipartiteNetworkPartitionTitle {
font-size: 1em;
font-weight: 500;
.NodeWithLabel_Node,
.NodeWithLabel_Label {
cursor: pointer;
}

.network-plot-container {
width: 100%;
height: 500px;
overflow-y: scroll;
}

.bpnet-hover-dropdown {
.net-hover-dropdown {
display: none;
}

.visx-network-node:hover .bpnet-hover-dropdown {
.visx-network-node:hover .net-hover-dropdown {
display: unset;
}

.visx-network-node .hover-trigger:hover {
display: unset;
fill: #00000017;
}

.NodeWithLabel_Node,
.NodeWithLabel_Label {
cursor: pointer;
}
Loading

0 comments on commit 05d427a

Please sign in to comment.