From e70f7f137836071eecb4eab0a70c9986e19b3633 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 25 Apr 2024 12:14:41 -0400 Subject: [PATCH 01/15] draft Network and update stories --- packages/libs/components/src/plots/Link.tsx | 28 ++ .../libs/components/src/plots/NetworkPlot.tsx | 354 ++++++++++++++++++ packages/libs/components/src/plots/Node.tsx | 87 +++++ .../src/stories/plots/NetworkPlot.stories.tsx | 83 ++++ .../components/src/types/plots/network.ts | 4 +- 5 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 packages/libs/components/src/plots/Link.tsx create mode 100755 packages/libs/components/src/plots/NetworkPlot.tsx create mode 100644 packages/libs/components/src/plots/Node.tsx create mode 100755 packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx 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/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx new file mode 100755 index 0000000000..8de2a53503 --- /dev/null +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -0,0 +1,354 @@ +import { LinkData, NetworkPartition, NodeData } from '../types/plots/network'; +import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis'; +import './Network.css'; +import { orderBy } from 'lodash'; +import { LabelPosition, NodeWithLabel } from './Node'; +import { Link } from './Link'; +import { Graph } from '@visx/network'; +import { Text } from '@visx/text'; +import { + CSSProperties, + ReactNode, + Ref, + forwardRef, + useImperativeHandle, + useRef, + useCallback, + useState, + useMemo, + useEffect, +} from 'react'; +import Spinner from '../components/Spinner'; +import { ToImgopts } from 'plotly.js'; +import { gray } from '@veupathdb/coreui/lib/definitions/colors'; +import { ExportPlotToImageButton } from './ExportPlotToImageButton'; +import { plotToImage } from './visxVEuPathDB'; +import { GlyphTriangle } from '@visx/visx'; + +import './BipartiteNetwork.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 NetworkPlotProps { + /** Network nodes */ + nodes: NodeData[] | undefined; + /** Network links */ + links: LinkData[] | undefined; + /** Partitions, optional. Used for k-partite networks only */ + partitions?: NetworkPartition[]; + /** styling for the plot's container */ + containerStyles?: CSSProperties; + /** 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; + /** container name */ + containerClass?: string; + /** shall we show the loading spinner? */ + showSpinner?: boolean; + /** Length of node label text before truncating with an ellipsis */ + labelTruncationLength?: number; + /** Additional error messaging to show when the network is empty */ + emptyNetworkContent?: ReactNode; + /** Entries for the actions that appear in the menu when you click a node */ + getNodeMenuActions?: (nodeId: string) => NodeMenuAction[]; +} + +const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ + id: item.toString(), + color: gray[100], + stroke: gray[300], +})); +const emptyLinks: LinkData[] = []; + +// The Network component draws a network of nodes and links. Optionaly, one can pass partitions which +// then will be used to draw a k-partite network. +// If no x,y coordinates are provided for nodes in the network, the network will +// be drawn in a circular layout, or in columns partitions are provided. +function NetworkPlot(props: NetworkPlotProps, ref: Ref) { + const { + nodes = emptyNodes, + links = emptyLinks, + partitions, + containerStyles, + svgStyleOverrides, + containerClass = 'web-components-plot', + showSpinner = false, + labelTruncationLength = 20, + emptyNetworkContent, + getNodeMenuActions: getNodeActions, + } = props; + + const [highlightedNodeId, setHighlightedNodeId] = useState(); + const [activeNodeId, setActiveNodeId] = useState(); + + // Use ref forwarding to enable screenshotting of the plot for thumbnail versions. + const plotRef = useRef(null); + + const toImage = useCallback(async (opts: ToImgopts) => { + return plotToImage(plotRef.current, opts); + }, []); + + useImperativeHandle( + ref, + () => ({ + // The thumbnail generator makePlotThumbnailUrl expects to call a toImage function + toImage, + }), + [toImage] + ); + + // Set up styles for the bipartite network and incorporate overrides + const svgStyles = { + width: Number(containerStyles?.width) || 400, + topPadding: 40, + nodeSpacing: 30, + columnPadding: 100, + ...svgStyleOverrides, + }; + + // Assign coordinates to links based on the newly created node coordinates + const linksWithCoordinates = useMemo( + () => + // Put highlighted links on top of gray links. + orderBy( + 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: + highlightedNodeId != null && + sourceNode?.id !== highlightedNodeId && + targetNode?.id !== highlightedNodeId + ? '#eee' + : link.color, + }; + }), + // Links that are added later will be on top. + // If a link is grayed out, it will be sorted before other links. + // In theory, it's possible to have a false positive here; + // but that's okay, because the overlapping colors will be the same. + (link) => (link.color === '#eee' ? -1 : 1) + ), + [links, highlightedNodeId, nodes] + ); + + const plotRect = plotRef.current?.getBoundingClientRect(); + const imageHeight = plotRect?.height; + const imageWidth = plotRect?.width; + + const nodesWithActions = useMemo( + () => + nodes.map((node) => ({ + x: Math.random() * 100, + y: Math.random() * 100, + labelPosition: 'right' as LabelPosition, + ...node, + actions: getNodeActions?.(node.id), + })), + [getNodeActions, nodes] + ); + + const activeNode = nodesWithActions.find((node) => node.id === activeNodeId); + + useEffect(() => { + const element = document.querySelector('.network-plot-container'); + if (element == null) return; + + element.addEventListener('click', handler); + + return () => { + element.removeEventListener('click', handler); + }; + + function handler() { + setActiveNodeId(undefined); + } + }, [containerClass]); + + return ( + <> +
+ {activeNode?.actions?.length && ( +
+ {activeNode.actions.map((action) => ( +
+ {action.href ? ( + + {action.label} + + ) : ( + + )} +
+ ))} +
+ )} +
+ {nodes.length > 0 ? ( + + { + return ; + }} + nodeComponent={({ node }) => { + const isHighlighted = highlightedNodeId === node.id; + const rectWidth = + (node.r ?? 6) * 2 + // node diameter + (node.label?.length ?? 0) * 6 + // label width + (12 + 6) + // button + space + (12 + 12 + 12); // paddingLeft + space-between-node-and-label + paddingRight + const rectX = + node.labelPosition === 'left' ? -rectWidth + 12 : -12; + const glyphLeft = + node.labelPosition === 'left' ? rectX + 12 : rectWidth - 24; + return ( + <> + {node.actions?.length && ( + + + + setActiveNodeId(node.id)} + /> + + )} + { + setHighlightedNodeId((id) => + id === node.id ? undefined : node.id + ); + }} + fontWeight={isHighlighted ? 600 : 400} + /> + + ); + }} + /> + + ) : ( + 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 && + } +
+
+ + + ); +} + +export default forwardRef(NetworkPlot); diff --git a/packages/libs/components/src/plots/Node.tsx b/packages/libs/components/src/plots/Node.tsx new file mode 100644 index 0000000000..7380a06e88 --- /dev/null +++ b/packages/libs/components/src/plots/Node.tsx @@ -0,0 +1,87 @@ +import { DefaultNode } from '@visx/network'; +import { Text } from '@visx/text'; +import { NodeData } from '../types/plots/network'; +import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis'; +import './Network.css'; + +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?: LabelPosition; + /** Font size for the label. Ex. "1em" */ + fontSize?: string; + /** Font weight for the label */ + fontWeight?: number; + /** Color for the label */ + labelColor?: string; + /** Length for labels before being truncated by ellipsis. Default 20 */ + truncationLength?: number; +} + +// 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 = 6; + const DEFAULT_NODE_COLOR = '#fff'; + const DEFAULT_STROKE_WIDTH = 1; + const DEFAULT_STROKE = '#111'; + + const { + node, + onClick, + labelPosition = 'right', + fontSize = '1em', + fontWeight = 400, + labelColor = '#000', + truncationLength = 20, + } = 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 && truncateWithEllipsis(label, truncationLength)} + + {label} + + ); +} 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..26d8f0f322 --- /dev/null +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -0,0 +1,83 @@ +import { Story, Meta } from '@storybook/react/types-6-0'; +import { NodeData, LinkData, NetworkPlotData } from '../../types/plots/network'; +import NetworkPlot from '../../plots/NetworkPlot'; + +export default { + title: 'Plots/Network', + component: NetworkPlot, +} as Meta; + +// For simplicity, make square svgs with the following height and width +const DEFAULT_PLOT_SIZE = 500; + +interface TemplateProps { + data: NetworkPlotData; +} + +// 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) => { + const nodeX = 10 + Math.floor(Math.random() * (width - 20)); // Add/Subtract a bit to keep the whole node in view + const nodeY = 10 + Math.floor(Math.random() * (height - 20)); + return { + x: nodeX, + y: nodeY, + 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/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 20f0adf430..fad26298d6 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -32,7 +32,7 @@ export type LinkData = { }; /** NetworkData is the same format accepted by visx's Graph component. */ -export type NetworkData = { +export type NetworkPlotData = { nodes: NodeData[]; links: LinkData[]; }; @@ -46,4 +46,4 @@ export type NodeIdList = { */ export type BipartiteNetworkData = { partitions: NodeIdList[]; -} & NetworkData; +} & NetworkPlotData; From 4d6ba3789d0828799b560a9c0244de3758ab4f84 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 26 Apr 2024 14:19:49 -0400 Subject: [PATCH 02/15] have BipartiteNetwork call NetworkPlot --- .../components/src/plots/BipartiteNetwork.tsx | 593 +++++++++--------- .../libs/components/src/plots/Network.css | 6 + .../libs/components/src/plots/NetworkPlot.tsx | 11 +- .../plots/BipartiteNetwork.stories.tsx | 15 +- .../components/src/types/plots/network.ts | 4 +- 5 files changed, 317 insertions(+), 312 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetwork.tsx index 5265201ef2..3f335bd183 100755 --- a/packages/libs/components/src/plots/BipartiteNetwork.tsx +++ b/packages/libs/components/src/plots/BipartiteNetwork.tsx @@ -1,28 +1,23 @@ -import { BipartiteNetworkData, NodeData } from '../types/plots/network'; -import { orderBy, partition } from 'lodash'; -import { LabelPosition, Link, NodeWithLabel } from './Network'; -import { Graph } from '@visx/network'; -import { Text } from '@visx/text'; +import { + BipartiteNetworkData, + LinkData, + NetworkPartition, + NodeData, +} from '../types/plots/network'; +import { partition } from 'lodash'; +import { LabelPosition } from './Network'; import { CSSProperties, ReactNode, Ref, forwardRef, - useImperativeHandle, useRef, - useCallback, - useState, useMemo, - useEffect, } from 'react'; -import Spinner from '../components/Spinner'; -import { ToImgopts } from 'plotly.js'; import { gray } from '@veupathdb/coreui/lib/definitions/colors'; -import { ExportPlotToImageButton } from './ExportPlotToImageButton'; -import { plotToImage } from './visxVEuPathDB'; -import { GlyphTriangle } from '@visx/visx'; import './BipartiteNetwork.css'; +import NetworkPlot, { NodeMenuAction } from './NetworkPlot'; export interface BipartiteNetworkSVGStyles { width?: number; // svg width @@ -31,15 +26,13 @@ export interface BipartiteNetworkSVGStyles { 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; + /** Nodes */ + nodes: NodeData[] | undefined; + /** Links */ + links: LinkData[] | undefined; + /** Partitions. An array of NetworkPartitions (an array of node ids) that defines the two node groups */ + partitions: NetworkPartition[] | undefined; /** Name of partition 1 */ partition1Name?: string; /** Name of partition 2 */ @@ -83,7 +76,9 @@ function BipartiteNetwork( ref: Ref ) { const { - data = EmptyBipartiteNetworkData, + nodes = EmptyBipartiteNetworkData.nodes, + links = EmptyBipartiteNetworkData.links, + partitions = EmptyBipartiteNetworkData.partitions, partition1Name, partition2Name, containerStyles, @@ -95,33 +90,40 @@ function BipartiteNetwork( getNodeMenuActions: getNodeActions, } = props; - const [highlightedNodeId, setHighlightedNodeId] = useState(); - const [activeNodeId, setActiveNodeId] = useState(); + // const [highlightedNodeId, setHighlightedNodeId] = useState(); + // const [activeNodeId, setActiveNodeId] = useState(); // Use ref forwarding to enable screenshotting of the plot for thumbnail versions. const plotRef = useRef(null); - const toImage = useCallback(async (opts: ToImgopts) => { - return plotToImage(plotRef.current, opts); - }, []); + // const toImage = useCallback(async (opts: ToImgopts) => { + // return plotToImage(plotRef.current, opts); + // }, []); - useImperativeHandle( - ref, - () => ({ - // The thumbnail generator makePlotThumbnailUrl expects to call a toImage function - toImage, - }), - [toImage] - ); + // useImperativeHandle( + // ref, + // () => ({ + // // The thumbnail generator makePlotThumbnailUrl expects to call a toImage function + // toImage, + // }), + // [toImage] + // ); // Set up styles for the bipartite network and incorporate overrides + const DEFAULT_TOP_PADDING = 40; + const DEFAULT_NODE_SPACING = 30; const svgStyles = { width: Number(containerStyles?.width) || 400, - topPadding: 40, - nodeSpacing: 30, + height: + Math.max(partitions[1].nodeIds.length, partitions[0].nodeIds.length) * + DEFAULT_NODE_SPACING + + DEFAULT_TOP_PADDING, + topPadding: DEFAULT_TOP_PADDING, + nodeSpacing: DEFAULT_NODE_SPACING, columnPadding: 100, ...svgStyleOverrides, }; + console.log(svgStyles.height); const column1Position = svgStyles.columnPadding; const column2Position = svgStyles.width - svgStyles.columnPadding; @@ -131,10 +133,10 @@ function BipartiteNetwork( // (given by partitionXNodeIDs) to finally assign the coordinates. const nodesByPartition: NodeData[][] = useMemo( () => - partition(data.nodes, (node) => { - return data.partitions[0].nodeIds.includes(node.id); + partition(nodes, (node) => { + return partitions[0].nodeIds.includes(node.id); }), - [data.nodes, data.partitions] + [nodes, partitions] ); const nodesByPartitionWithCoordinates = useMemo( @@ -142,9 +144,9 @@ function BipartiteNetwork( 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); + const indexInPartition = 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 @@ -160,279 +162,274 @@ function BipartiteNetwork( [ column1Position, column2Position, - data.partitions, + partitions, nodesByPartition, svgStyles.nodeSpacing, svgStyles.topPadding, ] ); - // Assign coordinates to links based on the newly created node coordinates - const linksWithCoordinates = 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 - ); - return { - ...link, - source: { - x: sourceNode?.x, - y: sourceNode?.y, - ...link.source, - }, - target: { - x: targetNode?.x, - y: targetNode?.y, - ...link.target, - }, - color: - highlightedNodeId != null && - sourceNode?.id !== highlightedNodeId && - targetNode?.id !== highlightedNodeId - ? '#eee' - : link.color, - }; - }), - // Links that are added later will be on top. - // If a link is grayed out, it will be sorted before other links. - // In theory, it's possible to have a false positive here; - // but that's okay, because the overlapping colors will be the same. - (link) => (link.color === '#eee' ? -1 : 1) - ), - [data.links, highlightedNodeId, nodesByPartitionWithCoordinates] - ); + // // Assign coordinates to links based on the newly created node coordinates + // const linksWithCoordinates = useMemo( + // () => + // // Put highlighted links on top of gray links. + // orderBy( + // 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 + // ); + // return { + // ...link, + // source: { + // x: sourceNode?.x, + // y: sourceNode?.y, + // ...link.source, + // }, + // target: { + // x: targetNode?.x, + // y: targetNode?.y, + // ...link.target, + // }, + // color: + // highlightedNodeId != null && + // sourceNode?.id !== highlightedNodeId && + // targetNode?.id !== highlightedNodeId + // ? '#eee' + // : link.color, + // }; + // }), + // // Links that are added later will be on top. + // // If a link is grayed out, it will be sorted before other links. + // // In theory, it's possible to have a false positive here; + // // 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] - ); + // const nodes = useMemo( + // () => + // nodesByPartitionWithCoordinates[0] + // .concat(nodesByPartitionWithCoordinates[1]) + // .map((node) => ({ + // ...node, + // actions: getNodeActions?.(node.id), + // })), + // [getNodeActions, nodesByPartitionWithCoordinates] + // ); - const activeNode = nodes.find((node) => node.id === activeNodeId); + // const activeNode = nodes.find((node) => node.id === activeNodeId); - useEffect(() => { - const element = document.querySelector('.bpnet-plot-container'); - if (element == null) return; + // useEffect(() => { + // const element = document.querySelector('.bpnet-plot-container'); + // if (element == null) return; - element.addEventListener('click', handler); + // element.addEventListener('click', handler); - return () => { - element.removeEventListener('click', handler); - }; + // return () => { + // element.removeEventListener('click', handler); + // }; - function handler() { - setActiveNodeId(undefined); - } - }, [containerClass]); + // function handler() { + // setActiveNodeId(undefined); + // } + // }, [containerClass]); return ( - <> -
- {activeNode?.actions?.length && ( -
- {activeNode.actions.map((action) => ( -
- {action.href ? ( - - {action.label} - - ) : ( - - )} -
- ))} -
- )} -
- {nodesByPartitionWithCoordinates[0].length > 0 ? ( - - {/* Draw names of node colums if they exist */} - {partition1Name && ( - - {partition1Name} - - )} - {partition2Name && ( - - {partition2Name} - - )} - - { - return ; - }} - nodeComponent={({ node }) => { - const isHighlighted = highlightedNodeId === node.id; - const rectWidth = - (node.r ?? 6) * 2 + // node diameter - (node.label?.length ?? 0) * 6 + // label width - (12 + 6) + // button + space - (12 + 12 + 12); // paddingLeft + space-between-node-and-label + paddingRight - const rectX = - node.labelPosition === 'left' ? -rectWidth + 12 : -12; - const glyphLeft = - node.labelPosition === 'left' ? rectX + 12 : rectWidth - 24; - return ( - <> - {node.actions?.length && ( - - - - setActiveNodeId(node.id)} - /> - - )} - { - setHighlightedNodeId((id) => - id === node.id ? undefined : node.id - ); - }} - fontWeight={isHighlighted ? 600 : 400} - /> - - ); - }} - /> - - ) : ( - 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 && - } -
-
- - + ); + + // <> + //
+ // {activeNode?.actions?.length && ( + //
+ // {activeNode.actions.map((action) => ( + //
+ // {action.href ? ( + // + // {action.label} + // + // ) : ( + // + // )} + //
+ // ))} + //
+ // )} + //
+ // {nodesByPartitionWithCoordinates[0].length > 0 ? ( + // + // {/* Draw names of node colums if they exist */} + // {partition1Name && ( + // + // {partition1Name} + // + // )} + // {partition2Name && ( + // + // {partition2Name} + // + // )} + + // { + // return ; + // }} + // nodeComponent={({ node }) => { + // const isHighlighted = highlightedNodeId === node.id; + // const rectWidth = + // (node.r ?? 6) * 2 + // node diameter + // (node.label?.length ?? 0) * 6 + // label width + // (12 + 6) + // button + space + // (12 + 12 + 12); // paddingLeft + space-between-node-and-label + paddingRight + // const rectX = + // node.labelPosition === 'left' ? -rectWidth + 12 : -12; + // const glyphLeft = + // node.labelPosition === 'left' ? rectX + 12 : rectWidth - 24; + // return ( + // <> + // {node.actions?.length && ( + // + // + // + // setActiveNodeId(node.id)} + // /> + // + // )} + // { + // setHighlightedNodeId((id) => + // id === node.id ? undefined : node.id + // ); + // }} + // fontWeight={isHighlighted ? 600 : 400} + // /> + // + // ); + // }} + // /> + // + // ) : ( + // emptyNetworkContent ??

No nodes in the network

+ // )} } export default forwardRef(BipartiteNetwork); diff --git a/packages/libs/components/src/plots/Network.css b/packages/libs/components/src/plots/Network.css index da3992a358..ce812b0a93 100644 --- a/packages/libs/components/src/plots/Network.css +++ b/packages/libs/components/src/plots/Network.css @@ -1,3 +1,9 @@ .NodeWithLabel_Node { cursor: default; } + +.network-plot-container { + width: 100%; + height: 500px; + overflow-y: scroll; +} diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 8de2a53503..4dd3c686d0 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -26,9 +26,11 @@ import { plotToImage } from './visxVEuPathDB'; import { GlyphTriangle } from '@visx/visx'; import './BipartiteNetwork.css'; +import { container } from 'webpack'; export interface BipartiteNetworkSVGStyles { width?: number; // svg width + height?: number; // svg height 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. @@ -112,6 +114,7 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { // Set up styles for the bipartite network and incorporate overrides const svgStyles = { width: Number(containerStyles?.width) || 400, + height: Number(containerStyles?.height) || 500, topPadding: 40, nodeSpacing: 30, columnPadding: 100, @@ -245,13 +248,9 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { ))}
)} -
+
{nodes.length > 0 ? ( - + = (args) => { const [selectedNodeIds, setSelectedNodeIds] = useState([]); const bipartiteNetworkProps: BipartiteNetworkProps = { - data: args.data, + nodes: args.data.nodes, + links: args.data.links, + partitions: args.data.partitions, partition1Name: args.partition1Name, partition2Name: args.partition2Name, - showSpinner: args.loading, + showSpinner: args.showSpinner, containerStyles: args.containerStyles, svgStyleOverrides: args.svgStyleOverrides, labelTruncationLength: args.labelTruncationLength, @@ -72,7 +74,7 @@ const Template: Story = (args) => { <>

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

- + Bipartite network snapshot )} @@ -111,7 +113,7 @@ Loading.args = { data: simpleData, partition1Name: 'Partition 1', partition2Name: 'Partition 2', - loading: true, + showSpinner: true, }; // Empty bipartite network @@ -177,6 +179,7 @@ WithActions.args = { partition1Name: 'Partition 1', partition2Name: 'Partition 2', getNodeMenuActions: getNodeActions, + isSelectable: false, }; export const WithSelection = Template.bind({}); diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index fad26298d6..1bc2000517 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -37,7 +37,7 @@ export type NetworkPlotData = { links: LinkData[]; }; -export type NodeIdList = { +export type NetworkPartition = { nodeIds: string[]; }; @@ -45,5 +45,5 @@ export type NodeIdList = { * nodes in each of the two columns. IDs in columnXNodeIDs must match node ids exactly. */ export type BipartiteNetworkData = { - partitions: NodeIdList[]; + partitions: NetworkPartition[]; } & NetworkPlotData; From a4e08c4156f310dd0d9fe8477e2c011fb150dbb8 Mon Sep 17 00:00:00 2001 From: asizemore Date: Fri, 26 Apr 2024 14:39:45 -0400 Subject: [PATCH 03/15] rename so that networks in components follow Plot convention --- ...teNetwork.css => BipartiteNetworkPlot.css} | 0 ...teNetwork.tsx => BipartiteNetworkPlot.tsx} | 0 .../libs/components/src/plots/Network.tsx | 114 ------------------ .../plots/{Network.css => NetworkPlot.css} | 0 .../libs/components/src/plots/NetworkPlot.tsx | 3 +- ...s.tsx => BipartiteNetworkPlot.stories.tsx} | 0 .../src/stories/plots/Network.stories.tsx | 90 -------------- 7 files changed, 1 insertion(+), 206 deletions(-) rename packages/libs/components/src/plots/{BipartiteNetwork.css => BipartiteNetworkPlot.css} (100%) rename packages/libs/components/src/plots/{BipartiteNetwork.tsx => BipartiteNetworkPlot.tsx} (100%) delete mode 100755 packages/libs/components/src/plots/Network.tsx rename packages/libs/components/src/plots/{Network.css => NetworkPlot.css} (100%) rename packages/libs/components/src/stories/plots/{BipartiteNetwork.stories.tsx => BipartiteNetworkPlot.stories.tsx} (100%) delete mode 100755 packages/libs/components/src/stories/plots/Network.stories.tsx diff --git a/packages/libs/components/src/plots/BipartiteNetwork.css b/packages/libs/components/src/plots/BipartiteNetworkPlot.css similarity index 100% rename from packages/libs/components/src/plots/BipartiteNetwork.css rename to packages/libs/components/src/plots/BipartiteNetworkPlot.css diff --git a/packages/libs/components/src/plots/BipartiteNetwork.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx similarity index 100% rename from packages/libs/components/src/plots/BipartiteNetwork.tsx rename to packages/libs/components/src/plots/BipartiteNetworkPlot.tsx diff --git a/packages/libs/components/src/plots/Network.tsx b/packages/libs/components/src/plots/Network.tsx deleted file mode 100755 index 2a79138bab..0000000000 --- a/packages/libs/components/src/plots/Network.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { DefaultNode } from '@visx/network'; -import { Text } from '@visx/text'; -import { LinkData, NodeData } from '../types/plots/network'; -import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis'; -import './Network.css'; - -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?: LabelPosition; - /** Font size for the label. Ex. "1em" */ - fontSize?: string; - /** Font weight for the label */ - fontWeight?: number; - /** Color for the label */ - labelColor?: string; - /** Length for labels before being truncated by ellipsis. Default 20 */ - truncationLength?: number; -} - -// 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 = 6; - const DEFAULT_NODE_COLOR = '#fff'; - const DEFAULT_STROKE_WIDTH = 1; - const DEFAULT_STROKE = '#111'; - - const { - node, - onClick, - labelPosition = 'right', - fontSize = '1em', - fontWeight = 400, - labelColor = '#000', - truncationLength = 20, - } = 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 && truncateWithEllipsis(label, truncationLength)} - - {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.css b/packages/libs/components/src/plots/NetworkPlot.css similarity index 100% rename from packages/libs/components/src/plots/Network.css rename to packages/libs/components/src/plots/NetworkPlot.css diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 4dd3c686d0..058775f8cc 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -25,8 +25,7 @@ import { ExportPlotToImageButton } from './ExportPlotToImageButton'; import { plotToImage } from './visxVEuPathDB'; import { GlyphTriangle } from '@visx/visx'; -import './BipartiteNetwork.css'; -import { container } from 'webpack'; +import './Network.css'; export interface BipartiteNetworkSVGStyles { width?: number; // svg width diff --git a/packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx similarity index 100% rename from packages/libs/components/src/stories/plots/BipartiteNetwork.stories.tsx rename to packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx 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; -} From 16ec12d39ff185b195ab559c4fceaac0d1362a0d Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 29 Apr 2024 07:17:01 -0400 Subject: [PATCH 04/15] consolidate network style props --- .../src/plots/BipartiteNetworkPlot.tsx | 46 +++++++-------- .../libs/components/src/plots/NetworkPlot.tsx | 16 ++--- packages/libs/components/src/plots/Node.tsx | 2 +- .../plots/BipartiteNetworkPlot.stories.tsx | 58 +++++++++---------- .../stories/plots/NodeWithLabel.stories.tsx | 2 +- .../components/src/types/plots/network.ts | 1 + 6 files changed, 55 insertions(+), 70 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index 3f335bd183..355f378859 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -5,7 +5,7 @@ import { NodeData, } from '../types/plots/network'; import { partition } from 'lodash'; -import { LabelPosition } from './Network'; +import { LabelPosition } from './Node'; import { CSSProperties, ReactNode, @@ -13,14 +13,14 @@ import { forwardRef, useRef, useMemo, + SVGAttributes, } from 'react'; import { gray } from '@veupathdb/coreui/lib/definitions/colors'; -import './BipartiteNetwork.css'; +import './BipartiteNetworkPlot.css'; import NetworkPlot, { NodeMenuAction } from './NetworkPlot'; -export interface BipartiteNetworkSVGStyles { - width?: number; // svg width +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. @@ -33,10 +33,6 @@ export interface BipartiteNetworkProps { links: LinkData[] | undefined; /** Partitions. An array of NetworkPartitions (an array of node ids) that defines the two node groups */ partitions: NetworkPartition[] | undefined; - /** Name of partition 1 */ - partition1Name?: string; - /** Name of partition 2 */ - partition2Name?: string; /** styling for the plot's container */ containerStyles?: CSSProperties; /** bipartite network-specific styling for the svg itself. These @@ -58,20 +54,21 @@ export interface BipartiteNetworkProps { // 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'] }, + { 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 BipartiteNetwork function takes a network w two partitions of nodes and draws those partitions as columns. +// 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. -function BipartiteNetwork( +function BipartiteNetworkPlot( props: BipartiteNetworkProps, ref: Ref ) { @@ -79,15 +76,8 @@ function BipartiteNetwork( nodes = EmptyBipartiteNetworkData.nodes, links = EmptyBipartiteNetworkData.links, partitions = EmptyBipartiteNetworkData.partitions, - partition1Name, - partition2Name, containerStyles, svgStyleOverrides, - containerClass = 'web-components-plot', - showSpinner = false, - labelTruncationLength = 20, - emptyNetworkContent, - getNodeMenuActions: getNodeActions, } = props; // const [highlightedNodeId, setHighlightedNodeId] = useState(); @@ -112,8 +102,10 @@ function BipartiteNetwork( // Set up styles for the bipartite network and incorporate overrides const DEFAULT_TOP_PADDING = 40; const DEFAULT_NODE_SPACING = 30; + const DEFAULT_SVG_WIDTH = 400; + const topPadding = partitions[0].name || partitions[1].name ? 100 : 20; const svgStyles = { - width: Number(containerStyles?.width) || 400, + width: Number(containerStyles?.width) || DEFAULT_SVG_WIDTH, height: Math.max(partitions[1].nodeIds.length, partitions[0].nodeIds.length) * DEFAULT_NODE_SPACING + @@ -123,10 +115,14 @@ function BipartiteNetwork( columnPadding: 100, ...svgStyleOverrides, }; - console.log(svgStyles.height); + // So maybe we make partitionDetails = {name: string, position: number}. + // that removes all but topPadding from svgStyleOverrides, which i'd like to + // make just regular svg styles. + // Alternatively, just set a decent topPadding that is 1 thing if a header + // and another thing if not. const column1Position = svgStyles.columnPadding; - const column2Position = svgStyles.width - svgStyles.columnPadding; + const column2Position = Number(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 @@ -151,7 +147,7 @@ function BipartiteNetwork( 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, + y: topPadding + svgStyles.nodeSpacing * indexInPartition, labelPosition: partitionIndex === 0 ? 'left' : ('right' as LabelPosition), ...node, @@ -165,7 +161,7 @@ function BipartiteNetwork( partitions, nodesByPartition, svgStyles.nodeSpacing, - svgStyles.topPadding, + topPadding, ] ); @@ -432,4 +428,4 @@ function BipartiteNetwork( // )} } -export default forwardRef(BipartiteNetwork); +export default forwardRef(BipartiteNetworkPlot); diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 058775f8cc..7653a98000 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -1,6 +1,5 @@ import { LinkData, NetworkPartition, NodeData } from '../types/plots/network'; import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis'; -import './Network.css'; import { orderBy } from 'lodash'; import { LabelPosition, NodeWithLabel } from './Node'; import { Link } from './Link'; @@ -17,6 +16,7 @@ import { useState, useMemo, useEffect, + SVGAttributes, } from 'react'; import Spinner from '../components/Spinner'; import { ToImgopts } from 'plotly.js'; @@ -25,7 +25,7 @@ import { ExportPlotToImageButton } from './ExportPlotToImageButton'; import { plotToImage } from './visxVEuPathDB'; import { GlyphTriangle } from '@visx/visx'; -import './Network.css'; +import './NetworkPlot.css'; export interface BipartiteNetworkSVGStyles { width?: number; // svg width @@ -46,14 +46,12 @@ export interface NetworkPlotProps { nodes: NodeData[] | undefined; /** Network links */ links: LinkData[] | undefined; - /** Partitions, optional. Used for k-partite networks only */ - partitions?: NetworkPartition[]; /** 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? */ @@ -81,7 +79,6 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { const { nodes = emptyNodes, links = emptyLinks, - partitions, containerStyles, svgStyleOverrides, containerClass = 'web-components-plot', @@ -114,9 +111,6 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { const svgStyles = { width: Number(containerStyles?.width) || 400, height: Number(containerStyles?.height) || 500, - topPadding: 40, - nodeSpacing: 30, - columnPadding: 100, ...svgStyleOverrides, }; @@ -249,7 +243,7 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { )}
{nodes.length > 0 ? ( - + = (args) => { const [selectedNodeIds, setSelectedNodeIds] = useState([]); const bipartiteNetworkProps: BipartiteNetworkProps = { - nodes: args.data.nodes, - links: args.data.links, - partitions: args.data.partitions, - partition1Name: args.partition1Name, - partition2Name: args.partition2Name, + nodes: args.data && args.data.nodes, + links: args.data && args.data.links, + partitions: args.data && args.data.partitions, showSpinner: args.showSpinner, containerStyles: args.containerStyles, svgStyleOverrides: args.svgStyleOverrides, @@ -69,7 +67,7 @@ const Template: Story = (args) => { }; return ( <> - + {args.showThumbnail && ( <>

@@ -86,33 +84,30 @@ 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, }; // 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, }; // With partition names +const simpleDataWithNames = genBipartiteNetwork(20, 10, true); export const WithPartitionNames = Template.bind({}); WithPartitionNames.args = { - data: simpleData, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', + data: simpleDataWithNames, }; // Loading with a spinner export const Loading = Template.bind({}); Loading.args = { - data: simpleData, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', + data: simpleDataWithNames, showSpinner: true, }; @@ -125,9 +120,7 @@ Empty.args = { // Show thumbnail export const Thumbnail = Template.bind({}); Thumbnail.args = { - data: genBipartiteNetwork(10, 10), - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', + data: genBipartiteNetwork(10, 10, true), showThumbnail: true, }; @@ -147,8 +140,6 @@ export const WithStyle = Template.bind({}); WithStyle.args = { data: manyPointsData, containerStyles: plotContainerStyles, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', svgStyleOverrides: svgStyleOverrides, labelTruncationLength: 5, }; @@ -176,8 +167,6 @@ WithActions.args = { containerStyles: { marginLeft: '200px', }, - partition1Name: 'Partition 1', - partition2Name: 'Partition 2', getNodeMenuActions: getNodeActions, isSelectable: false, }; @@ -188,14 +177,12 @@ WithSelection.args = { 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 noNodesData = genBipartiteNetwork(0, 0, false); const emptyNetworkContent = ( No nodes or links @@ -210,7 +197,8 @@ NoNodes.args = { // 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( @@ -253,8 +241,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/NodeWithLabel.stories.tsx b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx index 5581465b64..86a86a7039 100755 --- a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx +++ b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx @@ -1,6 +1,6 @@ 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 { diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 1bc2000517..e538e1afe2 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -39,6 +39,7 @@ export type NetworkPlotData = { export type NetworkPartition = { nodeIds: string[]; + name?: string; }; /** Bipartite network data is a regular network with addiitonal declarations of From c340fac1a4782133b2d3dea64d189ce91416481c Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 29 Apr 2024 07:52:07 -0400 Subject: [PATCH 05/15] add back partition names as annotations --- .../src/plots/BipartiteNetworkPlot.tsx | 32 ++++++++++++++++--- .../libs/components/src/plots/NetworkPlot.tsx | 7 ++++ .../src/stories/plots/NetworkPlot.stories.tsx | 21 ++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index 355f378859..bb08d9fb60 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -16,6 +16,7 @@ import { SVGAttributes, } from 'react'; import { gray } from '@veupathdb/coreui/lib/definitions/colors'; +import { Text } from '@visx/text'; import './BipartiteNetworkPlot.css'; import NetworkPlot, { NodeMenuAction } from './NetworkPlot'; @@ -103,14 +104,14 @@ function BipartiteNetworkPlot( const DEFAULT_TOP_PADDING = 40; const DEFAULT_NODE_SPACING = 30; const DEFAULT_SVG_WIDTH = 400; - const topPadding = partitions[0].name || partitions[1].name ? 100 : 20; 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: DEFAULT_TOP_PADDING, + topPadding: + partitions[0].name || partitions[1].name ? 60 : DEFAULT_TOP_PADDING, nodeSpacing: DEFAULT_NODE_SPACING, columnPadding: 100, ...svgStyleOverrides, @@ -147,7 +148,7 @@ function BipartiteNetworkPlot( return { // partitionIndex of 0 refers to the left-column nodes whereas 1 refers to right-column nodes x: partitionIndex === 0 ? column1Position : column2Position, - y: topPadding + svgStyles.nodeSpacing * indexInPartition, + y: svgStyles.topPadding + svgStyles.nodeSpacing * indexInPartition, labelPosition: partitionIndex === 0 ? 'left' : ('right' as LabelPosition), ...node, @@ -161,10 +162,32 @@ function BipartiteNetworkPlot( partitions, nodesByPartition, svgStyles.nodeSpacing, - topPadding, + svgStyles.topPadding, ] ); + // Create column labels if any exist + const leftColumnLabel = partitions[0].name && ( + + {partitions[0].name} + + ); + const rightColumnLabel = partitions[1].name && ( + + {partitions[1].name} + + ); + // // Assign coordinates to links based on the newly created node coordinates // const linksWithCoordinates = useMemo( // () => @@ -245,6 +268,7 @@ function BipartiteNetworkPlot( nodesByPartitionWithCoordinates[1] )} links={links} + annotations={[leftColumnLabel, rightColumnLabel]} svgStyleOverrides={svgStyles} /> ); diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 7653a98000..5538ebe6a1 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -62,6 +62,8 @@ export interface NetworkPlotProps { 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[]; } const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ @@ -86,6 +88,7 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { labelTruncationLength = 20, emptyNetworkContent, getNodeMenuActions: getNodeActions, + annotations, } = props; const [highlightedNodeId, setHighlightedNodeId] = useState(); @@ -322,6 +325,10 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { ); }} /> + {annotations && + annotations.map((annotation) => { + return annotation; + })} ) : ( emptyNetworkContent ??

No nodes in the network

diff --git a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx index 26d8f0f322..ef7d66e57c 100755 --- a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -1,6 +1,8 @@ import { Story, Meta } from '@storybook/react/types-6-0'; import { NodeData, LinkData, NetworkPlotData } from '../../types/plots/network'; import NetworkPlot from '../../plots/NetworkPlot'; +import { ReactNode } from 'react'; +import { Text } from '@visx/text'; export default { title: 'Plots/Network', @@ -12,6 +14,7 @@ const DEFAULT_PLOT_SIZE = 500; interface TemplateProps { data: NetworkPlotData; + annotations?: ReactNode[]; } // This template is a simple network that highlights our NodeWithLabel and Link components. @@ -19,6 +22,7 @@ const Template: Story = (args) => { return ( ); @@ -47,6 +51,23 @@ ManyPoints.args = { data: manyPointsData, }; +// A network with annotations +const annotation1 = ( + + I am an annotation + +); +const annotation2 = ( + + I am another annotation + +); +export const WithAnnotations = Template.bind({}); +WithAnnotations.args = { + data: simpleData, + annotations: [annotation1, annotation2], +}; + // Gerenate a network with a given number of nodes and random edges function genNetwork( nNodes: number, From 960af06df639a456b76d6b141464927f6fab6710 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 29 Apr 2024 08:41:31 -0400 Subject: [PATCH 06/15] handle empty Network data --- packages/libs/components/src/plots/NetworkPlot.tsx | 2 ++ .../components/src/stories/plots/NetworkPlot.stories.tsx | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 5538ebe6a1..5317614d2e 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -70,6 +70,8 @@ const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ id: item.toString(), color: gray[100], stroke: gray[300], + x: 30 + Math.random() * 300, + y: 30 + Math.random() * 300, })); const emptyLinks: LinkData[] = []; diff --git a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx index ef7d66e57c..1105c661e8 100755 --- a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -68,6 +68,12 @@ WithAnnotations.args = { annotations: [annotation1, annotation2], }; +// An empty network @ANN You are here making this nice looking +export const Empty = Template.bind({}); +Empty.args = { + data: undefined, +}; + // Gerenate a network with a given number of nodes and random edges function genNetwork( nNodes: number, From a57b70a7e93646260598a295b47bebbdd25b5a15 Mon Sep 17 00:00:00 2001 From: asizemore Date: Mon, 29 Apr 2024 12:08:41 -0400 Subject: [PATCH 07/15] add default layout for network --- .../libs/components/src/plots/NetworkPlot.tsx | 46 ++++++++++--------- .../src/stories/plots/NetworkPlot.stories.tsx | 28 +++++++++-- .../components/src/types/plots/network.ts | 4 ++ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 5317614d2e..23cd0a06dc 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -119,25 +119,45 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { ...svgStyleOverrides, }; + const plotRect = plotRef.current?.getBoundingClientRect(); + const imageHeight = plotRect?.height; + const imageWidth = plotRect?.width; + + const nodesWithActions = useMemo( + () => + nodes.map((node, index) => ({ + labelPosition: 'right' as LabelPosition, + ...node, + x: node.x ?? 230 + 200 * Math.cos(2 * Math.PI * (index / nodes.length)), + y: node.y ?? 230 + 200 * Math.sin(2 * Math.PI * (index / nodes.length)), + actions: getNodeActions?.(node.id), + })), + [getNodeActions, nodes] + ); + // Assign coordinates to links based on the newly created node coordinates const linksWithCoordinates = useMemo( () => // Put highlighted links on top of gray links. orderBy( links.map((link) => { - const sourceNode = nodes.find((node) => node.id === link.source.id); - const targetNode = nodes.find((node) => node.id === link.target.id); + const sourceNode = nodesWithActions.find( + (node) => node.id === link.source.id + ); + const targetNode = nodesWithActions.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 && @@ -153,23 +173,7 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { // but that's okay, because the overlapping colors will be the same. (link) => (link.color === '#eee' ? -1 : 1) ), - [links, highlightedNodeId, nodes] - ); - - const plotRect = plotRef.current?.getBoundingClientRect(); - const imageHeight = plotRect?.height; - const imageWidth = plotRect?.width; - - const nodesWithActions = useMemo( - () => - nodes.map((node) => ({ - x: Math.random() * 100, - y: Math.random() * 100, - labelPosition: 'right' as LabelPosition, - ...node, - actions: getNodeActions?.(node.id), - })), - [getNodeActions, nodes] + [links, highlightedNodeId, nodesWithActions] ); const activeNode = nodesWithActions.find((node) => node.id === activeNodeId); diff --git a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx index 1105c661e8..398f4496d6 100755 --- a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -33,7 +33,13 @@ const Template: Story = (args) => { */ // A simple network with node labels -const simpleData = genNetwork(20, true, DEFAULT_PLOT_SIZE, DEFAULT_PLOT_SIZE); +const simpleData = genNetwork( + 20, + true, + true, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); export const Simple = Template.bind({}); Simple.args = { data: simpleData, @@ -43,14 +49,29 @@ Simple.args = { const manyPointsData = genNetwork( 100, false, + true, DEFAULT_PLOT_SIZE, DEFAULT_PLOT_SIZE ); + export const ManyPoints = Template.bind({}); ManyPoints.args = { data: manyPointsData, }; +// Test the default layout +const defaultLayoutData = genNetwork( + 50, + false, + false, + DEFAULT_PLOT_SIZE, + DEFAULT_PLOT_SIZE +); +export const DefaultLayout = Template.bind({}); +DefaultLayout.args = { + data: defaultLayoutData, +}; + // A network with annotations const annotation1 = ( @@ -78,6 +99,7 @@ Empty.args = { function genNetwork( nNodes: number, addNodeLabel: boolean, + addNodeCoordinates: boolean, height: number, width: number ) { @@ -86,8 +108,8 @@ function genNetwork( const nodeX = 10 + Math.floor(Math.random() * (width - 20)); // Add/Subtract a bit to keep the whole node in view const nodeY = 10 + Math.floor(Math.random() * (height - 20)); return { - x: nodeX, - y: nodeY, + x: addNodeCoordinates ? nodeX : undefined, + y: addNodeCoordinates ? nodeY : undefined, id: String(i), label: addNodeLabel ? 'Node ' + String(i) : undefined, labelPosition: addNodeLabel diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index e538e1afe2..138e714cb6 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -1,3 +1,5 @@ +import { LabelPosition } from '../../plots/Node'; + // Types required for creating networks export type NodeData = { /** Node ID. Must be unique in the network! */ @@ -16,6 +18,8 @@ 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; }; export type LinkData = { From b5833d85f68c8c2333189b105e307e3c65e94e54 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 30 Apr 2024 11:33:24 -0400 Subject: [PATCH 08/15] Clean up Network stories --- .../src/plots/BipartiteNetworkPlot.tsx | 1 + .../libs/components/src/plots/NetworkPlot.tsx | 2 - .../plots/BipartiteNetworkPlot.stories.tsx | 94 ++----------- .../src/stories/plots/NetworkPlot.stories.tsx | 128 +++++++++++++++--- .../stories/plots/NodeWithLabel.stories.tsx | 2 +- 5 files changed, 121 insertions(+), 106 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index bb08d9fb60..62949cc516 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -270,6 +270,7 @@ function BipartiteNetworkPlot( links={links} annotations={[leftColumnLabel, rightColumnLabel]} svgStyleOverrides={svgStyles} + ref={ref} /> ); diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 23cd0a06dc..3abc312487 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -70,8 +70,6 @@ const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ id: item.toString(), color: gray[100], stroke: gray[300], - x: 30 + Math.random() * 300, - y: 30 + Math.random() * 300, })); const emptyLinks: LinkData[] = []; diff --git a/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx index 146d2e1b4c..f8ca7852e7 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.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, - NetworkPartition, } from '../../types/plots/network'; import BipartiteNetworkPlot, { BipartiteNetworkProps, - BipartiteNetworkSVGStyles, } from '../../plots/BipartiteNetworkPlot'; import { twoColorPalette } from '../../types/plots/addOns'; -import { Text } from '@visx/text'; -import { NodeMenuAction } from '../../plots/NetworkPlot'; export default { - title: 'Plots/Network/BipartiteNetwork', + title: 'Plots/Networks/BipartiteNetwork', component: BipartiteNetworkPlot, } as Meta; -interface TemplateProps { - data: BipartiteNetworkData; - partitions: NetworkPartition[]; - showSpinner?: boolean; +interface TemplateProps extends BipartiteNetworkProps { showThumbnail?: boolean; - containerStyles?: CSSProperties; - svgStyleOverrides?: BipartiteNetworkSVGStyles; - labelTruncationLength?: number; - emptyNetworkContent?: ReactNode; - getNodeMenuActions?: BipartiteNetworkProps['getNodeMenuActions']; isSelectable?: boolean; } @@ -49,14 +37,7 @@ const Template: Story = (args) => { const [selectedNodeIds, setSelectedNodeIds] = useState([]); const bipartiteNetworkProps: BipartiteNetworkProps = { - nodes: args.data && args.data.nodes, - links: args.data && args.data.links, - partitions: args.data && args.data.partitions, - showSpinner: args.showSpinner, - containerStyles: args.containerStyles, - svgStyleOverrides: args.svgStyleOverrides, - labelTruncationLength: args.labelTruncationLength, - emptyNetworkContent: args.emptyNetworkContent, + ...args, getNodeMenuActions: args.getNodeMenuActions, ...(args.isSelectable ? { @@ -87,40 +68,42 @@ const Template: Story = (args) => { 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, 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: simpleDataWithNames, + ...simpleDataWithNames, }; // Loading with a spinner export const Loading = Template.bind({}); Loading.args = { - data: simpleDataWithNames, + ...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, true), + ...simpleData, showThumbnail: true, }; @@ -134,66 +117,15 @@ 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, 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', - }, - getNodeMenuActions: getNodeActions, - isSelectable: false, -}; - -export const WithSelection = Template.bind({}); -WithSelection.args = { - data: simpleData, - containerStyles: { - marginLeft: '200px', - }, - getNodeMenuActions: getNodeActions, - isSelectable: true, -}; - -// With a network that has no nodes or links -const noNodesData = genBipartiteNetwork(0, 0, false); -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, diff --git a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx index 398f4496d6..a2d2672788 100755 --- a/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/NetworkPlot.stories.tsx @@ -1,30 +1,57 @@ import { Story, Meta } from '@storybook/react/types-6-0'; import { NodeData, LinkData, NetworkPlotData } from '../../types/plots/network'; -import NetworkPlot from '../../plots/NetworkPlot'; -import { ReactNode } from 'react'; +import NetworkPlot, { + NetworkPlotProps, + NodeMenuAction, +} from '../../plots/NetworkPlot'; import { Text } from '@visx/text'; +import { useEffect, useRef, useState } from 'react'; export default { - title: 'Plots/Network', + 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 { - data: NetworkPlotData; - annotations?: ReactNode[]; +interface TemplateProps extends NetworkPlotProps { + showThumbnail?: boolean; } -// This template is a simple network that highlights our NodeWithLabel and Link components. +// 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 + + )} + ); }; @@ -42,7 +69,7 @@ const simpleData = genNetwork( ); export const Simple = Template.bind({}); Simple.args = { - data: simpleData, + ...simpleData, }; // A network with lots and lots of points! @@ -53,10 +80,9 @@ const manyPointsData = genNetwork( DEFAULT_PLOT_SIZE, DEFAULT_PLOT_SIZE ); - export const ManyPoints = Template.bind({}); ManyPoints.args = { - data: manyPointsData, + ...manyPointsData, }; // Test the default layout @@ -69,10 +95,12 @@ const defaultLayoutData = genNetwork( ); export const DefaultLayout = Template.bind({}); DefaultLayout.args = { - data: defaultLayoutData, + ...defaultLayoutData, }; -// A network with annotations +// 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 @@ -85,16 +113,70 @@ const annotation2 = ( ); export const WithAnnotations = Template.bind({}); WithAnnotations.args = { - data: simpleData, + ...simpleData, annotations: [annotation1, annotation2], }; -// An empty network @ANN You are here making this nice looking +// An empty network. +// This is what will be shown by default before we receive any data export const Empty = Template.bind({}); Empty.args = { - data: undefined, + 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); + }, + }, + ]; +} + +export const WithActions = Template.bind({}); +WithActions.args = { + ...simpleData, + getNodeMenuActions: getNodeActions, }; +// Utility functions // Gerenate a network with a given number of nodes and random edges function genNetwork( nNodes: number, @@ -105,8 +187,10 @@ function genNetwork( ) { // Create nodes with random positioning, an id, and optionally a label const nodes: NodeData[] = [...Array(nNodes).keys()].map((i) => { - const nodeX = 10 + Math.floor(Math.random() * (width - 20)); // Add/Subtract a bit to keep the whole node in view - const nodeY = 10 + Math.floor(Math.random() * (height - 20)); + // 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, diff --git a/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx b/packages/libs/components/src/stories/plots/NodeWithLabel.stories.tsx index 86a86a7039..6df5628b66 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/Node'; import { Group } from '@visx/group'; export default { - title: 'Plots/Network/NodeWithLabel', + title: 'Plots/Networks/NodeWithLabel', component: NodeWithLabel, } as Meta; From a39c3b881992d9391baa1ce2a573b72b3a8ee0e0 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 1 May 2024 06:14:12 -0400 Subject: [PATCH 09/15] cleanup --- .../src/plots/BipartiteNetworkPlot.css | 18 - .../src/plots/BipartiteNetworkPlot.tsx | 317 +----------------- .../libs/components/src/plots/NetworkPlot.css | 18 +- .../libs/components/src/plots/NetworkPlot.tsx | 74 ++-- .../components/src/types/plots/network.ts | 9 +- 5 files changed, 67 insertions(+), 369 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.css b/packages/libs/components/src/plots/BipartiteNetworkPlot.css index 3048000b39..09f7ff547c 100644 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.css +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.css @@ -2,21 +2,3 @@ font-size: 1em; font-weight: 500; } - -.bpnet-hover-dropdown { - display: none; -} - -.visx-network-node:hover .bpnet-hover-dropdown { - display: unset; -} - -.visx-network-node .hover-trigger:hover { - display: unset; - fill: #00000017; -} - -.NodeWithLabel_Node, -.NodeWithLabel_Label { - cursor: pointer; -} diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index 62949cc516..8586a170da 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -1,25 +1,16 @@ import { BipartiteNetworkData, - LinkData, NetworkPartition, NodeData, } from '../types/plots/network'; import { partition } from 'lodash'; import { LabelPosition } from './Node'; -import { - CSSProperties, - ReactNode, - Ref, - forwardRef, - useRef, - useMemo, - SVGAttributes, -} from 'react'; +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, { NodeMenuAction } from './NetworkPlot'; +import NetworkPlot, { NetworkPlotProps } from './NetworkPlot'; export interface BipartiteNetworkSVGStyles extends SVGAttributes { topPadding?: number; // space between the top of the svg and the top-most node @@ -27,31 +18,19 @@ export interface BipartiteNetworkSVGStyles extends SVGAttributes { 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 BipartiteNetworkProps { - /** Nodes */ - nodes: NodeData[] | undefined; - /** Links */ - links: LinkData[] | undefined; - /** Partitions. An array of NetworkPartitions (an array of node ids) that defines the two node groups */ +export interface BipartiteNetworkProps extends NetworkPlotProps { + /** Partitions. An array of NetworkPartitions (an array of node ids and optional name) that defines the two node groups */ partitions: NetworkPartition[] | undefined; - /** styling for the plot's container */ - containerStyles?: CSSProperties; /** 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; - /** container name */ - containerClass?: string; - /** shall we show the loading spinner? */ - showSpinner?: boolean; - /** Length of node label text before truncating with an ellipsis */ - labelTruncationLength?: number; - /** Additional error messaging to show when the network is empty */ - emptyNetworkContent?: ReactNode; - /** Entries for the actions that appear in the menu when you click a node */ - getNodeMenuActions?: (nodeId: string) => NodeMenuAction[]; } +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: [ @@ -69,6 +48,8 @@ const EmptyBipartiteNetworkData: BipartiteNetworkData = { // 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: BipartiteNetworkProps, ref: Ref @@ -81,29 +62,7 @@ function BipartiteNetworkPlot( svgStyleOverrides, } = props; - // const [highlightedNodeId, setHighlightedNodeId] = useState(); - // const [activeNodeId, setActiveNodeId] = useState(); - - // Use ref forwarding to enable screenshotting of the plot for thumbnail versions. - const plotRef = useRef(null); - - // const toImage = useCallback(async (opts: ToImgopts) => { - // return plotToImage(plotRef.current, opts); - // }, []); - - // useImperativeHandle( - // ref, - // () => ({ - // // The thumbnail generator makePlotThumbnailUrl expects to call a toImage function - // toImage, - // }), - // [toImage] - // ); - // Set up styles for the bipartite network and incorporate overrides - const DEFAULT_TOP_PADDING = 40; - const DEFAULT_NODE_SPACING = 30; - const DEFAULT_SVG_WIDTH = 400; const svgStyles = { width: Number(containerStyles?.width) || DEFAULT_SVG_WIDTH, height: @@ -117,11 +76,6 @@ function BipartiteNetworkPlot( ...svgStyleOverrides, }; - // So maybe we make partitionDetails = {name: string, position: number}. - // that removes all but topPadding from svgStyleOverrides, which i'd like to - // make just regular svg styles. - // Alternatively, just set a decent topPadding that is 1 thing if a header - // and another thing if not. const column1Position = svgStyles.columnPadding; const column2Position = Number(svgStyles.width) - svgStyles.columnPadding; @@ -188,79 +142,6 @@ function BipartiteNetworkPlot( ); - // // Assign coordinates to links based on the newly created node coordinates - // const linksWithCoordinates = useMemo( - // () => - // // Put highlighted links on top of gray links. - // orderBy( - // 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 - // ); - // return { - // ...link, - // source: { - // x: sourceNode?.x, - // y: sourceNode?.y, - // ...link.source, - // }, - // target: { - // x: targetNode?.x, - // y: targetNode?.y, - // ...link.target, - // }, - // color: - // highlightedNodeId != null && - // sourceNode?.id !== highlightedNodeId && - // targetNode?.id !== highlightedNodeId - // ? '#eee' - // : link.color, - // }; - // }), - // // Links that are added later will be on top. - // // If a link is grayed out, it will be sorted before other links. - // // In theory, it's possible to have a false positive here; - // // 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] - // ); - - // const activeNode = nodes.find((node) => node.id === activeNodeId); - - // useEffect(() => { - // const element = document.querySelector('.bpnet-plot-container'); - // if (element == null) return; - - // element.addEventListener('click', handler); - - // return () => { - // element.removeEventListener('click', handler); - // }; - - // function handler() { - // setActiveNodeId(undefined); - // } - // }, [containerClass]); - return ( ); - - // <> - //
- // {activeNode?.actions?.length && ( - //
- // {activeNode.actions.map((action) => ( - //
- // {action.href ? ( - // - // {action.label} - // - // ) : ( - // - // )} - //
- // ))} - //
- // )} - //
- // {nodesByPartitionWithCoordinates[0].length > 0 ? ( - // - // {/* Draw names of node colums if they exist */} - // {partition1Name && ( - // - // {partition1Name} - // - // )} - // {partition2Name && ( - // - // {partition2Name} - // - // )} - - // { - // return ; - // }} - // nodeComponent={({ node }) => { - // const isHighlighted = highlightedNodeId === node.id; - // const rectWidth = - // (node.r ?? 6) * 2 + // node diameter - // (node.label?.length ?? 0) * 6 + // label width - // (12 + 6) + // button + space - // (12 + 12 + 12); // paddingLeft + space-between-node-and-label + paddingRight - // const rectX = - // node.labelPosition === 'left' ? -rectWidth + 12 : -12; - // const glyphLeft = - // node.labelPosition === 'left' ? rectX + 12 : rectWidth - 24; - // return ( - // <> - // {node.actions?.length && ( - // - // - // - // setActiveNodeId(node.id)} - // /> - // - // )} - // { - // setHighlightedNodeId((id) => - // id === node.id ? undefined : node.id - // ); - // }} - // fontWeight={isHighlighted ? 600 : 400} - // /> - // - // ); - // }} - // /> - // - // ) : ( - // emptyNetworkContent ??

No nodes in the network

- // )} } export default forwardRef(BipartiteNetworkPlot); diff --git a/packages/libs/components/src/plots/NetworkPlot.css b/packages/libs/components/src/plots/NetworkPlot.css index ce812b0a93..b1e796f578 100644 --- a/packages/libs/components/src/plots/NetworkPlot.css +++ b/packages/libs/components/src/plots/NetworkPlot.css @@ -1,5 +1,6 @@ -.NodeWithLabel_Node { - cursor: default; +.NodeWithLabel_Node, +.NodeWithLabel_Label { + cursor: pointer; } .network-plot-container { @@ -7,3 +8,16 @@ height: 500px; overflow-y: scroll; } + +.net-hover-dropdown { + display: none; +} + +.visx-network-node:hover .net-hover-dropdown { + display: unset; +} + +.visx-network-node .hover-trigger:hover { + display: unset; + fill: #00000017; +} diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index 3abc312487..f533b234fa 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -1,10 +1,8 @@ -import { LinkData, NetworkPartition, NodeData } from '../types/plots/network'; -import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis'; -import { orderBy } from 'lodash'; +import { LinkData, NodeData } from '../types/plots/network'; +import { isNumber, orderBy } from 'lodash'; import { LabelPosition, NodeWithLabel } from './Node'; import { Link } from './Link'; import { Graph } from '@visx/network'; -import { Text } from '@visx/text'; import { CSSProperties, ReactNode, @@ -27,14 +25,6 @@ import { GlyphTriangle } from '@visx/visx'; import './NetworkPlot.css'; -export interface BipartiteNetworkSVGStyles { - width?: number; // svg width - height?: number; // svg height - 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; @@ -66,6 +56,9 @@ export interface NetworkPlotProps { annotations?: ReactNode[]; } +const DEFAULT_PLOT_WIDTH = 500; +const DEFAULT_PLOT_HEIGHT = 500; + const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ id: item.toString(), color: gray[100], @@ -73,10 +66,9 @@ const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ })); const emptyLinks: LinkData[] = []; -// The Network component draws a network of nodes and links. Optionaly, one can pass partitions which -// then will be used to draw a k-partite network. -// If no x,y coordinates are provided for nodes in the network, the network will -// be drawn in a circular layout, or in columns partitions are provided. +// 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 { nodes = emptyNodes, @@ -110,18 +102,26 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { [toImage] ); - // Set up styles for the bipartite network and incorporate overrides - const svgStyles = { - width: Number(containerStyles?.width) || 400, - height: Number(containerStyles?.height) || 500, - ...svgStyleOverrides, - }; - const plotRect = plotRef.current?.getBoundingClientRect(); const imageHeight = plotRect?.height; const imageWidth = plotRect?.width; - const nodesWithActions = useMemo( + // Set up styles for the network and incorporate overrides + const svgStyles = { + width: + containerStyles?.width && isNumber(containerStyles?.width) + ? containerStyles.width + : DEFAULT_PLOT_WIDTH, + height: + containerStyles?.height && isNumber(containerStyles?.height) + ? containerStyles.height + : DEFAULT_PLOT_HEIGHT, + ...svgStyleOverrides, + }; + + // Node processing. + // Add actions and default coordinates. The default coordinates arrange nodes in a circle. + const processedNodes = useMemo( () => nodes.map((node, index) => ({ labelPosition: 'right' as LabelPosition, @@ -133,16 +133,18 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { [getNodeActions, nodes] ); - // 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( links.map((link) => { - const sourceNode = nodesWithActions.find( + const sourceNode = processedNodes.find( (node) => node.id === link.source.id ); - const targetNode = nodesWithActions.find( + const targetNode = processedNodes.find( (node) => node.id === link.target.id ); return { @@ -171,10 +173,10 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { // but that's okay, because the overlapping colors will be the same. (link) => (link.color === '#eee' ? -1 : 1) ), - [links, highlightedNodeId, nodesWithActions] + [links, highlightedNodeId, processedNodes] ); - const activeNode = nodesWithActions.find((node) => node.id === activeNodeId); + const activeNode = processedNodes.find((node) => node.id === activeNodeId); useEffect(() => { const element = document.querySelector('.network-plot-container'); @@ -253,8 +255,8 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { ) { return ( <> {node.actions?.length && ( - + ) { ) : ( 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 && }
Date: Thu, 2 May 2024 10:09:03 -0400 Subject: [PATCH 10/15] update imports for BipartiteNetworkPlot --- .../libs/components/src/plots/BipartiteNetworkPlot.tsx | 4 ++-- .../src/stories/plots/BipartiteNetworkPlot.stories.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index 8586a170da..c5482cf485 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -18,7 +18,7 @@ export interface BipartiteNetworkSVGStyles extends SVGAttributes { 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 BipartiteNetworkProps extends NetworkPlotProps { +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 @@ -51,7 +51,7 @@ const EmptyBipartiteNetworkData: BipartiteNetworkData = { // The BipartiteNetworkPlot effectively wraps NetworkPlot by using the 'partitions' argument // to layout the network and assigning helpful defaults. function BipartiteNetworkPlot( - props: BipartiteNetworkProps, + props: BipartiteNetworkPlotProps, ref: Ref ) { const { diff --git a/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx index f8ca7852e7..b5e4d62efe 100755 --- a/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx +++ b/packages/libs/components/src/stories/plots/BipartiteNetworkPlot.stories.tsx @@ -6,7 +6,7 @@ import { BipartiteNetworkData, } from '../../types/plots/network'; import BipartiteNetworkPlot, { - BipartiteNetworkProps, + BipartiteNetworkPlotProps, } from '../../plots/BipartiteNetworkPlot'; import { twoColorPalette } from '../../types/plots/addOns'; @@ -15,7 +15,7 @@ export default { component: BipartiteNetworkPlot, } as Meta; -interface TemplateProps extends BipartiteNetworkProps { +interface TemplateProps extends BipartiteNetworkPlotProps { showThumbnail?: boolean; isSelectable?: boolean; } @@ -36,7 +36,7 @@ const Template: Story = (args) => { const [selectedNodeIds, setSelectedNodeIds] = useState([]); - const bipartiteNetworkProps: BipartiteNetworkProps = { + const bipartiteNetworkPlotProps: BipartiteNetworkPlotProps = { ...args, getNodeMenuActions: args.getNodeMenuActions, ...(args.isSelectable @@ -48,7 +48,7 @@ const Template: Story = (args) => { }; return ( <> - + {args.showThumbnail && ( <>

From 02b9ec536e9b948c644cab9700eddec07b71f9c6 Mon Sep 17 00:00:00 2001 From: asizemore Date: Thu, 2 May 2024 10:20:14 -0400 Subject: [PATCH 11/15] update NodeMenuActions imports --- .../BipartiteNetworkVisualization.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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..df740071c6 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/plots/NetworkPlot'; // 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 = <> ; From dee9ffc336d20a9f0e75dfcc7b26a2028cddcfa7 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 8 May 2024 06:36:24 -0400 Subject: [PATCH 12/15] simplify coordinate logic in bpnet --- .../src/plots/BipartiteNetworkPlot.tsx | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index c5482cf485..cb5eb23a3a 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -79,42 +79,33 @@ function BipartiteNetworkPlot( const column1Position = svgStyles.columnPadding; const column2Position = Number(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( + // 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( () => - partition(nodes, (node) => { - return partitions[0].nodeIds.includes(node.id); - }), - [nodes, partitions] - ); - - const nodesByPartitionWithCoordinates = useMemo( - () => - nodesByPartition.map((partition, partitionIndex) => { - const partitionWithCoordinates = partition.map((node) => { - // Find the index of the node in the partition - const indexInPartition = partitions[partitionIndex].nodeIds.findIndex( - (id) => id === node.id - ); + 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 { - // 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; + 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, + }; }), [ + nodes, + partitions, column1Position, column2Position, - partitions, - nodesByPartition, svgStyles.nodeSpacing, svgStyles.topPadding, ] @@ -145,9 +136,7 @@ function BipartiteNetworkPlot( return ( Date: Wed, 8 May 2024 07:16:35 -0400 Subject: [PATCH 13/15] remove a loop over the nodes --- .../src/plots/BipartiteNetworkPlot.tsx | 9 ++-- .../libs/components/src/plots/NetworkPlot.tsx | 49 +++++-------------- .../src/stories/plots/NetworkPlot.stories.tsx | 31 +++++------- .../components/src/types/plots/network.ts | 10 ++++ 4 files changed, 39 insertions(+), 60 deletions(-) diff --git a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx index cb5eb23a3a..15c40ce238 100755 --- a/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx +++ b/packages/libs/components/src/plots/BipartiteNetworkPlot.tsx @@ -1,9 +1,4 @@ -import { - BipartiteNetworkData, - NetworkPartition, - NodeData, -} from '../types/plots/network'; -import { partition } from 'lodash'; +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'; @@ -60,6 +55,7 @@ function BipartiteNetworkPlot( partitions = EmptyBipartiteNetworkData.partitions, containerStyles, svgStyleOverrides, + getNodeMenuActions: getNodeActions, } = props; // Set up styles for the bipartite network and incorporate overrides @@ -99,6 +95,7 @@ function BipartiteNetworkPlot( labelPosition: partitionIndex === 0 ? 'left' : ('right' as LabelPosition), ...node, + actions: getNodeActions?.(node.id), }; }), [ diff --git a/packages/libs/components/src/plots/NetworkPlot.tsx b/packages/libs/components/src/plots/NetworkPlot.tsx index f533b234fa..f8dec667a1 100755 --- a/packages/libs/components/src/plots/NetworkPlot.tsx +++ b/packages/libs/components/src/plots/NetworkPlot.tsx @@ -1,6 +1,6 @@ -import { LinkData, NodeData } from '../types/plots/network'; +import { LinkData, NodeData, NodeMenuAction } from '../types/plots/network'; import { isNumber, orderBy } from 'lodash'; -import { LabelPosition, NodeWithLabel } from './Node'; +import { NodeWithLabel } from './Node'; import { Link } from './Link'; import { Graph } from '@visx/network'; import { @@ -25,12 +25,6 @@ import { GlyphTriangle } from '@visx/visx'; import './NetworkPlot.css'; -export interface NodeMenuAction { - label: ReactNode; - onClick?: () => void; - href?: string; -} - export interface NetworkPlotProps { /** Network nodes */ nodes: NodeData[] | undefined; @@ -59,10 +53,12 @@ export interface NetworkPlotProps { const DEFAULT_PLOT_WIDTH = 500; const DEFAULT_PLOT_HEIGHT = 500; -const emptyNodes: NodeData[] = [...Array(9).keys()].map((item) => ({ +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[] = []; @@ -79,7 +75,6 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { showSpinner = false, labelTruncationLength = 20, emptyNetworkContent, - getNodeMenuActions: getNodeActions, annotations, } = props; @@ -119,20 +114,6 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { ...svgStyleOverrides, }; - // Node processing. - // Add actions and default coordinates. The default coordinates arrange nodes in a circle. - const processedNodes = useMemo( - () => - nodes.map((node, index) => ({ - labelPosition: 'right' as LabelPosition, - ...node, - x: node.x ?? 230 + 200 * Math.cos(2 * Math.PI * (index / nodes.length)), - y: node.y ?? 230 + 200 * Math.sin(2 * Math.PI * (index / nodes.length)), - actions: getNodeActions?.(node.id), - })), - [getNodeActions, nodes] - ); - // 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). @@ -141,12 +122,8 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { // Put highlighted links on top of gray links. orderBy( links.map((link) => { - const sourceNode = processedNodes.find( - (node) => node.id === link.source.id - ); - const targetNode = processedNodes.find( - (node) => node.id === link.target.id - ); + const sourceNode = nodes.find((node) => node.id === link.source.id); + const targetNode = nodes.find((node) => node.id === link.target.id); return { ...link, source: { @@ -173,10 +150,10 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) { // but that's okay, because the overlapping colors will be the same. (link) => (link.color === '#eee' ? -1 : 1) ), - [links, highlightedNodeId, processedNodes] + [links, highlightedNodeId, nodes] ); - const activeNode = processedNodes.find((node) => node.id === activeNodeId); + const activeNode = nodes.find((node) => node.id === activeNodeId); useEffect(() => { const element = document.querySelector('.network-plot-container'); @@ -203,8 +180,8 @@ function NetworkPlot(props: NetworkPlotProps, ref: Ref) {
) { ) { node.labelPosition === 'left' ? rectX + 12 : rectWidth - 24; return ( <> - {node.actions?.length && ( + {node.actions && node.actions?.length && ( ({ + ...node, + actions: getNodeActions(node.id), +})); + export const WithActions = Template.bind({}); WithActions.args = { - ...simpleData, + ...simpleWithActions, getNodeMenuActions: getNodeActions, }; diff --git a/packages/libs/components/src/types/plots/network.ts b/packages/libs/components/src/types/plots/network.ts index 05fce8d0f6..25f5a3d8e3 100755 --- a/packages/libs/components/src/types/plots/network.ts +++ b/packages/libs/components/src/types/plots/network.ts @@ -1,6 +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; @@ -20,6 +28,8 @@ export type NodeData = { 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 = { From 21babcccf87b4dbba1d8190099c43b641a954424 Mon Sep 17 00:00:00 2001 From: asizemore Date: Wed, 8 May 2024 07:25:50 -0400 Subject: [PATCH 14/15] update imports --- .../implementations/BipartiteNetworkVisualization.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df740071c6..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 @@ -44,7 +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/plots/NetworkPlot'; +import { NodeMenuAction } from '@veupathdb/components/lib/types/plots/network'; // end imports // Defaults From 995aa381746169e97470c6cb0e24057799b6c573 Mon Sep 17 00:00:00 2001 From: asizemore Date: Tue, 14 May 2024 06:07:25 -0400 Subject: [PATCH 15/15] setup input for node filter --- .../plugins/differentialabundance.tsx | 1 + .../BipartiteNetworkVisualization.tsx | 165 +++++++++++++----- .../implementations/ValuePicker.tsx | 5 + 3 files changed, 125 insertions(+), 46 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index f32480754e..d8d3312fa9 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -387,6 +387,7 @@ export function DifferentialAbundanceConfiguration( showClearSelectionButton={false} disableInput={disableGroupValueSelectors} isLoading={continuousVariableBins.pending} + isSearchable={true} /> ) { const { id: studyId } = studyMetadata; const entities = useStudyEntities(filters); const dataClient: DataClient = useDataClient(); + const findEntityAndVariableCollection = useFindEntityAndVariableCollection(); const computationConfiguration: CorrelationConfig = computation.descriptor .configuration as CorrelationConfig; @@ -177,6 +179,14 @@ function BipartiteNetworkViz(props: VisualizationProps) { ]) ); + const createNodeList = () => + data.value + ? data.value.bipartitenetwork.data.nodes.map((node) => node.id) + : []; + const [keepNodeIds, setKeepNodeIds] = useState(createNodeList); + // console.log(data.value?.bipartitenetwork.data.nodes.map((node) => node.id)); + console.log(keepNodeIds); + // Determine min and max stroke widths. For use in scaling the strokes (weightToStrokeWidthMap) and the legend. const dataWeights = data.value?.bipartitenetwork.data.links.map( @@ -223,8 +233,9 @@ function BipartiteNetworkViz(props: VisualizationProps) { .range(twoColorPalette); // the output palette may change if this visualization is reused in other contexts (ex. not a correlation app). // Find display labels - const nodesWithLabels = data.value.bipartitenetwork.data.nodes.map( - (node) => { + const nodesWithLabels = data.value.bipartitenetwork.data.nodes + .filter((node) => keepNodeIds.includes(node.id)) + .map((node) => { // node.id is the entityId.variableId const displayLabel = fixVarIdLabel( node.id.split('.')[1], @@ -236,8 +247,7 @@ function BipartiteNetworkViz(props: VisualizationProps) { id: node.id, label: displayLabel, }; - } - ); + }); const nodesById = new Map(nodesWithLabels.map((n) => [n.id, n])); @@ -264,16 +274,24 @@ function BipartiteNetworkViz(props: VisualizationProps) { ...data.value.bipartitenetwork.data, partitions: orderedPartitions, nodes: nodesWithLabels, - links: data.value.bipartitenetwork.data.links.map((link) => { - return { - source: link.source, - target: link.target, - strokeWidth: weightToStrokeWidthMap(Number(link.weight)), - color: link.color ? linkColorScale(link.color.toString()) : '#000000', - }; - }), + links: data.value.bipartitenetwork.data.links + .filter( + (link) => + keepNodeIds.includes(link.source.id) && + keepNodeIds.includes(link.target.id) + ) + .map((link) => { + return { + source: link.source, + target: link.target, + strokeWidth: weightToStrokeWidthMap(Number(link.weight)), + color: link.color + ? linkColorScale(link.color.toString()) + : '#000000', + }; + }), }; - }, [data.value, entities, minDataWeight, maxDataWeight]); + }, [data.value, entities, minDataWeight, maxDataWeight, keepNodeIds]); const getNodeMenuActions = options?.makeGetNodeMenuActions?.(studyMetadata); @@ -313,6 +331,11 @@ function BipartiteNetworkViz(props: VisualizationProps) { [cleanedData] ); + const data1DisplayName = + findEntityAndVariableCollection( + computationConfiguration.data1?.collectionSpec + )?.variableCollection.displayName ?? 'Column 1'; + // Have the bpnet component say "No nodes" or whatev and have an extra // prop called errorMessage or something that displays when there are no nodes. // that error message can say "your thresholds of blah and blah are too high, change them" @@ -450,37 +473,87 @@ function BipartiteNetworkViz(props: VisualizationProps) { return (
{!hideInputsAndControls && ( - - - updateVizConfig({ correlationCoefThreshold: Number(newValue) }) - } - label={'Absolute correlation coefficient'} - minValue={0} - maxValue={1} - value={ - vizConfig.correlationCoefThreshold ?? - DEFAULT_CORRELATION_COEF_THRESHOLD - } - step={0.05} - applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} - /> - - - updateVizConfig({ significanceThreshold: Number(newValue) }) - } - minValue={0} - maxValue={1} - value={ - vizConfig.significanceThreshold ?? DEFAULT_SIGNIFICANCE_THRESHOLD - } - containerStyles={{ marginLeft: 10 }} - step={0.001} - applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} - /> - + <> + + + updateVizConfig({ correlationCoefThreshold: Number(newValue) }) + } + label={'Absolute correlation coefficient'} + minValue={0} + maxValue={1} + value={ + vizConfig.correlationCoefThreshold ?? + DEFAULT_CORRELATION_COEF_THRESHOLD + } + step={0.05} + applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} + /> + + + updateVizConfig({ significanceThreshold: Number(newValue) }) + } + minValue={0} + maxValue={1} + value={ + vizConfig.significanceThreshold ?? + DEFAULT_SIGNIFICANCE_THRESHOLD + } + containerStyles={{ marginLeft: 10 }} + step={0.001} + applyWarningStyles={cleanedData && cleanedData.nodes.length === 0} + /> + + + {/* */} + {data1DisplayName} + ({ + display: {node.id}, + value: node.id, + })) + : [] + } + onChange={setKeepNodeIds} + value={keepNodeIds} + isDisabled={false} + isLoading={false} + /> + Metadata + ({ + display: {node.id}, + value: node.id, + })) + : [] + } + onChange={setKeepNodeIds} + value={keepNodeIds} + isDisabled={false} + isLoading={false} + /> + + )} ({ display: {value}, @@ -37,6 +41,7 @@ export function ValuePicker({ return ( <> + {isSearchable && }