From d760592d7a3122e77b2f5032236f8d3ac30dbd7f Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 21 Sep 2023 14:24:57 -0400 Subject: [PATCH 1/9] bipartite network appears in story --- .../components/src/plots/BipartiteNetwork.tsx | 106 +++++++++++ .../libs/components/src/plots/Network.tsx | 4 +- .../plots/BipartiteNetwork.stories.tsx | 175 ++++++++++++++++++ .../components/src/types/plots/network.ts | 13 +- 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100755 packages/libs/components/src/plots/BipartiteNetwork.tsx create mode 100755 packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx new file mode 100755 index 0000000000..c9e2702f18 --- /dev/null +++ b/packages/libs/components/src/plots/BipartiteNetwork.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/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx index c9e2702f18..eb5ebea41c 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 */ 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..74c9d8b9c4 --- /dev/null +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -0,0 +1,175 @@ +import { Story, Meta } from '@storybook/react/types-6-0'; +import { Graph } from '@visx/network'; +import { + NodeData, + LinkData, + BipartiteNetworkData, +} from '../../types/plots/network'; +import { LabelPosition, Link, NodeWithLabel } from '../../plots/Network'; + +export default { + title: 'Plots/BipartiteNetwork', + component: NodeWithLabel, +} as Meta; + +// For simplicity, make square svgs with the following height and width +const DEFAULT_PLOT_SIZE = 500; + +interface TemplateProps { + data: BipartiteNetworkData; +} + +// This template is a simple network that highlights our NodeWithLabel and Link components. +const Template: Story = (args) => { + // BIPARTITE network should position nodes!!! + + // The backend can't do it because we eventually want to click nodes and have them reposition. + const allNodes = args.data.nodes; + const nodes = allNodes.map((node) => { + const columnNumber = args.data.column1NodeIDs.includes(node.id) ? 0 : 1; + + type ColumnName = keyof typeof args.data; + const columnName = ('column' + + (columnNumber + 1) + + 'NodeIDs') as ColumnName; + const indexInColumn = args.data[columnName].findIndex( + (id) => id === node.id + ); + + return { + x: 90 + columnNumber * 100, + y: 30 + 30 * indexInColumn, + labelPosition: columnNumber === 0 ? 'left' : ('right' as LabelPosition), + ...node, + }; + }); + + const links = args.data.links.map((link) => { + const sourceNode = nodes.find((node) => node.id === link.source.id); + const targetNode = nodes.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, + }, + color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + }; + }); + + // also bpnet should set the label left/right appropriatey + return ( + + } + // 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, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + + ); +}; + +/** + * Stories + */ + +// A simple network with node labels +const simpleData = genBipartiteNetwork( + 20, + 10, + 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 = genBipartiteNetwork( + 1000, + 100, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const ManyPoints = Template.bind({}); +ManyPoints.args = { + data: manyPointsData, +}; + +/** 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; + +// Gerenate a network with a given number of nodes and random edges +function genBipartiteNetwork( + column1nNodes: number, + column2nNodes: number, + height: number, + width: number +) { + // 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), + }; + }); + + // Create links + // @ANN come back and mmake this more tunable + 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 ? 'positive' : 'negative', + }; + }); + + const nodes = column1Nodes.concat(column2Nodes); + const column1NodeIDs = column1Nodes.map((node) => node.id); + const column2NodeIDs = column2Nodes.map((node) => node.id); + + return { + nodes, + links, + column1NodeIDs, + column2NodeIDs, + } as BipartiteNetworkData; +} diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index fadf279582..81ccbc1314 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -1,20 +1,19 @@ // 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 */ r?: number; /** User-friendly node label */ label?: string; + /** Draw the label on the right or left of the node */ + // labelPosition?: 'right' | 'left' | undefined; /** Color for the stroke of the node */ stroke?: string; /** Width of node stroke */ From 6ca856637669bf48e0266ae57bd91341eea2ab49 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 14:47:35 -0400 Subject: [PATCH 2/9] functional BipartiteNetwork component --- .../components/src/plots/BipartiteNetwork.tsx | 176 +++++++++--------- .../plots/BipartiteNetwork.stories.tsx | 73 +------- 2 files changed, 94 insertions(+), 155 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index c9e2702f18..d63ca5766d 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -1,106 +1,100 @@ -import { DefaultNode } from '@visx/network'; -import { Text } from '@visx/text'; -import { LinkData, NodeData } from '../types/plots/network'; +import { BipartiteNetworkData, NodeData } from '../types/plots/network'; +import { partition } from 'lodash'; +import { LabelPosition, Link, NodeWithLabel } from './Network'; +import { Graph } from '@visx/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; +interface BipartiteNetworkProps { + /** Bipartite network data */ + data: BipartiteNetworkData; } // 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; +export function BipartiteNetwork(props: BipartiteNetworkProps) { + const { data } = props; - const { - node, - onClick, - labelPosition = 'right', - fontSize = '1em', - fontWeight = 200, - labelColor = '#000', - } = props; + // BIPARTITE network should position nodes!!! - const { color, label, stroke, strokeWidth } = node; + // The backend can't do it because we eventually want to click nodes and have them reposition. + const nodesByColumn: NodeData[][] = partition(data.nodes, (node) => { + return data.column1NodeIDs.includes(node.id); + }); - const nodeRadius = node.r ?? DEFAULT_NODE_RADIUS; + const nodesByColumnWithCoordinates = nodesByColumn.map( + (column, columnIndex) => { + const columnWithCoordinates = column.map((node) => { + type ColumnName = keyof typeof data; + const columnName = ('column' + + (columnIndex + 1) + + 'NodeIDs') as ColumnName; + const indexInColumn = data[columnName].findIndex( + (id) => id === node.id + ); - // 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} - - + return { + x: 90 + (columnIndex + 1) * 100, + y: 30 + 30 * indexInColumn, + labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), + ...node, + }; + }); + return columnWithCoordinates; + } ); -} -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; + const links = 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, + }, + color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + }; + }); + // also bpnet should set the label left/right appropriatey return ( - + + } + // 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, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + ); } diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 74c9d8b9c4..9483510d1f 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -1,11 +1,15 @@ import { Story, Meta } from '@storybook/react/types-6-0'; -import { Graph } from '@visx/network'; import { NodeData, LinkData, BipartiteNetworkData, } from '../../types/plots/network'; import { LabelPosition, Link, NodeWithLabel } from '../../plots/Network'; +import { partition } from 'lodash'; +import { + BipartiteNetwork, + BipartiteNetworkProps, +} from '../../plots/BipartiteNetwork'; export default { title: 'Plots/BipartiteNetwork', @@ -21,69 +25,10 @@ interface TemplateProps { // This template is a simple network that highlights our NodeWithLabel and Link components. const Template: Story = (args) => { - // BIPARTITE network should position nodes!!! - - // The backend can't do it because we eventually want to click nodes and have them reposition. - const allNodes = args.data.nodes; - const nodes = allNodes.map((node) => { - const columnNumber = args.data.column1NodeIDs.includes(node.id) ? 0 : 1; - - type ColumnName = keyof typeof args.data; - const columnName = ('column' + - (columnNumber + 1) + - 'NodeIDs') as ColumnName; - const indexInColumn = args.data[columnName].findIndex( - (id) => id === node.id - ); - - return { - x: 90 + columnNumber * 100, - y: 30 + 30 * indexInColumn, - labelPosition: columnNumber === 0 ? 'left' : ('right' as LabelPosition), - ...node, - }; - }); - - const links = args.data.links.map((link) => { - const sourceNode = nodes.find((node) => node.id === link.source.id); - const targetNode = nodes.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, - }, - color: link.color === 'positive' ? '#116699' : '#994411', //fake colors - }; - }); - - // also bpnet should set the label left/right appropriatey - return ( - - } - // 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, - labelPosition: node.labelPosition, - }; - return ; - }} - /> - - ); + const bipartiteNetworkProps: BipartiteNetworkProps = { + data: args.data, + }; + return ; }; /** From 9ee85113d189895b0d2071a5acd183c9353181b0 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:11:31 -0400 Subject: [PATCH 3/9] add column name for bipartite network --- .../components/src/plots/BipartiteNetwork.tsx | 23 ++++++++++--- .../plots/BipartiteNetwork.stories.tsx | 34 ++++++++----------- .../stories/plots/NodeWithLabel.stories.tsx | 2 +- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index d63ca5766d..946b47d8dd 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -2,16 +2,21 @@ 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'; -interface BipartiteNetworkProps { +export interface BipartiteNetworkProps { /** Bipartite network data */ data: BipartiteNetworkData; + /** Name of column 1 */ + column1Name?: string; + /** Name of column 2 */ + column2Name?: string; } // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. export function BipartiteNetwork(props: BipartiteNetworkProps) { - const { data } = props; + const { data, column1Name, column2Name } = props; // BIPARTITE network should position nodes!!! @@ -33,7 +38,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { return { x: 90 + (columnIndex + 1) * 100, - y: 30 + 30 * indexInColumn, + y: 40 + 30 * indexInColumn, labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), ...node, }; @@ -65,7 +70,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { }; }); - // also bpnet should set the label left/right appropriatey return ( + {/* Draw names of node colums if they exist */} + {column1Name && ( + + {column1Name} + + )} + {column2Name && ( + + {column2Name} + + )} = (args) => { const bipartiteNetworkProps: BipartiteNetworkProps = { data: args.data, + column1Name: args.column1Name, + column2Name: args.column2Name, }; return ; }; @@ -59,19 +61,13 @@ ManyPoints.args = { data: manyPointsData, }; -/** 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; +// With column names +export const WithColumnNames = Template.bind({}); +WithColumnNames.args = { + data: simpleData, + column1Name: 'Column 1', + column2Name: 'Column 2', +}; // Gerenate a network with a given number of nodes and random edges function genBipartiteNetwork( @@ -92,7 +88,7 @@ function genBipartiteNetwork( const column2Nodes: NodeData[] = [...Array(column2nNodes).keys()].map((i) => { return { id: String(i + column1nNodes), - label: 'Node ' + String(i), + label: 'Node ' + String(i + column1nNodes), }; }); 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; From 31cce71256fff72c321da1cf26bed57ec7321208 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:26:07 -0400 Subject: [PATCH 4/9] some small styling tweaks --- .../components/src/plots/BipartiteNetwork.tsx | 109 +++++++++++------- .../plots/BipartiteNetwork.stories.tsx | 11 ++ 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 946b47d8dd..4a7c0dd4a5 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -3,6 +3,9 @@ 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 */ @@ -11,12 +14,25 @@ export interface BipartiteNetworkProps { 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; } // NodeWithLabel draws one node and an optional label for the node. Both the node and // label can be styled. export function BipartiteNetwork(props: BipartiteNetworkProps) { - const { data, column1Name, column2Name } = props; + const { + data, + column1Name, + column2Name, + containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, + containerClass = 'web-components-plot', + showSpinner = false, + } = props; // BIPARTITE network should position nodes!!! @@ -71,45 +87,58 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { }); return ( - - {/* Draw names of node colums if they exist */} - {column1Name && ( - - {column1Name} - - )} - {column2Name && ( - - {column2Name} - - )} - } - // 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, - labelPosition: node.labelPosition, - }; - return ; - }} - /> - + + {/* Draw names of node colums if they exist */} + {column1Name && ( + + {column1Name} + + )} + {column2Name && ( + + {column2Name} + + )} + } + // 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, + stroke: '#111', + strokeWidth: 1, + color: '#fff', + r: 6, + }, + labelPosition: node.labelPosition, + }; + return ; + }} + /> + + {showSpinner && } + ); } diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 11fae90d5f..328a756548 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -21,6 +21,7 @@ interface TemplateProps { data: BipartiteNetworkData; column1Name?: string; column2Name?: string; + loading?: boolean; } // This template is a simple network that highlights our BipartiteNetwork component. @@ -29,6 +30,7 @@ const Template: Story = (args) => { data: args.data, column1Name: args.column1Name, column2Name: args.column2Name, + showSpinner: args.loading, }; return ; }; @@ -69,6 +71,15 @@ WithColumnNames.args = { 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 network with a given number of nodes and random edges function genBipartiteNetwork( column1nNodes: number, From a8807ca29f37989d53e487c9cef61c07c8564e9b Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:41:19 -0400 Subject: [PATCH 5/9] set default constants for layout --- .../components/src/plots/BipartiteNetwork.tsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 4a7c0dd4a5..128c67c2ab 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -22,8 +22,7 @@ export interface BipartiteNetworkProps { showSpinner?: boolean; } -// NodeWithLabel draws one node and an optional label for the node. Both the node and -// label can be styled. +// TODO Document export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -34,9 +33,15 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { showSpinner = false, } = props; - // BIPARTITE network should position nodes!!! + // Defaults + const DEFAULT_COLUMN1_X = 100; + const DEFAULT_COLUMN2_X = 300; + const DEFAULT_NODE_VERTICAL_SPACE = 30; + const DEFAULT_TOP_PADDING = 40; - // The backend can't do it because we eventually want to click nodes and have them reposition. + // 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); }); @@ -44,6 +49,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { 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) + @@ -53,8 +59,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { ); return { - x: 90 + (columnIndex + 1) * 100, - y: 40 + 30 * indexInColumn, + x: columnIndex ? DEFAULT_COLUMN2_X : DEFAULT_COLUMN1_X, + y: DEFAULT_TOP_PADDING + DEFAULT_NODE_VERTICAL_SPACE * indexInColumn, labelPosition: columnIndex ? 'right' : ('left' as LabelPosition), ...node, }; @@ -95,18 +101,26 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { width={400} height={ Math.max(data.column1NodeIDs.length, data.column2NodeIDs.length) * - 30 + - 50 + DEFAULT_NODE_VERTICAL_SPACE + + DEFAULT_TOP_PADDING } > {/* Draw names of node colums if they exist */} {column1Name && ( - + {column1Name} )} {column2Name && ( - + {column2Name} )} From 641fbfd0c7c7fabf54b509abf62ffce6368af7a3 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 15:55:12 -0400 Subject: [PATCH 6/9] add twoColorPalette --- .../libs/components/src/plots/BipartiteNetwork.tsx | 12 ++++++++---- packages/libs/components/src/types/plots/addOns.ts | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 128c67c2ab..7d57e012d7 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -6,6 +6,7 @@ import { Text } from '@visx/text'; import { CSSProperties } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; +import { twoColorPalette } from '../types/plots'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -22,7 +23,8 @@ export interface BipartiteNetworkProps { showSpinner?: boolean; } -// TODO Document +// The BipartiteNetwork function draws a two-column network using visx. This component handles +// the positioning of each column, and consequently the positioning of nodes. export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -69,7 +71,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { } ); - const links = data.links.map((link) => { + // 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 ); @@ -88,7 +91,8 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { y: targetNode?.y, ...link.target, }, - color: link.color === 'positive' ? '#116699' : '#994411', //fake colors + color: + link.color === 'positive' ? twoColorPalette[0] : twoColorPalette[1], }; }); @@ -129,7 +133,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { nodes: nodesByColumnWithCoordinates[0].concat( nodesByColumnWithCoordinates[1] ), - links, + links: linksWithCoordinates, }} // Our Link component has nice defaults and in the future can // carry more complex events. 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) */ From 42985d9eb6e2d686a4e99a154963acd6a59640f6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 22 Sep 2023 16:16:33 -0400 Subject: [PATCH 7/9] set node style defaults and fix link color logic --- .../components/src/plots/BipartiteNetwork.tsx | 21 ++++++---------- .../libs/components/src/plots/Network.tsx | 7 +++--- .../plots/BipartiteNetwork.stories.tsx | 24 ++++--------------- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 7d57e012d7..22ea4a4574 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -21,6 +21,8 @@ export interface BipartiteNetworkProps { containerClass?: string; /** shall we show the loading spinner? */ showSpinner?: boolean; + /** Array of colors to assign to links */ + linkPalette?: string[]; } // The BipartiteNetwork function draws a two-column network using visx. This component handles @@ -33,6 +35,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, + linkPalette, } = props; // Defaults @@ -91,8 +94,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { y: targetNode?.y, ...link.target, }, - color: - link.color === 'positive' ? twoColorPalette[0] : twoColorPalette[1], }; }); @@ -128,6 +129,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { {column2Name} )} + } - // 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, - stroke: '#111', - strokeWidth: 1, - color: '#fff', - r: 6, - }, + node: node, labelPosition: node.labelPosition, }; return ; diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx index eb5ebea41c..66243bb1cf 100755 --- a/packages/libs/components/src/plots/Network.tsx +++ b/packages/libs/components/src/plots/Network.tsx @@ -22,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, @@ -60,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 index 328a756548..67b3c705ca 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -8,6 +8,7 @@ import { BipartiteNetwork, BipartiteNetworkProps, } from '../../plots/BipartiteNetwork'; +import { twoColorPalette } from '../../types/plots'; export default { title: 'Plots/Network/BipartiteNetwork', @@ -40,24 +41,14 @@ const Template: Story = (args) => { */ // A simple network with node labels -const simpleData = genBipartiteNetwork( - 20, - 10, - DEFAULT_PLOT_SIZE, - DEFAULT_PLOT_SIZE -); +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, - DEFAULT_PLOT_SIZE, - DEFAULT_PLOT_SIZE -); +const manyPointsData = genBipartiteNetwork(1000, 100); export const ManyPoints = Template.bind({}); ManyPoints.args = { data: manyPointsData, @@ -81,12 +72,7 @@ Loading.args = { }; // Gerenate a network with a given number of nodes and random edges -function genBipartiteNetwork( - column1nNodes: number, - column2nNodes: number, - height: number, - width: number -) { +function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { // Create the first column of nodes const column1Nodes: NodeData[] = [...Array(column1nNodes).keys()].map((i) => { return { @@ -110,7 +96,7 @@ function genBipartiteNetwork( source: column1Nodes[Math.floor(Math.random() * column1nNodes)], target: column2Nodes[Math.floor(Math.random() * column2nNodes)], strokeWidth: Math.random() * 2, - color: Math.random() > 0.5 ? 'positive' : 'negative', + color: Math.random() > 0.5 ? twoColorPalette[0] : twoColorPalette[1], }; }); From a120e2cc13077b9cc61f17f7fb6cf836ee627b09 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 25 Sep 2023 06:21:22 -0400 Subject: [PATCH 8/9] cleanup --- .../components/src/plots/BipartiteNetwork.tsx | 6 +----- .../plots/BipartiteNetwork.stories.tsx | 20 ++++++++++--------- .../components/src/types/plots/network.ts | 2 -- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 22ea4a4574..053e634ffb 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -6,7 +6,6 @@ import { Text } from '@visx/text'; import { CSSProperties } from 'react'; import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot'; import Spinner from '../components/Spinner'; -import { twoColorPalette } from '../types/plots'; export interface BipartiteNetworkProps { /** Bipartite network data */ @@ -21,12 +20,10 @@ export interface BipartiteNetworkProps { containerClass?: string; /** shall we show the loading spinner? */ showSpinner?: boolean; - /** Array of colors to assign to links */ - linkPalette?: string[]; } // The BipartiteNetwork function draws a two-column network using visx. This component handles -// the positioning of each column, and consequently the positioning of nodes. +// the positioning of each column, and consequently the positioning of nodes and links. export function BipartiteNetwork(props: BipartiteNetworkProps) { const { data, @@ -35,7 +32,6 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, - linkPalette, } = props; // Defaults diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx index 67b3c705ca..0c253dcaa9 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx @@ -15,9 +15,6 @@ export default { component: BipartiteNetwork, } as Meta; -// For simplicity, make square svgs with the following height and width -const DEFAULT_PLOT_SIZE = 500; - interface TemplateProps { data: BipartiteNetworkData; column1Name?: string; @@ -25,7 +22,7 @@ interface TemplateProps { loading?: boolean; } -// This template is a simple network that highlights our BipartiteNetwork component. +// Template for showcasing our BipartiteNetwork component. const Template: Story = (args) => { const bipartiteNetworkProps: BipartiteNetworkProps = { data: args.data, @@ -40,7 +37,7 @@ const Template: Story = (args) => { * Stories */ -// A simple network with node labels +// A basic bipartite network const simpleData = genBipartiteNetwork(20, 10); export const Simple = Template.bind({}); Simple.args = { @@ -71,8 +68,11 @@ Loading.args = { loading: true, }; -// Gerenate a network with a given number of nodes and random edges -function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { +// 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 { @@ -90,7 +90,9 @@ function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { }); // Create links - // @ANN come back and mmake this more tunable + // 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)], @@ -109,5 +111,5 @@ function genBipartiteNetwork(column1nNodes: number, column2nNodes: number) { links, column1NodeIDs, column2NodeIDs, - } as BipartiteNetworkData; + }; } diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 81ccbc1314..a8cfb8a5c4 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -12,8 +12,6 @@ export type NodeData = { r?: number; /** User-friendly node label */ label?: string; - /** Draw the label on the right or left of the node */ - // labelPosition?: 'right' | 'left' | undefined; /** Color for the stroke of the node */ stroke?: string; /** Width of node stroke */ From 56ad38df74ef197fabc89e9a3ebe1e672fd044a3 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 26 Sep 2023 06:55:14 -0400 Subject: [PATCH 9/9] add optional width prop and clarify logic --- .../components/src/plots/BipartiteNetwork.tsx | 17 ++++++++++++----- .../stories/plots/BipartiteNetwork.stories.tsx | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 053e634ffb..577099871d 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -20,6 +20,8 @@ export interface BipartiteNetworkProps { 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 @@ -32,13 +34,16 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { containerStyles = { width: '100%', height: DEFAULT_CONTAINER_HEIGHT }, containerClass = 'web-components-plot', showSpinner = false, + width, } = props; // Defaults - const DEFAULT_COLUMN1_X = 100; - const DEFAULT_COLUMN2_X = 300; + // 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 @@ -60,9 +65,11 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { ); return { - x: columnIndex ? DEFAULT_COLUMN2_X : DEFAULT_COLUMN1_X, + // 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 ? 'right' : ('left' as LabelPosition), + labelPosition: + columnIndex === 0 ? 'left' : ('right' as LabelPosition), ...node, }; }); @@ -99,7 +106,7 @@ export function BipartiteNetwork(props: BipartiteNetworkProps) { style={{ ...containerStyles, position: 'relative' }} > = (args) => { column1Name: args.column1Name, column2Name: args.column2Name, showSpinner: args.loading, + width: 500, }; return ; };