Skip to content

Commit

Permalink
feat(sankey): improve Sankey diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 23, 2017
1 parent b90de33 commit aa5c847
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 105 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Several libraries already exist for React d3 integration, but just a few provide
- [`<Line />`](https://nivo-api.herokuapp.com/samples/line.svg)
- [`<Pie />`](https://nivo-api.herokuapp.com/samples/pie.svg)
- [`<Radar />`](https://nivo-api.herokuapp.com/samples/radar.svg)
- [`<Sankey />`](https://nivo-api.herokuapp.com/samples/sankey.svg)
- [`<Sunburst />`](https://nivo-api.herokuapp.com/samples/sunburst.svg)
- [`<TreeMap />`](https://nivo-api.herokuapp.com/samples/treemap.svg)

Expand Down
19 changes: 13 additions & 6 deletions src/Nivo.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,19 @@ export const defaultTheme = {
fontSize: '11px',
},
tooltip: {
background: 'white',
color: 'inherit',
fontSize: 'inherit',
borderRadius: '2px',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.25)',
padding: '5px 9px',
container: {
display: 'flex',
alignItems: 'center',
background: 'white',
color: 'inherit',
fontSize: 'inherit',
borderRadius: '2px',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.25)',
padding: '5px 9px',
},
basic: {
whiteSpace: 'pre',
},
table: {
fontSize: 'inherit',
},
Expand Down
7 changes: 1 addition & 6 deletions src/components/charts/radar/Radar.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,7 @@ const enhance = compose(
}, {}),
})),
withPropsOnChange(
(props, nextProps) =>
props.keys !== nextProps.keys ||
props.indexBy !== nextProps.indexBy ||
props.data !== nextProps.data ||
props.width !== nextProps.width ||
props.height !== nextProps.height,
['keys', 'indexBy', 'data', 'width', 'height'],
({ data, keys, width, height }) => {
const maxValue = max(data.reduce((acc, d) => [...acc, ...keys.map(key => d[key])], []))

Expand Down
106 changes: 69 additions & 37 deletions src/components/charts/sankey/Sankey.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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 { sankey as d3Sankey } from 'd3-sankey'
Expand All @@ -21,6 +22,7 @@ import SvgWrapper from '../SvgWrapper'
import SankeyNodes from './SankeyNodes'
import SankeyLinks from './SankeyLinks'
import SankeyLabels from './SankeyLabels'
import Container from '../Container'

const getId = d => d.id

Expand All @@ -38,15 +40,22 @@ const Sankey = ({

// nodes
nodeOpacity,
nodeHoverOpacity,
nodeWidth,
nodePadding,
nodePaddingX,
nodePaddingY,
nodeBorderWidth,
getNodeBorderColor, // computed
setCurrentNode, // injected
currentNode, // injected

// links
linkOpacity,
linkHoverOpacity,
linkContract,
getLinkColor,
getLinkColor, // computed
setCurrentLink, // injected
currentLink, // injected

// labels
enableLabels,
Expand All @@ -70,7 +79,7 @@ const Sankey = ({
const sankey = d3Sankey()
.nodeAlign(sankeyAlignmentFromProp(align))
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.nodePadding(nodePaddingY)
.size([width, height])
.nodeId(getId)

Expand All @@ -80,10 +89,14 @@ const Sankey = ({

data.nodes.forEach(node => {
node.color = getColor(node)
node.x = node.x0
node.x = node.x0 + nodePaddingX
node.y = node.y0
node.width = node.x1 - node.x0
node.height = node.y1 - node.y0
node.width = Math.max(node.x1 - node.x0 - nodePaddingX * 2, 0)
node.height = Math.max(node.y1 - node.y0, 0)
})

data.links.forEach(link => {
link.color = getLinkColor(link)
})

const motionProps = {
Expand All @@ -93,33 +106,44 @@ const Sankey = ({
}

return (
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin}>
<SankeyLinks
links={data.links}
linkContract={linkContract}
linkOpacity={linkOpacity}
getLinkColor={getLinkColor}
{...motionProps}
/>
<SankeyNodes
nodes={data.nodes}
nodeOpacity={nodeOpacity}
nodeBorderWidth={nodeBorderWidth}
getNodeBorderColor={getNodeBorderColor}
{...motionProps}
/>
{enableLabels &&
<SankeyLabels
nodes={data.nodes}
width={width}
labelPosition={labelPosition}
labelPadding={labelPadding}
labelOrientation={labelOrientation}
getLabelTextColor={getLabelTextColor}
theme={theme}
{...motionProps}
/>}
</SvgWrapper>
<Container isInteractive={isInteractive} theme={theme}>
{({ showTooltip, hideTooltip }) =>
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin}>
<SankeyLinks
links={data.links}
linkContract={linkContract}
linkOpacity={linkOpacity}
linkHoverOpacity={linkHoverOpacity}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
theme={theme}
{...motionProps}
/>
<SankeyNodes
nodes={data.nodes}
nodePaddingX={nodePaddingX}
nodeOpacity={nodeOpacity}
nodeHoverOpacity={nodeHoverOpacity}
nodeBorderWidth={nodeBorderWidth}
getNodeBorderColor={getNodeBorderColor}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
theme={theme}
{...motionProps}
/>
{enableLabels &&
<SankeyLabels
nodes={data.nodes}
width={width}
labelPosition={labelPosition}
labelPadding={labelPadding}
labelOrientation={labelOrientation}
getLabelTextColor={getLabelTextColor}
theme={theme}
{...motionProps}
/>}
</SvgWrapper>}
</Container>
)
}

Expand All @@ -142,13 +166,16 @@ Sankey.propTypes = {

// nodes
nodeOpacity: PropTypes.number.isRequired,
nodeHoverOpacity: PropTypes.number.isRequired,
nodeWidth: PropTypes.number.isRequired,
nodePadding: 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
Expand All @@ -168,28 +195,33 @@ export const SankeyDefaultProps = {

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

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

// labels
enableLabels: true,
labelPosition: 'inside',
labelPadding: 9,
labelOrientation: 'horizontal',
labelTextColor: 'inherit:darker(0.5)',
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',
Expand Down
6 changes: 5 additions & 1 deletion src/components/charts/sankey/SankeyLabels.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ const SankeyLabels = ({
transform={`translate(${style.x}, ${style.y}) rotate(${style.rotation})`}
alignmentBaseline="central"
textAnchor={data.textAnchor}
style={{ ...theme.sankey.label, fill: color }}
style={{
...theme.sankey.label,
fill: color,
pointerEvents: 'none',
}}
>
{data.id}
</text>
Expand Down
58 changes: 42 additions & 16 deletions src/components/charts/sankey/SankeyLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import { sankeyLinkHorizontal } from 'd3-sankey'
import { motionPropTypes } from '../../../props'
import SmartMotion from '../../SmartMotion'
import SankeyLinksItem from './SankeyLinksItem'

const getLinkPath = sankeyLinkHorizontal()

Expand All @@ -19,25 +21,35 @@ const SankeyLinks = ({

// links
linkOpacity,
linkHoverOpacity,
linkContract,
getLinkColor,

// motion
animate,
motionDamping,
motionStiffness,

showTooltip,
hideTooltip,

theme,
}) => {
if (animate !== true) {
return (
<g>
{links.map(link =>
<path
<SankeyLinksItem
key={`${link.source.id}.${link.target.id}`}
fill="none"
d={getLinkPath(link)}
strokeWidth={Math.max(1, link.width - linkContract * 2)}
stroke={getLinkColor(link)}
strokeOpacity={linkOpacity}
link={link}
path={getLinkPath(link)}
width={Math.max(1, link.width - linkContract * 2)}
color={link.color}
opacity={linkOpacity}
hoverOpacity={linkHoverOpacity}
contract={linkContract}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
theme={theme}
/>
)}
</g>
Expand All @@ -55,16 +67,22 @@ const SankeyLinks = ({
<SmartMotion
key={`${link.source.id}.${link.target.id}`}
style={spring => ({
d: spring(getLinkPath(link), springConfig),
strokeWidth: spring(
Math.max(1, link.width - linkContract * 2),
springConfig
),
stroke: spring(getLinkColor(link), springConfig),
strokeOpacity: spring(linkOpacity, springConfig),
path: spring(getLinkPath(link), springConfig),
width: spring(Math.max(1, link.width - linkContract * 2), springConfig),
color: spring(link.color, springConfig),
opacity: spring(linkOpacity, springConfig),
contract: spring(linkContract, springConfig),
})}
>
{style => <path fill="none" {...style} />}
{style =>
<SankeyLinksItem
link={link}
{...style}
hoverOpacity={linkHoverOpacity}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
theme={theme}
/>}
</SmartMotion>
)}
</g>
Expand All @@ -81,13 +99,21 @@ SankeyLinks.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}).isRequired,
width: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
})
).isRequired,

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

theme: PropTypes.object.isRequired,

...motionPropTypes,

showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
}

export default pure(SankeyLinks)

0 comments on commit aa5c847

Please sign in to comment.