-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #517 from VEuPathDB/bipartite-network-component
Add bipartite network component
- Loading branch information
Showing
6 changed files
with
289 additions
and
12 deletions.
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
packages/libs/components/src/plots/BipartiteNetwork.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div | ||
className={containerClass} | ||
style={{ ...containerStyles, position: 'relative' }} | ||
> | ||
<svg | ||
width={width ?? DEFAULT_WIDTH} | ||
height={ | ||
Math.max(data.column1NodeIDs.length, data.column2NodeIDs.length) * | ||
DEFAULT_NODE_VERTICAL_SPACE + | ||
DEFAULT_TOP_PADDING | ||
} | ||
> | ||
{/* Draw names of node colums if they exist */} | ||
{column1Name && ( | ||
<Text | ||
x={DEFAULT_COLUMN1_X} | ||
y={DEFAULT_TOP_PADDING / 2} | ||
textAnchor="end" | ||
> | ||
{column1Name} | ||
</Text> | ||
)} | ||
{column2Name && ( | ||
<Text | ||
x={DEFAULT_COLUMN2_X} | ||
y={DEFAULT_TOP_PADDING / 2} | ||
textAnchor="start" | ||
> | ||
{column2Name} | ||
</Text> | ||
)} | ||
|
||
<Graph | ||
graph={{ | ||
nodes: nodesByColumnWithCoordinates[0].concat( | ||
nodesByColumnWithCoordinates[1] | ||
), | ||
links: linksWithCoordinates, | ||
}} | ||
// Using our Link component so that it uses our nice defaults and | ||
// can better expand to handle more complex events (hover and such). | ||
linkComponent={({ link }) => <Link link={link} />} | ||
nodeComponent={({ node }) => { | ||
const nodeWithLabelProps = { | ||
node: node, | ||
labelPosition: node.labelPosition, | ||
}; | ||
return <NodeWithLabel {...nodeWithLabelProps} />; | ||
}} | ||
/> | ||
</svg> | ||
{showSpinner && <Spinner />} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TemplateProps> = (args) => { | ||
const bipartiteNetworkProps: BipartiteNetworkProps = { | ||
data: args.data, | ||
column1Name: args.column1Name, | ||
column2Name: args.column2Name, | ||
showSpinner: args.loading, | ||
width: 500, | ||
}; | ||
return <BipartiteNetwork {...bipartiteNetworkProps} />; | ||
}; | ||
|
||
/** | ||
* 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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters