diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx
new file mode 100755
index 0000000000..c9e2702f18
--- /dev/null
+++ b/packages/libs/components/src/plots/Network.tsx
@@ -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 (
+ <>
+
+ {/* Note that Text becomes a tspan */}
+
+ {label}
+
+ >
+ );
+}
+
+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 (
+
+ );
+}
diff --git a/packages/libs/components/src/stories/plots/Network.stories.tsx b/packages/libs/components/src/stories/plots/Network.stories.tsx
new file mode 100755
index 0000000000..fa4d41c9b8
--- /dev/null
+++ b/packages/libs/components/src/stories/plots/Network.stories.tsx
@@ -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 = (args) => {
+ return (
+
+ );
+};
+
+/**
+ * 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;
+}
diff --git a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx
new file mode 100755
index 0000000000..f69368b550
--- /dev/null
+++ b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx
@@ -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 = (args) => {
+ const { data, labelPosition, fontWeight, labelColor, onClick } = args;
+
+ const nodeWithLabelProps = {
+ node: data,
+ onClick: onClick,
+ labelPosition: labelPosition,
+ fontWeight: fontWeight,
+ labelColor: labelColor,
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * 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!');
+ },
+};
diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts
new file mode 100755
index 0000000000..fadf279582
--- /dev/null
+++ b/packages/libs/components/src/types/plots/network.ts
@@ -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;