Skip to content

Commit

Permalink
feat(sankey): improve sankey interactivity
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Sep 5, 2017
1 parent c320c23 commit 27a5ff5
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 141 deletions.
145 changes: 43 additions & 102 deletions src/components/charts/sankey/Sankey.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,16 @@
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import { cloneDeep } from 'lodash'
import compose from 'recompose/compose'
import defaultProps from 'recompose/defaultProps'
import withState from 'recompose/withState'
import withPropsOnChange from 'recompose/withPropsOnChange'
import pure from 'recompose/pure'
import { cloneDeep, uniq } from 'lodash'
import { sankey as d3Sankey } from 'd3-sankey'
import { getInheritedColorGenerator } from '../../../lib/colors'
import { withColors, withTheme, withDimensions, withMotion } from '../../../hocs'
import { sankeyAlignmentPropType, sankeyAlignmentFromProp } from '../../../props'
import { sankeyAlignmentFromProp } from '../../../props'
import SvgWrapper from '../SvgWrapper'
import SankeyNodes from './SankeyNodes'
import SankeyLinks from './SankeyLinks'
import SankeyLabels from './SankeyLabels'
import Container from '../Container'
import { SankeyPropTypes } from './props'
import enhance from './enhance'

const getId = d => d.id

Expand All @@ -41,6 +35,7 @@ const Sankey = ({
// nodes
nodeOpacity,
nodeHoverOpacity,
nodeHoverOthersOpacity,
nodeWidth,
nodePaddingX,
nodePaddingY,
Expand All @@ -52,6 +47,7 @@ const Sankey = ({
// links
linkOpacity,
linkHoverOpacity,
linkHoverOthersOpacity,
linkContract,
getLinkColor, // computed
setCurrentLink, // injected
Expand Down Expand Up @@ -105,6 +101,32 @@ const Sankey = ({
motionStiffness,
}

let isCurrentNode = () => false
let isCurrentLink = () => false

if (currentLink) {
isCurrentNode = ({ id }) => id === currentLink.source.id || id === currentLink.target.id
isCurrentLink = ({ source, target }) =>
source.id === currentLink.source.id && target.id === currentLink.target.id
}

if (currentNode) {
let currentNodeIds = [currentNode.id]
data.links
.filter(
({ source, target }) => source.id === currentNode.id || target.id === currentNode.id
)
.forEach(({ source, target }) => {
currentNodeIds.push(source.id)
currentNodeIds.push(target.id)
})

currentNodeIds = uniq(currentNodeIds)
isCurrentNode = ({ id }) => currentNodeIds.includes(id)
isCurrentLink = ({ source, target }) =>
source.id === currentNode.id || target.id === currentNode.id
}

return (
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) =>
Expand All @@ -114,8 +136,13 @@ const Sankey = ({
linkContract={linkContract}
linkOpacity={linkOpacity}
linkHoverOpacity={linkHoverOpacity}
linkHoverOthersOpacity={linkHoverOthersOpacity}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrentLink={setCurrentLink}
currentNode={currentNode}
currentLink={currentLink}
isCurrentLink={isCurrentLink}
theme={theme}
{...motionProps}
/>
Expand All @@ -124,10 +151,15 @@ const Sankey = ({
nodePaddingX={nodePaddingX}
nodeOpacity={nodeOpacity}
nodeHoverOpacity={nodeHoverOpacity}
nodeHoverOthersOpacity={nodeHoverOthersOpacity}
nodeBorderWidth={nodeBorderWidth}
getNodeBorderColor={getNodeBorderColor}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrentNode={setCurrentNode}
currentNode={currentNode}
currentLink={currentLink}
isCurrentNode={isCurrentNode}
theme={theme}
{...motionProps}
/>
Expand All @@ -147,97 +179,6 @@ const Sankey = ({
)
}

Sankey.propTypes = {
data: PropTypes.shape({
nodes: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
links: PropTypes.arrayOf(
PropTypes.shape({
source: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
target: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
}).isRequired,

align: sankeyAlignmentPropType.isRequired,

// nodes
nodeOpacity: PropTypes.number.isRequired,
nodeHoverOpacity: PropTypes.number.isRequired,
nodeWidth: PropTypes.number.isRequired,
nodePaddingX: PropTypes.number.isRequired,
nodePaddingY: PropTypes.number.isRequired,
nodeBorderWidth: PropTypes.number.isRequired,
nodeBorderColor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

// links
linkOpacity: PropTypes.number.isRequired,
linkHoverOpacity: PropTypes.number.isRequired,
linkContract: PropTypes.number.isRequired,

// labels
enableLabels: PropTypes.bool.isRequired,
labelPosition: PropTypes.oneOf(['inside', 'outside']).isRequired,
labelPadding: PropTypes.number.isRequired,
labelOrientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
labelTextColor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
getLabelTextColor: PropTypes.func.isRequired, // computed

// interactivity
isInteractive: PropTypes.bool.isRequired,
}

export const SankeyDefaultProps = {
align: 'center',

// nodes
nodeOpacity: 0.65,
nodeHoverOpacity: 1,
nodeWidth: 12,
nodePaddingX: 0,
nodePaddingY: 12,
nodeBorderWidth: 1,
nodeBorderColor: 'inherit:darker(0.5)',

// links
linkOpacity: 0.2,
linkHoverOpacity: 0.4,
linkContract: 0,

// labels
enableLabels: true,
labelPosition: 'inside',
labelPadding: 9,
labelOrientation: 'horizontal',
labelTextColor: 'inherit:darker(0.8)',

// interactivity
isInteractive: true,
}

const enhance = compose(
defaultProps(SankeyDefaultProps),
withState('currentNode', 'setCurrentNode', null),
withState('currentLink', 'setCurrentLink', null),
withColors(),
withColors({
colorByKey: 'linkColorBy',
destKey: 'getLinkColor',
defaultColorBy: 'source.id',
}),
withTheme(),
withDimensions(),
withMotion(),
withPropsOnChange(['nodeBorderColor'], ({ nodeBorderColor }) => ({
getNodeBorderColor: getInheritedColorGenerator(nodeBorderColor),
})),
withPropsOnChange(['labelTextColor'], ({ labelTextColor }) => ({
getLabelTextColor: getInheritedColorGenerator(labelTextColor),
})),
pure
)
Sankey.propTypes = SankeyPropTypes

export default enhance(Sankey)
25 changes: 21 additions & 4 deletions src/components/charts/sankey/SankeyLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,30 @@ const SankeyLinks = ({
// links
linkOpacity,
linkHoverOpacity,
linkHoverOthersOpacity,
linkContract,

// motion
animate,
motionDamping,
motionStiffness,

// interactivity
showTooltip,
hideTooltip,
setCurrentLink,
currentNode,
currentLink,
isCurrentLink,

theme,
}) => {
const getOpacity = link => {
if (!currentNode && !currentLink) return linkOpacity
if (isCurrentLink(link)) return linkHoverOpacity
return linkHoverOthersOpacity
}

if (animate !== true) {
return (
<g>
Expand All @@ -44,11 +56,11 @@ const SankeyLinks = ({
path={getLinkPath(link)}
width={Math.max(1, link.width - linkContract * 2)}
color={link.color}
opacity={linkOpacity}
hoverOpacity={linkHoverOpacity}
opacity={getOpacity(link)}
contract={linkContract}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrent={setCurrentLink}
theme={theme}
/>
)}
Expand All @@ -70,17 +82,17 @@ const SankeyLinks = ({
path: spring(getLinkPath(link), springConfig),
width: spring(Math.max(1, link.width - linkContract * 2), springConfig),
color: spring(link.color, springConfig),
opacity: spring(linkOpacity, springConfig),
opacity: spring(getOpacity(link), springConfig),
contract: spring(linkContract, springConfig),
})}
>
{style =>
<SankeyLinksItem
link={link}
{...style}
hoverOpacity={linkHoverOpacity}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrent={setCurrentLink}
theme={theme}
/>}
</SmartMotion>
Expand All @@ -106,14 +118,19 @@ SankeyLinks.propTypes = {
// links
linkOpacity: PropTypes.number.isRequired,
linkHoverOpacity: PropTypes.number.isRequired,
linkHoverOthersOpacity: PropTypes.number.isRequired,
linkContract: PropTypes.number.isRequired,

theme: PropTypes.object.isRequired,

...motionPropTypes,

// interactivity
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
setCurrentLink: PropTypes.func.isRequired,
currentLink: PropTypes.object,
isCurrentLink: PropTypes.func.isRequired,
}

export default pure(SankeyLinks)
33 changes: 19 additions & 14 deletions src/components/charts/sankey/SankeyLinksItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,27 @@ const TooltipContent = ({ link }) =>

const SankeyLinksItem = ({
link,

path,
width,
color,
opacity,
hoverOpacity,
contract,
showTooltip,
hideTooltip,
isHover,

// interactivity
handleMouseEnter,
handleMouseMove,
handleMouseLeave,
}) =>
<path
fill="none"
d={path}
strokeWidth={Math.max(1, width - contract * 2)}
stroke={color}
strokeOpacity={isHover ? hoverOpacity : opacity}
onMouseEnter={showTooltip}
onMouseMove={showTooltip}
onMouseLeave={hideTooltip}
strokeOpacity={opacity}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
/>

SankeyLinksItem.propTypes = {
Expand All @@ -79,27 +81,30 @@ SankeyLinksItem.propTypes = {
width: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
opacity: PropTypes.number.isRequired,
hoverOpacity: PropTypes.number.isRequired,
contract: PropTypes.number.isRequired,

theme: PropTypes.object.isRequired,

// interactivity
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
setCurrent: PropTypes.func.isRequired,
}

const enhance = compose(
withState('isHover', 'setIsHover', false),
withPropsOnChange(['link', 'theme'], ({ link, theme }) => ({
tooltip: <BasicTooltip id={<TooltipContent link={link} />} theme={theme} />,
})),
withHandlers({
showTooltip: ({ showTooltip, setIsHover, tooltip }) => e => {
setIsHover(true)
handleMouseEnter: ({ showTooltip, setCurrent, link, tooltip }) => e => {
setCurrent(link)
showTooltip(tooltip, e)
},
handleMouseMove: ({ showTooltip, tooltip }) => e => {
showTooltip(tooltip, e)
},
hideTooltip: ({ hideTooltip, setIsHover }) => () => {
setIsHover(false)
handleMouseLeave: ({ hideTooltip, setCurrent }) => () => {
setCurrent(null)
hideTooltip()
},
}),
Expand Down

0 comments on commit 27a5ff5

Please sign in to comment.