Skip to content

Commit

Permalink
feat(network): add support for custom link component
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Dec 31, 2021
1 parent 8e85cf1 commit 2b26bf3
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 75 deletions.
19 changes: 6 additions & 13 deletions packages/network/src/Network.tsx
@@ -1,9 +1,8 @@
import { Fragment, ReactNode, useCallback, createElement } from 'react'
import { Container, useDimensions, SvgWrapper, useTheme } from '@nivo/core'
import { useInheritedColor } from '@nivo/colors'
import { Container, useDimensions, SvgWrapper } from '@nivo/core'
import { useTooltip } from '@nivo/tooltip'
import { svgDefaultProps } from './defaults'
import { useNetwork, useLinkThickness } from './hooks'
import { useNetwork } from './hooks'
import { NetworkNodes } from './NetworkNodes'
import { NetworkLinks } from './NetworkLinks'
import { NetworkInputNode, NetworkLayerId, NetworkSvgProps } from './types'
Expand Down Expand Up @@ -33,6 +32,7 @@ const InnerNetwork = <N extends NetworkInputNode>({
nodeBorderWidth = svgDefaultProps.nodeBorderWidth,
nodeBorderColor = svgDefaultProps.nodeBorderColor,

linkComponent = svgDefaultProps.linkComponent,
linkThickness = svgDefaultProps.linkThickness,
linkColor = svgDefaultProps.linkColor,

Expand All @@ -48,10 +48,6 @@ const InnerNetwork = <N extends NetworkInputNode>({
partialMargin
)

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

const [nodes, links] = useNetwork<N>({
nodes: rawNodes,
links: rawLinks,
Expand All @@ -64,6 +60,8 @@ const InnerNetwork = <N extends NetworkInputNode>({
nodeColor,
nodeBorderWidth,
nodeBorderColor,
linkThickness,
linkColor,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()
Expand All @@ -86,12 +84,7 @@ const InnerNetwork = <N extends NetworkInputNode>({

if (layers.includes('links') && links !== null) {
layerById.links = (
<NetworkLinks<N>
key="links"
links={links}
linkThickness={getLinkThickness}
linkColor={getLinkColor}
/>
<NetworkLinks<N> key="links" links={links} linkComponent={linkComponent} />
)
}

Expand Down
24 changes: 7 additions & 17 deletions packages/network/src/NetworkCanvas.tsx
@@ -1,9 +1,8 @@
import { useCallback, useRef, useEffect, createElement, MouseEvent } from 'react'
import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } from '@nivo/core'
import { useInheritedColor } from '@nivo/colors'
import { useTooltip } from '@nivo/tooltip'
import { canvasDefaultProps } from './defaults'
import { useNetwork, useLinkThickness } from './hooks'
import { useNetwork } from './hooks'
import { NetworkCanvasProps, NetworkInputNode } from './types'

type InnerNetworkCanvasProps<N extends NetworkInputNode> = Omit<
Expand Down Expand Up @@ -32,6 +31,7 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
nodeBorderWidth = canvasDefaultProps.nodeBorderWidth,
nodeBorderColor = canvasDefaultProps.nodeBorderColor,

renderLink = canvasDefaultProps.renderLink,
linkThickness = canvasDefaultProps.linkThickness,
linkColor = canvasDefaultProps.linkColor,

Expand All @@ -58,11 +58,11 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
nodeColor,
nodeBorderWidth,
nodeBorderColor,
linkThickness,
linkColor,
})

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

useEffect(() => {
if (canvasEl.current === null) return
Expand All @@ -80,18 +80,9 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({

layers.forEach(layer => {
if (layer === 'links' && links !== null) {
links.forEach(link => {
ctx.strokeStyle = getLinkColor(link)
ctx.lineWidth = getLinkThickness(link)
ctx.beginPath()
ctx.moveTo(link.source.x, link.source.y)
ctx.lineTo(link.target.x, link.target.y)
ctx.stroke()
})
links.forEach(link => renderLink(ctx, link))
} else if (layer === 'nodes' && nodes !== null) {
nodes.forEach(node => {
renderNode(ctx, node)
})
nodes.forEach(node => renderNode(ctx, node))
} else if (typeof layer === 'function' && nodes !== null && links !== null) {
layer(ctx, {
// ...props,
Expand All @@ -112,8 +103,7 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
nodes,
links,
renderNode,
getLinkThickness,
getLinkColor,
renderLink,
])

const getNodeFromMouseEvent = useCallback(
Expand Down
20 changes: 4 additions & 16 deletions packages/network/src/NetworkLink.tsx
@@ -1,26 +1,14 @@
import { AnimatedProps, animated } from '@react-spring/web'
import { ComputedLink, NetworkInputNode } from './types'

interface NetworkLinkProps<N extends NetworkInputNode> {
link: ComputedLink<N>
thickness: number
animated: AnimatedProps<{
x1: number
y1: number
x2: number
y2: number
color: string
}>
}
import { animated } from '@react-spring/web'
import { NetworkInputNode, NetworkLinkProps } from './types'

export const NetworkLink = <N extends NetworkInputNode>({
thickness,
link,
animated: animatedProps,
}: NetworkLinkProps<N>) => {
return (
<animated.line
stroke={animatedProps.color}
strokeWidth={thickness}
strokeWidth={link.thickness}
strokeLinecap="round"
x1={animatedProps.x1}
y1={animatedProps.y1}
Expand Down
38 changes: 12 additions & 26 deletions packages/network/src/NetworkLinks.tsx
@@ -1,62 +1,49 @@
import { createElement, useMemo } from 'react'
import { useTransition } from '@react-spring/web'
import { useMotionConfig } from '@nivo/core'
import { NetworkLink } from './NetworkLink'
import { ComputedLink, NetworkInputNode } from './types'
import { ComputedLink, NetworkInputNode, NetworkLinkComponent } from './types'

interface NetworkLinksProps<N extends NetworkInputNode> {
links: ComputedLink<N>[]
linkThickness: (link: ComputedLink<N>) => number
linkColor: (link: ComputedLink<N>) => string
linkComponent: NetworkLinkComponent<N>
}

const getEnterTransition = <N extends NetworkInputNode>(
linkColor: NetworkLinksProps<N>['linkColor']
) => (link: ComputedLink<N>) => ({
const getEnterTransition = <N extends NetworkInputNode>() => (link: ComputedLink<N>) => ({
x1: link.source.x,
y1: link.source.y,
x2: link.source.x,
y2: link.source.y,
color: linkColor(link),
color: link.color,
opacity: 0,
})

const getRegularTransition = <N extends NetworkInputNode>(
linkColor: NetworkLinksProps<N>['linkColor']
) => (link: ComputedLink<N>) => ({
const getRegularTransition = <N extends NetworkInputNode>() => (link: ComputedLink<N>) => ({
x1: link.source.x,
y1: link.source.y,
x2: link.target.x,
y2: link.target.y,
color: linkColor(link),
color: link.color,
opacity: 1,
})

const getExitTransition = <N extends NetworkInputNode>(
linkColor: NetworkLinksProps<N>['linkColor']
) => (link: ComputedLink<N>) => ({
const getExitTransition = <N extends NetworkInputNode>() => (link: ComputedLink<N>) => ({
x1: link.source.x,
y1: link.source.y,
x2: link.source.x,
y2: link.source.y,
color: linkColor(link),
color: link.color,
opacity: 0,
})

export const NetworkLinks = <N extends NetworkInputNode>({
links,
linkThickness,
linkColor,
linkComponent,
}: NetworkLinksProps<N>) => {
const { animate, config: springConfig } = useMotionConfig()

const [enterTransition, regularTransition, exitTransition] = useMemo(
() => [
getEnterTransition<N>(linkColor),
getRegularTransition<N>(linkColor),
getExitTransition<N>(linkColor),
],
[linkColor]
() => [getEnterTransition<N>(), getRegularTransition<N>(), getExitTransition<N>()],
[]
)

const transition = useTransition<
Expand Down Expand Up @@ -84,10 +71,9 @@ export const NetworkLinks = <N extends NetworkInputNode>({
return (
<>
{transition((transitionProps, link) => {
return createElement(NetworkLink, {
return createElement(linkComponent, {
key: link.id,
link,
thickness: linkThickness(link),
animated: transitionProps,
})
})}
Expand Down
4 changes: 4 additions & 0 deletions packages/network/src/defaults.ts
@@ -1,6 +1,8 @@
import { NetworkLayerId } from './types'
import { NetworkNode } from './NetworkNode'
import { renderCanvasNode } from './renderCanvasNode'
import { NetworkLink } from './NetworkLink'
import { renderCanvasLink } from './renderCanvasLink'
import { NetworkNodeTooltip } from './NetworkNodeTooltip'

export const commonDefaultProps = {
Expand Down Expand Up @@ -31,10 +33,12 @@ export const commonDefaultProps = {
export const svgDefaultProps = {
...commonDefaultProps,
nodeComponent: NetworkNode,
linkComponent: NetworkLink,
}

export const canvasDefaultProps = {
...commonDefaultProps,
renderNode: renderCanvasNode,
renderLink: renderCanvasLink,
pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
}
20 changes: 19 additions & 1 deletion packages/network/src/hooks.ts
Expand Up @@ -63,6 +63,8 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
nodeColor,
nodeBorderWidth,
nodeBorderColor,
linkThickness,
linkColor,
}: {
nodes: N[]
links: InputLink[]
Expand All @@ -75,6 +77,8 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
nodeColor: NetworkCommonProps<N>['nodeColor']
nodeBorderWidth: NetworkCommonProps<N>['nodeBorderWidth']
nodeBorderColor: NetworkCommonProps<N>['nodeBorderColor']
linkThickness: NetworkCommonProps<N>['linkThickness']
linkColor: NetworkCommonProps<N>['linkColor']
}): [null | NetworkComputedNode<N>[], null | ComputedLink<N>[]] => {
const [currentNodes, setCurrentNodes] = useState<null | NetworkComputedNode<N>[]>(null)
const [currentLinks, setCurrentLinks] = useState<null | ComputedLink<N>[]>(null)
Expand Down Expand Up @@ -135,6 +139,8 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
const theme = useTheme()
const getNodeColor = useNodeColor<N>(nodeColor)
const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme)
const getLinkThickness = useLinkThickness<N>(linkThickness)
const getLinkColor = useInheritedColor(linkColor, theme)

const enhancedNodes: NetworkComputedNode<N>[] | null = useMemo(() => {
if (currentNodes === null) return null
Expand All @@ -149,7 +155,19 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
})
}, [currentNodes, getNodeColor, nodeBorderWidth, getNodeBorderColor])

return [enhancedNodes, currentLinks]
const enhancedLinks: ComputedLink<N>[] | null = useMemo(() => {
if (currentLinks === null) return null

return currentLinks.map(link => {
return {
...link,
thickness: getLinkThickness(link),
color: getLinkColor(link),
}
})
}, [currentLinks, getLinkThickness, getLinkColor])

return [enhancedNodes, enhancedLinks]
}

export const useNodeColor = <N extends NetworkInputNode>(color: NetworkNodeColor<N>) =>
Expand Down
14 changes: 14 additions & 0 deletions packages/network/src/renderCanvasLink.ts
@@ -0,0 +1,14 @@
import { NetworkInputNode, ComputedLink } from './types'

export const renderCanvasLink = <N extends NetworkInputNode>(
ctx: CanvasRenderingContext2D,
link: ComputedLink<N>
) => {
ctx.strokeStyle = link.color
ctx.lineWidth = link.thickness

ctx.beginPath()
ctx.moveTo(link.source.x, link.source.y)
ctx.lineTo(link.target.x, link.target.y)
ctx.stroke()
}
20 changes: 18 additions & 2 deletions packages/network/src/types.ts
Expand Up @@ -49,14 +49,16 @@ export type NetworkNodeCanvasRenderer<N extends NetworkInputNode> = (
export interface InputLink {
source: string
target: string
[key: string]: unknown
}

export interface ComputedLink<N extends NetworkInputNode> {
id: string
source: NetworkComputedNode<N>
previousSource?: NetworkComputedNode<N>
target: NetworkComputedNode<N>
[key: string]: unknown
previousTarget?: NetworkComputedNode<N>
thickness: number
color: string
}

export interface LinkAnimatedProps {
Expand All @@ -68,6 +70,18 @@ export interface LinkAnimatedProps {
opacity: number
}

export interface NetworkLinkProps<N extends NetworkInputNode> {
link: ComputedLink<N>
animated: AnimatedProps<LinkAnimatedProps>
}
export type NetworkLinkComponent<N extends NetworkInputNode> = FunctionComponent<
NetworkLinkProps<N>
>
export type NetworkLinkCanvasRenderer<N extends NetworkInputNode> = (
ctx: CanvasRenderingContext2D,
node: ComputedLink<N>
) => void

export interface NetworkDataProps<N extends NetworkInputNode> {
data: {
nodes: N[]
Expand Down Expand Up @@ -145,6 +159,7 @@ export type NetworkSvgProps<N extends NetworkInputNode> = Partial<NetworkCommonP
ModernMotionProps & {
layers?: (NetworkLayerId | NetworkCustomLayer<N>)[]
nodeComponent?: NetworkNodeComponent<N>
linkComponent?: NetworkLinkComponent<N>
}

export type NetworkCanvasProps<N extends NetworkInputNode> = Partial<NetworkCommonProps<N>> &
Expand All @@ -154,5 +169,6 @@ export type NetworkCanvasProps<N extends NetworkInputNode> = Partial<NetworkComm
ModernMotionProps & {
layers?: (NetworkLayerId | NetworkCustomCanvasLayer<N>)[]
renderNode?: NetworkNodeCanvasRenderer<N>
renderLink?: NetworkLinkCanvasRenderer<N>
pixelRatio?: number
}

0 comments on commit 2b26bf3

Please sign in to comment.