From 5575928da59f2c03e69272ff4ec55bb52b35ccf5 Mon Sep 17 00:00:00 2001 From: plouc Date: Thu, 30 Dec 2021 08:57:50 +0900 Subject: [PATCH] feat(network): add support for active nodes and annotations --- packages/network/package.json | 1 + packages/network/src/Network.tsx | 71 ++++-- packages/network/src/NetworkCanvas.tsx | 31 +-- packages/network/src/NetworkLink.tsx | 8 +- packages/network/src/NetworkLinks.tsx | 31 +-- packages/network/src/NetworkNode.tsx | 10 +- .../network/src/NetworkNodeAnnotations.tsx | 23 ++ packages/network/src/NetworkNodeTooltip.tsx | 8 +- packages/network/src/NetworkNodes.tsx | 50 ++-- packages/network/src/ResponsiveNetwork.tsx | 8 +- .../network/src/ResponsiveNetworkCanvas.tsx | 8 +- packages/network/src/defaults.ts | 30 ++- packages/network/src/hooks.ts | 213 ++++++++++++++---- packages/network/src/renderCanvasLink.ts | 6 +- packages/network/src/renderCanvasNode.ts | 8 +- packages/network/src/types.ts | 163 +++++++------- packages/network/tests/Network.test.tsx | 19 ++ website/src/data/components/network/mapper.ts | 22 ++ website/src/data/components/network/props.ts | 86 +++++-- website/src/pages/network/canvas.tsx | 17 +- website/src/pages/network/index.tsx | 50 +++- 21 files changed, 586 insertions(+), 277 deletions(-) create mode 100644 packages/network/src/NetworkNodeAnnotations.tsx create mode 100644 website/src/data/components/network/mapper.ts diff --git a/packages/network/package.json b/packages/network/package.json index fa6f7fe006..f1cfc45cb8 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -31,6 +31,7 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { + "@nivo/annotations": "0.76.0", "@nivo/colors": "0.76.0", "@nivo/tooltip": "0.76.0", "@react-spring/web": "9.3.1", diff --git a/packages/network/src/Network.tsx b/packages/network/src/Network.tsx index 8ccf52c376..0abb007a2f 100644 --- a/packages/network/src/Network.tsx +++ b/packages/network/src/Network.tsx @@ -3,16 +3,18 @@ import { Container, useDimensions, SvgWrapper } from '@nivo/core' import { useTooltip } from '@nivo/tooltip' import { svgDefaultProps } from './defaults' import { useNetwork } from './hooks' -import { NetworkNodes } from './NetworkNodes' import { NetworkLinks } from './NetworkLinks' -import { NetworkInputNode, NetworkLayerId, NetworkSvgProps } from './types' +import { NetworkNodes } from './NetworkNodes' +import { NetworkNodeAnnotations } from './NetworkNodeAnnotations' + +import { InputNode, LayerId, NodeTooltip, NetworkSvgProps, ComputedNode } from './types' -type InnerNetworkProps = Omit< - NetworkSvgProps, +type InnerNetworkProps = Omit< + NetworkSvgProps, 'animate' | 'motionConfig' | 'renderWrapper' | 'theme' > -const InnerNetwork = ({ +const InnerNetwork = ({ width, height, margin: partialMargin, @@ -27,31 +29,42 @@ const InnerNetwork = ({ layers = svgDefaultProps.layers, - nodeComponent = svgDefaultProps.nodeComponent, + nodeComponent = svgDefaultProps.nodeComponent as NonNullable< + NetworkSvgProps['nodeComponent'] + >, + nodeSize = svgDefaultProps.nodeSize, + activeNodeSize = svgDefaultProps.activeNodeSize, + inactiveNodeSize = svgDefaultProps.inactiveNodeSize, nodeColor = svgDefaultProps.nodeColor, + nodeBlendMode = svgDefaultProps.nodeBlendMode, nodeBorderWidth = svgDefaultProps.nodeBorderWidth, nodeBorderColor = svgDefaultProps.nodeBorderColor, - linkComponent = svgDefaultProps.linkComponent, + linkComponent = svgDefaultProps.linkComponent as NonNullable< + NetworkSvgProps['linkComponent'] + >, linkThickness = svgDefaultProps.linkThickness, linkColor = svgDefaultProps.linkColor, + linkBlendMode = svgDefaultProps.linkBlendMode, + + annotations = svgDefaultProps.annotations as NonNullable['annotations']>, isInteractive = svgDefaultProps.isInteractive, - nodeTooltip = svgDefaultProps.nodeTooltip, + nodeTooltip = svgDefaultProps.nodeTooltip as NodeTooltip, onClick, role = svgDefaultProps.role, ariaLabel, ariaLabelledBy, ariaDescribedBy, -}: InnerNetworkProps) => { +}: InnerNetworkProps) => { const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( width, height, partialMargin ) - const [nodes, links] = useNetwork({ + const { nodes, links, setActiveNodeIds } = useNetwork({ center: [innerWidth / 2, innerHeight / 2], nodes: rawNodes, links: rawLinks, @@ -60,6 +73,9 @@ const InnerNetwork = ({ distanceMin, distanceMax, iterations, + nodeSize, + activeNodeSize, + inactiveNodeSize, nodeColor, nodeBorderWidth, nodeBorderColor, @@ -70,33 +86,44 @@ const InnerNetwork = ({ const { showTooltipFromEvent, hideTooltip } = useTooltip() const handleNodeHover = useCallback( - (node, event) => { + (node: ComputedNode, event) => { showTooltipFromEvent(createElement(nodeTooltip, { node }), event) + setActiveNodeIds([node.id]) }, - [showTooltipFromEvent, nodeTooltip] + [showTooltipFromEvent, nodeTooltip, setActiveNodeIds] ) const handleNodeLeave = useCallback(() => { hideTooltip() - }, [hideTooltip]) + setActiveNodeIds([]) + }, [hideTooltip, setActiveNodeIds]) - const layerById: Record = { + const layerById: Record = { links: null, nodes: null, + annotations: null, } + console.log(nodes) + if (layers.includes('links') && links !== null) { layerById.links = ( - key="links" links={links} linkComponent={linkComponent} /> + + key="links" + links={links} + linkComponent={linkComponent} + blendMode={linkBlendMode} + /> ) } if (layers.includes('nodes') && nodes !== null) { layerById.nodes = ( - + key="nodes" nodes={nodes} nodeComponent={nodeComponent} + blendMode={nodeBlendMode} onClick={isInteractive ? onClick : undefined} onMouseEnter={isInteractive ? handleNodeHover : undefined} onMouseMove={isInteractive ? handleNodeHover : undefined} @@ -105,6 +132,12 @@ const InnerNetwork = ({ ) } + if (layers.includes('annotations') && nodes !== null) { + layerById.annotations = ( + nodes={nodes} annotations={annotations} /> + ) + } + return ( ({ ) } -export const Network = ({ +export const Network = ({ isInteractive = svgDefaultProps.isInteractive, animate = svgDefaultProps.animate, motionConfig = svgDefaultProps.motionConfig, theme, renderWrapper, ...otherProps -}: NetworkSvgProps) => ( +}: NetworkSvgProps) => ( ({ theme, }} > - isInteractive={isInteractive} {...otherProps} /> + isInteractive={isInteractive} {...otherProps} /> ) diff --git a/packages/network/src/NetworkCanvas.tsx b/packages/network/src/NetworkCanvas.tsx index 0706edaab3..dd9d224989 100644 --- a/packages/network/src/NetworkCanvas.tsx +++ b/packages/network/src/NetworkCanvas.tsx @@ -3,14 +3,14 @@ import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } fr import { useTooltip } from '@nivo/tooltip' import { canvasDefaultProps } from './defaults' import { useNetwork } from './hooks' -import { NetworkCanvasProps, NetworkInputNode } from './types' +import { NetworkCanvasProps, InputNode, NodeTooltip } from './types' -type InnerNetworkCanvasProps = Omit< - NetworkCanvasProps, +type InnerNetworkCanvasProps = Omit< + NetworkCanvasProps, 'renderWrapper' | 'theme' > -const InnerNetworkCanvas = ({ +const InnerNetworkCanvas = ({ width, height, margin: partialMargin, @@ -27,6 +27,7 @@ const InnerNetworkCanvas = ({ layers = canvasDefaultProps.layers, renderNode = canvasDefaultProps.renderNode, + nodeSize = canvasDefaultProps.nodeSize, nodeColor = canvasDefaultProps.nodeColor, nodeBorderWidth = canvasDefaultProps.nodeBorderWidth, nodeBorderColor = canvasDefaultProps.nodeBorderColor, @@ -36,9 +37,9 @@ const InnerNetworkCanvas = ({ linkColor = canvasDefaultProps.linkColor, isInteractive = canvasDefaultProps.isInteractive, - nodeTooltip = canvasDefaultProps.nodeTooltip, + nodeTooltip = canvasDefaultProps.nodeTooltip as NodeTooltip, onClick, -}: InnerNetworkCanvasProps) => { +}: InnerNetworkCanvasProps) => { const canvasEl = useRef(null) const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( width, @@ -46,7 +47,7 @@ const InnerNetworkCanvas = ({ partialMargin ) - const [nodes, links] = useNetwork({ + const { nodes, links, setActiveNodeIds } = useNetwork({ center: [innerWidth / 2, innerHeight / 2], nodes: rawNodes, links: rawLinks, @@ -55,6 +56,7 @@ const InnerNetworkCanvas = ({ distanceMin, distanceMax, iterations, + nodeSize, nodeColor, nodeBorderWidth, nodeBorderColor, @@ -119,7 +121,7 @@ const InnerNetworkCanvas = ({ x - margin.left, y - margin.top ) - return distanceFromNode <= node.radius + return distanceFromNode <= node.size / 2 }) }, [canvasEl, margin, nodes] @@ -132,16 +134,19 @@ const InnerNetworkCanvas = ({ const node = getNodeFromMouseEvent(event) if (node) { showTooltipFromEvent(createElement(nodeTooltip, { node }), event) + setActiveNodeIds([node.id]) } else { hideTooltip() + setActiveNodeIds([]) } }, - [getNodeFromMouseEvent, showTooltipFromEvent, nodeTooltip, hideTooltip] + [getNodeFromMouseEvent, showTooltipFromEvent, nodeTooltip, hideTooltip, setActiveNodeIds] ) const handleMouseLeave = useCallback(() => { hideTooltip() - }, [hideTooltip]) + setActiveNodeIds([]) + }, [hideTooltip, setActiveNodeIds]) const handleClick = useCallback( (event: MouseEvent) => { @@ -173,15 +178,15 @@ const InnerNetworkCanvas = ({ ) } -export const NetworkCanvas = ({ +export const NetworkCanvas = ({ theme, isInteractive = canvasDefaultProps.isInteractive, animate = canvasDefaultProps.animate, motionConfig = canvasDefaultProps.motionConfig, renderWrapper, ...otherProps -}: NetworkCanvasProps) => ( +}: NetworkCanvasProps) => ( - isInteractive={isInteractive} {...otherProps} /> + isInteractive={isInteractive} {...otherProps} /> ) diff --git a/packages/network/src/NetworkLink.tsx b/packages/network/src/NetworkLink.tsx index 56ecb7d68e..56bfdd835d 100644 --- a/packages/network/src/NetworkLink.tsx +++ b/packages/network/src/NetworkLink.tsx @@ -1,13 +1,15 @@ import { animated } from '@react-spring/web' -import { NetworkInputNode, NetworkLinkProps } from './types' +import { InputNode, LinkProps } from './types' -export const NetworkLink = ({ +export const NetworkLink = ({ link, animated: animatedProps, -}: NetworkLinkProps) => { + blendMode, +}: LinkProps) => { return ( { - links: ComputedLink[] - linkComponent: NetworkLinkComponent +interface NetworkLinksProps { + links: ComputedLink[] + linkComponent: LinkComponent + blendMode: NonNullable['linkBlendMode']> } const getEnterTransition = - () => - (link: ComputedLink) => ({ + () => + (link: ComputedLink) => ({ x1: link.source.x, y1: link.source.y, x2: link.source.x, @@ -20,8 +21,8 @@ const getEnterTransition = }) const getRegularTransition = - () => - (link: ComputedLink) => ({ + () => + (link: ComputedLink) => ({ x1: link.source.x, y1: link.source.y, x2: link.target.x, @@ -31,8 +32,8 @@ const getRegularTransition = }) const getExitTransition = - () => - (link: ComputedLink) => ({ + () => + (link: ComputedLink) => ({ x1: link.source.x, y1: link.source.y, x2: link.source.x, @@ -41,19 +42,20 @@ const getExitTransition = opacity: 0, }) -export const NetworkLinks = ({ +export const NetworkLinks = ({ links, linkComponent, -}: NetworkLinksProps) => { + blendMode, +}: NetworkLinksProps) => { const { animate, config: springConfig } = useMotionConfig() const [enterTransition, regularTransition, exitTransition] = useMemo( - () => [getEnterTransition(), getRegularTransition(), getExitTransition()], + () => [getEnterTransition(), getRegularTransition(), getExitTransition()], [] ) const transition = useTransition< - ComputedLink, + ComputedLink, { x1: number y1: number @@ -81,6 +83,7 @@ export const NetworkLinks = ({ key: link.id, link, animated: transitionProps, + blendMode, }) })} diff --git a/packages/network/src/NetworkNode.tsx b/packages/network/src/NetworkNode.tsx index 10a962b488..94dac8a2c3 100644 --- a/packages/network/src/NetworkNode.tsx +++ b/packages/network/src/NetworkNode.tsx @@ -1,14 +1,15 @@ import { animated, to } from '@react-spring/web' -import { NetworkInputNode, NetworkNodeProps } from './types' +import { InputNode, NodeProps } from './types' -export const NetworkNode = ({ +export const NetworkNode = ({ node, animated: animatedProps, + blendMode, onClick, onMouseEnter, onMouseMove, onMouseLeave, -}: NetworkNodeProps) => { +}: NodeProps) => { return ( ({ return `translate(${x},${y}) scale(${scale})` } )} - r={animatedProps.radius} + 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} diff --git a/packages/network/src/NetworkNodeAnnotations.tsx b/packages/network/src/NetworkNodeAnnotations.tsx new file mode 100644 index 0000000000..07601ab8d0 --- /dev/null +++ b/packages/network/src/NetworkNodeAnnotations.tsx @@ -0,0 +1,23 @@ +import { Annotation } from '@nivo/annotations' +import { ComputedNode, InputNode, NetworkSvgProps } from './types' +import { useNodeAnnotations } from './hooks' + +interface NetworkNodeAnnotationsProps { + nodes: ComputedNode[] + annotations: NonNullable['annotations']> +} + +export const NetworkNodeAnnotations = ({ + nodes, + annotations, +}: NetworkNodeAnnotationsProps) => { + const boundAnnotations = useNodeAnnotations(nodes, annotations) + + return ( + <> + {boundAnnotations.map((annotation, i) => ( + + ))} + + ) +} diff --git a/packages/network/src/NetworkNodeTooltip.tsx b/packages/network/src/NetworkNodeTooltip.tsx index 56b5a51c44..be335a0efe 100644 --- a/packages/network/src/NetworkNodeTooltip.tsx +++ b/packages/network/src/NetworkNodeTooltip.tsx @@ -1,6 +1,6 @@ import { BasicTooltip } from '@nivo/tooltip' -import { NetworkInputNode, NetworkNodeTooltipProps } from './types' +import { InputNode, NodeTooltipProps } from './types' -export const NetworkNodeTooltip = ({ - node, -}: NetworkNodeTooltipProps) => +export const NetworkNodeTooltip = ({ node }: NodeTooltipProps) => ( + +) diff --git a/packages/network/src/NetworkNodes.tsx b/packages/network/src/NetworkNodes.tsx index d6ebf39f95..43f7eb1fc8 100644 --- a/packages/network/src/NetworkNodes.tsx +++ b/packages/network/src/NetworkNodes.tsx @@ -1,28 +1,24 @@ import { createElement, MouseEvent, useMemo } from 'react' import { useTransition } from '@react-spring/web' import { useMotionConfig } from '@nivo/core' -import { - NetworkComputedNode, - NetworkInputNode, - NetworkNodeAnimatedProps, - NetworkNodeComponent, -} from './types' +import { InputNode, ComputedNode, NodeAnimatedProps, NodeComponent, NetworkSvgProps } from './types' -interface NetworkNodesProps { - nodes: NetworkComputedNode[] - nodeComponent: NetworkNodeComponent - onClick?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseEnter?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseMove?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseLeave?: (node: NetworkComputedNode, event: MouseEvent) => void +interface NetworkNodesProps { + nodes: ComputedNode[] + nodeComponent: NodeComponent + blendMode: NonNullable['nodeBlendMode']> + onClick?: (node: ComputedNode, event: MouseEvent) => void + onMouseEnter?: (node: ComputedNode, event: MouseEvent) => void + onMouseMove?: (node: ComputedNode, event: MouseEvent) => void + onMouseLeave?: (node: ComputedNode, event: MouseEvent) => void } const getEnterTransition = - () => - (node: NetworkComputedNode) => ({ + () => + (node: ComputedNode) => ({ x: node.x, y: node.y, - radius: node.radius, + size: node.size, color: node.color, borderWidth: node.borderWidth, borderColor: node.borderColor, @@ -30,11 +26,11 @@ const getEnterTransition = }) const getRegularTransition = - () => - (node: NetworkComputedNode) => ({ + () => + (node: ComputedNode) => ({ x: node.x, y: node.y, - radius: node.radius, + size: node.size, color: node.color, borderWidth: node.borderWidth, borderColor: node.borderColor, @@ -42,33 +38,34 @@ const getRegularTransition = }) const getExitTransition = - () => - (node: NetworkComputedNode) => ({ + () => + (node: ComputedNode) => ({ x: node.x, y: node.y, - radius: node.radius, + size: node.size, color: node.color, borderWidth: node.borderWidth, borderColor: node.borderColor, scale: 0, }) -export const NetworkNodes = ({ +export const NetworkNodes = ({ nodes, nodeComponent, + blendMode, onClick, onMouseEnter, onMouseMove, onMouseLeave, -}: NetworkNodesProps) => { +}: NetworkNodesProps) => { const { animate, config: springConfig } = useMotionConfig() const [enterTransition, regularTransition, exitTransition] = useMemo( - () => [getEnterTransition(), getRegularTransition(), getExitTransition()], + () => [getEnterTransition(), getRegularTransition(), getExitTransition()], [] ) - const transition = useTransition, NetworkNodeAnimatedProps>(nodes, { + const transition = useTransition, NodeAnimatedProps>(nodes, { keys: node => node.id, initial: regularTransition, from: enterTransition, @@ -86,6 +83,7 @@ export const NetworkNodes = ({ key: node.id, node, animated: transitionProps, + blendMode, onClick, onMouseEnter, onMouseMove, diff --git a/packages/network/src/ResponsiveNetwork.tsx b/packages/network/src/ResponsiveNetwork.tsx index 873aece1ea..e5693e8de6 100644 --- a/packages/network/src/ResponsiveNetwork.tsx +++ b/packages/network/src/ResponsiveNetwork.tsx @@ -1,11 +1,11 @@ import { ResponsiveWrapper } from '@nivo/core' -import { NetworkInputNode, NetworkSvgProps } from './types' +import { InputNode, NetworkSvgProps } from './types' import { Network } from './Network' -export const ResponsiveNetwork = ( - props: Omit, 'height' | 'width'> +export const ResponsiveNetwork = ( + props: Omit, 'height' | 'width'> ) => ( - {({ width, height }) => width={width} height={height} {...props} />} + {({ width, height }) => width={width} height={height} {...props} />} ) diff --git a/packages/network/src/ResponsiveNetworkCanvas.tsx b/packages/network/src/ResponsiveNetworkCanvas.tsx index 22064a923e..53a651cb11 100644 --- a/packages/network/src/ResponsiveNetworkCanvas.tsx +++ b/packages/network/src/ResponsiveNetworkCanvas.tsx @@ -1,11 +1,11 @@ import { ResponsiveWrapper } from '@nivo/core' -import { NetworkCanvasProps, NetworkInputNode } from './types' +import { NetworkCanvasProps, InputNode } from './types' import { NetworkCanvas } from './NetworkCanvas' -export const ResponsiveNetworkCanvas = ( - props: Omit, 'height' | 'width'> +export const ResponsiveNetworkCanvas = ( + props: Omit, 'height' | 'width'> ) => ( - {({ width, height }) => width={width} height={height} {...props} />} + {({ width, height }) => width={width} height={height} {...props} />} ) diff --git a/packages/network/src/defaults.ts b/packages/network/src/defaults.ts index c255519b3a..a93b3fbee3 100644 --- a/packages/network/src/defaults.ts +++ b/packages/network/src/defaults.ts @@ -1,12 +1,25 @@ -import { NetworkLayerId } from './types' +import { NetworkCommonProps, InputNode, LayerId, NetworkSvgProps } 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 = { - layers: ['links', 'nodes'] as NetworkLayerId[], +export const commonDefaultProps: Omit< + NetworkCommonProps, + | 'margin' + | 'theme' + | 'activeLinkThickness' + | 'defaultActiveNodeIds' + | 'onClick' + | 'renderWrapper' + | 'ariaLabel' + | 'ariaLabelledBy' + | 'ariaDescribedBy' +> & { + layers: LayerId[] +} = { + layers: ['links', 'nodes', 'annotations'], linkDistance: 30, repulsivity: 10, @@ -14,6 +27,9 @@ export const commonDefaultProps = { distanceMax: Infinity, iterations: 90, + nodeSize: 12, + activeNodeSize: 18, + inactiveNodeSize: 8, nodeColor: '#000000', nodeBorderWidth: 0, nodeBorderColor: { from: 'color' }, @@ -24,6 +40,8 @@ export const commonDefaultProps = { isInteractive: true, nodeTooltip: NetworkNodeTooltip, + annotations: [], + animate: true, motionConfig: 'gentle' as const, @@ -32,8 +50,10 @@ export const commonDefaultProps = { export const svgDefaultProps = { ...commonDefaultProps, - nodeComponent: NetworkNode, - linkComponent: NetworkLink, + nodeComponent: NetworkNode as NonNullable['nodeComponent']>, + nodeBlendMode: 'normal' as NonNullable['nodeBlendMode']>, + linkComponent: NetworkLink as NonNullable['linkComponent']>, + linkBlendMode: 'normal' as NonNullable['linkBlendMode']>, } export const canvasDefaultProps = { diff --git a/packages/network/src/hooks.ts b/packages/network/src/hooks.ts index 6a20b7114c..4e0d3feed5 100644 --- a/packages/network/src/hooks.ts +++ b/packages/network/src/hooks.ts @@ -1,32 +1,33 @@ -import { useState, useEffect, useMemo } from 'react' +import { useState, useEffect, useMemo, useCallback } from 'react' 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 { useInheritedColor } from '@nivo/colors' +import { AnnotationMatcher, useAnnotations } from '@nivo/annotations' import { commonDefaultProps } from './defaults' import { InputLink, - NetworkInputNode, + InputNode, NetworkCommonProps, - NetworkNodeColor, - NetworkLinkThickness, - NetworkComputedNode, + NodeDerivedProp, + LinkDerivedProp, + ComputedNode, ComputedLink, } from './types' -const computeForces = ({ +const computeForces = ({ linkDistance, repulsivity, distanceMin, distanceMax, center, }: { - linkDistance: NetworkCommonProps['linkDistance'] - repulsivity: NetworkCommonProps['repulsivity'] - distanceMin: NetworkCommonProps['distanceMin'] - distanceMax: NetworkCommonProps['distanceMax'] + linkDistance: NetworkCommonProps['linkDistance'] + repulsivity: NetworkCommonProps['repulsivity'] + distanceMin: NetworkCommonProps['distanceMin'] + distanceMax: NetworkCommonProps['distanceMax'] center: [number, number] }) => { let getLinkDistance @@ -52,19 +53,90 @@ const computeForces = ({ return { link: linkForce, charge: chargeForce, center: centerForce } } -const useNodeColor = (color: NetworkNodeColor) => +const useNodeDerivedProp = ( + instruction: NodeDerivedProp +) => useMemo(() => { - if (typeof color === 'function') return color - return () => color - }, [color]) + if (typeof instruction === 'function') return instruction + return () => instruction + }, [instruction]) -const useLinkThickness = (thickness: NetworkLinkThickness) => +const useLinkDerivedProp = ( + instruction: LinkDerivedProp +) => useMemo(() => { - if (typeof thickness === 'function') return thickness - return () => thickness - }, [thickness]) + if (typeof instruction === 'function') return instruction + return () => instruction + }, [instruction]) -export const useNetwork = ({ +const useNodeStyle = ({ + size, + activeSize, + inactiveSize, + color, + borderWidth, + borderColor, + isInteractive, + activeNodeIds, +}: { + size: NetworkCommonProps['nodeSize'] + activeSize: NetworkCommonProps['activeNodeSize'] + inactiveSize: NetworkCommonProps['inactiveNodeSize'] + color: NetworkCommonProps['nodeColor'] + borderWidth: NetworkCommonProps['nodeBorderWidth'] + borderColor: NetworkCommonProps['nodeBorderColor'] + isInteractive: NetworkCommonProps['isInteractive'] + activeNodeIds: string[] +}) => { + const theme = useTheme() + + const getSize = useNodeDerivedProp(size) + const getColor = useNodeDerivedProp(color) + const getBorderWidth = useNodeDerivedProp(borderWidth) + const getBorderColor = useInheritedColor(borderColor, theme) + const getNormalStyle = useCallback( + (node: ComputedNode) => ({ + size: getSize(node), + color: getColor(node), + borderWidth: getBorderWidth(node), + borderColor: getBorderColor(node), + }), + [getSize, getColor, getBorderWidth, getBorderColor] + ) + + const getActiveSize = useNodeDerivedProp(activeSize) + const getActiveStyle = useCallback( + (node: ComputedNode) => ({ + size: getActiveSize(node), + color: getColor(node), + borderWidth: getBorderWidth(node), + borderColor: getBorderColor(node), + }), + [getActiveSize, getColor, getBorderWidth, getBorderColor] + ) + + const getInactiveSize = useNodeDerivedProp(inactiveSize) + const getInactiveStyle = useCallback( + (node: ComputedNode) => ({ + size: getInactiveSize(node), + color: getColor(node), + borderWidth: getBorderWidth(node), + borderColor: getBorderColor(node), + }), + [getInactiveSize, getColor, getBorderWidth, getBorderColor] + ) + + return useCallback( + (node: ComputedNode) => { + if (!isInteractive || activeNodeIds.length === 0) return getNormalStyle(node) + if (activeNodeIds.includes(node.id)) return getActiveStyle(node) + return getInactiveStyle(node) + }, + [getNormalStyle, getActiveStyle, getInactiveStyle, isInteractive, activeNodeIds] + ) +} + +export const useNetwork = ({ center, nodes, links, @@ -73,36 +145,44 @@ export const useNetwork = ({ distanceMin = commonDefaultProps.distanceMin, distanceMax = commonDefaultProps.distanceMax, iterations = commonDefaultProps.iterations, + nodeSize = commonDefaultProps.nodeSize, + activeNodeSize = commonDefaultProps.activeNodeSize, + inactiveNodeSize = commonDefaultProps.inactiveNodeSize, nodeColor = commonDefaultProps.nodeColor, nodeBorderWidth = commonDefaultProps.nodeBorderWidth, nodeBorderColor = commonDefaultProps.nodeBorderColor, linkThickness = commonDefaultProps.linkThickness, linkColor = commonDefaultProps.linkColor, + isInteractive = commonDefaultProps.isInteractive, }: { center: [number, number] - nodes: N[] + nodes: Node[] links: InputLink[] - linkDistance?: NetworkCommonProps['linkDistance'] - repulsivity?: NetworkCommonProps['repulsivity'] - distanceMin?: NetworkCommonProps['distanceMin'] - distanceMax?: NetworkCommonProps['distanceMax'] - iterations?: NetworkCommonProps['iterations'] - nodeColor?: NetworkCommonProps['nodeColor'] - nodeBorderWidth?: NetworkCommonProps['nodeBorderWidth'] - nodeBorderColor?: NetworkCommonProps['nodeBorderColor'] - linkThickness?: NetworkCommonProps['linkThickness'] - linkColor?: NetworkCommonProps['linkColor'] -}): [null | NetworkComputedNode[], null | ComputedLink[]] => { + linkDistance?: NetworkCommonProps['linkDistance'] + repulsivity?: NetworkCommonProps['repulsivity'] + distanceMin?: NetworkCommonProps['distanceMin'] + distanceMax?: NetworkCommonProps['distanceMax'] + iterations?: NetworkCommonProps['iterations'] + nodeSize?: NetworkCommonProps['nodeSize'] + activeNodeSize?: NetworkCommonProps['activeNodeSize'] + inactiveNodeSize?: NetworkCommonProps['inactiveNodeSize'] + nodeColor?: NetworkCommonProps['nodeColor'] + nodeBorderWidth?: NetworkCommonProps['nodeBorderWidth'] + nodeBorderColor?: NetworkCommonProps['nodeBorderColor'] + linkThickness?: NetworkCommonProps['linkThickness'] + linkColor?: NetworkCommonProps['linkColor'] + isInteractive?: NetworkCommonProps['isInteractive'] +}) => { // 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) - const [currentLinks, setCurrentLinks] = useState[]>(null) + const [currentNodes, setCurrentNodes] = useState[]>(null) + const [currentLinks, setCurrentLinks] = useState[]>(null) const centerX = center[0] const centerY = center[1] useEffect(() => { - const forces = computeForces({ + const forces = computeForces({ linkDistance, repulsivity, distanceMin, @@ -110,8 +190,9 @@ export const useNetwork = ({ center: [centerX, centerY], }) - const nodesCopy: N[] = nodes.map(node => ({ ...node })) + const nodesCopy: Node[] = nodes.map(node => ({ ...node })) const linksCopy: InputLink[] = links.map(link => ({ + // generate a unique id for each link id: `${link.source}.${link.target}`, ...link, })) @@ -125,9 +206,9 @@ export const useNetwork = ({ simulation.tick(iterations) // d3 mutates data, hence the castings - setCurrentNodes(nodesCopy as unknown as NetworkComputedNode[]) + setCurrentNodes(nodesCopy as unknown as ComputedNode[]) setCurrentLinks( - (linksCopy as unknown as ComputedLink[]).map(link => { + (linksCopy as unknown as ComputedLink[]).map(link => { link.previousSource = currentNodes ? currentNodes.find(n => n.id === link.source.id) : undefined @@ -140,8 +221,7 @@ export const useNetwork = ({ ) return () => { - // prevent the simulation from continuing in case the data is updated, - // would be a waste of resource. + // prevent the simulation from continuing in case the data is updated. simulation.stop() } }, [ @@ -156,26 +236,35 @@ export const useNetwork = ({ iterations, ]) + const [activeNodeIds, setActiveNodeIds] = useState([]) + const theme = useTheme() - const getNodeColor = useNodeColor(nodeColor) - const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme) - const getLinkThickness = useLinkThickness(linkThickness) + const getLinkThickness = useLinkDerivedProp(linkThickness) const getLinkColor = useInheritedColor(linkColor, theme) - const enhancedNodes: NetworkComputedNode[] | null = useMemo(() => { + const getNodeStyle = useNodeStyle({ + size: nodeSize, + activeSize: activeNodeSize, + inactiveSize: inactiveNodeSize, + color: nodeColor, + borderWidth: nodeBorderWidth, + borderColor: nodeBorderColor, + isInteractive, + activeNodeIds, + }) + + const enhancedNodes: ComputedNode[] | null = useMemo(() => { if (currentNodes === null) return null return currentNodes.map(node => { return { ...node, - color: getNodeColor(node), - borderWidth: nodeBorderWidth, - borderColor: getNodeBorderColor(node), + ...getNodeStyle(node), } }) - }, [currentNodes, getNodeColor, nodeBorderWidth, getNodeBorderColor]) + }, [currentNodes, getNodeStyle]) - const enhancedLinks: ComputedLink[] | null = useMemo(() => { + const enhancedLinks: ComputedLink[] | null = useMemo(() => { if (currentLinks === null) return null return currentLinks.map(link => { @@ -187,5 +276,31 @@ export const useNetwork = ({ }) }, [currentLinks, getLinkThickness, getLinkColor]) - return [enhancedNodes, enhancedLinks] + return { + nodes: enhancedNodes, + links: enhancedLinks, + setActiveNodeIds, + } } + +const getNodeAnnotationPosition = (node: ComputedNode) => ({ + x: node.x, + y: node.y, +}) + +const getNodeAnnotationDimensions = (node: ComputedNode) => ({ + size: node.size, + width: node.size, + height: node.size, +}) + +export const useNodeAnnotations = ( + nodes: ComputedNode[], + annotations: AnnotationMatcher>[] +) => + useAnnotations>({ + data: nodes, + annotations, + getPosition: getNodeAnnotationPosition, + getDimensions: getNodeAnnotationDimensions, + }) diff --git a/packages/network/src/renderCanvasLink.ts b/packages/network/src/renderCanvasLink.ts index 5837d0bb31..3f019632a8 100644 --- a/packages/network/src/renderCanvasLink.ts +++ b/packages/network/src/renderCanvasLink.ts @@ -1,8 +1,8 @@ -import { NetworkInputNode, ComputedLink } from './types' +import { InputNode, ComputedLink } from './types' -export const renderCanvasLink = ( +export const renderCanvasLink = ( ctx: CanvasRenderingContext2D, - link: ComputedLink + link: ComputedLink ) => { ctx.strokeStyle = link.color ctx.lineWidth = link.thickness diff --git a/packages/network/src/renderCanvasNode.ts b/packages/network/src/renderCanvasNode.ts index e5555a785f..78643a0aae 100644 --- a/packages/network/src/renderCanvasNode.ts +++ b/packages/network/src/renderCanvasNode.ts @@ -1,12 +1,12 @@ -import { NetworkComputedNode, NetworkInputNode } from './types' +import { ComputedNode, InputNode } from './types' -export const renderCanvasNode = ( +export const renderCanvasNode = ( ctx: CanvasRenderingContext2D, - node: NetworkComputedNode + node: ComputedNode ) => { ctx.fillStyle = node.color ctx.beginPath() - ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI) + ctx.arc(node.x, node.y, node.size / 2, 0, 2 * Math.PI) ctx.fill() if (node.borderWidth > 0) { diff --git a/packages/network/src/types.ts b/packages/network/src/types.ts index 642139225d..a6a0b0ab3b 100644 --- a/packages/network/src/types.ts +++ b/packages/network/src/types.ts @@ -1,27 +1,28 @@ import { AriaAttributes, MouseEvent, FunctionComponent } from 'react' import { AnimatedProps } from '@react-spring/web' -import { Box, Theme, Dimensions, ModernMotionProps } from '@nivo/core' +import { Box, Theme, Dimensions, ModernMotionProps, CssMixBlendMode } from '@nivo/core' import { InheritedColorConfig } from '@nivo/colors' +import { AnnotationMatcher } from '@nivo/annotations' -export interface NetworkInputNode { +export interface InputNode { id: string } -export interface NetworkComputedNode { +export interface ComputedNode { id: string + data: Node x: number y: number - radius: number + size: number color: string borderWidth: number borderColor: string - data: N } -export interface NetworkNodeAnimatedProps { +export interface NodeAnimatedProps { x: number y: number - radius: number + size: number color: string borderWidth: number borderColor: string @@ -29,21 +30,19 @@ export interface NetworkNodeAnimatedProps { scale: number } -export interface NetworkNodeProps { - node: NetworkComputedNode - animated: AnimatedProps - scale?: number - onClick?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseEnter?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseMove?: (node: NetworkComputedNode, event: MouseEvent) => void - onMouseLeave?: (node: NetworkComputedNode, event: MouseEvent) => void +export interface NodeProps { + node: ComputedNode + animated: AnimatedProps + blendMode: NonNullable['nodeBlendMode']> + onClick?: (node: ComputedNode, event: MouseEvent) => void + onMouseEnter?: (node: ComputedNode, event: MouseEvent) => void + onMouseMove?: (node: ComputedNode, event: MouseEvent) => void + onMouseLeave?: (node: ComputedNode, event: MouseEvent) => void } -export type NetworkNodeComponent = FunctionComponent< - NetworkNodeProps -> -export type NetworkNodeCanvasRenderer = ( +export type NodeComponent = FunctionComponent> +export type NodeCanvasRenderer = ( ctx: CanvasRenderingContext2D, - node: NetworkComputedNode + node: ComputedNode ) => void export interface InputLink { @@ -51,12 +50,12 @@ export interface InputLink { target: string } -export interface ComputedLink { +export interface ComputedLink { id: string - source: NetworkComputedNode - previousSource?: NetworkComputedNode - target: NetworkComputedNode - previousTarget?: NetworkComputedNode + source: ComputedNode + previousSource?: ComputedNode + target: ComputedNode + previousTarget?: ComputedNode thickness: number color: string } @@ -70,63 +69,47 @@ export interface LinkAnimatedProps { opacity: number } -export interface NetworkLinkProps { - link: ComputedLink +export interface LinkProps { + link: ComputedLink animated: AnimatedProps + blendMode: NonNullable['linkBlendMode']> } -export type NetworkLinkComponent = FunctionComponent< - NetworkLinkProps -> -export type NetworkLinkCanvasRenderer = ( +export type LinkComponent = FunctionComponent> +export type LinkCanvasRenderer = ( ctx: CanvasRenderingContext2D, - node: ComputedLink + node: ComputedLink ) => void -export interface NetworkDataProps { +export interface NetworkDataProps { data: { - nodes: N[] + nodes: Node[] links: InputLink[] } } -export type NetworkLayerId = 'links' | 'nodes' -export interface NetworkCustomLayerProps { - nodes: NetworkComputedNode[] - links: ComputedLink[] +export type LayerId = 'links' | 'nodes' | 'annotations' +export interface CustomLayerProps { + nodes: ComputedNode[] + links: ComputedLink[] } -export type NetworkCustomLayer = FunctionComponent< - NetworkCustomLayerProps -> -export type NetworkCustomCanvasLayer = ( +export type CustomLayer = FunctionComponent> +export type CustomCanvasLayer = ( ctx: CanvasRenderingContext2D, - props: NetworkCustomLayerProps + props: CustomLayerProps ) => void -export interface NetworkNodeTooltipProps { - node: NetworkComputedNode +export interface NodeTooltipProps { + node: ComputedNode } -export type NetworkNodeTooltipComponent = FunctionComponent< - NetworkNodeTooltipProps -> +export type NodeTooltip = FunctionComponent> -// support static color or a dynamic function receiving the node -export type NetworkNodeColor = - | string - | ((node: NetworkComputedNode) => string) +export type NodeDerivedProp = T | ((node: ComputedNode) => T) +export type LinkDerivedProp = T | ((link: ComputedLink) => T) -// support static distance, a property access if passing a string -// or a dynamic function receiving the link. -export type NetworkLinkDistance = number | string | ((link: InputLink) => number) - -// support static thickness or a dynamic function receiving the link -export type NetworkLinkThickness = - | number - | ((link: ComputedLink) => number) - -export interface NetworkCommonProps { +export type NetworkCommonProps = { margin: Box - linkDistance: NetworkLinkDistance + linkDistance: LinkDerivedProp repulsivity: number distanceMin: number distanceMax: number @@ -134,16 +117,23 @@ export interface NetworkCommonProps { theme: Theme - nodeColor: NetworkNodeColor - nodeBorderWidth: number - nodeBorderColor: InheritedColorConfig> + nodeSize: NodeDerivedProp + activeNodeSize: NodeDerivedProp + inactiveNodeSize: NodeDerivedProp + nodeColor: NodeDerivedProp + nodeBorderWidth: NodeDerivedProp + nodeBorderColor: InheritedColorConfig> + + linkThickness: LinkDerivedProp + activeLinkThickness: LinkDerivedProp + linkColor: InheritedColorConfig> - linkThickness: NetworkLinkThickness - linkColor: InheritedColorConfig> + annotations: AnnotationMatcher>[] isInteractive: boolean - nodeTooltip: NetworkNodeTooltipComponent - onClick: (node: NetworkComputedNode, event: MouseEvent) => void + defaultActiveNodeIds: string[] + nodeTooltip: NodeTooltip + onClick: (node: ComputedNode, event: MouseEvent) => void renderWrapper: boolean @@ -151,24 +141,23 @@ export interface NetworkCommonProps { ariaLabel: AriaAttributes['aria-label'] ariaLabelledBy: AriaAttributes['aria-labelledby'] ariaDescribedBy: AriaAttributes['aria-describedby'] -} - -export type NetworkSvgProps = Partial> & - NetworkDataProps & - Dimensions & - ModernMotionProps & { - layers?: (NetworkLayerId | NetworkCustomLayer)[] - nodeComponent?: NetworkNodeComponent - linkComponent?: NetworkLinkComponent +} & Required + +export type NetworkSvgProps = Partial> & + NetworkDataProps & + Dimensions & { + layers?: (LayerId | CustomLayer)[] + nodeComponent?: NodeComponent + nodeBlendMode?: CssMixBlendMode + linkComponent?: LinkComponent + linkBlendMode?: CssMixBlendMode } -export type NetworkCanvasProps = Partial> & - NetworkDataProps & - Dimensions & - // only used by tooltips - ModernMotionProps & { - layers?: (NetworkLayerId | NetworkCustomCanvasLayer)[] - renderNode?: NetworkNodeCanvasRenderer - renderLink?: NetworkLinkCanvasRenderer +export type NetworkCanvasProps = Partial> & + NetworkDataProps & + Dimensions & { + layers?: (LayerId | CustomCanvasLayer)[] + renderNode?: NodeCanvasRenderer + renderLink?: LinkCanvasRenderer pixelRatio?: number } diff --git a/packages/network/tests/Network.test.tsx b/packages/network/tests/Network.test.tsx index e69de29bb2..d4ebdaaaaa 100644 --- a/packages/network/tests/Network.test.tsx +++ b/packages/network/tests/Network.test.tsx @@ -0,0 +1,19 @@ +import { mount } from 'enzyme' +// @ts-ignore +import { Network, NetworkSvgProps, NetworkInputNode } from '../src' + +const baseProps: NetworkSvgProps = { + width: 600, + height: 600, + data: { + nodes: [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }], + links: [ + { source: 'A', target: 'B' }, + { source: 'B', target: 'C' }, + { source: 'C', target: 'D' }, + { source: 'D', target: 'E' }, + { source: 'E', target: 'A' }, + ], + }, + animate: false, +} diff --git a/website/src/data/components/network/mapper.ts b/website/src/data/components/network/mapper.ts new file mode 100644 index 0000000000..e4028a4ba3 --- /dev/null +++ b/website/src/data/components/network/mapper.ts @@ -0,0 +1,22 @@ +import { settingsMapper } from '../../../lib/settings' + +export const dynamicNodeSizeValue = 'dynamic: (node: InputNode) => node.radius * 2' +export const dynamicLinkThicknessValue = + 'dynamic: (link: ComputedLink) => (2 - link.source.depth) * 2' + +export default settingsMapper({ + nodeSize: (value: number | typeof dynamicNodeSizeValue) => { + if (value === dynamicNodeSizeValue) { + return (node: any) => node.radius * 2 + } + + return value + }, + linkThickness: (value: number | typeof dynamicLinkThicknessValue) => { + if (value === dynamicLinkThicknessValue) { + return (link: any) => (2 - link.source.depth) * 3 + } + + return value + }, +}) diff --git a/website/src/data/components/network/props.ts b/website/src/data/components/network/props.ts index 3150fce3fe..c4d63d6452 100644 --- a/website/src/data/components/network/props.ts +++ b/website/src/data/components/network/props.ts @@ -1,11 +1,13 @@ -import { commonDefaultProps } from '@nivo/network' +import { commonDefaultProps as defaults } from '@nivo/network' import { groupProperties, themeProperty, motionProperties } from '../../../lib/componentProperties' import { chartDimensions, isInteractive, commonAccessibilityProps, + blendMode, } from '../../../lib/chart-properties' import { ChartProperty, Flavor } from '../../../types' +import { dynamicNodeSizeValue, dynamicLinkThicknessValue } from './mapper' const allFlavors: Flavor[] = ['svg', 'canvas'] @@ -24,14 +26,15 @@ const props: ChartProperty[] = [ { nodes: { id: string - }[], + }[] links: { - source: string, // ref to node id - target: string, // ref to node id - value: number + source: string // ref to node id + target: string // ref to node id }[] } \`\`\` + + Please note that each node id **must** be unique. `, }, ...chartDimensions(allFlavors), @@ -64,12 +67,12 @@ const props: ChartProperty[] = [ of \`distanceMin\` and \`distanceMax\`. `, flavors: allFlavors, + defaultValue: defaults.repulsivity, control: { type: 'range', min: 1, max: 100, }, - defaultValue: commonDefaultProps.repulsivity, }, { key: 'distanceMin', @@ -78,7 +81,7 @@ const props: ChartProperty[] = [ required: false, help: 'Sets the minimum distance between nodes for the many-body force.', flavors: allFlavors, - defaultValue: commonDefaultProps.distanceMin, + defaultValue: defaults.distanceMin, }, { key: 'distanceMax', @@ -87,7 +90,7 @@ const props: ChartProperty[] = [ required: false, help: 'Sets the maximum disteance between nodes for the many-body force.', flavors: allFlavors, - defaultValue: commonDefaultProps.distanceMax, + defaultValue: defaults.distanceMax, }, { key: 'iterations', @@ -100,7 +103,7 @@ const props: ChartProperty[] = [ type: 'number', required: false, flavors: allFlavors, - defaultValue: commonDefaultProps.iterations, + defaultValue: defaults.iterations, control: { type: 'range', min: 60, @@ -125,6 +128,23 @@ const props: ChartProperty[] = [ help: `Custom node rendering for the canvas implementation.`, flavors: ['canvas'], }, + { + key: 'nodeSize', + group: 'Nodes', + type: 'number | (node: InputNode) => number', + required: false, + help: `Control nodes' size.`, + flavors: allFlavors, + defaultValue: defaults.nodeSize, + control: { + type: 'switchableRange', + disabledValue: dynamicNodeSizeValue, + defaultValue: defaults.nodeSize, + unit: 'px', + min: 4, + max: 64, + }, + }, { key: 'nodeColor', group: 'Nodes', @@ -132,8 +152,15 @@ const props: ChartProperty[] = [ required: false, help: `Control nodes' color.`, flavors: allFlavors, - defaultValue: commonDefaultProps.nodeColor, + defaultValue: defaults.nodeColor, }, + blendMode({ + group: 'Nodes', + key: 'nodeBlendMode', + target: 'nodes', + flavors: ['svg'], + defaultValue: defaults.nodeBlendMode, + }), { key: 'nodeBorderWidth', group: 'Nodes', @@ -141,8 +168,8 @@ const props: ChartProperty[] = [ required: false, help: `Control nodes' border width.`, flavors: allFlavors, + defaultValue: defaults.nodeBorderWidth, control: { type: 'lineWidth' }, - defaultValue: commonDefaultProps.nodeBorderWidth, }, { key: 'nodeBorderColor', @@ -151,8 +178,8 @@ const props: ChartProperty[] = [ required: false, help: `Control nodes' border color.`, flavors: allFlavors, + defaultValue: defaults.nodeBorderColor, control: { type: 'inheritedColor' }, - defaultValue: commonDefaultProps.nodeBorderColor, }, { key: 'linkComponent', @@ -173,29 +200,42 @@ const props: ChartProperty[] = [ }, { key: 'linkThickness', - enableControlForFlavors: ['canvas'], group: 'Links', type: 'number | (link: NetworkComputedLink) => number', required: false, help: `Control links' thickness.`, flavors: allFlavors, - control: { type: 'lineWidth' }, - defaultValue: commonDefaultProps.linkThickness, + defaultValue: defaults.linkThickness, + control: { + type: 'switchableRange', + disabledValue: dynamicLinkThicknessValue, + defaultValue: defaults.linkThickness, + unit: 'px', + min: 1, + max: 12, + }, }, { key: 'linkColor', group: 'Links', - type: 'InheritedColorConfig', + type: 'InheritedColorConfig', required: false, help: `Control links' color.`, flavors: allFlavors, + defaultValue: defaults.linkColor, control: { type: 'inheritedColor', inheritableProperties: ['source.color', 'target.color'], }, - defaultValue: commonDefaultProps.linkColor, }, - isInteractive({ flavors: allFlavors, defaultValue: commonDefaultProps.isInteractive }), + blendMode({ + group: 'Links', + key: 'linkBlendMode', + target: 'links', + flavors: ['svg'], + defaultValue: defaults.linkBlendMode, + }), + isInteractive({ flavors: allFlavors, defaultValue: defaults.isInteractive }), { key: 'nodeTooltip', group: 'Interactivity', @@ -221,7 +261,7 @@ const props: ChartProperty[] = [ key: 'onMouseEnter', group: 'Interactivity', help: 'onMouseEnter handler.', - type: '(node: NetworkComputedNode, event: MouseEvent) => void', + type: '(node: ComputedNode, event: MouseEvent) => void', required: false, flavors: ['svg', 'canvas'], }, @@ -229,7 +269,7 @@ const props: ChartProperty[] = [ key: 'onMouseMove', group: 'Interactivity', help: 'onMouseMove handler.', - type: '(node: NetworkComputedNode, event: MouseEvent) => void', + type: '(node: ComputedNode, event: MouseEvent) => void', required: false, flavors: ['svg', 'canvas'], }, @@ -237,7 +277,7 @@ const props: ChartProperty[] = [ key: 'onMouseLeave', group: 'Interactivity', help: 'onMouseLeave handler.', - type: '(node: NetworkComputedNode, event: MouseEvent) => void', + type: '(node: ComputedNode, event: MouseEvent) => void', required: false, flavors: allFlavors, }, @@ -247,11 +287,11 @@ const props: ChartProperty[] = [ group: 'Customization', help: 'Defines the order of layers and add custom layers.', required: false, - defaultValue: commonDefaultProps.layers, + defaultValue: defaults.layers, flavors: ['svg', 'canvas'], }, ...commonAccessibilityProps(['svg']), - ...motionProperties(['svg'], commonDefaultProps, 'react-spring'), + ...motionProperties(['svg'], defaults, 'react-spring'), ] export const groups = groupProperties(props) diff --git a/website/src/pages/network/canvas.tsx b/website/src/pages/network/canvas.tsx index 2130b4ba12..6a5c6d2e10 100644 --- a/website/src/pages/network/canvas.tsx +++ b/website/src/pages/network/canvas.tsx @@ -1,10 +1,11 @@ import React from 'react' -import { ResponsiveNetworkCanvas, canvasDefaultProps } from '@nivo/network' +import { graphql, useStaticQuery } from 'gatsby' +import { ResponsiveNetworkCanvas, canvasDefaultProps as defaults } from '@nivo/network' +import { generateNetworkData } from '@nivo/generators' import { ComponentTemplate } from '../../components/components/ComponentTemplate' import meta from '../../data/components/network/meta.yml' +import mapper from '../../data/components/network/mapper' import { groups } from '../../data/components/network/props' -import { generateNetworkData } from '@nivo/generators' -import { graphql, useStaticQuery } from 'gatsby' const initialProperties = Object.freeze({ pixelRatio: @@ -21,12 +22,13 @@ const initialProperties = Object.freeze({ repulsivity: 4, iterations: 60, - nodeColor: node => node.color, + nodeSize: defaults.nodeSize, + nodeColor: '#ff0000', // node => node.color, nodeBorderWidth: 1, nodeBorderColor: { theme: 'background' }, - linkColor: canvasDefaultProps.linkColor, - linkThickness: 1, + linkColor: defaults.linkColor, + linkThickness: defaults.linkThickness, isInteractive: true, }) @@ -57,7 +59,8 @@ const NetworkCanvas = () => { currentFlavor="canvas" properties={groups} initialProperties={initialProperties} - defaultProperties={canvasDefaultProps} + defaultProperties={defaults} + propertiesMapper={mapper} generateData={() => generateData({ rootNodeRadius: 10, diff --git a/website/src/pages/network/index.tsx b/website/src/pages/network/index.tsx index c2c404a334..6216ac2b04 100644 --- a/website/src/pages/network/index.tsx +++ b/website/src/pages/network/index.tsx @@ -1,10 +1,14 @@ import React from 'react' -import { ResponsiveNetwork, svgDefaultProps } from '@nivo/network' +import { graphql, useStaticQuery } from 'gatsby' +import { ResponsiveNetwork, svgDefaultProps as defaults } from '@nivo/network' +import { generateNetworkData } from '@nivo/generators' import { ComponentTemplate } from '../../components/components/ComponentTemplate' import meta from '../../data/components/network/meta.yml' +import mapper, { + dynamicNodeSizeValue, + dynamicLinkThicknessValue, +} from '../../data/components/network/mapper' import { groups } from '../../data/components/network/props' -import { generateNetworkData } from '@nivo/generators' -import { graphql, useStaticQuery } from 'gatsby' const initialProperties = Object.freeze({ margin: { @@ -18,15 +22,18 @@ const initialProperties = Object.freeze({ repulsivity: 6, iterations: 60, - nodeColor: node => node.color, + nodeSize: dynamicNodeSizeValue, + nodeColor: (node: any) => node.color, + nodeBlendMode: 'normal', nodeBorderWidth: 1, nodeBorderColor: { from: 'color', modifiers: [['darker', 0.8]], }, - linkColor: svgDefaultProps.linkColor, - linkThickness: link => (2 - link.source.depth) * 2, + linkThickness: dynamicLinkThicknessValue, + linkColor: defaults.linkColor, + linkBlendMode: 'multiply', isInteractive: true, @@ -60,7 +67,8 @@ const Network = () => { currentFlavor="svg" properties={groups} initialProperties={initialProperties} - defaultProperties={svgDefaultProps} + defaultProperties={defaults} + propertiesMapper={mapper} generateData={generateData} getDataSize={data => data.nodes.length} image={image} @@ -68,9 +76,35 @@ const Network = () => { {(properties, data, theme, logAction) => { return ( { logAction({ type: 'click',