From 2b26bf3d137e60197eb3bf92e9a0d17e723f5d4e Mon Sep 17 00:00:00 2001 From: plouc Date: Sun, 12 Sep 2021 11:23:50 +0900 Subject: [PATCH] feat(network): add support for custom link component --- packages/network/src/Network.tsx | 19 ++++-------- packages/network/src/NetworkCanvas.tsx | 24 +++++---------- packages/network/src/NetworkLink.tsx | 20 +++---------- packages/network/src/NetworkLinks.tsx | 38 ++++++++---------------- packages/network/src/defaults.ts | 4 +++ packages/network/src/hooks.ts | 20 ++++++++++++- packages/network/src/renderCanvasLink.ts | 14 +++++++++ packages/network/src/types.ts | 20 +++++++++++-- 8 files changed, 84 insertions(+), 75 deletions(-) create mode 100644 packages/network/src/renderCanvasLink.ts diff --git a/packages/network/src/Network.tsx b/packages/network/src/Network.tsx index b2e4f6779c..e83780d6ed 100644 --- a/packages/network/src/Network.tsx +++ b/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' @@ -33,6 +32,7 @@ const InnerNetwork = ({ nodeBorderWidth = svgDefaultProps.nodeBorderWidth, nodeBorderColor = svgDefaultProps.nodeBorderColor, + linkComponent = svgDefaultProps.linkComponent, linkThickness = svgDefaultProps.linkThickness, linkColor = svgDefaultProps.linkColor, @@ -48,10 +48,6 @@ const InnerNetwork = ({ partialMargin ) - const theme = useTheme() - const getLinkThickness = useLinkThickness(linkThickness) - const getLinkColor = useInheritedColor(linkColor, theme) - const [nodes, links] = useNetwork({ nodes: rawNodes, links: rawLinks, @@ -64,6 +60,8 @@ const InnerNetwork = ({ nodeColor, nodeBorderWidth, nodeBorderColor, + linkThickness, + linkColor, }) const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -86,12 +84,7 @@ const InnerNetwork = ({ if (layers.includes('links') && links !== null) { layerById.links = ( - - key="links" - links={links} - linkThickness={getLinkThickness} - linkColor={getLinkColor} - /> + key="links" links={links} linkComponent={linkComponent} /> ) } diff --git a/packages/network/src/NetworkCanvas.tsx b/packages/network/src/NetworkCanvas.tsx index 082b2dd1f3..1da1b3ff6d 100644 --- a/packages/network/src/NetworkCanvas.tsx +++ b/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 = Omit< @@ -32,6 +31,7 @@ const InnerNetworkCanvas = ({ nodeBorderWidth = canvasDefaultProps.nodeBorderWidth, nodeBorderColor = canvasDefaultProps.nodeBorderColor, + renderLink = canvasDefaultProps.renderLink, linkThickness = canvasDefaultProps.linkThickness, linkColor = canvasDefaultProps.linkColor, @@ -58,11 +58,11 @@ const InnerNetworkCanvas = ({ nodeColor, nodeBorderWidth, nodeBorderColor, + linkThickness, + linkColor, }) const theme = useTheme() - const getLinkThickness = useLinkThickness(linkThickness) - const getLinkColor = useInheritedColor(linkColor, theme) useEffect(() => { if (canvasEl.current === null) return @@ -80,18 +80,9 @@ const InnerNetworkCanvas = ({ 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, @@ -112,8 +103,7 @@ const InnerNetworkCanvas = ({ nodes, links, renderNode, - getLinkThickness, - getLinkColor, + renderLink, ]) const getNodeFromMouseEvent = useCallback( diff --git a/packages/network/src/NetworkLink.tsx b/packages/network/src/NetworkLink.tsx index 5a88029f9e..56ecb7d68e 100644 --- a/packages/network/src/NetworkLink.tsx +++ b/packages/network/src/NetworkLink.tsx @@ -1,26 +1,14 @@ -import { AnimatedProps, animated } from '@react-spring/web' -import { ComputedLink, NetworkInputNode } from './types' - -interface NetworkLinkProps { - link: ComputedLink - 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 = ({ - thickness, + link, animated: animatedProps, }: NetworkLinkProps) => { return ( { links: ComputedLink[] - linkThickness: (link: ComputedLink) => number - linkColor: (link: ComputedLink) => string + linkComponent: NetworkLinkComponent } -const getEnterTransition = ( - linkColor: NetworkLinksProps['linkColor'] -) => (link: ComputedLink) => ({ +const getEnterTransition = () => (link: ComputedLink) => ({ 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 = ( - linkColor: NetworkLinksProps['linkColor'] -) => (link: ComputedLink) => ({ +const getRegularTransition = () => (link: ComputedLink) => ({ 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 = ( - linkColor: NetworkLinksProps['linkColor'] -) => (link: ComputedLink) => ({ +const getExitTransition = () => (link: ComputedLink) => ({ 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 = ({ links, - linkThickness, - linkColor, + linkComponent, }: NetworkLinksProps) => { const { animate, config: springConfig } = useMotionConfig() const [enterTransition, regularTransition, exitTransition] = useMemo( - () => [ - getEnterTransition(linkColor), - getRegularTransition(linkColor), - getExitTransition(linkColor), - ], - [linkColor] + () => [getEnterTransition(), getRegularTransition(), getExitTransition()], + [] ) const transition = useTransition< @@ -84,10 +71,9 @@ export const NetworkLinks = ({ return ( <> {transition((transitionProps, link) => { - return createElement(NetworkLink, { + return createElement(linkComponent, { key: link.id, link, - thickness: linkThickness(link), animated: transitionProps, }) })} diff --git a/packages/network/src/defaults.ts b/packages/network/src/defaults.ts index 39f6a86728..c255519b3a 100644 --- a/packages/network/src/defaults.ts +++ b/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 = { @@ -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, } diff --git a/packages/network/src/hooks.ts b/packages/network/src/hooks.ts index 4bfacb8d19..6a40d864da 100644 --- a/packages/network/src/hooks.ts +++ b/packages/network/src/hooks.ts @@ -63,6 +63,8 @@ export const useNetwork = ({ nodeColor, nodeBorderWidth, nodeBorderColor, + linkThickness, + linkColor, }: { nodes: N[] links: InputLink[] @@ -75,6 +77,8 @@ export const useNetwork = ({ nodeColor: NetworkCommonProps['nodeColor'] nodeBorderWidth: NetworkCommonProps['nodeBorderWidth'] nodeBorderColor: NetworkCommonProps['nodeBorderColor'] + linkThickness: NetworkCommonProps['linkThickness'] + linkColor: NetworkCommonProps['linkColor'] }): [null | NetworkComputedNode[], null | ComputedLink[]] => { const [currentNodes, setCurrentNodes] = useState[]>(null) const [currentLinks, setCurrentLinks] = useState[]>(null) @@ -135,6 +139,8 @@ export const useNetwork = ({ const theme = useTheme() const getNodeColor = useNodeColor(nodeColor) const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme) + const getLinkThickness = useLinkThickness(linkThickness) + const getLinkColor = useInheritedColor(linkColor, theme) const enhancedNodes: NetworkComputedNode[] | null = useMemo(() => { if (currentNodes === null) return null @@ -149,7 +155,19 @@ export const useNetwork = ({ }) }, [currentNodes, getNodeColor, nodeBorderWidth, getNodeBorderColor]) - return [enhancedNodes, currentLinks] + const enhancedLinks: ComputedLink[] | 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 = (color: NetworkNodeColor) => diff --git a/packages/network/src/renderCanvasLink.ts b/packages/network/src/renderCanvasLink.ts new file mode 100644 index 0000000000..5837d0bb31 --- /dev/null +++ b/packages/network/src/renderCanvasLink.ts @@ -0,0 +1,14 @@ +import { NetworkInputNode, ComputedLink } from './types' + +export const renderCanvasLink = ( + ctx: CanvasRenderingContext2D, + link: ComputedLink +) => { + 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() +} diff --git a/packages/network/src/types.ts b/packages/network/src/types.ts index bd63441448..642139225d 100644 --- a/packages/network/src/types.ts +++ b/packages/network/src/types.ts @@ -49,14 +49,16 @@ export type NetworkNodeCanvasRenderer = ( export interface InputLink { source: string target: string - [key: string]: unknown } export interface ComputedLink { id: string source: NetworkComputedNode + previousSource?: NetworkComputedNode target: NetworkComputedNode - [key: string]: unknown + previousTarget?: NetworkComputedNode + thickness: number + color: string } export interface LinkAnimatedProps { @@ -68,6 +70,18 @@ export interface LinkAnimatedProps { opacity: number } +export interface NetworkLinkProps { + link: ComputedLink + animated: AnimatedProps +} +export type NetworkLinkComponent = FunctionComponent< + NetworkLinkProps +> +export type NetworkLinkCanvasRenderer = ( + ctx: CanvasRenderingContext2D, + node: ComputedLink +) => void + export interface NetworkDataProps { data: { nodes: N[] @@ -145,6 +159,7 @@ export type NetworkSvgProps = Partial)[] nodeComponent?: NetworkNodeComponent + linkComponent?: NetworkLinkComponent } export type NetworkCanvasProps = Partial> & @@ -154,5 +169,6 @@ export type NetworkCanvasProps = Partial)[] renderNode?: NetworkNodeCanvasRenderer + renderLink?: NetworkLinkCanvasRenderer pixelRatio?: number }