From 05d427acd3eecba89d647efb831d4666ee01b763 Mon Sep 17 00:00:00 2001 From: Ann Sizemore Blevins Date: Thu, 9 May 2024 13:43:07 -0400 Subject: [PATCH] Consolidate network components (#1052) * 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 --- .../src/plots/BipartiteNetworkPlot.css | 4 + .../src/plots/BipartiteNetworkPlot.tsx | 145 ++++++++++ packages/libs/components/src/plots/Link.tsx | 28 ++ .../libs/components/src/plots/Network.css | 3 - .../{BipartiteNetwork.css => NetworkPlot.css} | 21 +- .../{BipartiteNetwork.tsx => NetworkPlot.tsx} | 250 +++++------------- .../src/plots/{Network.tsx => Node.tsx} | 31 +-- ...s.tsx => BipartiteNetworkPlot.stories.tsx} | 139 +++------- .../src/stories/plots/Network.stories.tsx | 90 ------- .../src/stories/plots/NetworkPlot.stories.tsx | 211 +++++++++++++++ .../stories/plots/NodeWithLabel.stories.tsx | 4 +- .../components/src/types/plots/network.ts | 32 ++- .../BipartiteNetworkVisualization.tsx | 16 +- 13 files changed, 542 insertions(+), 432 deletions(-) create mode 100644 packages/libs/components/src/plots/BipartiteNetworkPlot.css create mode 100755 packages/libs/components/src/plots/BipartiteNetworkPlot.tsx create mode 100644 packages/libs/components/src/plots/Link.tsx delete mode 100644 packages/libs/components/src/plots/Network.css rename packages/libs/components/src/plots/{BipartiteNetwork.css => NetworkPlot.css} (56%) rename packages/libs/components/src/plots/{BipartiteNetwork.tsx => NetworkPlot.tsx} (58%) rename packages/libs/components/src/plots/{Network.tsx => Node.tsx} (74%) mode change 100755 => 100644 rename packages/libs/components/src/stories/plots/{BipartiteNetwork.stories.tsx => BipartiteNetworkPlot.stories.tsx} (55%) delete mode 100755 packages/libs/components/src/stories/plots/Network.stories.tsx create mode 100755 packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.css b/packages/libs/components/src/plots/BipartiteNetworkPlot.css new file mode 100644 index 0000000000..09f7ff547c --- /dev/null +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.css @@ -0,0 +1,4 @@ +.BipartiteNetworkPartitionTitle { + font-size: 1em; + font-weight: 500; +} diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx new file mode 100755 index 0000000000..15c40ce238 --- /dev/null +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -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 { + 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 +) { + 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 && ( + + {partitions[0].name} + + ); + const rightColumnLabel = partitions[1].name && ( + + {partitions[1].name} + + ); + + return ( + + ); +} + +export default forwardRef(BipartiteNetworkPlot); diff --git a/packages/libs/components/src/plots/Link.tsx b/packages/libs/components/src/plots/Link.tsx new file mode 100644 index 0000000000..38f7f090bf --- /dev/null +++ b/packages/libs/components/src/plots/Link.tsx @@ -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 ( + + ); +} diff --git a/packages/libs/components/src/plots/Network.css b/packages/libs/components/src/plots/Network.css deleted file mode 100644 index da3992a358..0000000000 --- a/packages/libs/components/src/plots/Network.css +++ /dev/null @@ -1,3 +0,0 @@ -.NodeWithLabel_Node { - cursor: default; -} diff --git a/packages/libs/components/src/plots/BipartiteNetwork.css b/packages/libs/components/src/plots/NetworkPlot.css similarity index 56% rename from packages/libs/components/src/plots/BipartiteNetwork.css rename to packages/libs/components/src/plots/NetworkPlot.css index 3048000b39..b1e796f578 100644 --- a/packages/libs/components/src/plots/BipartiteNetwork.css +++ b/packages/libs/components/src/plots/NetworkPlot.css @@ -1,13 +1,19 @@ -.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; } @@ -15,8 +21,3 @@ display: unset; fill: #00000017; } - -.NodeWithLabel_Node, -.NodeWithLabel_Label { - cursor: pointer; -} diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx similarity index 58% rename from packages/libs/components/src/plots/BipartiteNetwork.tsx rename to packages/libs/components/src/plots/NetworkPlot.tsx index 5265201ef2..f8dec667a1 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -1,8 +1,8 @@ -import { BipartiteNetworkData, NodeData } from '../types/plots/network'; -import { orderBy, partition } from 'lodash'; -import { LabelPosition, Link, NodeWithLabel } from './Network'; +import { LinkData, NodeData, NodeMenuAction } from '../types/plots/network'; +import { isNumber, orderBy } from 'lodash'; +import { NodeWithLabel } from './Node'; +import { Link } from './Link'; import { Graph } from '@visx/network'; -import { Text } from '@visx/text'; import { CSSProperties, ReactNode, @@ -14,6 +14,7 @@ import { useState, useMemo, useEffect, + SVGAttributes, } from 'react'; import Spinner from '../components/Spinner'; import { ToImgopts } from 'plotly.js'; @@ -22,34 +23,19 @@ import { ExportPlotToImageButton } from './ExportPlotToImageButton'; import { plotToImage } from './visxVEuPathDB'; import { GlyphTriangle } from '@visx/visx'; -import './BipartiteNetwork.css'; +import './NetworkPlot.css'; -export interface BipartiteNetworkSVGStyles { - width?: number; // svg width - 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 NodeMenuAction { - label: ReactNode; - onClick?: () => void; - href?: string; -} - -export interface BipartiteNetworkProps { - /** Bipartite network data */ - data: BipartiteNetworkData | undefined; - /** Name of partition 1 */ - partition1Name?: string; - /** Name of partition 2 */ - partition2Name?: string; +export interface NetworkPlotProps { + /** Network nodes */ + nodes: NodeData[] | undefined; + /** Network links */ + links: LinkData[] | undefined; /** styling for the plot's container */ containerStyles?: CSSProperties; - /** bipartite network-specific styling for the svg itself. These + /** 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; + svgStyleOverrides?: SVGAttributes; /** container name */ containerClass?: string; /** shall we show the loading spinner? */ @@ -60,39 +46,36 @@ export interface BipartiteNetworkProps { emptyNetworkContent?: ReactNode; /** Entries for the actions that appear in the menu when you click a node */ getNodeMenuActions?: (nodeId: string) => NodeMenuAction[]; + /** Labels, notes, and other annotations to add to the network */ + annotations?: ReactNode[]; } -// Show a few gray nodes when there is no real data. -const EmptyBipartiteNetworkData: BipartiteNetworkData = { - partitions: [ - { nodeIds: ['0', '1', '2', '3', '4', '5'] }, - { nodeIds: ['6', '7', '8'] }, - ], - nodes: [...Array(9).keys()].map((item) => ({ - id: item.toString(), - color: gray[100], - stroke: gray[300], - })), - links: [], -}; +const DEFAULT_PLOT_WIDTH = 500; +const DEFAULT_PLOT_HEIGHT = 500; -// The BipartiteNetwork 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. -function BipartiteNetwork( - props: BipartiteNetworkProps, - ref: Ref -) { +const emptyNodes: NodeData[] = [...Array(9).keys()].map((item, index) => ({ + id: item.toString(), + color: gray[100], + stroke: gray[300], + x: 230 + 200 * Math.cos(2 * Math.PI * (index / 9)), + y: 230 + 200 * Math.sin(2 * Math.PI * (index / 9)), +})); +const emptyLinks: LinkData[] = []; + +// The Network component draws a network of nodes and links. +// If no x,y coordinates are provided for nodes in the network, the nodes will +// be drawn with a default circular layout. +function NetworkPlot(props: NetworkPlotProps, ref: Ref) { const { - data = EmptyBipartiteNetworkData, - partition1Name, - partition2Name, + nodes = emptyNodes, + links = emptyLinks, containerStyles, svgStyleOverrides, containerClass = 'web-components-plot', showSpinner = false, labelTruncationLength = 20, emptyNetworkContent, - getNodeMenuActions: getNodeActions, + annotations, } = props; const [highlightedNodeId, setHighlightedNodeId] = useState(); @@ -114,82 +97,44 @@ function BipartiteNetwork( [toImage] ); - // Set up styles for the bipartite network and incorporate overrides + const plotRect = plotRef.current?.getBoundingClientRect(); + const imageHeight = plotRect?.height; + const imageWidth = plotRect?.width; + + // Set up styles for the network and incorporate overrides const svgStyles = { - width: Number(containerStyles?.width) || 400, - topPadding: 40, - nodeSpacing: 30, - columnPadding: 100, + width: + containerStyles?.width && isNumber(containerStyles?.width) + ? containerStyles.width + : DEFAULT_PLOT_WIDTH, + height: + containerStyles?.height && isNumber(containerStyles?.height) + ? containerStyles.height + : DEFAULT_PLOT_HEIGHT, ...svgStyleOverrides, }; - const column1Position = svgStyles.columnPadding; - const column2Position = svgStyles.width - svgStyles.columnPadding; - - // In order to assign coordinates to each node, we'll separate the - // nodes based on their partition, then will use their order in the partition - // (given by partitionXNodeIDs) to finally assign the coordinates. - const nodesByPartition: NodeData[][] = useMemo( - () => - partition(data.nodes, (node) => { - return data.partitions[0].nodeIds.includes(node.id); - }), - [data.nodes, data.partitions] - ); - - const nodesByPartitionWithCoordinates = useMemo( - () => - nodesByPartition.map((partition, partitionIndex) => { - const partitionWithCoordinates = partition.map((node) => { - // Find the index of the node in the partition - const indexInPartition = data.partitions[ - partitionIndex - ].nodeIds.findIndex((id) => id === node.id); - - return { - // partitionIndex of 0 refers to the left-column nodes whereas 1 refers to right-column nodes - x: partitionIndex === 0 ? column1Position : column2Position, - y: svgStyles.topPadding + svgStyles.nodeSpacing * indexInPartition, - labelPosition: - partitionIndex === 0 ? 'left' : ('right' as LabelPosition), - ...node, - }; - }); - return partitionWithCoordinates; - }), - [ - column1Position, - column2Position, - data.partitions, - nodesByPartition, - svgStyles.nodeSpacing, - svgStyles.topPadding, - ] - ); - - // Assign coordinates to links based on the newly created node coordinates - const linksWithCoordinates = useMemo( + // Link processing. + // Assign coordinates to links based on the newly created node coordinates. + // Additionally order links so that the highlighted ones get drawn on top (are at the end of the array). + const processedLinks = useMemo( () => // Put highlighted links on top of gray links. orderBy( - data.links.map((link) => { - const sourceNode = nodesByPartitionWithCoordinates[0].find( - (node) => node.id === link.source.id - ); - const targetNode = nodesByPartitionWithCoordinates[1].find( - (node) => node.id === link.target.id - ); + 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: { + ...link.source, x: sourceNode?.x, y: sourceNode?.y, - ...link.source, }, target: { + ...link.target, x: targetNode?.x, y: targetNode?.y, - ...link.target, }, color: highlightedNodeId != null && @@ -205,28 +150,13 @@ function BipartiteNetwork( // but that's okay, because the overlapping colors will be the same. (link) => (link.color === '#eee' ? -1 : 1) ), - [data.links, highlightedNodeId, nodesByPartitionWithCoordinates] - ); - - const plotRect = plotRef.current?.getBoundingClientRect(); - const imageHeight = plotRect?.height; - const imageWidth = plotRect?.width; - - const nodes = useMemo( - () => - nodesByPartitionWithCoordinates[0] - .concat(nodesByPartitionWithCoordinates[1]) - .map((node) => ({ - ...node, - actions: getNodeActions?.(node.id), - })), - [getNodeActions, nodesByPartitionWithCoordinates] + [links, highlightedNodeId, nodes] ); const activeNode = nodes.find((node) => node.id === activeNodeId); useEffect(() => { - const element = document.querySelector('.bpnet-plot-container'); + const element = document.querySelector('.network-plot-container'); if (element == null) return; element.addEventListener('click', handler); @@ -250,8 +180,8 @@ function BipartiteNetwork(
)} -
- {nodesByPartitionWithCoordinates[0].length > 0 ? ( - - {/* Draw names of node colums if they exist */} - {partition1Name && ( - - {partition1Name} - - )} - {partition2Name && ( - - {partition2Name} - - )} - +
+ {nodes.length > 0 ? ( + - {node.actions?.length && ( - + {node.actions && node.actions?.length && ( + + {annotations && + annotations.map((annotation) => { + return annotation; + })} ) : ( emptyNetworkContent ??

No nodes in the network

)} - { - // Note that the spinner shows up in the middle of the network. So when - // the network is very long, the spinner will be further down the page than in other vizs. - showSpinner && - } + {showSpinner && }
); } - -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/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx similarity index 55% rename from packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx rename to packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx index 7cf86a5edd..b5e4d62efe 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx @@ -1,34 +1,22 @@ -import { useState, useEffect, useRef, CSSProperties, ReactNode } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { NodeData, LinkData, BipartiteNetworkData, } from '../../types/plots/network'; -import BipartiteNetwork, { - BipartiteNetworkProps, - BipartiteNetworkSVGStyles, - NodeMenuAction, -} from '../../plots/BipartiteNetwork'; +import BipartiteNetworkPlot, { + BipartiteNetworkPlotProps, +} from '../../plots/BipartiteNetworkPlot'; import { twoColorPalette } from '../../types/plots/addOns'; -import { Text } from '@visx/text'; export default { - title: 'Plots/Network/BipartiteNetwork', - component: BipartiteNetwork, + title: 'Plots/Networks/BipartiteNetwork', + component: BipartiteNetworkPlot, } as Meta; -interface TemplateProps { - data: BipartiteNetworkData; - partition1Name?: string; - partition2Name?: string; - loading?: boolean; +interface TemplateProps extends BipartiteNetworkPlotProps { showThumbnail?: boolean; - containerStyles?: CSSProperties; - svgStyleOverrides?: BipartiteNetworkSVGStyles; - labelTruncationLength?: number; - emptyNetworkContent?: ReactNode; - getNodeMenuActions?: BipartiteNetworkProps['getNodeMenuActions']; isSelectable?: boolean; } @@ -48,15 +36,8 @@ const Template: Story = (args) => { const [selectedNodeIds, setSelectedNodeIds] = useState([]); - const bipartiteNetworkProps: BipartiteNetworkProps = { - data: args.data, - partition1Name: args.partition1Name, - partition2Name: args.partition2Name, - showSpinner: args.loading, - containerStyles: args.containerStyles, - svgStyleOverrides: args.svgStyleOverrides, - labelTruncationLength: args.labelTruncationLength, - emptyNetworkContent: args.emptyNetworkContent, + const bipartiteNetworkPlotProps: BipartiteNetworkPlotProps = { + ...args, getNodeMenuActions: args.getNodeMenuActions, ...(args.isSelectable ? { @@ -67,12 +48,12 @@ const Template: Story = (args) => { }; return ( <> - + {args.showThumbnail && ( <>

A snapshot of the plot will appear below after two sconds...

- + Bipartite network snapshot )} @@ -84,48 +65,45 @@ const Template: Story = (args) => { */ // A basic bipartite network -const simpleData = genBipartiteNetwork(20, 10); +const simpleData = genBipartiteNetwork(20, 10, false); export const Simple = Template.bind({}); Simple.args = { - data: simpleData, + ...simpleData, }; // A network with lots and lots of points! -const manyPointsData = genBipartiteNetwork(1000, 100); +const manyPointsData = genBipartiteNetwork(1000, 100, false); export const ManyPoints = Template.bind({}); ManyPoints.args = { - data: manyPointsData, + ...manyPointsData, }; // With partition names +const simpleDataWithNames = genBipartiteNetwork(20, 10, true); export const WithPartitionNames = Template.bind({}); WithPartitionNames.args = { - data: simpleData, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', + ...simpleDataWithNames, }; // Loading with a spinner export const Loading = Template.bind({}); Loading.args = { - data: simpleData, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', - loading: true, + ...simpleDataWithNames, + showSpinner: true, }; // Empty bipartite network export const Empty = Template.bind({}); Empty.args = { - data: undefined, + nodes: undefined, + links: undefined, + partitions: undefined, }; // Show thumbnail export const Thumbnail = Template.bind({}); Thumbnail.args = { - data: genBipartiteNetwork(10, 10), - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', + ...simpleData, showThumbnail: true, }; @@ -139,75 +117,20 @@ const plotContainerStyles = { const svgStyleOverrides = { columnPadding: 150, topPadding: 100, - // width: 300, // should override the plotContainerStyles.width }; export const WithStyle = Template.bind({}); WithStyle.args = { - data: manyPointsData, + ...manyPointsData, containerStyles: plotContainerStyles, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', svgStyleOverrides: svgStyleOverrides, labelTruncationLength: 5, }; -function getNodeActions(nodeId: string): NodeMenuAction[] { - return [ - { - label: 'Click me!!', - onClick() { - alert('You clicked node ' + nodeId); - }, - }, - { - label: 'Click me, too!!', - onClick() { - alert('You clicked node ' + nodeId); - }, - }, - ]; -} - -export const WithActions = Template.bind({}); -WithActions.args = { - data: simpleData, - containerStyles: { - marginLeft: '200px', - }, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', - getNodeMenuActions: getNodeActions, -}; - -export const WithSelection = Template.bind({}); -WithSelection.args = { - data: simpleData, - containerStyles: { - marginLeft: '200px', - }, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', - getNodeMenuActions: getNodeActions, - isSelectable: true, -}; - -// With a network that has no nodes or links -const noNodesData = genBipartiteNetwork(0, 0); -const emptyNetworkContent = ( - - No nodes or links - -); -export const NoNodes = Template.bind({}); -NoNodes.args = { - data: noNodesData, - emptyNetworkContent, -}; - // Gerenate a bipartite network with a given number of nodes and random edges function genBipartiteNetwork( partition1nNodes: number, - partition2nNodes: number + partition2nNodes: number, + addPartitionNames: boolean ): BipartiteNetworkData { // Create the first partition of nodes const partition1Nodes: NodeData[] = [...Array(partition1nNodes).keys()].map( @@ -250,8 +173,14 @@ function genBipartiteNetwork( nodes, links, partitions: [ - { nodeIds: partition1NodeIDs }, - { nodeIds: partition2NodeIDs }, + { + nodeIds: partition1NodeIDs, + name: addPartitionNames ? 'Partition 1' : undefined, + }, + { + nodeIds: partition2NodeIDs, + name: addPartitionNames ? 'Partition 2' : undefined, + }, ], }; } diff --git a/packages/libs/components/src/stories/plots/Network.stories.tsx b/packages/libs/components/src/stories/plots/Network.stories.tsx deleted file mode 100755 index fa4d41c9b8..0000000000 --- a/packages/libs/components/src/stories/plots/Network.stories.tsx +++ /dev/null @@ -1,90 +0,0 @@ -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 ( - - } - // 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 ; - }} - /> - - ); -}; - -/** - * 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/NetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx new file mode 100755 index 0000000000..a4879f4fce --- /dev/null +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -0,0 +1,211 @@ +import { Story, Meta } from '@storybook/react/types-6-0'; +import { + NodeData, + LinkData, + NetworkPlotData, + NodeMenuAction, +} from '../../types/plots/network'; +import NetworkPlot, { NetworkPlotProps } from '../../plots/NetworkPlot'; +import { Text } from '@visx/text'; +import { useEffect, useRef, useState } from 'react'; + +export default { + title: 'Plots/Networks/NetworkPlot', + component: NetworkPlot, +} as Meta; + +// For simplicity, make square svgs with the following height and width +const DEFAULT_PLOT_SIZE = 500; + +interface TemplateProps extends NetworkPlotProps { + showThumbnail?: boolean; +} + +// Showcase our NetworkPlot component. +const Template: Story = (args) => { + // Generate a jpeg version of the network (svg). + // Mimicks the makePlotThumbnailUrl process in web-eda. + const ref = useRef(null); + const [img, setImg] = useState(''); + useEffect(() => { + setTimeout(() => { + ref.current + ?.toImage({ + format: 'jpeg', + height: DEFAULT_PLOT_SIZE, + width: DEFAULT_PLOT_SIZE, + }) + .then((src: string) => setImg(src)); + }, 2000); + }, []); + + return ( + <> + + {args.showThumbnail && ( + <> +

+

A snapshot of the plot will appear below after two sconds...

+ Network snapshot + + )} + + ); +}; + +/** + * Stories + */ + +// A simple network with node labels +const simpleData = genNetwork( + 20, + true, + true, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const Simple = Template.bind({}); +Simple.args = { + ...simpleData, +}; + +// A network with lots and lots of points! +const manyPointsData = genNetwork( + 100, + false, + true, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const ManyPoints = Template.bind({}); +ManyPoints.args = { + ...manyPointsData, +}; + +// A network with annotations. +// These can be used to add column labels in the bipartite network, call out +// a specific node of interest, or just generally add some more info. +const annotation1 = ( + + I am an annotation + +); +const annotation2 = ( + + I am another annotation + +); +export const WithAnnotations = Template.bind({}); +WithAnnotations.args = { + ...simpleData, + annotations: [annotation1, annotation2], +}; + +// An empty network. +// This is what will be shown by default before we receive any data +export const Empty = Template.bind({}); +Empty.args = { + nodes: undefined, + links: undefined, +}; + +// Loading +export const Loading = Template.bind({}); +Loading.args = { + ...simpleData, + showSpinner: true, +}; + +// Pass an empty network with no nodes +const emptyNetworkContent = ( + + No nodes or links. Try something else. + +); +export const NoNodes = Template.bind({}); +NoNodes.args = { + nodes: [], + links: [], + emptyNetworkContent, +}; + +// Show thumbnail +export const Thumbnail = Template.bind({}); +Thumbnail.args = { + ...simpleData, + showThumbnail: true, +}; + +// Test node actions +function getNodeActions(nodeId: string): NodeMenuAction[] { + return [ + { + label: 'Click me!!', + onClick() { + alert('You clicked node ' + nodeId); + }, + }, + { + label: 'Click me, too!!', + onClick() { + alert('You clicked node ' + nodeId); + }, + }, + ]; +} + +const simpleWithActions = simpleData; +simpleWithActions.nodes = simpleData.nodes.map((node) => ({ + ...node, + actions: getNodeActions(node.id), +})); + +export const WithActions = Template.bind({}); +WithActions.args = { + ...simpleWithActions, + getNodeMenuActions: getNodeActions, +}; + +// Utility functions +// Gerenate a network with a given number of nodes and random edges +function genNetwork( + nNodes: number, + addNodeLabel: boolean, + addNodeCoordinates: boolean, + height: number, + width: number +) { + // Create nodes with random positioning, an id, and optionally a label + const nodes: NodeData[] = [...Array(nNodes).keys()].map((i) => { + // Postion nodes randomly across the plot, but add some padding to prevent the nodes + // from getting cut off at the edges. + const nodeX = 10 + Math.floor(Math.random() * (width - 20)); // Range: [10, width - 10] + const nodeY = 10 + Math.floor(Math.random() * (height - 20)); // Range: [10, height - 10] + return { + x: addNodeCoordinates ? nodeX : undefined, + y: addNodeCoordinates ? nodeY : undefined, + id: String(i), + label: addNodeLabel ? 'Node ' + String(i) : undefined, + labelPosition: addNodeLabel + ? nodeX > width / 2 + ? 'left' + : 'right' + : 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 NetworkPlotData; +} diff --git a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx index 5581465b64..6df5628b66 100755 --- a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx +++ b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx @@ -1,10 +1,10 @@ import { Story, Meta } from '@storybook/react/types-6-0'; import { NodeData } from '../../types/plots/network'; -import { NodeWithLabel } from '../../plots/Network'; +import { NodeWithLabel } from '../../plots/Node'; import { Group } from '@visx/group'; export default { - title: 'Plots/Network/NodeWithLabel', + title: 'Plots/Networks/NodeWithLabel', component: NodeWithLabel, } as Meta; diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 20f0adf430..25f5a3d8e3 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -1,4 +1,14 @@ +import { LabelPosition } from '../../plots/Node'; + // Types required for creating networks +import { ReactNode } from 'react'; + +export interface NodeMenuAction { + label: ReactNode; + onClick?: () => void; + href?: string; +} + export type NodeData = { /** Node ID. Must be unique in the network! */ id: string; @@ -16,6 +26,10 @@ export type NodeData = { stroke?: string; /** Width of node stroke */ strokeWidth?: number; + /** Should the node label be drawn to the right or left of the node? */ + labelPosition?: LabelPosition; + /** Action menu items for the node */ + actions?: NodeMenuAction[]; }; export type LinkData = { @@ -32,18 +46,20 @@ export type LinkData = { }; /** NetworkData is the same format accepted by visx's Graph component. */ -export type NetworkData = { +export type NetworkPlotData = { nodes: NodeData[]; links: LinkData[]; }; -export type NodeIdList = { - nodeIds: string[]; -}; - /** 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. + * nodes in each of the two columns. */ +export type NetworkPartition = { + /** Ids that allow us to match node ids in NodeData[] of a NetworkPlotData object to this partition. */ + nodeIds: string[]; + /** Name of the partition. Ex. "Species" */ + name?: string; +}; export type BipartiteNetworkData = { - partitions: NodeIdList[]; -} & NetworkData; + partitions: NetworkPartition[]; +} & NetworkPlotData; diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx index 246891bb02..2d815c4d04 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BipartiteNetworkVisualization.tsx @@ -10,10 +10,9 @@ import { import { RequestOptions } from '../options/types'; // Bipartite network imports -import BipartiteNetwork, { - BipartiteNetworkProps, - NodeMenuAction, -} from '@veupathdb/components/lib/plots/BipartiteNetwork'; +import BipartiteNetworkPlot, { + BipartiteNetworkPlotProps, +} from '@veupathdb/components/lib/plots/BipartiteNetworkPlot'; import BipartiteNetworkSVG from './selectorIcons/BipartiteNetworkSVG'; import { BipartiteNetworkRequestParams, @@ -45,6 +44,7 @@ import { FacetedPlotLayout } from '../../layouts/FacetedPlotLayout'; import { H6 } from '@veupathdb/coreui'; import { CorrelationConfig } from '../../../types/apps'; import { StudyMetadata } from '../../..'; +import { NodeMenuAction } from '@veupathdb/components/lib/types/plots/network'; // end imports // Defaults @@ -332,8 +332,10 @@ function BipartiteNetworkViz(props: VisualizationProps) {
); - const bipartiteNetworkProps: BipartiteNetworkProps = { - data: cleanedData ?? undefined, + const bipartiteNetworkPlotProps: BipartiteNetworkPlotProps = { + nodes: cleanedData ? cleanedData.nodes : undefined, + links: cleanedData ? cleanedData.links : undefined, + partitions: cleanedData ? cleanedData.partitions : undefined, showSpinner: data.pending, containerStyles: finalPlotContainerStyles, svgStyleOverrides: bipartiteNetworkSVGStyles, @@ -345,7 +347,7 @@ function BipartiteNetworkViz(props: VisualizationProps) { const plotNode = ( //@ts-ignore - + ); const controlsNode = <> ;