diff --git a/.storybook/config.js b/.storybook/config.js index 75c980d9d..a0c6791c3 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -56,6 +56,7 @@ function loadStories() { require('../packages/heatmap/stories/heatmap.stories') require('../packages/line/stories/line.stories') require('../packages/line/stories/LineCanvas.stories') + require('../packages/network/stories/network.stories') require('../packages/pie/stories/pie.stories') require('../packages/radar/stories/radar.stories') require('../packages/sankey/stories/sankey.stories') diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c66bc0d8..5d9579668 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ ## Setup -Nivo is structured into multiple packages handled by [lerna](https://lernajs.io/). +Nivo is structured into multiple packages handled by [lerna](https://lerna.js.org/). In order to install all the required dependencies and to establish links between the various packages, please execute the following: diff --git a/packages/network/src/AnimatedNodes.js b/packages/network/src/AnimatedNodes.js index 540c3bd29..993eb0b1d 100644 --- a/packages/network/src/AnimatedNodes.js +++ b/packages/network/src/AnimatedNodes.js @@ -26,7 +26,14 @@ const willLeave = springConfig => ({ style }) => ({ scale: spring(0, springConfig), }) -const AnimatedNodes = ({ nodes, color, borderWidth, borderColor }) => { +const AnimatedNodes = ({ + nodes, + color, + borderWidth, + borderColor, + handleNodeHover, + handleNodeLeave, +}) => { const { springConfig } = useMotionConfig() return ( @@ -58,6 +65,8 @@ const AnimatedNodes = ({ nodes, color, borderWidth, borderColor }) => { borderWidth={borderWidth} borderColor={borderColor(node)} scale={Math.max(style.scale, 0)} + handleNodeHover={handleNodeHover} + handleNodeLeave={handleNodeLeave} /> ) })} @@ -72,6 +81,8 @@ AnimatedNodes.propTypes = { color: PropTypes.func.isRequired, borderWidth: PropTypes.number.isRequired, borderColor: PropTypes.func.isRequired, + handleNodeHover: PropTypes.func.isRequired, + handleNodeLeave: PropTypes.func.isRequired, } export default memo(AnimatedNodes) diff --git a/packages/network/src/Network.js b/packages/network/src/Network.js index fcc23863a..249b6e83c 100644 --- a/packages/network/src/Network.js +++ b/packages/network/src/Network.js @@ -6,15 +6,17 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Fragment } from 'react' +import React, { Fragment, useCallback } from 'react' import { withContainer, useDimensions, SvgWrapper, useTheme, useMotionConfig } from '@nivo/core' import { useInheritedColor } from '@nivo/colors' +import { useTooltip } from '@nivo/tooltip' import { NetworkPropTypes, NetworkDefaultProps } from './props' import { useNetwork, useNodeColor, useLinkThickness } from './hooks' import AnimatedNodes from './AnimatedNodes' import StaticNodes from './StaticNodes' import AnimatedLinks from './AnimatedLinks' import StaticLinks from './StaticLinks' +import NetworkNodeTooltip from './NetworkNodeTooltip' const Network = props => { const { @@ -39,6 +41,8 @@ const Network = props => { linkThickness, linkColor, + tooltip, + isInteractive, } = props const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( @@ -65,6 +69,19 @@ const Network = props => { center: [innerWidth / 2, innerHeight / 2], }) + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const handleNodeHover = useCallback( + (node, event) => { + showTooltipFromEvent(, event) + }, + [showTooltipFromEvent, tooltip] + ) + + const handleNodeLeave = useCallback(() => { + hideTooltip() + }, [hideTooltip]) + const layerById = { links: React.createElement(animate === true ? AnimatedLinks : StaticLinks, { key: 'links', @@ -78,6 +95,8 @@ const Network = props => { color: getColor, borderWidth: nodeBorderWidth, borderColor: getBorderColor, + handleNodeHover: isInteractive ? handleNodeHover : undefined, + handleNodeLeave: isInteractive ? handleNodeLeave : undefined, }), } diff --git a/packages/network/src/NetworkNodeTooltip.js b/packages/network/src/NetworkNodeTooltip.js new file mode 100644 index 000000000..6bace38c7 --- /dev/null +++ b/packages/network/src/NetworkNodeTooltip.js @@ -0,0 +1,24 @@ +import React, { memo } from 'react' +import PropTypes from 'prop-types' +import { BasicTooltip } from '@nivo/tooltip' + +const NetworkNodeTooltip = ({ node, format, tooltip }) => ( + +) + +NetworkNodeTooltip.propTypes = { + node: PropTypes.shape({ + id: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + }).isRequired, + format: PropTypes.func, + tooltip: PropTypes.func, +} + +export default memo(NetworkNodeTooltip) diff --git a/packages/network/src/Node.js b/packages/network/src/Node.js index a31cc9bcf..b1932c77a 100644 --- a/packages/network/src/Node.js +++ b/packages/network/src/Node.js @@ -9,7 +9,18 @@ import React, { memo } from 'react' import PropTypes from 'prop-types' -const Node = ({ x, y, radius, color, borderWidth, borderColor, scale = 1 }) => { +const Node = ({ + node, + x, + y, + radius, + color, + borderWidth, + borderColor, + scale = 1, + handleNodeHover, + handleNodeLeave, +}) => { return ( { fill={color} strokeWidth={borderWidth} stroke={borderColor} + onMouseEnter={event => handleNodeHover(node, event)} + onMouseMove={event => handleNodeHover(node, event)} + onMouseLeave={handleNodeLeave} /> ) } @@ -30,6 +44,8 @@ Node.propTypes = { borderWidth: PropTypes.number.isRequired, borderColor: PropTypes.string.isRequired, scale: PropTypes.number, + handleNodeHover: PropTypes.func.isRequired, + handleNodeLeave: PropTypes.func.isRequired, } export default memo(Node) diff --git a/packages/network/src/StaticNodes.js b/packages/network/src/StaticNodes.js index 113a397b5..8845a1730 100644 --- a/packages/network/src/StaticNodes.js +++ b/packages/network/src/StaticNodes.js @@ -10,7 +10,14 @@ import React, { memo } from 'react' import PropTypes from 'prop-types' import Node from './Node' -const StaticNodes = ({ nodes, color, borderWidth, borderColor }) => { +const StaticNodes = ({ + nodes, + color, + borderWidth, + borderColor, + handleNodeHover, + handleNodeLeave, +}) => { return nodes.map(node => { return ( { color={color(node)} borderWidth={borderWidth} borderColor={borderColor(node)} + handleNodeHover={handleNodeHover} + handleNodeLeave={handleNodeLeave} /> ) }) @@ -32,6 +41,8 @@ StaticNodes.propTypes = { color: PropTypes.func.isRequired, borderWidth: PropTypes.number.isRequired, borderColor: PropTypes.func.isRequired, + handleNodeHover: PropTypes.func.isRequired, + handleNodeLeave: PropTypes.func.isRequired, } export default memo(StaticNodes) diff --git a/packages/network/stories/network.stories.js b/packages/network/stories/network.stories.js new file mode 100644 index 000000000..925719c91 --- /dev/null +++ b/packages/network/stories/network.stories.js @@ -0,0 +1,43 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { NetworkDefaultProps } from '../src/props' +import { generateData } from '../../../website/src/data/components/network/generator' +import { Network } from '../src' + +const data = generateData() + +const commonProperties = { + ...NetworkDefaultProps, + nodes: data.nodes, + links: data.links, + width: 900, + height: 340, + nodeColor: function (t) { + return t.color + }, + repulsivity: 6, + iterations: 60, +} + +const stories = storiesOf('Network', module) + +stories.add('default', () => ) + +stories.add('custom tooltip', () => ( + { + return ( +
+
+ ID: {node.id} +
+ Depth: {node.depth} +
+ Radius: {node.radius} +
+
+ ) + }} + /> +)) diff --git a/website/src/data/components/network/props.js b/website/src/data/components/network/props.js index 7d88d41da..33ae215a4 100644 --- a/website/src/data/components/network/props.js +++ b/website/src/data/components/network/props.js @@ -79,7 +79,7 @@ const props = [ from the corresponding link property, thus, this property should exist on each link. - If you use a **function**, it will receive a link and must return + If you use a **function**, it will receive a link and must return the desired distance. `, }, @@ -170,6 +170,18 @@ const props = [ inheritableProperties: ['source.color', 'target.color'], }, }, + { + key: 'tooltip', + group: 'Interactivity', + type: 'Function', + required: false, + help: 'Custom tooltip component.', + description: ` + A function allowing complete tooltip customisation, + it must return a valid HTML + element and will receive the node's data. + `, + }, { key: 'layers', group: 'Customization',