Skip to content

Commit

Permalink
feat(network): add basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Dec 31, 2021
1 parent c004350 commit 07a1e33
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 162 deletions.
6 changes: 5 additions & 1 deletion packages/network/src/Network.tsx
Expand Up @@ -132,7 +132,11 @@ const InnerNetwork = <Node extends InputNode>({

if (layers.includes('annotations') && nodes !== null) {
layerById.annotations = (
<NetworkNodeAnnotations<Node> nodes={nodes} annotations={annotations} />
<NetworkNodeAnnotations<Node>
key="annotations"
nodes={nodes}
annotations={annotations}
/>
)
}

Expand Down
27 changes: 13 additions & 14 deletions packages/network/src/NetworkLink.tsx
Expand Up @@ -5,17 +5,16 @@ export const NetworkLink = <Node extends InputNode>({
link,
animated: animatedProps,
blendMode,
}: LinkProps<Node>) => {
return (
<animated.line
stroke={animatedProps.color}
style={{ mixBlendMode: blendMode }}
strokeWidth={link.thickness}
strokeLinecap="round"
x1={animatedProps.x1}
y1={animatedProps.y1}
x2={animatedProps.x2}
y2={animatedProps.y2}
/>
)
}
}: LinkProps<Node>) => (
<animated.line
data-testid={`link.${link.id}`}
stroke={animatedProps.color}
style={{ mixBlendMode: blendMode }}
strokeWidth={link.thickness}
strokeLinecap="round"
x1={animatedProps.x1}
y1={animatedProps.y1}
x2={animatedProps.x2}
y2={animatedProps.y2}
/>
)
38 changes: 17 additions & 21 deletions packages/network/src/NetworkNode.tsx
Expand Up @@ -9,24 +9,20 @@ export const NetworkNode = <Node extends InputNode>({
onMouseEnter,
onMouseMove,
onMouseLeave,
}: NodeProps<Node>) => {
return (
<animated.circle
transform={to(
[animatedProps.x, animatedProps.y, animatedProps.scale],
(x, y, scale) => {
return `translate(${x},${y}) scale(${scale})`
}
)}
r={to([animatedProps.size], size => size / 2)}
fill={animatedProps.color}
style={{ mixBlendMode: blendMode }}
strokeWidth={animatedProps.borderWidth}
stroke={animatedProps.borderColor}
onClick={onClick ? event => onClick(node, event) : undefined}
onMouseEnter={onMouseEnter ? event => onMouseEnter(node, event) : undefined}
onMouseMove={onMouseMove ? event => onMouseMove(node, event) : undefined}
onMouseLeave={onMouseLeave ? event => onMouseLeave(node, event) : undefined}
/>
)
}
}: NodeProps<Node>) => (
<animated.circle
data-testid={`node.${node.id}`}
transform={to([animatedProps.x, animatedProps.y, animatedProps.scale], (x, y, scale) => {
return `translate(${x},${y}) scale(${scale})`
})}
r={to([animatedProps.size], size => size / 2)}
fill={animatedProps.color}
style={{ mixBlendMode: blendMode }}
strokeWidth={animatedProps.borderWidth}
stroke={animatedProps.borderColor}
onClick={onClick ? event => onClick(node, event) : undefined}
onMouseEnter={onMouseEnter ? event => onMouseEnter(node, event) : undefined}
onMouseMove={onMouseMove ? event => onMouseMove(node, event) : undefined}
onMouseLeave={onMouseLeave ? event => onMouseLeave(node, event) : undefined}
/>
)
138 changes: 81 additions & 57 deletions packages/network/src/hooks.ts
Expand Up @@ -11,8 +11,7 @@ import {
InputLink,
InputNode,
NetworkCommonProps,
NodeDerivedProp,
LinkDerivedProp,
DerivedProp,
ComputedNode,
ComputedLink,
} from './types'
Expand Down Expand Up @@ -53,16 +52,8 @@ const computeForces = <Node extends InputNode>({
return { link: linkForce, charge: chargeForce, center: centerForce }
}

const useNodeDerivedProp = <Node extends InputNode, T extends string | number>(
instruction: NodeDerivedProp<Node, T>
) =>
useMemo(() => {
if (typeof instruction === 'function') return instruction
return () => instruction
}, [instruction])

const useLinkDerivedProp = <Node extends InputNode, T extends string | number>(
instruction: LinkDerivedProp<Node, T>
const useDerivedProp = <Target, Output extends string | number>(
instruction: DerivedProp<Target, Output>
) =>
useMemo(() => {
if (typeof instruction === 'function') return instruction
Expand All @@ -88,46 +79,60 @@ const useNodeStyle = <Node extends InputNode>({
isInteractive: NetworkCommonProps<Node>['isInteractive']
activeNodeIds: string[]
}) => {
type IntermediateNode = Pick<ComputedNode<Node>, 'id' | 'data' | 'index' | 'x' | 'y'>

const theme = useTheme()

const getSize = useNodeDerivedProp(size)
const getColor = useNodeDerivedProp(color)
const getBorderWidth = useNodeDerivedProp(borderWidth)
const getSize = useDerivedProp(size)
const getColor = useDerivedProp(color)
const getBorderWidth = useDerivedProp(borderWidth)
const getBorderColor = useInheritedColor(borderColor, theme)
const getNormalStyle = useCallback(
(node: ComputedNode<Node>) => ({
size: getSize(node),
color: getColor(node),
borderWidth: getBorderWidth(node),
borderColor: getBorderColor(node),
}),
(node: IntermediateNode) => {
const color = getColor(node.data)

return {
size: getSize(node.data),
color,
borderWidth: getBorderWidth(node.data),
borderColor: getBorderColor({ ...node, color }),
}
},
[getSize, getColor, getBorderWidth, getBorderColor]
)

const getActiveSize = useNodeDerivedProp(activeSize)
const getActiveSize = useDerivedProp(activeSize)
const getActiveStyle = useCallback(
(node: ComputedNode<Node>) => ({
size: getActiveSize(node),
color: getColor(node),
borderWidth: getBorderWidth(node),
borderColor: getBorderColor(node),
}),
(node: IntermediateNode) => {
const color = getColor(node.data)

return {
size: getActiveSize(node.data),
color,
borderWidth: getBorderWidth(node.data),
borderColor: getBorderColor({ ...node, color }),
}
},
[getActiveSize, getColor, getBorderWidth, getBorderColor]
)

const getInactiveSize = useNodeDerivedProp(inactiveSize)
const getInactiveSize = useDerivedProp(inactiveSize)
const getInactiveStyle = useCallback(
(node: ComputedNode<Node>) => ({
size: getInactiveSize(node),
color: getColor(node),
borderWidth: getBorderWidth(node),
borderColor: getBorderColor(node),
}),
(node: IntermediateNode) => {
const color = getColor(node.data)

return {
size: getInactiveSize(node.data),
color,
borderWidth: getBorderWidth(node.data),
borderColor: getBorderColor({ ...node, color }),
}
},
[getInactiveSize, getColor, getBorderWidth, getBorderColor]
)

return useCallback(
(node: ComputedNode<Node>) => {
(node: IntermediateNode) => {
if (!isInteractive || activeNodeIds.length === 0) return getNormalStyle(node)
if (activeNodeIds.includes(node.id)) return getActiveStyle(node)
return getInactiveStyle(node)
Expand Down Expand Up @@ -175,7 +180,9 @@ export const useNetwork = <Node extends InputNode = InputNode>({
}) => {
// we're using `null` instead of empty array so that we can dissociate
// initial rendering from updates when using transitions.
const [currentNodes, setCurrentNodes] = useState<null | ComputedNode<Node>[]>(null)
const [currentNodes, setCurrentNodes] = useState<
null | Pick<ComputedNode<Node>, 'id' | 'data' | 'index' | 'x' | 'y'>[]
>(null)
const [currentLinks, setCurrentLinks] = useState<null | ComputedLink<Node>[]>(null)

const centerX = center[0]
Expand All @@ -190,7 +197,15 @@ export const useNetwork = <Node extends InputNode = InputNode>({
center: [centerX, centerY],
})

const nodesCopy: Node[] = nodes.map(node => ({ ...node }))
const nodesCopy: Pick<ComputedNode<Node>, 'id' | 'data' | 'index' | 'x' | 'y'>[] =
nodes.map(node => ({
id: node.id,
data: { ...node },
// populated later by D3, mutation
index: 0,
x: 0,
y: 0,
}))
const linksCopy: InputLink[] = links.map(link => ({
// generate a unique id for each link
id: `${link.source}.${link.target}`,
Expand All @@ -206,15 +221,22 @@ export const useNetwork = <Node extends InputNode = InputNode>({
simulation.tick(iterations)

// d3 mutates data, hence the castings
setCurrentNodes(nodesCopy as unknown as ComputedNode<Node>[])
setCurrentNodes(nodesCopy)
setCurrentLinks(
(linksCopy as unknown as ComputedLink<Node>[]).map(link => {
link.previousSource = currentNodes
const previousSource = currentNodes
? currentNodes.find(n => n.id === link.source.id)
: undefined
link.previousTarget = currentNodes
link.previousSource = previousSource
? (previousSource as ComputedNode<Node>)
: undefined

const previousTarget = currentNodes
? currentNodes.find(n => n.id === link.target.id)
: undefined
link.previousTarget = previousTarget
? (previousTarget as ComputedNode<Node>)
: undefined

return link
})
Expand All @@ -238,10 +260,6 @@ export const useNetwork = <Node extends InputNode = InputNode>({

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

const theme = useTheme()
const getLinkThickness = useLinkDerivedProp(linkThickness)
const getLinkColor = useInheritedColor(linkColor, theme)

const getNodeStyle = useNodeStyle<Node>({
size: nodeSize,
activeSize: activeNodeSize,
Expand All @@ -252,29 +270,35 @@ export const useNetwork = <Node extends InputNode = InputNode>({
isInteractive,
activeNodeIds,
})

const enhancedNodes: ComputedNode<Node>[] | null = useMemo(() => {
if (currentNodes === null) return null

return currentNodes.map(node => {
return {
...node,
...getNodeStyle(node),
}
})
return currentNodes.map(node => ({
...node,
...getNodeStyle(node),
}))
}, [currentNodes, getNodeStyle])

const theme = useTheme()
const getLinkThickness = useDerivedProp(linkThickness)
const getLinkColor = useInheritedColor(linkColor, theme)
const enhancedLinks: ComputedLink<Node>[] | null = useMemo(() => {
if (currentLinks === null) return null
if (currentLinks === null || enhancedNodes === null) return null

return currentLinks.map(link => {
return {
const linkWithEnhancedNodes = {
...link,
thickness: getLinkThickness(link),
color: getLinkColor(link),
source: enhancedNodes.find(node => node.id === link.source.id)!,
target: enhancedNodes.find(node => node.id === link.target.id)!,
}

return {
...linkWithEnhancedNodes,
thickness: getLinkThickness(linkWithEnhancedNodes),
color: getLinkColor(linkWithEnhancedNodes),
}
})
}, [currentLinks, getLinkThickness, getLinkColor])
}, [currentLinks, getLinkThickness, getLinkColor, enhancedNodes])

return {
nodes: enhancedNodes,
Expand Down
28 changes: 16 additions & 12 deletions packages/network/src/types.ts
Expand Up @@ -13,6 +13,7 @@ export interface ComputedNode<Node extends InputNode> {
data: Node
x: number
y: number
index: number
size: number
color: string
borderWidth: number
Expand Down Expand Up @@ -103,29 +104,32 @@ export interface NodeTooltipProps<Node extends InputNode> {
}
export type NodeTooltip<Node extends InputNode> = FunctionComponent<NodeTooltipProps<Node>>

export type NodeDerivedProp<Node extends InputNode, T> = T | ((node: ComputedNode<Node>) => T)
export type LinkDerivedProp<Node extends InputNode, T> = T | ((link: ComputedLink<Node>) => T)
export type DerivedProp<Target, Output extends number | string> =
| Output
| ((target: Target) => Output)

export type NetworkCommonProps<Node extends InputNode> = {
margin: Box

linkDistance: LinkDerivedProp<Node, number>
linkDistance: DerivedProp<ComputedLink<Node>, number>
repulsivity: number
distanceMin: number
distanceMax: number
iterations: number

theme: Theme

nodeSize: NodeDerivedProp<Node, number>
activeNodeSize: NodeDerivedProp<Node, number>
inactiveNodeSize: NodeDerivedProp<Node, number>
nodeColor: NodeDerivedProp<Node, string>
nodeBorderWidth: NodeDerivedProp<Node, number>
nodeBorderColor: InheritedColorConfig<ComputedNode<Node>>

linkThickness: LinkDerivedProp<Node, number>
activeLinkThickness: LinkDerivedProp<Node, number>
nodeSize: DerivedProp<Node, number>
activeNodeSize: DerivedProp<Node, number>
inactiveNodeSize: DerivedProp<Node, number>
nodeColor: DerivedProp<Node, string>
nodeBorderWidth: DerivedProp<Node, number>
nodeBorderColor: InheritedColorConfig<
Omit<ComputedNode<Node>, 'size' | 'borderWidth' | 'borderColor'>
>

linkThickness: DerivedProp<ComputedLink<Node>, number>
activeLinkThickness: DerivedProp<ComputedLink<Node>, number>
linkColor: InheritedColorConfig<ComputedLink<Node>>

annotations: AnnotationMatcher<ComputedNode<Node>>[]
Expand Down

0 comments on commit 07a1e33

Please sign in to comment.