diff --git a/packages/network/src/Network.tsx b/packages/network/src/Network.tsx index cdcbc320a0..b2e4f6779c 100644 --- a/packages/network/src/Network.tsx +++ b/packages/network/src/Network.tsx @@ -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' @@ -49,8 +49,6 @@ const InnerNetwork = ({ ) const theme = useTheme() - const getColor = useNodeColor(nodeColor) - const getBorderColor = useInheritedColor(nodeBorderColor, theme) const getLinkThickness = useLinkThickness(linkThickness) const getLinkColor = useInheritedColor(linkColor, theme) @@ -63,6 +61,9 @@ const InnerNetwork = ({ distanceMax, iterations, center: [innerWidth / 2, innerHeight / 2], + nodeColor, + nodeBorderWidth, + nodeBorderColor, }) const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -100,9 +101,6 @@ const InnerNetwork = ({ key="nodes" nodes={nodes} nodeComponent={nodeComponent} - color={getColor} - borderWidth={nodeBorderWidth} - borderColor={getBorderColor} onClick={isInteractive ? onClick : undefined} onMouseEnter={isInteractive ? handleNodeHover : undefined} onMouseMove={isInteractive ? handleNodeHover : undefined} diff --git a/packages/network/src/NetworkCanvas.tsx b/packages/network/src/NetworkCanvas.tsx index cbeb47d784..082b2dd1f3 100644 --- a/packages/network/src/NetworkCanvas.tsx +++ b/packages/network/src/NetworkCanvas.tsx @@ -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 = Omit< @@ -27,6 +27,7 @@ const InnerNetworkCanvas = ({ layers = canvasDefaultProps.layers, + renderNode = canvasDefaultProps.renderNode, nodeColor = canvasDefaultProps.nodeColor, nodeBorderWidth = canvasDefaultProps.nodeBorderWidth, nodeBorderColor = canvasDefaultProps.nodeBorderColor, @@ -54,11 +55,12 @@ const InnerNetworkCanvas = ({ 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) @@ -88,16 +90,7 @@ const InnerNetworkCanvas = ({ }) } 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, { @@ -118,9 +111,7 @@ const InnerNetworkCanvas = ({ theme, nodes, links, - getNodeColor, - nodeBorderWidth, - getBorderColor, + renderNode, getLinkThickness, getLinkColor, ]) diff --git a/packages/network/src/NetworkNodes.tsx b/packages/network/src/NetworkNodes.tsx index 7f729f8a33..55bbfb33b2 100644 --- a/packages/network/src/NetworkNodes.tsx +++ b/packages/network/src/NetworkNodes.tsx @@ -5,70 +5,51 @@ import { NetworkComputedNode, NetworkInputNode, NetworkNodeAnimatedProps, - NetworkNodeColor, NetworkNodeComponent, } from './types' interface NetworkNodesProps { nodes: NetworkComputedNode[] nodeComponent: NetworkNodeComponent - color: Exclude, string> - borderWidth: number - borderColor: (node: NetworkComputedNode) => string onClick?: (node: NetworkComputedNode, event: MouseEvent) => void onMouseEnter?: (node: NetworkComputedNode, event: MouseEvent) => void onMouseMove?: (node: NetworkComputedNode, event: MouseEvent) => void onMouseLeave?: (node: NetworkComputedNode, event: MouseEvent) => void } -const getEnterTransition = ( - color: NetworkNodesProps['color'], - borderWidth: number, - borderColor: NetworkNodesProps['borderColor'] -) => (node: NetworkComputedNode) => ({ +const getEnterTransition = () => (node: NetworkComputedNode) => ({ 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 = ( - color: NetworkNodesProps['color'], - borderWidth: number, - borderColor: NetworkNodesProps['borderColor'] -) => (node: NetworkComputedNode) => ({ +const getRegularTransition = () => (node: NetworkComputedNode) => ({ 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 = ( - color: NetworkNodesProps['color'], - borderWidth: number, - borderColor: NetworkNodesProps['borderColor'] -) => (node: NetworkComputedNode) => ({ +const getExitTransition = () => (node: NetworkComputedNode) => ({ 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 = ({ nodes, nodeComponent, - color, - borderColor, - borderWidth, onClick, onMouseEnter, onMouseMove, @@ -77,12 +58,8 @@ export const NetworkNodes = ({ 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(), getRegularTransition(), getExitTransition()], + [] ) const transition = useTransition, NetworkNodeAnimatedProps>(nodes, { diff --git a/packages/network/src/defaults.ts b/packages/network/src/defaults.ts index feb558883b..39f6a86728 100644 --- a/packages/network/src/defaults.ts +++ b/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[], @@ -34,5 +35,6 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...commonDefaultProps, + renderNode: renderCanvasNode, pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, } diff --git a/packages/network/src/hooks.ts b/packages/network/src/hooks.ts index 9ff764f479..4bfacb8d19 100644 --- a/packages/network/src/hooks.ts +++ b/packages/network/src/hooks.ts @@ -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, @@ -12,6 +13,7 @@ import { NetworkComputedNode, ComputedLink, } from './types' +import { useInheritedColor } from '@nivo/colors' const computeForces = ({ linkDistance, @@ -58,6 +60,9 @@ export const useNetwork = ({ distanceMax, center, iterations, + nodeColor, + nodeBorderWidth, + nodeBorderColor, }: { nodes: N[] links: InputLink[] @@ -67,6 +72,9 @@ export const useNetwork = ({ distanceMax: NetworkCommonProps['distanceMax'] center: [number, number] iterations: NetworkCommonProps['iterations'] + nodeColor: NetworkCommonProps['nodeColor'] + nodeBorderWidth: NetworkCommonProps['nodeBorderWidth'] + nodeBorderColor: NetworkCommonProps['nodeBorderColor'] }): [null | NetworkComputedNode[], null | ComputedLink[]] => { const [currentNodes, setCurrentNodes] = useState[]>(null) const [currentLinks, setCurrentLinks] = useState[]>(null) @@ -124,7 +132,24 @@ export const useNetwork = ({ center[1], ]) - return [currentNodes, currentLinks] + const theme = useTheme() + const getNodeColor = useNodeColor(nodeColor) + const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme) + + const enhancedNodes: NetworkComputedNode[] | 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 = (color: NetworkNodeColor) => diff --git a/packages/network/src/renderCanvasNode.ts b/packages/network/src/renderCanvasNode.ts new file mode 100644 index 0000000000..e5555a785f --- /dev/null +++ b/packages/network/src/renderCanvasNode.ts @@ -0,0 +1,17 @@ +import { NetworkComputedNode, NetworkInputNode } from './types' + +export const renderCanvasNode = ( + ctx: CanvasRenderingContext2D, + node: NetworkComputedNode +) => { + 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() + } +} diff --git a/packages/network/src/types.ts b/packages/network/src/types.ts index 38b05be282..bd63441448 100644 --- a/packages/network/src/types.ts +++ b/packages/network/src/types.ts @@ -13,8 +13,9 @@ export interface NetworkComputedNode { y: number radius: number color: string + borderWidth: number + borderColor: string data: N - [key: string]: unknown } export interface NetworkNodeAnimatedProps { @@ -40,6 +41,10 @@ export interface NetworkNodeProps { export type NetworkNodeComponent = FunctionComponent< NetworkNodeProps > +export type NetworkNodeCanvasRenderer = ( + ctx: CanvasRenderingContext2D, + node: NetworkComputedNode +) => void export interface InputLink { source: string @@ -148,5 +153,6 @@ export type NetworkCanvasProps = Partial)[] + renderNode?: NetworkNodeCanvasRenderer pixelRatio?: number } diff --git a/website/src/data/components/network/props.ts b/website/src/data/components/network/props.ts index 21b56c9e22..946cde8c63 100644 --- a/website/src/data/components/network/props.ts +++ b/website/src/data/components/network/props.ts @@ -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', @@ -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'],