Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(network): add support for tooltips #1080

Merged
merged 2 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
13 changes: 12 additions & 1 deletion packages/network/src/AnimatedNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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}
/>
)
})}
Expand All @@ -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)
21 changes: 20 additions & 1 deletion packages/network/src/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -39,6 +41,8 @@ const Network = props => {

linkThickness,
linkColor,
tooltip,
isInteractive,
} = props

const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(
Expand All @@ -65,6 +69,19 @@ const Network = props => {
center: [innerWidth / 2, innerHeight / 2],
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()

const handleNodeHover = useCallback(
(node, event) => {
showTooltipFromEvent(<NetworkNodeTooltip node={node} tooltip={tooltip} />, event)
},
[showTooltipFromEvent, tooltip]
)

const handleNodeLeave = useCallback(() => {
hideTooltip()
}, [hideTooltip])

const layerById = {
links: React.createElement(animate === true ? AnimatedLinks : StaticLinks, {
key: 'links',
Expand All @@ -78,6 +95,8 @@ const Network = props => {
color: getColor,
borderWidth: nodeBorderWidth,
borderColor: getBorderColor,
handleNodeHover: isInteractive ? handleNodeHover : undefined,
handleNodeLeave: isInteractive ? handleNodeLeave : undefined,
}),
}

Expand Down
24 changes: 24 additions & 0 deletions packages/network/src/NetworkNodeTooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { memo } from 'react'
import PropTypes from 'prop-types'
import { BasicTooltip } from '@nivo/tooltip'

const NetworkNodeTooltip = ({ node, format, tooltip }) => (
<BasicTooltip
id={node.id}
enableChip={true}
color={node.color}
format={format}
renderContent={typeof tooltip === 'function' ? tooltip.bind(null, { ...node }) : null}
/>
)

NetworkNodeTooltip.propTypes = {
node: PropTypes.shape({
id: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
}).isRequired,
format: PropTypes.func,
tooltip: PropTypes.func,
}

export default memo(NetworkNodeTooltip)
18 changes: 17 additions & 1 deletion packages/network/src/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@
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 (
<circle
transform={`translate(${x},${y}) scale(${scale})`}
r={radius}
fill={color}
strokeWidth={borderWidth}
stroke={borderColor}
onMouseEnter={event => handleNodeHover(node, event)}
onMouseMove={event => handleNodeHover(node, event)}
onMouseLeave={handleNodeLeave}
/>
)
}
Expand All @@ -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)
13 changes: 12 additions & 1 deletion packages/network/src/StaticNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Node
Expand All @@ -22,6 +29,8 @@ const StaticNodes = ({ nodes, color, borderWidth, borderColor }) => {
color={color(node)}
borderWidth={borderWidth}
borderColor={borderColor(node)}
handleNodeHover={handleNodeHover}
handleNodeLeave={handleNodeLeave}
/>
)
})
Expand All @@ -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)
43 changes: 43 additions & 0 deletions packages/network/stories/network.stories.js
Original file line number Diff line number Diff line change
@@ -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', () => <Network {...commonProperties} />)

stories.add('custom tooltip', () => (
<Network
{...commonProperties}
tooltip={node => {
return (
<div>
<div>
<strong style={{ color: node.color }}>ID: {node.id}</strong>
<br />
Depth: {node.depth}
<br />
Radius: {node.radius}
</div>
</div>
)
}}
/>
))
14 changes: 13 additions & 1 deletion website/src/data/components/network/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`,
},
Expand Down Expand Up @@ -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',
Expand Down