Skip to content

Commit

Permalink
feat(network): add missing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Dec 31, 2021
1 parent aed3590 commit ca5ee4e
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 78 deletions.
70 changes: 35 additions & 35 deletions packages/network/src/Network.tsx
@@ -1,12 +1,18 @@
import { Fragment, ReactNode, useCallback, createElement } from 'react'
import { Fragment, ReactNode, createElement, useMemo } from 'react'
import { Container, useDimensions, SvgWrapper } from '@nivo/core'
import { useTooltip } from '@nivo/tooltip'
import { svgDefaultProps } from './defaults'
import { useNetwork } from './hooks'
import { NetworkLinks } from './NetworkLinks'
import { NetworkNodes } from './NetworkNodes'
import { NetworkNodeAnnotations } from './NetworkNodeAnnotations'
import { InputNode, LayerId, NodeTooltip, NetworkSvgProps, ComputedNode, InputLink } from './types'
import {
InputNode,
LayerId,
NodeTooltip,
NetworkSvgProps,
InputLink,
CustomLayerProps,
} from './types'

type InnerNetworkProps<Node extends InputNode, Link extends InputLink> = Omit<
NetworkSvgProps<Node, Link>,
Expand Down Expand Up @@ -51,7 +57,11 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({
>,

isInteractive = svgDefaultProps.isInteractive,
defaultActiveNodeIds = svgDefaultProps.defaultActiveNodeIds,
nodeTooltip = svgDefaultProps.nodeTooltip as NodeTooltip<Node>,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,

role = svgDefaultProps.role,
Expand All @@ -65,7 +75,7 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({
partialMargin
)

const { nodes, links, setActiveNodeIds } = useNetwork<Node, Link>({
const { nodes, links, activeNodeIds, setActiveNodeIds } = useNetwork<Node, Link>({
center: [innerWidth / 2, innerHeight / 2],
nodes: rawNodes,
links: rawLinks,
Expand All @@ -83,23 +93,10 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({
nodeBorderColor,
linkThickness,
linkColor,
isInteractive,
defaultActiveNodeIds,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()

const handleNodeHover = useCallback(
(node: ComputedNode<Node>, event) => {
showTooltipFromEvent(createElement(nodeTooltip, { node }), event)
setActiveNodeIds([node.id])
},
[showTooltipFromEvent, nodeTooltip, setActiveNodeIds]
)

const handleNodeLeave = useCallback(() => {
hideTooltip()
setActiveNodeIds([])
}, [hideTooltip, setActiveNodeIds])

const layerById: Record<LayerId, ReactNode> = {
links: null,
nodes: null,
Expand All @@ -119,14 +116,17 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({

if (layers.includes('nodes') && nodes !== null) {
layerById.nodes = (
<NetworkNodes<Node>
<NetworkNodes<Node, Link>
key="nodes"
nodes={nodes}
nodeComponent={nodeComponent}
onClick={isInteractive ? onClick : undefined}
onMouseEnter={isInteractive ? handleNodeHover : undefined}
onMouseMove={isInteractive ? handleNodeHover : undefined}
onMouseLeave={isInteractive ? handleNodeLeave : undefined}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
tooltip={nodeTooltip}
setActiveNodeIds={setActiveNodeIds}
isInteractive={isInteractive}
/>
)
}
Expand All @@ -141,6 +141,16 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({
)
}

const customLayerProps: CustomLayerProps<Node, Link> = useMemo(
() => ({
nodes: nodes || [],
links: links || [],
activeNodeIds,
setActiveNodeIds,
}),
[nodes, links, activeNodeIds, setActiveNodeIds]
)

return (
<SvgWrapper
width={outerWidth}
Expand All @@ -153,17 +163,7 @@ const InnerNetwork = <Node extends InputNode, Link extends InputLink>({
>
{layers.map((layer, i) => {
if (typeof layer === 'function') {
return (
<Fragment key={i}>
{createElement(layer, {
// ...props,
// innerWidth,
// innerHeight,
nodes: nodes || [],
links: links || [],
})}
</Fragment>
)
return <Fragment key={i}>{createElement(layer, customLayerProps)}</Fragment>
}

return layerById?.[layer] ?? null
Expand Down
25 changes: 18 additions & 7 deletions packages/network/src/NetworkCanvas.tsx
@@ -1,4 +1,4 @@
import { useCallback, useRef, useEffect, createElement, MouseEvent } from 'react'
import { useCallback, useRef, useEffect, createElement, MouseEvent, useMemo } from 'react'
import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } from '@nivo/core'
import { useTooltip } from '@nivo/tooltip'
import { useComputedAnnotations, renderAnnotationsToCanvas } from '@nivo/annotations'
Expand All @@ -11,6 +11,7 @@ import {
NodeTooltip,
InputLink,
NetworkSvgProps,
CustomLayerProps,
} from './types'

type InnerNetworkCanvasProps<Node extends InputNode, Link extends InputLink> = Omit<
Expand Down Expand Up @@ -52,6 +53,7 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
>,

isInteractive = canvasDefaultProps.isInteractive,
defaultActiveNodeIds = canvasDefaultProps.defaultActiveNodeIds,
nodeTooltip = canvasDefaultProps.nodeTooltip as NodeTooltip<Node>,
onClick,
}: InnerNetworkCanvasProps<Node, Link>) => {
Expand All @@ -62,7 +64,7 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
partialMargin
)

const { nodes, links, setActiveNodeIds } = useNetwork<Node, Link>({
const { nodes, links, activeNodeIds, setActiveNodeIds } = useNetwork<Node, Link>({
center: [innerWidth / 2, innerHeight / 2],
nodes: rawNodes,
links: rawLinks,
Expand All @@ -80,13 +82,25 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
nodeBorderColor,
linkThickness,
linkColor,
isInteractive,
defaultActiveNodeIds,
})

const boundAnnotations = useNodeAnnotations<Node>(nodes!, annotations)
const computedAnnotations = useComputedAnnotations<ComputedNode<Node>>({
annotations: boundAnnotations,
})

const customLayerProps: CustomLayerProps<Node, Link> = useMemo(
() => ({
nodes: nodes || [],
links: links || [],
activeNodeIds,
setActiveNodeIds,
}),
[nodes, links, activeNodeIds, setActiveNodeIds]
)

const theme = useTheme()

useEffect(() => {
Expand Down Expand Up @@ -114,11 +128,7 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
theme,
})
} else if (typeof layer === 'function' && nodes !== null && links !== null) {
layer(ctx, {
// ...props,
nodes,
links,
})
layer(ctx, customLayerProps)
}
})
}, [
Expand All @@ -135,6 +145,7 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
renderNode,
renderLink,
computedAnnotations,
customLayerProps,
])

const getNodeFromMouseEvent = useCallback(
Expand Down
65 changes: 50 additions & 15 deletions packages/network/src/NetworkNodes.tsx
@@ -1,15 +1,19 @@
import { createElement, MouseEvent, useMemo } from 'react'
import { createElement, useCallback, useMemo, MouseEvent } from 'react'
import { useTransition } from '@react-spring/web'
import { useMotionConfig } from '@nivo/core'
import { InputNode, ComputedNode, NodeAnimatedProps, NodeComponent } from './types'
import { useTooltip } from '@nivo/tooltip'
import { InputNode, ComputedNode, NodeAnimatedProps, NetworkSvgProps, InputLink } from './types'

interface NetworkNodesProps<Node extends InputNode> {
interface NetworkNodesProps<Node extends InputNode, Link extends InputLink> {
nodes: ComputedNode<Node>[]
nodeComponent: NodeComponent<Node>
onClick?: (node: ComputedNode<Node>, event: MouseEvent) => void
onMouseEnter?: (node: ComputedNode<Node>, event: MouseEvent) => void
onMouseMove?: (node: ComputedNode<Node>, event: MouseEvent) => void
onMouseLeave?: (node: ComputedNode<Node>, event: MouseEvent) => void
nodeComponent: NonNullable<NetworkSvgProps<Node, Link>['nodeComponent']>
onMouseEnter: NetworkSvgProps<Node, Link>['onMouseEnter']
onMouseMove: NetworkSvgProps<Node, Link>['onMouseMove']
onMouseLeave: NetworkSvgProps<Node, Link>['onMouseLeave']
onClick: NetworkSvgProps<Node, Link>['onClick']
tooltip: NonNullable<NetworkSvgProps<Node, Link>['nodeTooltip']>
setActiveNodeIds: (nodeIds: string[]) => void
isInteractive: NonNullable<NetworkSvgProps<Node, Link>['isInteractive']>
}

const getEnterTransition =
Expand Down Expand Up @@ -51,14 +55,17 @@ const getExitTransition =
opacity: 0,
})

export const NetworkNodes = <Node extends InputNode>({
export const NetworkNodes = <Node extends InputNode, Link extends InputLink>({
nodes,
nodeComponent,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
}: NetworkNodesProps<Node>) => {
onClick,
tooltip,
setActiveNodeIds,
isInteractive,
}: NetworkNodesProps<Node, Link>) => {
const { animate, config: springConfig } = useMotionConfig()

const [enterTransition, regularTransition, exitTransition] = useMemo(
Expand All @@ -77,17 +84,45 @@ export const NetworkNodes = <Node extends InputNode>({
immediate: !animate,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()

const handleMouseEnter = useCallback(
(node: ComputedNode<Node>, event: MouseEvent) => {
showTooltipFromEvent(createElement(tooltip, { node }), event)
setActiveNodeIds([node.id])
onMouseEnter?.(node, event)
},
[showTooltipFromEvent, tooltip, setActiveNodeIds, onMouseEnter]
)

const handleMouseMove = useCallback(
(node: ComputedNode<Node>, event: MouseEvent) => {
showTooltipFromEvent(createElement(tooltip, { node }), event)
onMouseMove?.(node, event)
},
[showTooltipFromEvent, tooltip, onMouseMove]
)

const handleMouseLeave = useCallback(
(node: ComputedNode<Node>, event: MouseEvent) => {
hideTooltip()
setActiveNodeIds([])
onMouseLeave?.(node, event)
},
[hideTooltip, setActiveNodeIds, onMouseLeave]
)

return (
<>
{transition((transitionProps, node) =>
createElement(nodeComponent, {
key: node.id,
node,
animated: transitionProps,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onMouseEnter: isInteractive ? handleMouseEnter : undefined,
onMouseMove: isInteractive ? handleMouseMove : undefined,
onMouseLeave: isInteractive ? handleMouseLeave : undefined,
onClick: isInteractive ? onClick : undefined,
})
)}
</>
Expand Down
3 changes: 1 addition & 2 deletions packages/network/src/defaults.ts
Expand Up @@ -9,8 +9,6 @@ export const commonDefaultProps: Omit<
NetworkCommonProps<InputNode, InputLink>,
| 'margin'
| 'theme'
| 'activeLinkThickness'
| 'defaultActiveNodeIds'
| 'onClick'
| 'renderWrapper'
| 'ariaLabel'
Expand Down Expand Up @@ -39,6 +37,7 @@ export const commonDefaultProps: Omit<
linkColor: { from: 'source.color' },

isInteractive: true,
defaultActiveNodeIds: [],
nodeTooltip: NetworkNodeTooltip,

annotations: [],
Expand Down
5 changes: 4 additions & 1 deletion packages/network/src/hooks.ts
Expand Up @@ -165,6 +165,7 @@ export const useNetwork = <Node extends InputNode = InputNode, Link extends Inpu
linkThickness = commonDefaultProps.linkThickness,
linkColor = commonDefaultProps.linkColor,
isInteractive = commonDefaultProps.isInteractive,
defaultActiveNodeIds = commonDefaultProps.defaultActiveNodeIds,
}: {
center: [number, number]
nodes: Node[]
Expand All @@ -184,6 +185,7 @@ export const useNetwork = <Node extends InputNode = InputNode, Link extends Inpu
linkThickness?: NetworkCommonProps<Node, Link>['linkThickness']
linkColor?: NetworkCommonProps<Node, Link>['linkColor']
isInteractive?: NetworkCommonProps<Node, Link>['isInteractive']
defaultActiveNodeIds?: NetworkCommonProps<Node, Link>['defaultActiveNodeIds']
}) => {
// we're using `null` instead of empty array so that we can dissociate
// initial rendering from updates when using transitions.
Expand Down Expand Up @@ -239,7 +241,7 @@ export const useNetwork = <Node extends InputNode = InputNode, Link extends Inpu
}
}, [nodes, links, forces, iterations, setTransientNodes, setTransientLinks])

const [activeNodeIds, setActiveNodeIds] = useState<string[]>([])
const [activeNodeIds, setActiveNodeIds] = useState<string[]>(defaultActiveNodeIds)

const getNodeStyle = useNodeStyle<Node, Link>({
size: nodeSize,
Expand Down Expand Up @@ -287,6 +289,7 @@ export const useNetwork = <Node extends InputNode = InputNode, Link extends Inpu
return {
nodes: computedNodes,
links: computedLinks,
activeNodeIds,
setActiveNodeIds,
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/network/src/types.ts
Expand Up @@ -114,6 +114,8 @@ export type LayerId = 'links' | 'nodes' | 'annotations'
export interface CustomLayerProps<Node extends InputNode, Link extends InputLink> {
nodes: ComputedNode<Node>[]
links: ComputedLink<Node, Link>[]
activeNodeIds: string[]
setActiveNodeIds: (nodeIds: string[]) => void
}
export type CustomLayer<Node extends InputNode, Link extends InputLink> = FunctionComponent<
CustomLayerProps<Node, Link>
Expand Down Expand Up @@ -154,7 +156,6 @@ export type NetworkCommonProps<Node extends InputNode, Link extends InputLink> =
>

linkThickness: DerivedProp<Omit<ComputedLink<Node, Link>, 'color' | 'thickness'>, number>
activeLinkThickness: DerivedProp<Omit<ComputedLink<Node, Link>, 'color' | 'thickness'>, number>
linkColor: InheritedColorConfig<Omit<ComputedLink<Node, Link>, 'color' | 'thickness'>>

annotations: AnnotationMatcher<ComputedNode<Node>>[]
Expand All @@ -181,6 +182,9 @@ export type NetworkSvgProps<Node extends InputNode, Link extends InputLink> = Pa
nodeComponent?: NodeComponent<Node>
linkComponent?: LinkComponent<Node, Link>
linkBlendMode?: CssMixBlendMode
onMouseEnter?: (node: ComputedNode<Node>, event: MouseEvent) => void
onMouseMove?: (node: ComputedNode<Node>, event: MouseEvent) => void
onMouseLeave?: (node: ComputedNode<Node>, event: MouseEvent) => void
}

export type NetworkCanvasProps<Node extends InputNode, Link extends InputLink> = Partial<
Expand Down

0 comments on commit ca5ee4e

Please sign in to comment.