Skip to content

Commit

Permalink
feat(network): add support for custom node component
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Dec 31, 2021
1 parent c3d5dd1 commit 8e85cf1
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 62 deletions.
10 changes: 4 additions & 6 deletions packages/network/src/Network.tsx
Expand Up @@ -3,7 +3,7 @@ import { Container, useDimensions, SvgWrapper, useTheme } from '@nivo/core'
import { useInheritedColor } from '@nivo/colors'
import { useTooltip } from '@nivo/tooltip'
import { svgDefaultProps } from './defaults'
import { useNetwork, useNodeColor, useLinkThickness } from './hooks'
import { useNetwork, useLinkThickness } from './hooks'
import { NetworkNodes } from './NetworkNodes'
import { NetworkLinks } from './NetworkLinks'
import { NetworkInputNode, NetworkLayerId, NetworkSvgProps } from './types'
Expand Down Expand Up @@ -49,8 +49,6 @@ const InnerNetwork = <N extends NetworkInputNode>({
)

const theme = useTheme()
const getColor = useNodeColor<N>(nodeColor)
const getBorderColor = useInheritedColor(nodeBorderColor, theme)
const getLinkThickness = useLinkThickness(linkThickness)
const getLinkColor = useInheritedColor(linkColor, theme)

Expand All @@ -63,6 +61,9 @@ const InnerNetwork = <N extends NetworkInputNode>({
distanceMax,
iterations,
center: [innerWidth / 2, innerHeight / 2],
nodeColor,
nodeBorderWidth,
nodeBorderColor,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()
Expand Down Expand Up @@ -100,9 +101,6 @@ const InnerNetwork = <N extends NetworkInputNode>({
key="nodes"
nodes={nodes}
nodeComponent={nodeComponent}
color={getColor}
borderWidth={nodeBorderWidth}
borderColor={getBorderColor}
onClick={isInteractive ? onClick : undefined}
onMouseEnter={isInteractive ? handleNodeHover : undefined}
onMouseMove={isInteractive ? handleNodeHover : undefined}
Expand Down
23 changes: 7 additions & 16 deletions packages/network/src/NetworkCanvas.tsx
Expand Up @@ -3,7 +3,7 @@ import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } fr
import { useInheritedColor } from '@nivo/colors'
import { useTooltip } from '@nivo/tooltip'
import { canvasDefaultProps } from './defaults'
import { useNetwork, useNodeColor, useLinkThickness } from './hooks'
import { useNetwork, useLinkThickness } from './hooks'
import { NetworkCanvasProps, NetworkInputNode } from './types'

type InnerNetworkCanvasProps<N extends NetworkInputNode> = Omit<
Expand All @@ -27,6 +27,7 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({

layers = canvasDefaultProps.layers,

renderNode = canvasDefaultProps.renderNode,
nodeColor = canvasDefaultProps.nodeColor,
nodeBorderWidth = canvasDefaultProps.nodeBorderWidth,
nodeBorderColor = canvasDefaultProps.nodeBorderColor,
Expand Down Expand Up @@ -54,11 +55,12 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
distanceMax,
iterations,
center: [innerWidth / 2, innerHeight / 2],
nodeColor,
nodeBorderWidth,
nodeBorderColor,
})

const theme = useTheme()
const getNodeColor = useNodeColor(nodeColor)
const getBorderColor = useInheritedColor(nodeBorderColor, theme)
const getLinkThickness = useLinkThickness(linkThickness)
const getLinkColor = useInheritedColor(linkColor, theme)

Expand Down Expand Up @@ -88,16 +90,7 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
})
} else if (layer === 'nodes' && nodes !== null) {
nodes.forEach(node => {
ctx.fillStyle = getNodeColor(node)
ctx.beginPath()
ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI)
ctx.fill()

if (nodeBorderWidth > 0) {
ctx.strokeStyle = getBorderColor(node)
ctx.lineWidth = nodeBorderWidth
ctx.stroke()
}
renderNode(ctx, node)
})
} else if (typeof layer === 'function' && nodes !== null && links !== null) {
layer(ctx, {
Expand All @@ -118,9 +111,7 @@ const InnerNetworkCanvas = <N extends NetworkInputNode>({
theme,
nodes,
links,
getNodeColor,
nodeBorderWidth,
getBorderColor,
renderNode,
getLinkThickness,
getLinkColor,
])
Expand Down
51 changes: 14 additions & 37 deletions packages/network/src/NetworkNodes.tsx
Expand Up @@ -5,70 +5,51 @@ import {
NetworkComputedNode,
NetworkInputNode,
NetworkNodeAnimatedProps,
NetworkNodeColor,
NetworkNodeComponent,
} from './types'

interface NetworkNodesProps<N extends NetworkInputNode> {
nodes: NetworkComputedNode<N>[]
nodeComponent: NetworkNodeComponent<N>
color: Exclude<NetworkNodeColor<N>, string>
borderWidth: number
borderColor: (node: NetworkComputedNode<N>) => string
onClick?: (node: NetworkComputedNode<N>, event: MouseEvent) => void
onMouseEnter?: (node: NetworkComputedNode<N>, event: MouseEvent) => void
onMouseMove?: (node: NetworkComputedNode<N>, event: MouseEvent) => void
onMouseLeave?: (node: NetworkComputedNode<N>, event: MouseEvent) => void
}

const getEnterTransition = <N extends NetworkInputNode>(
color: NetworkNodesProps<N>['color'],
borderWidth: number,
borderColor: NetworkNodesProps<N>['borderColor']
) => (node: NetworkComputedNode<N>) => ({
const getEnterTransition = <N extends NetworkInputNode>() => (node: NetworkComputedNode<N>) => ({
x: node.x,
y: node.y,
radius: node.radius,
color: color(node),
borderWidth,
borderColor: borderColor(node),
color: node.color,
borderWidth: node.borderWidth,
borderColor: node.borderColor,
scale: 0,
})

const getRegularTransition = <N extends NetworkInputNode>(
color: NetworkNodesProps<N>['color'],
borderWidth: number,
borderColor: NetworkNodesProps<N>['borderColor']
) => (node: NetworkComputedNode<N>) => ({
const getRegularTransition = <N extends NetworkInputNode>() => (node: NetworkComputedNode<N>) => ({
x: node.x,
y: node.y,
radius: node.radius,
color: color(node),
borderWidth,
borderColor: borderColor(node),
color: node.color,
borderWidth: node.borderWidth,
borderColor: node.borderColor,
scale: 1,
})

const getExitTransition = <N extends NetworkInputNode>(
color: NetworkNodesProps<N>['color'],
borderWidth: number,
borderColor: NetworkNodesProps<N>['borderColor']
) => (node: NetworkComputedNode<N>) => ({
const getExitTransition = <N extends NetworkInputNode>() => (node: NetworkComputedNode<N>) => ({
x: node.x,
y: node.y,
radius: node.radius,
color: color(node),
borderWidth,
borderColor: borderColor(node),
color: node.color,
borderWidth: node.borderWidth,
borderColor: node.borderColor,
scale: 0,
})

export const NetworkNodes = <N extends NetworkInputNode>({
nodes,
nodeComponent,
color,
borderColor,
borderWidth,
onClick,
onMouseEnter,
onMouseMove,
Expand All @@ -77,12 +58,8 @@ export const NetworkNodes = <N extends NetworkInputNode>({
const { animate, config: springConfig } = useMotionConfig()

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

const transition = useTransition<NetworkComputedNode<N>, NetworkNodeAnimatedProps>(nodes, {
Expand Down
4 changes: 3 additions & 1 deletion packages/network/src/defaults.ts
@@ -1,6 +1,7 @@
import { NetworkLayerId } from './types'
import { NetworkNode } from './NetworkNode'
import { renderCanvasNode } from './renderCanvasNode'
import { NetworkNodeTooltip } from './NetworkNodeTooltip'
import { NetworkLayerId } from './types'

export const commonDefaultProps = {
layers: ['links', 'nodes'] as NetworkLayerId[],
Expand Down Expand Up @@ -34,5 +35,6 @@ export const svgDefaultProps = {

export const canvasDefaultProps = {
...commonDefaultProps,
renderNode: renderCanvasNode,
pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
}
27 changes: 26 additions & 1 deletion packages/network/src/hooks.ts
Expand Up @@ -3,6 +3,7 @@ import get from 'lodash/get'
import isString from 'lodash/isString'
import isNumber from 'lodash/isNumber'
import { forceSimulation, forceManyBody, forceCenter, forceLink } from 'd3-force'
import { useTheme } from '@nivo/core'
import {
InputLink,
NetworkInputNode,
Expand All @@ -12,6 +13,7 @@ import {
NetworkComputedNode,
ComputedLink,
} from './types'
import { useInheritedColor } from '@nivo/colors'

const computeForces = <N extends NetworkInputNode>({
linkDistance,
Expand Down Expand Up @@ -58,6 +60,9 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
distanceMax,
center,
iterations,
nodeColor,
nodeBorderWidth,
nodeBorderColor,
}: {
nodes: N[]
links: InputLink[]
Expand All @@ -67,6 +72,9 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
distanceMax: NetworkCommonProps<N>['distanceMax']
center: [number, number]
iterations: NetworkCommonProps<N>['iterations']
nodeColor: NetworkCommonProps<N>['nodeColor']
nodeBorderWidth: NetworkCommonProps<N>['nodeBorderWidth']
nodeBorderColor: NetworkCommonProps<N>['nodeBorderColor']
}): [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 @@ -124,7 +132,24 @@ export const useNetwork = <N extends NetworkInputNode = NetworkInputNode>({
center[1],
])

return [currentNodes, currentLinks]
const theme = useTheme()
const getNodeColor = useNodeColor<N>(nodeColor)
const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme)

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

return currentNodes.map(node => {
return {
...node,
color: getNodeColor(node),
borderWidth: nodeBorderWidth,
borderColor: getNodeBorderColor(node),
}
})
}, [currentNodes, getNodeColor, nodeBorderWidth, getNodeBorderColor])

return [enhancedNodes, currentLinks]
}

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

export const renderCanvasNode = <N extends NetworkInputNode>(
ctx: CanvasRenderingContext2D,
node: NetworkComputedNode<N>
) => {
ctx.fillStyle = node.color
ctx.beginPath()
ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI)
ctx.fill()

if (node.borderWidth > 0) {
ctx.strokeStyle = node.borderColor
ctx.lineWidth = node.borderWidth
ctx.stroke()
}
}
8 changes: 7 additions & 1 deletion packages/network/src/types.ts
Expand Up @@ -13,8 +13,9 @@ export interface NetworkComputedNode<N extends NetworkInputNode> {
y: number
radius: number
color: string
borderWidth: number
borderColor: string
data: N
[key: string]: unknown
}

export interface NetworkNodeAnimatedProps {
Expand All @@ -40,6 +41,10 @@ export interface NetworkNodeProps<N extends NetworkInputNode> {
export type NetworkNodeComponent<N extends NetworkInputNode> = FunctionComponent<
NetworkNodeProps<N>
>
export type NetworkNodeCanvasRenderer<N extends NetworkInputNode> = (
ctx: CanvasRenderingContext2D,
node: NetworkComputedNode<N>
) => void

export interface InputLink {
source: string
Expand Down Expand Up @@ -148,5 +153,6 @@ export type NetworkCanvasProps<N extends NetworkInputNode> = Partial<NetworkComm
// only used by tooltips
ModernMotionProps & {
layers?: (NetworkLayerId | NetworkCustomCanvasLayer<N>)[]
renderNode?: NetworkNodeCanvasRenderer<N>
pixelRatio?: number
}
34 changes: 34 additions & 0 deletions website/src/data/components/network/props.ts
Expand Up @@ -122,6 +122,23 @@ const props: ChartProperty[] = [
},
},
themeProperty(['svg', 'canvas']),
{
key: 'nodeComponent',
group: 'Nodes',
type: 'NetworkNodeComponent',
required: false,
help: `Custom node component for the SVG implementation.`,
flavors: ['svg'],
defaultValue: 'NetworkNode',
},
{
key: 'renderNode',
group: 'Nodes',
type: 'NetworkNodeCanvasRenderer',
required: false,
help: `Custom node rendering for the canvas implementation.`,
flavors: ['canvas'],
},
{
key: 'nodeColor',
group: 'Nodes',
Expand Down Expand Up @@ -168,6 +185,23 @@ const props: ChartProperty[] = [
controlType: 'inheritedColor',
>>>>>>> feat(network): types are now valid
},
{
key: 'linkComponent',
group: 'Links',
type: 'NetworkLinkComponent',
required: false,
help: `Custom link component for the SVG implementation.`,
flavors: ['svg'],
defaultValue: 'NetworkLink',
},
{
key: 'renderLink',
group: 'Links',
type: 'NetworkLinkCanvasRenderer',
required: false,
help: `Custom link rendering for the canvas implementation.`,
flavors: ['canvas'],
},
{
key: 'linkThickness',
enableControlForFlavors: ['canvas'],
Expand Down

0 comments on commit 8e85cf1

Please sign in to comment.