From 375252a5472d588ae1f6b9f0c4bd556dced49ba5 Mon Sep 17 00:00:00 2001 From: plouc Date: Sun, 12 Sep 2021 09:29:42 +0900 Subject: [PATCH] feat(network): add support for generic node datum --- packages/network/src/Network.tsx | 37 ++--- packages/network/src/NetworkCanvas.tsx | 21 +-- packages/network/src/NetworkLinks.tsx | 34 ++--- packages/network/src/NetworkNode.tsx | 19 +-- packages/network/src/NetworkNodeTooltip.tsx | 5 +- packages/network/src/NetworkNodes.tsx | 129 +++++++++++------- packages/network/src/ResponsiveNetwork.tsx | 8 +- .../network/src/ResponsiveNetworkCanvas.tsx | 8 +- .../network/src/{props.ts => defaults.ts} | 13 +- packages/network/src/hooks.ts | 32 ++--- packages/network/src/index.ts | 2 +- packages/network/src/types.ts | 84 ++++++++---- packages/network/stories/network.stories.tsx | 69 +++++----- .../network/stories/networkCanvas.stories.tsx | 69 +++++----- website/src/data/components/network/props.ts | 82 ++++++++++- 15 files changed, 371 insertions(+), 241 deletions(-) rename packages/network/src/{props.ts => defaults.ts} (64%) diff --git a/packages/network/src/Network.tsx b/packages/network/src/Network.tsx index 259fcb8e4c..eb238d2c75 100644 --- a/packages/network/src/Network.tsx +++ b/packages/network/src/Network.tsx @@ -1,20 +1,19 @@ -import { Fragment, ReactNode, useCallback } from 'react' +import { Fragment, ReactNode, useCallback, createElement } from 'react' import { Container, useDimensions, SvgWrapper, useTheme } from '@nivo/core' import { useInheritedColor } from '@nivo/colors' import { useTooltip } from '@nivo/tooltip' -import { svgDefaultProps } from './props' +import { svgDefaultProps } from './defaults' import { useNetwork, useNodeColor, useLinkThickness } from './hooks' import { NetworkNodes } from './NetworkNodes' import { NetworkLinks } from './NetworkLinks' -import NetworkNodeTooltip from './NetworkNodeTooltip' -import { NetworkLayerId, NetworkSvgProps } from './types' +import { NetworkInputNode, NetworkLayerId, NetworkSvgProps } from './types' -type InnerNetworkProps = Omit< - NetworkSvgProps, +type InnerNetworkProps = Omit< + NetworkSvgProps, 'animate' | 'motionConfig' | 'renderWrapper' | 'theme' > -const InnerNetwork = (props: InnerNetworkProps) => { +const InnerNetwork = (props: InnerNetworkProps) => { const { width, height, @@ -30,6 +29,7 @@ const InnerNetwork = (props: InnerNetworkProps) => { layers = svgDefaultProps.layers, + nodeComponent = svgDefaultProps.nodeComponent, nodeColor = svgDefaultProps.nodeColor, nodeBorderWidth = svgDefaultProps.nodeBorderWidth, nodeBorderColor = svgDefaultProps.nodeBorderColor, @@ -37,8 +37,8 @@ const InnerNetwork = (props: InnerNetworkProps) => { linkThickness = svgDefaultProps.linkThickness, linkColor = svgDefaultProps.linkColor, - tooltip = svgDefaultProps.tooltip, isInteractive = svgDefaultProps.isInteractive, + nodeTooltip = svgDefaultProps.nodeTooltip, onClick, role = svgDefaultProps.role, @@ -51,12 +51,12 @@ const InnerNetwork = (props: InnerNetworkProps) => { ) const theme = useTheme() - const getColor = useNodeColor(nodeColor) + const getColor = useNodeColor(nodeColor) const getBorderColor = useInheritedColor(nodeBorderColor, theme) const getLinkThickness = useLinkThickness(linkThickness) const getLinkColor = useInheritedColor(linkColor, theme) - const [nodes, links] = useNetwork({ + const [nodes, links] = useNetwork({ nodes: rawNodes, links: rawLinks, linkDistance, @@ -71,9 +71,9 @@ const InnerNetwork = (props: InnerNetworkProps) => { const handleNodeHover = useCallback( (node, event) => { - showTooltipFromEvent(, event) + showTooltipFromEvent(createElement(nodeTooltip, { node }), event) }, - [showTooltipFromEvent, tooltip] + [showTooltipFromEvent, nodeTooltip] ) const handleNodeLeave = useCallback(() => { @@ -85,7 +85,7 @@ const InnerNetwork = (props: InnerNetworkProps) => { nodes: null, } - if (layers.includes('links')) { + if (layers.includes('links') && false) { layerById.links = ( { ) } - if (layers.includes('crap')) { + if (layers.includes('nodes')) { layerById.nodes = ( - key="nodes" nodes={nodes} + nodeComponent={nodeComponent} color={getColor} borderWidth={nodeBorderWidth} borderColor={getBorderColor} @@ -135,14 +136,14 @@ const InnerNetwork = (props: InnerNetworkProps) => { ) } -export const Network = ({ +export const Network = ({ isInteractive = svgDefaultProps.isInteractive, animate = svgDefaultProps.animate, motionConfig = svgDefaultProps.motionConfig, theme, renderWrapper, ...otherProps -}: NetworkSvgProps) => ( +}: NetworkSvgProps) => ( - + isInteractive={isInteractive} {...otherProps} /> ) diff --git a/packages/network/src/NetworkCanvas.tsx b/packages/network/src/NetworkCanvas.tsx index a88705eb10..441fc3f319 100644 --- a/packages/network/src/NetworkCanvas.tsx +++ b/packages/network/src/NetworkCanvas.tsx @@ -1,16 +1,19 @@ -import { useCallback, useRef, useEffect, ForwardedRef } from 'react' +import { useCallback, useRef, useEffect } from 'react' import * as React from 'react' import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } from '@nivo/core' import { useInheritedColor } from '@nivo/colors' import { useTooltip } from '@nivo/tooltip' -import { canvasDefaultProps } from './props' +import { canvasDefaultProps } from './defaults' import { useNetwork, useNodeColor, useLinkThickness } from './hooks' -import NetworkNodeTooltip from './NetworkNodeTooltip' -import { NetworkCanvasProps } from './types' +import { NetworkNodeTooltip } from './NetworkNodeTooltip' +import { NetworkCanvasProps, NetworkInputNode } from './types' -type InnerNetworkCanvasProps = Omit +type InnerNetworkCanvasProps = Omit< + NetworkCanvasProps, + 'renderWrapper' | 'theme' +> -const InnerNetworkCanvas = (props: InnerNetworkCanvasProps) => { +const InnerNetworkCanvas = (props: InnerNetworkCanvasProps) => { const { width, height, @@ -188,15 +191,15 @@ const InnerNetworkCanvas = (props: InnerNetworkCanvasProps) => { ) } -export const NetworkCanvas = ({ +export const NetworkCanvas = ({ theme, isInteractive = canvasDefaultProps.isInteractive, animate = canvasDefaultProps.animate, motionConfig = canvasDefaultProps.motionConfig, renderWrapper, ...otherProps -}: NetworkCanvasProps) => ( +}: NetworkCanvasProps) => ( - + isInteractive={isInteractive} {...otherProps} /> ) diff --git a/packages/network/src/NetworkLinks.tsx b/packages/network/src/NetworkLinks.tsx index 1553177449..2e9ef9ddd1 100644 --- a/packages/network/src/NetworkLinks.tsx +++ b/packages/network/src/NetworkLinks.tsx @@ -1,4 +1,4 @@ -import { createElement } from 'react' +import { createElement, useCallback, useMemo } from 'react' import { useTransition } from '@react-spring/web' import { useMotionConfig } from '@nivo/core' import { NetworkLink } from './NetworkLink' @@ -10,11 +10,24 @@ interface NetworkLinksProps { linkColor: (link: ComputedLink) => string } +const getRegularTransition = (linkColor: NetworkLinksProps['linkColor']) => ( + link: ComputedLink +) => ({ + x1: link.source.x, + y1: link.source.y, + x2: link.target.x, + y2: link.target.y, + color: linkColor(link), + opacity: 1, +}) + export const NetworkLinks = ({ links, linkThickness, linkColor }: NetworkLinksProps) => { const { animate, config: springConfig } = useMotionConfig() console.log('immediate', !animate) + const regularTransition = useMemo(() => getRegularTransition(linkColor), [linkColor]) + const transition = useTransition< ComputedLink, { @@ -27,6 +40,7 @@ export const NetworkLinks = ({ links, linkThickness, linkColor }: NetworkLinksPr } >(links, { keys: link => link.id, + initial: regularTransition, from: link => ({ x1: link.source.x, y1: link.source.y, @@ -35,22 +49,8 @@ export const NetworkLinks = ({ links, linkThickness, linkColor }: NetworkLinksPr color: linkColor(link), opacity: 0, }), - enter: link => ({ - x1: link.source.x, - y1: link.source.y, - x2: link.target.x, - y2: link.target.y, - color: linkColor(link), - opacity: 1, - }), - update: link => ({ - x1: link.source.x, - y1: link.source.y, - x2: link.target.x, - y2: link.target.y, - color: linkColor(link), - opacity: 1, - }), + enter: regularTransition, + update: regularTransition, leave: link => ({ x1: link.source.x, y1: link.source.y, diff --git a/packages/network/src/NetworkNode.tsx b/packages/network/src/NetworkNode.tsx index 6807113959..10a962b488 100644 --- a/packages/network/src/NetworkNode.tsx +++ b/packages/network/src/NetworkNode.tsx @@ -1,25 +1,14 @@ -import { MouseEvent } from 'react' -import { AnimatedProps, animated, to } from '@react-spring/web' -import { ComputedNode, NodeAnimatedProps } from './types' +import { animated, to } from '@react-spring/web' +import { NetworkInputNode, NetworkNodeProps } from './types' -interface NetworkNodeProps { - node: ComputedNode - animated: AnimatedProps - scale?: number - 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 const NetworkNode = ({ +export const NetworkNode = ({ node, animated: animatedProps, onClick, onMouseEnter, onMouseMove, onMouseLeave, -}: NetworkNodeProps) => { +}: NetworkNodeProps) => { return ( ( +export const NetworkNodeTooltip = ({ node }: NetworkNodeTooltipProps) => ( ( // renderContent={typeof tooltip === 'function' ? tooltip.bind(null, { ...node }) : null} /> ) - -export default memo(NetworkNodeTooltip) diff --git a/packages/network/src/NetworkNodes.tsx b/packages/network/src/NetworkNodes.tsx index 8f225082e1..7f729f8a33 100644 --- a/packages/network/src/NetworkNodes.tsx +++ b/packages/network/src/NetworkNodes.tsx @@ -1,22 +1,71 @@ -import { createElement, MouseEvent } from 'react' +import { createElement, MouseEvent, useMemo } from 'react' import { useTransition } from '@react-spring/web' import { useMotionConfig } from '@nivo/core' -import { NetworkNode } from './NetworkNode' -import { ComputedNode, NodeAnimatedProps } from './types' +import { + NetworkComputedNode, + NetworkInputNode, + NetworkNodeAnimatedProps, + NetworkNodeColor, + NetworkNodeComponent, +} from './types' -interface NetworkNodesProps { - nodes: ComputedNode[] - color: (node: ComputedNode) => string +interface NetworkNodesProps { + nodes: NetworkComputedNode[] + nodeComponent: NetworkNodeComponent + color: Exclude, string> borderWidth: number - borderColor: (node: ComputedNode) => string - onClick?: (node: ComputedNode, event: MouseEvent) => void - onMouseEnter?: (node: ComputedNode, event: MouseEvent) => void - onMouseMove?: (node: ComputedNode, event: MouseEvent) => void - onMouseLeave?: (node: ComputedNode, event: MouseEvent) => void + 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 } -export const NetworkNodes = ({ +const getEnterTransition = ( + color: NetworkNodesProps['color'], + borderWidth: number, + borderColor: NetworkNodesProps['borderColor'] +) => (node: NetworkComputedNode) => ({ + x: node.x, + y: node.y, + radius: node.radius, + color: color(node), + borderWidth, + borderColor: borderColor(node), + scale: 0, +}) + +const getRegularTransition = ( + color: NetworkNodesProps['color'], + borderWidth: number, + borderColor: NetworkNodesProps['borderColor'] +) => (node: NetworkComputedNode) => ({ + x: node.x, + y: node.y, + radius: node.radius, + color: color(node), + borderWidth, + borderColor: borderColor(node), + scale: 1, +}) + +const getExitTransition = ( + color: NetworkNodesProps['color'], + borderWidth: number, + borderColor: NetworkNodesProps['borderColor'] +) => (node: NetworkComputedNode) => ({ + x: node.x, + y: node.y, + radius: node.radius, + color: color(node), + borderWidth, + borderColor: borderColor(node), + scale: 0, +}) + +export const NetworkNodes = ({ nodes, + nodeComponent, color, borderColor, borderWidth, @@ -24,47 +73,25 @@ export const NetworkNodes = ({ onMouseEnter, onMouseMove, onMouseLeave, -}: NetworkNodesProps) => { +}: NetworkNodesProps) => { const { animate, config: springConfig } = useMotionConfig() - const transition = useTransition(nodes, { + const [enterTransition, regularTransition, exitTransition] = useMemo( + () => [ + getEnterTransition(color, borderWidth, borderColor), + getRegularTransition(color, borderWidth, borderColor), + getExitTransition(color, borderWidth, borderColor), + ], + [color, borderWidth, borderColor] + ) + + const transition = useTransition, NetworkNodeAnimatedProps>(nodes, { keys: node => node.id, - initial: node => ({ - x: node.x, - y: node.y, - radius: node.radius, - color: color(node), - borderWidth, - borderColor: borderColor(node), - scale: 1, - }), - enter: node => ({ - x: node.x, - y: node.y, - radius: node.radius, - color: color(node), - borderWidth, - borderColor: borderColor(node), - scale: 1, - }), - update: node => ({ - x: node.x, - y: node.y, - radius: node.radius, - color: color(node), - borderWidth, - borderColor: borderColor(node), - scale: 1, - }), - leave: node => ({ - x: node.x, - y: node.y, - radius: node.radius, - color: color(node), - borderWidth, - borderColor: borderColor(node), - scale: 0, - }), + initial: regularTransition, + from: enterTransition, + enter: regularTransition, + update: regularTransition, + leave: exitTransition, config: springConfig, immediate: !animate, }) @@ -72,7 +99,7 @@ export const NetworkNodes = ({ return ( <> {transition((transitionProps, node) => - createElement(NetworkNode, { + createElement(nodeComponent, { key: node.id, node, animated: transitionProps, diff --git a/packages/network/src/ResponsiveNetwork.tsx b/packages/network/src/ResponsiveNetwork.tsx index 11dbd13bbb..873aece1ea 100644 --- a/packages/network/src/ResponsiveNetwork.tsx +++ b/packages/network/src/ResponsiveNetwork.tsx @@ -1,9 +1,11 @@ import { ResponsiveWrapper } from '@nivo/core' -import { NetworkSvgProps } from './types' +import { NetworkInputNode, NetworkSvgProps } from './types' import { Network } from './Network' -export const ResponsiveNetwork = (props: Omit) => ( +export const ResponsiveNetwork = ( + props: Omit, 'height' | 'width'> +) => ( - {({ width, height }) => } + {({ width, height }) => width={width} height={height} {...props} />} ) diff --git a/packages/network/src/ResponsiveNetworkCanvas.tsx b/packages/network/src/ResponsiveNetworkCanvas.tsx index d76b02eea3..22064a923e 100644 --- a/packages/network/src/ResponsiveNetworkCanvas.tsx +++ b/packages/network/src/ResponsiveNetworkCanvas.tsx @@ -1,9 +1,11 @@ import { ResponsiveWrapper } from '@nivo/core' -import { NetworkCanvasProps } from './types' +import { NetworkCanvasProps, NetworkInputNode } from './types' import { NetworkCanvas } from './NetworkCanvas' -export const ResponsiveNetworkCanvas = (props: Omit) => ( +export const ResponsiveNetworkCanvas = ( + props: Omit, 'height' | 'width'> +) => ( - {({ width, height }) => } + {({ width, height }) => width={width} height={height} {...props} />} ) diff --git a/packages/network/src/props.ts b/packages/network/src/defaults.ts similarity index 64% rename from packages/network/src/props.ts rename to packages/network/src/defaults.ts index 3adf51a817..f0c2ddcfab 100644 --- a/packages/network/src/props.ts +++ b/packages/network/src/defaults.ts @@ -1,5 +1,9 @@ +import { NetworkNode } from './NetworkNode' +import { NetworkNodeTooltip } from './NetworkNodeTooltip' +import { NetworkLayerId } from './types' + const commonDefaultProps = { - layers: ['links', 'nodes'], + layers: ['links', 'nodes'] as NetworkLayerId[], linkDistance: 30, repulsivity: 10, @@ -7,6 +11,7 @@ const commonDefaultProps = { distanceMax: Infinity, iterations: 90, + nodeColor: '#000000', nodeBorderWidth: 0, nodeBorderColor: { from: 'color' }, @@ -14,6 +19,7 @@ const commonDefaultProps = { linkColor: { from: 'source.color' }, isInteractive: true, + nodeTooltip: NetworkNodeTooltip, animate: true, motionConfig: 'gentle' as const, @@ -25,7 +31,10 @@ export const NetworkDefaultProps = { ...commonDefaultProps, } -export const svgDefaultProps = NetworkDefaultProps +export const svgDefaultProps = { + ...NetworkDefaultProps, + nodeComponent: NetworkNode, +} export const NetworkCanvasDefaultProps = { ...commonDefaultProps, diff --git a/packages/network/src/hooks.ts b/packages/network/src/hooks.ts index e057776d70..be2151b56e 100644 --- a/packages/network/src/hooks.ts +++ b/packages/network/src/hooks.ts @@ -3,19 +3,19 @@ import get from 'lodash/get' import isString from 'lodash/isString' import isNumber from 'lodash/isNumber' import { forceSimulation, forceManyBody, forceCenter, forceLink } from 'd3-force' -import { InputLink, InputNode, NetworkCommonProps } from './types' +import { InputLink, NetworkInputNode, NetworkCommonProps, NetworkNodeColor } 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 computedLinkDistance @@ -41,7 +41,7 @@ const computeForces = ({ return { link: linkForce, charge: chargeForce, center: centerForce } } -export const useNetwork = ({ +export const useNetwork = ({ nodes, links, linkDistance, @@ -51,20 +51,20 @@ export const useNetwork = ({ center, iterations, }: { - nodes: InputNode[] + nodes: N[] links: InputLink[] - 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] - iterations: NetworkCommonProps['iterations'] + iterations: NetworkCommonProps['iterations'] }) => { const [currentNodes, setCurrentNodes] = useState([]) const [currentLinks, setCurrentLinks] = useState([]) useEffect(() => { - const forces = computeForces({ + const forces = computeForces({ linkDistance, repulsivity, distanceMin, @@ -72,7 +72,7 @@ export const useNetwork = ({ center, }) - const nodesCopy: InputNode[] = nodes.map(node => ({ ...node })) + const nodesCopy: N[] = nodes.map(node => ({ ...node })) const linksCopy: InputLink[] = links.map(link => ({ id: `${link.source}.${link.target}`, ...link, @@ -114,7 +114,7 @@ export const useNetwork = ({ return [currentNodes, currentLinks] } -export const useNodeColor = color => +export const useNodeColor = (color: NetworkNodeColor) => useMemo(() => { if (typeof color === 'function') return color return () => color diff --git a/packages/network/src/index.ts b/packages/network/src/index.ts index 0d5b93d4a1..2ca300f23e 100644 --- a/packages/network/src/index.ts +++ b/packages/network/src/index.ts @@ -2,6 +2,6 @@ export * from './Network' export * from './ResponsiveNetwork' export * from './NetworkCanvas' export * from './ResponsiveNetworkCanvas' -export * from './props' +export * from './defaults' export * from './hooks' export * from './types' diff --git a/packages/network/src/types.ts b/packages/network/src/types.ts index 98a48e66c3..5638df39be 100644 --- a/packages/network/src/types.ts +++ b/packages/network/src/types.ts @@ -1,23 +1,23 @@ import { AriaAttributes, MouseEvent, FunctionComponent } from 'react' +import { AnimatedProps } from '@react-spring/web' import { Box, Theme, Dimensions, ModernMotionProps, PropertyAccessor } from '@nivo/core' import { InheritedColorConfig } from '@nivo/colors' -export interface InputNode { +export interface NetworkInputNode { id: string - [key: string]: unknown } -export interface ComputedNode { +export interface NetworkComputedNode { id: string x: number y: number radius: number color: string - data: InputNode + data: N [key: string]: unknown } -export interface NodeAnimatedProps { +export interface NetworkNodeAnimatedProps { x: number y: number radius: number @@ -28,16 +28,29 @@ export interface NodeAnimatedProps { 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 type NetworkNodeComponent = FunctionComponent< + NetworkNodeProps +> + export interface InputLink { source: string target: string [key: string]: unknown } -export interface ComputedLink { +export interface ComputedLink { id: string - source: ComputedNode - target: ComputedNode + source: NetworkComputedNode + target: NetworkComputedNode [key: string]: unknown } @@ -50,29 +63,38 @@ export interface LinkAnimatedProps { opacity: number } -export interface NetworkDataProps { +export interface NetworkDataProps { data: { - nodes: InputNode[] + nodes: N[] links: InputLink[] } } export type NetworkLayerId = 'links' | 'nodes' -export interface NetworkCustomLayerProps { - nodes: ComputedNode[] - links: ComputedLink[] +export interface NetworkCustomLayerProps { + nodes: NetworkComputedNode[] + links: ComputedLink[] } -export type NetworkCustomLayer = FunctionComponent +export type NetworkCustomLayer = FunctionComponent< + NetworkCustomLayerProps +> -export interface NetworkNodeTooltipProps { - node: ComputedNode +export interface NetworkNodeTooltipProps { + node: NetworkComputedNode } -export type NetworkNodeTooltipComponent = FunctionComponent +export type NetworkNodeTooltipComponent = FunctionComponent< + NetworkNodeTooltipProps +> -export interface NetworkCommonProps { +// support static color or a dynamic function receiving the node +export type NetworkNodeColor = + | string + | ((node: NetworkComputedNode) => string) + +export interface NetworkCommonProps { margin: Box - layers: (NetworkLayerId | NetworkCustomLayer)[] + layers: (NetworkLayerId | NetworkCustomLayer)[] linkDistance: number | PropertyAccessor repulsivity: number @@ -82,16 +104,16 @@ export interface NetworkCommonProps { theme: Theme - nodeColor: string | PropertyAccessor + nodeColor: NetworkNodeColor nodeBorderWidth: number - nodeBorderColor: InheritedColorConfig + nodeBorderColor: InheritedColorConfig> - linkThickness: number | PropertyAccessor - linkColor: InheritedColorConfig + linkThickness: number | PropertyAccessor, number> + linkColor: InheritedColorConfig> isInteractive: boolean - tooltip: NetworkNodeTooltipComponent - onClick: (node: ComputedNode, event: MouseEvent) => void + nodeTooltip: NetworkNodeTooltipComponent + onClick: (node: NetworkComputedNode, event: MouseEvent) => void renderWrapper: boolean @@ -101,13 +123,15 @@ export interface NetworkCommonProps { ariaDescribedBy: AriaAttributes['aria-describedby'] } -export type NetworkSvgProps = Partial & - NetworkDataProps & +export type NetworkSvgProps = Partial> & + NetworkDataProps & Dimensions & - ModernMotionProps + ModernMotionProps & { + nodeComponent?: NetworkNodeComponent + } -export type NetworkCanvasProps = Partial & - NetworkDataProps & +export type NetworkCanvasProps = Partial> & + NetworkDataProps & Dimensions & // only used by tooltips ModernMotionProps & { diff --git a/packages/network/stories/network.stories.tsx b/packages/network/stories/network.stories.tsx index e266298f88..6ef136156f 100644 --- a/packages/network/stories/network.stories.tsx +++ b/packages/network/stories/network.stories.tsx @@ -1,47 +1,50 @@ +import { Meta } from '@storybook/react' import { action } from '@storybook/addon-actions' -import { storiesOf } from '@storybook/react' -import { NetworkDefaultProps } from '../src/props' +import { withKnobs } from '@storybook/addon-knobs' import { generateNetworkData } from '@nivo/generators' -import { Network } from '../src' +import { + Network, + NetworkInputNode, + NetworkNodeTooltipProps, + NetworkSvgProps, + svgDefaultProps, + // @ts-ignore +} from '../src' + +export default { + component: Network, + title: 'Network', + decorators: [withKnobs], +} as Meta const data = generateNetworkData() -const commonProperties = { - ...NetworkDefaultProps, - nodes: data.nodes, - links: data.links, +const commonProperties: NetworkSvgProps = { + ...svgDefaultProps, + data, width: 900, height: 340, - nodeColor: function (t) { - return t.color - }, + nodeColor: node => node.color, repulsivity: 6, iterations: 60, } -const stories = storiesOf('Network', module) +export const Default = () => -stories.add('default', () => ) +const CustomNodeTooltipComponent = ({ node }: NetworkNodeTooltipProps) => ( +
+
+ ID: {node.id} +
+ Depth: {node.depth} +
+ Radius: {node.radius} +
+
+) -stories.add('custom tooltip', () => ( - { - return ( -
-
- ID: {node.id} -
- Depth: {node.depth} -
- Radius: {node.radius} -
-
- ) - }} - /> -)) +export const CustomNodeTooltip = () => ( + +) -stories.add('supports onClick for the node', () => ( - -)) +export const OnClickHandler = () => diff --git a/packages/network/stories/networkCanvas.stories.tsx b/packages/network/stories/networkCanvas.stories.tsx index 615e9cab6b..81d095f73b 100644 --- a/packages/network/stories/networkCanvas.stories.tsx +++ b/packages/network/stories/networkCanvas.stories.tsx @@ -1,47 +1,52 @@ +import { Meta } from '@storybook/react' import { action } from '@storybook/addon-actions' -import { storiesOf } from '@storybook/react' -import { NetworkDefaultProps } from '../src/props' +import { withKnobs } from '@storybook/addon-knobs' import { generateNetworkData } from '@nivo/generators' -import { NetworkCanvas } from '../src' +import { + NetworkCanvas, + canvasDefaultProps, + NetworkCanvasProps, + NetworkInputNode, + NetworkNodeTooltipProps, + // @ts-ignore +} from '../src' + +export default { + component: NetworkCanvas, + title: 'NetworkCanvas', + decorators: [withKnobs], +} as Meta const data = generateNetworkData() -const commonProperties = { - ...NetworkDefaultProps, - nodes: data.nodes, - links: data.links, +const commonProperties: NetworkCanvasProps = { + ...canvasDefaultProps, + data, width: 900, height: 340, - nodeColor: function (t) { - return t.color - }, + nodeColor: node => node.color, repulsivity: 6, iterations: 60, } -const stories = storiesOf('NetworkCanvas', module) +export const Default = () => -stories.add('default', () => ) +const CustomNodeTooltipComponent = ({ node }: NetworkNodeTooltipProps) => ( +
+
+ ID: {node.id} +
+ Depth: {node.depth} +
+ Radius: {node.radius} +
+
+) -stories.add('custom tooltip', () => ( - { - return ( -
-
- ID: {node.id} -
- Depth: {node.depth} -
- Radius: {node.radius} -
-
- ) - }} - /> -)) +export const CustomNodeTooltip = () => ( + +) -stories.add('supports onClick for the node', () => ( +export const OnClickHandler = () => ( -)) +) diff --git a/website/src/data/components/network/props.ts b/website/src/data/components/network/props.ts index 820dd96e64..465e6aa973 100644 --- a/website/src/data/components/network/props.ts +++ b/website/src/data/components/network/props.ts @@ -1,6 +1,6 @@ // @ts-ignore import { NetworkDefaultProps } from '@nivo/network' -import { motionProperties, groupProperties } from '../../../lib/componentProperties' +import { motionProperties, groupProperties, themeProperty } from '../../../lib/componentProperties' import { chartDimensions, pixelRatio } from '../../../lib/chart-properties' import { ChartProperty, Flavor } from '../../../types' @@ -105,13 +105,15 @@ const props: ChartProperty[] = [ max: 260, }, }, + themeProperty(['svg', 'canvas']), { key: 'nodeColor', group: 'Nodes', - type: 'string | (node: Node) => string', + type: 'string | (node: InputNode) => string', required: false, help: `Control nodes' color.`, flavors: allFlavors, + defaultValue: NetworkDefaultProps.nodeColor, }, { key: 'nodeBorderWidth', @@ -158,12 +160,22 @@ const props: ChartProperty[] = [ }, }, { - key: 'tooltip', + key: 'isInteractive', group: 'Interactivity', - type: 'Function', + type: 'boolean', required: false, - help: 'Custom tooltip component.', - flavors: allFlavors, + help: 'Enable/disable interactivity.', + flavors: ['svg'], + defaultValue: NetworkDefaultProps.isInteractive, + controlType: 'switch', + }, + { + key: 'nodeTooltip', + group: 'Interactivity', + type: 'NetworkNodeTooltipComponent', + required: false, + help: 'Custom tooltip component for nodes.', + flavors: ['svg', 'canvas'], description: ` A function allowing complete tooltip customisation, it must return a valid HTML @@ -174,7 +186,31 @@ const props: ChartProperty[] = [ key: 'onClick', group: 'Interactivity', help: 'onClick handler.', - type: '(node, event) => void', + type: '(node: NetworkComputedNode, event: MouseEvent) => void', + required: false, + flavors: ['svg', 'canvas'], + }, + { + key: 'onMouseEnter', + group: 'Interactivity', + help: 'onMouseEnter handler.', + type: '(node: NetworkComputedNode, event: MouseEvent) => void', + required: false, + flavors: ['svg', 'canvas'], + }, + { + key: 'onMouseMove', + group: 'Interactivity', + help: 'onMouseMove handler.', + type: '(node: NetworkComputedNode, event: MouseEvent) => void', + required: false, + flavors: ['svg', 'canvas'], + }, + { + key: 'onMouseLeave', + group: 'Interactivity', + help: 'onMouseLeave handler.', + type: '(node: NetworkComputedNode, event: MouseEvent) => void', required: false, flavors: allFlavors, }, @@ -187,6 +223,38 @@ const props: ChartProperty[] = [ defaultValue: NetworkDefaultProps.layers, flavors: ['svg', 'canvas'], }, + { + key: 'role', + group: 'Accessibility', + type: 'string', + required: false, + help: 'Main element role attribute.', + flavors: ['svg'], + }, + { + key: 'ariaLabel', + group: 'Accessibility', + type: 'string', + required: false, + help: 'Main element [aria-label](https://www.w3.org/TR/wai-aria/#aria-label).', + flavors: ['svg'], + }, + { + key: 'ariaLabelledBy', + group: 'Accessibility', + type: 'string', + required: false, + help: 'Main element [aria-labelledby](https://www.w3.org/TR/wai-aria/#aria-labelledby).', + flavors: ['svg'], + }, + { + key: 'ariaDescribedBy', + group: 'Accessibility', + type: 'string', + required: false, + help: 'Main element [aria-describedby](https://www.w3.org/TR/wai-aria/#aria-describedby).', + flavors: ['svg'], + }, ...motionProperties(['svg'], NetworkDefaultProps, 'react-spring'), ]