-
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 #412 from VEuPathDB/add-basic-network-components
Add basic network components
- Loading branch information
Showing
4 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
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,106 @@ | ||
import { DefaultNode } from '@visx/network'; | ||
import { Text } from '@visx/text'; | ||
import { LinkData, NodeData } from '../types/plots/network'; | ||
|
||
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'; | ||
/** Font size for the label. Ex. "1em" */ | ||
fontSize?: string; | ||
/** Font weight for the label */ | ||
fontWeight?: number; | ||
/** Color for the label */ | ||
labelColor?: string; | ||
} | ||
|
||
// 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_STROKE_WIDTH = 1; | ||
|
||
const { | ||
node, | ||
onClick, | ||
labelPosition = 'right', | ||
fontSize = '1em', | ||
fontWeight = 200, | ||
labelColor = '#000', | ||
} = props; | ||
|
||
const { color, label, stroke, strokeWidth } = node; | ||
|
||
const nodeRadius = node.r ?? DEFAULT_NODE_RADIUS; | ||
|
||
// Calculate where the label should be posiitoned based on | ||
// total size of the node. | ||
let textXOffset: number; | ||
let textAnchor: 'start' | 'end'; | ||
|
||
if (labelPosition === 'right') { | ||
textXOffset = 4 + nodeRadius; | ||
if (strokeWidth) textXOffset = textXOffset + strokeWidth; | ||
textAnchor = 'start'; | ||
} else { | ||
textXOffset = -4 - nodeRadius; | ||
if (strokeWidth) textXOffset = textXOffset - strokeWidth; | ||
textAnchor = 'end'; | ||
} | ||
|
||
return ( | ||
<> | ||
<DefaultNode | ||
r={nodeRadius} | ||
fill={color ?? DEFAULT_NODE_COLOR} | ||
onClick={onClick} | ||
stroke={stroke} | ||
strokeWidth={strokeWidth ?? DEFAULT_STROKE_WIDTH} | ||
/> | ||
{/* Note that Text becomes a tspan */} | ||
<Text | ||
x={textXOffset} | ||
textAnchor={textAnchor} | ||
fontSize={fontSize} | ||
verticalAnchor="middle" | ||
onClick={onClick} | ||
fontWeight={fontWeight} | ||
fill={labelColor} | ||
style={{ cursor: 'pointer' }} | ||
> | ||
{label} | ||
</Text> | ||
</> | ||
); | ||
} | ||
|
||
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} | ||
/> | ||
); | ||
} |
90 changes: 90 additions & 0 deletions
90
packages/libs/components/src/stories/plots/Network.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,90 @@ | ||
import { Story, Meta } from '@storybook/react/types-6-0'; | ||
import { Graph } from '@visx/network'; | ||
import { NodeData, LinkData, NetworkData } from '../../types/plots/network'; | ||
import { Link, NodeWithLabel } from '../../plots/Network'; | ||
|
||
export default { | ||
title: 'Plots/Network', | ||
component: NodeWithLabel, | ||
} as Meta; | ||
|
||
// For simplicity, make square svgs with the following height and width | ||
const DEFAULT_PLOT_SIZE = 500; | ||
|
||
interface TemplateProps { | ||
data: NetworkData; | ||
} | ||
|
||
// This template is a simple network that highlights our NodeWithLabel and Link components. | ||
const Template: Story<TemplateProps> = (args) => { | ||
return ( | ||
<svg width={DEFAULT_PLOT_SIZE} height={DEFAULT_PLOT_SIZE}> | ||
<Graph | ||
graph={args.data} | ||
// Our Link component has nice defaults and in the future can | ||
// carry more complex events. | ||
linkComponent={({ link }) => <Link link={link} />} | ||
// The node components are already transformed using x and y. | ||
// So inside the node component all coords should be relative to this | ||
// initial transform. | ||
nodeComponent={({ node }) => { | ||
const nodeWithLabelProps = { | ||
node: node, | ||
}; | ||
return <NodeWithLabel {...nodeWithLabelProps} />; | ||
}} | ||
/> | ||
</svg> | ||
); | ||
}; | ||
|
||
/** | ||
* Stories | ||
*/ | ||
|
||
// A simple network with node labels | ||
const simpleData = genNetwork(20, true, DEFAULT_PLOT_SIZE, DEFAULT_PLOT_SIZE); | ||
export const Simple = Template.bind({}); | ||
Simple.args = { | ||
data: simpleData, | ||
}; | ||
|
||
// A network with lots and lots of points! | ||
const manyPointsData = genNetwork( | ||
100, | ||
false, | ||
DEFAULT_PLOT_SIZE, | ||
DEFAULT_PLOT_SIZE | ||
); | ||
export const ManyPoints = Template.bind({}); | ||
ManyPoints.args = { | ||
data: manyPointsData, | ||
}; | ||
|
||
// Gerenate a network with a given number of nodes and random edges | ||
function genNetwork( | ||
nNodes: number, | ||
addNodeLabel: boolean, | ||
height: number, | ||
width: number | ||
) { | ||
// Create nodes with random positioning, an id, and optionally a label | ||
const nodes: NodeData[] = [...Array(nNodes).keys()].map((i) => { | ||
return { | ||
x: Math.floor(Math.random() * width), | ||
y: Math.floor(Math.random() * height), | ||
id: String(i), | ||
label: addNodeLabel ? 'Node ' + String(i) : undefined, | ||
}; | ||
}); | ||
|
||
// Create {nNodes} links. Just basic links no weighting or colors for now. | ||
const links: LinkData[] = [...Array(nNodes).keys()].map(() => { | ||
return { | ||
source: nodes[Math.floor(Math.random() * nNodes)], | ||
target: nodes[Math.floor(Math.random() * nNodes)], | ||
}; | ||
}); | ||
|
||
return { nodes, links } as NetworkData; | ||
} |
86 changes: 86 additions & 0 deletions
86
packages/libs/components/src/stories/plots/NodeWithLabel.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,86 @@ | ||
import { Story, Meta } from '@storybook/react/types-6-0'; | ||
import { NodeData } from '../../types/plots/network'; | ||
import { NodeWithLabel } from '../../plots/Network'; | ||
import { Group } from '@visx/group'; | ||
|
||
export default { | ||
title: 'Plots/Network', | ||
component: NodeWithLabel, | ||
} as Meta; | ||
|
||
interface TemplateProps { | ||
data: NodeData; | ||
onClick: () => void; | ||
labelPosition?: 'right' | 'left'; | ||
fontWeight?: number; | ||
labelColor?: string; | ||
} | ||
|
||
// Simply draw a node! | ||
const Template: Story<TemplateProps> = (args) => { | ||
const { data, labelPosition, fontWeight, labelColor, onClick } = args; | ||
|
||
const nodeWithLabelProps = { | ||
node: data, | ||
onClick: onClick, | ||
labelPosition: labelPosition, | ||
fontWeight: fontWeight, | ||
labelColor: labelColor, | ||
}; | ||
|
||
return ( | ||
<svg width={400} height={400}> | ||
<Group transform={'translate(' + data.x + ', ' + data.y + ')'}> | ||
<NodeWithLabel {...nodeWithLabelProps} /> | ||
</Group> | ||
</svg> | ||
); | ||
}; | ||
|
||
/** | ||
* Stories | ||
*/ | ||
|
||
// Basic node with a label | ||
const myNode = { | ||
x: 100, | ||
y: 100, | ||
id: 'id', | ||
label: 'label', | ||
}; | ||
|
||
export const NodeWithALabel = Template.bind({}); | ||
NodeWithALabel.args = { | ||
data: myNode, | ||
labelPosition: 'left', | ||
}; | ||
|
||
const myFancyNode = { | ||
x: 100, | ||
y: 100, | ||
id: 'id', | ||
label: 'a fancy long label', | ||
r: 9, | ||
color: '#118899', | ||
stroke: '#000', | ||
strokeWidth: 3, | ||
}; | ||
|
||
export const FancyNodeWithLabel = Template.bind({}); | ||
FancyNodeWithLabel.args = { | ||
data: myFancyNode, | ||
labelPosition: 'right', | ||
labelColor: '#008822', | ||
fontWeight: 600, | ||
}; | ||
|
||
export const ClickNodeOrLabel = Template.bind({}); | ||
ClickNodeOrLabel.args = { | ||
data: myNode, | ||
labelPosition: 'right', | ||
labelColor: '#008822', | ||
fontWeight: 600, | ||
onClick: () => { | ||
console.log('clicked!'); | ||
}, | ||
}; |
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,49 @@ | ||
// 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; | ||
/** Node color */ | ||
color?: string; | ||
/** Node radius */ | ||
r?: number; | ||
/** User-friendly node label */ | ||
label?: string; | ||
/** Color for the stroke of the node */ | ||
stroke?: string; | ||
/** Width of node stroke */ | ||
strokeWidth?: number; | ||
}; | ||
|
||
export type LinkData = { | ||
/** The beginning node of the link */ | ||
source: NodeData; | ||
/** The ending node of the link */ | ||
target: NodeData; | ||
/** Link stroke width */ | ||
strokeWidth?: number; | ||
/** Link color */ | ||
color?: string; | ||
/** Link opacity. Must be between 0 and 1 */ | ||
opacity?: number; | ||
}; | ||
|
||
/** NetworkData is the same format accepted by visx's Graph component. */ | ||
export type NetworkData = { | ||
nodes: NodeData[]; | ||
links: LinkData[]; | ||
}; | ||
|
||
/** Bipartite network data is a regular network with addiitonal declarations of | ||
* nodes in each of the two columns. IDs in columnXNodeIDs must match node ids exactly. | ||
*/ | ||
export type BipartiteNetworkData = { | ||
column1NodeIDs: string[]; | ||
column2NodeIDs: string[]; | ||
} & NetworkData; |