Skip to content

Commit

Permalink
feat(sankey): add sankey diagram component
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 21, 2017
1 parent 7a9ae92 commit f358d2f
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 5 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Several libraries already exist for React d3 integration, but just a few provide
- Radar
- [`<Radar />`](http://nivo.rocks/#/radar)
- [`<ResponsiveRadar />`](http://nivo.rocks/#/radar)
- Sankey
- [`<Sankey />`](http://nivo.rocks/#/sankey)
- [`<ResponsiveSankey />`](http://nivo.rocks/#/sankey)
- Stream
- [`<Stream />`](http://nivo.rocks/#/stream)
- [`<ResponsiveStream />`](http://nivo.rocks/#/stream)
Expand All @@ -62,7 +65,7 @@ Several libraries already exist for React d3 integration, but just a few provide
- [`<ResponsiveTreeMapHTML />`](http://nivo.rocks/#/treemap/html)
- [`<TreeMapPlaceholders />`](http://nivo.rocks/#/treemap/placeholders)
- [`<ResponsiveTreeMapPlaceholders />`](http://nivo.rocks/#/treemap/placeholders)
- Voronoi
- Voronoi `experimental`
- [`<Voronoi />`](http://nivo.rocks/#/voronoi)
- [`<ResponsiveVoronoi />`](http://nivo.rocks/#/voronoi)

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"d3-format": "^1.2.0",
"d3-hierarchy": "^1.1.5",
"d3-interpolate": "^1.1.5",
"d3-sankey": "^0.7.1",
"d3-scale": "^1.0.6",
"d3-scale-chromatic": "^1.1.1",
"d3-shape": "^1.2.0",
Expand Down Expand Up @@ -66,7 +67,7 @@
"husky": "^0.14.3",
"jest": "^20.0.4",
"lint-staged": "^4.0.2",
"nivo-generators": "^0.6.0",
"nivo-generators": "^0.7.0",
"prettier": "^1.5.3",
"react": "^15.4.1",
"react-dom": "^15.4.1",
Expand Down
18 changes: 18 additions & 0 deletions src/components/charts/sankey/ResponsiveSankey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import ResponsiveWrapper from '../ResponsiveWrapper'
import Sankey from './Sankey'

const ResponsiveSankey = props =>
<ResponsiveWrapper>
{({ width, height }) => <Sankey width={width} height={height} {...props} />}
</ResponsiveWrapper>

export default ResponsiveSankey
153 changes: 153 additions & 0 deletions src/components/charts/sankey/Sankey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* 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 withPropsOnChange from 'recompose/withPropsOnChange'
import pure from 'recompose/pure'
import { withColors, withTheme, withDimensions, withMotion } from '../../../hocs'
import { getInheritedColorGenerator } from '../../../lib/colorUtils'
import SvgWrapper from '../SvgWrapper'
import { sankey as d3Sankey, sankeyLinkHorizontal } from 'd3-sankey'
import SankeyNodes from './SankeyNodes'
import SankeyLinks from './SankeyLinks'

const getLinkPath = sankeyLinkHorizontal()

const Sankey = ({
data: _data,

// dimensions
margin,
width,
height,
outerWidth,
outerHeight,

// nodes
nodeOpacity,
nodeWidth,
nodePadding,
nodeBorderWidth,
getNodeBorderColor,

// links
linkOpacity,
getLinkColor,

// theming
getColor, // computed

// motion
animate,
motionDamping,
motionStiffness,

// interactivity
isInteractive,
}) => {
const sankey = d3Sankey()
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.size([width, height])
.nodeId(d => d.id)

const data = cloneDeep(_data)
sankey(data)

const motionProps = {
animate,
motionDamping,
motionStiffness,
}

return (
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin}>
<SankeyLinks
links={data.links}
linkOpacity={linkOpacity}
getLinkColor={getLinkColor}
{...motionProps}
/>
<SankeyNodes
nodes={data.nodes}
getColor={getColor}
nodeOpacity={nodeOpacity}
nodeBorderWidth={nodeBorderWidth}
getNodeBorderColor={getNodeBorderColor}
{...motionProps}
/>
</SvgWrapper>
)
}

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,

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

// links
linkOpacity: PropTypes.number.isRequired,

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

export const SankeyDefaultProps = {
// nodes
nodeOpacity: 0.65,
nodeWidth: 12,
nodePadding: 12,
nodeBorderWidth: 1,
nodeBorderColor: 'inherit:darker(0.5)',

// links
linkOpacity: 0.25,

// interactivity
isInteractive: true,
}

const enhance = compose(
defaultProps(SankeyDefaultProps),
withColors(),
withColors({
colorByKey: 'linkColorBy',
destKey: 'getLinkColor',
defaultColorBy: 'source.id',
}),
withTheme(),
withDimensions(),
withMotion(),
withPropsOnChange(['nodeBorderColor'], ({ nodeBorderColor }) => ({
getNodeBorderColor: getInheritedColorGenerator(nodeBorderColor),
})),
pure
)

export default enhance(Sankey)
88 changes: 88 additions & 0 deletions src/components/charts/sankey/SankeyLinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import { sankeyLinkHorizontal } from 'd3-sankey'
import SmartMotion from '../../SmartMotion'

const getLinkPath = sankeyLinkHorizontal()

const SankeyLinks = ({
links,

// links
linkOpacity,
getLinkColor,

// motion
animate,
motionDamping,
motionStiffness,
}) => {
if (animate !== true) {
return (
<g>
{links.map(link =>
<path
key={`${link.source.id}.${link.target.id}`}
fill="none"
d={getLinkPath(link)}
strokeWidth={Math.max(1, link.width)}
stroke={getLinkColor(link)}
strokeOpacity={linkOpacity}
/>
)}
</g>
)
}

const springConfig = {
stiffness: motionStiffness,
damping: motionDamping,
}

return (
<g>
{links.map(link =>
<SmartMotion
key={`${link.source.id}.${link.target.id}`}
style={spring => ({
d: spring(getLinkPath(link), springConfig),
strokeWidth: spring(Math.max(1, link.width), springConfig),
stroke: spring(getLinkColor(link), springConfig),
strokeOpacity: spring(linkOpacity, springConfig),
})}
>
{style => <path fill="none" {...style} />}
</SmartMotion>
)}
</g>
)
}

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

// links
linkOpacity: PropTypes.number.isRequired,
getLinkColor: PropTypes.func.isRequired,
}

export default pure(SankeyLinks)
Loading

0 comments on commit f358d2f

Please sign in to comment.