Skip to content

Commit

Permalink
feat(sankey): add support for label on Sankey diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 22, 2017
1 parent f358d2f commit b90de33
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/Nivo.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const defaultTheme = {
padding: '3px 5px',
},
},
sankey: {
label: {},
},
}

export default {
Expand Down
70 changes: 64 additions & 6 deletions src/components/charts/sankey/Sankey.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ 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 { sankey as d3Sankey } from 'd3-sankey'
import { getInheritedColorGenerator } from '../../../lib/colorUtils'
import { withColors, withTheme, withDimensions, withMotion } from '../../../hocs'
import { sankeyAlignmentPropType, sankeyAlignmentFromProp } from '../../../props'
import SvgWrapper from '../SvgWrapper'
import { sankey as d3Sankey, sankeyLinkHorizontal } from 'd3-sankey'
import SankeyNodes from './SankeyNodes'
import SankeyLinks from './SankeyLinks'
import SankeyLabels from './SankeyLabels'

const getLinkPath = sankeyLinkHorizontal()
const getId = d => d.id

const Sankey = ({
data: _data,

align,

// dimensions
margin,
width,
Expand All @@ -37,13 +41,22 @@ const Sankey = ({
nodeWidth,
nodePadding,
nodeBorderWidth,
getNodeBorderColor,
getNodeBorderColor, // computed

// links
linkOpacity,
linkContract,
getLinkColor,

// labels
enableLabels,
labelPosition,
labelPadding,
labelOrientation,
getLabelTextColor, // computed

// theming
theme,
getColor, // computed

// motion
Expand All @@ -55,14 +68,24 @@ const Sankey = ({
isInteractive,
}) => {
const sankey = d3Sankey()
.nodeAlign(sankeyAlignmentFromProp(align))
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.size([width, height])
.nodeId(d => d.id)
.nodeId(getId)

// deep clone is required as the sankey diagram mutates data
const data = cloneDeep(_data)
sankey(data)

data.nodes.forEach(node => {
node.color = getColor(node)
node.x = node.x0
node.y = node.y0
node.width = node.x1 - node.x0
node.height = node.y1 - node.y0
})

const motionProps = {
animate,
motionDamping,
Expand All @@ -73,18 +96,29 @@ const Sankey = ({
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin}>
<SankeyLinks
links={data.links}
linkContract={linkContract}
linkOpacity={linkOpacity}
getLinkColor={getLinkColor}
{...motionProps}
/>
<SankeyNodes
nodes={data.nodes}
getColor={getColor}
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>
)
}
Expand All @@ -104,6 +138,8 @@ Sankey.propTypes = {
).isRequired,
}).isRequired,

align: sankeyAlignmentPropType.isRequired,

// nodes
nodeOpacity: PropTypes.number.isRequired,
nodeWidth: PropTypes.number.isRequired,
Expand All @@ -113,12 +149,23 @@ Sankey.propTypes = {

// links
linkOpacity: 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,
nodeWidth: 12,
Expand All @@ -128,6 +175,14 @@ export const SankeyDefaultProps = {

// links
linkOpacity: 0.25,
linkContract: 0,

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

// interactivity
isInteractive: true,
Expand All @@ -147,6 +202,9 @@ const enhance = compose(
withPropsOnChange(['nodeBorderColor'], ({ nodeBorderColor }) => ({
getNodeBorderColor: getInheritedColorGenerator(nodeBorderColor),
})),
withPropsOnChange(['labelTextColor'], ({ labelTextColor }) => ({
getLabelTextColor: getInheritedColorGenerator(labelTextColor),
})),
pure
)

Expand Down
158 changes: 158 additions & 0 deletions src/components/charts/sankey/SankeyLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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 pure from 'recompose/pure'
import { TransitionMotion, spring } from 'react-motion'
import { extractRGB } from '../../../lib/colorUtils'
import { motionPropTypes } from '../../../props'

const SankeyLabels = ({
nodes,

width,

labelPosition,
labelPadding,
labelOrientation,
getLabelTextColor,

theme,

// motion
animate,
motionDamping,
motionStiffness,
}) => {
const labelRotation = labelOrientation === 'vertical' ? -90 : 0
const labels = nodes.map(node => {
let x
let textAnchor
if (node.x < width / 2) {
if (labelPosition === 'inside') {
x = node.x1 + labelPadding
textAnchor = labelOrientation === 'vertical' ? 'middle' : 'start'
} else {
x = node.x - labelPadding
textAnchor = labelOrientation === 'vertical' ? 'middle' : 'end'
}
} else {
if (labelPosition === 'inside') {
x = node.x - labelPadding
textAnchor = labelOrientation === 'vertical' ? 'middle' : 'end'
} else {
x = node.x1 + labelPadding
textAnchor = labelOrientation === 'vertical' ? 'middle' : 'start'
}
}

return {
id: node.id,
x,
y: node.y + node.height / 2,
textAnchor,
color: getLabelTextColor(node),
}
})

if (!animate) {
return (
<g>
{labels.map(label => {
return (
<text
key={label.id}
alignmentBaseline="central"
textAnchor={label.textAnchor}
transform={`translate(${label.x}, ${label.y}) rotate(${labelRotation})`}
style={{ ...theme.sankey.label, fill: label.color }}
>
{label.id}
</text>
)
})}
</g>
)
}

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

return (
<TransitionMotion
styles={labels.map(label => {
return {
key: label.id,
data: label,
style: {
x: spring(label.x, springProps),
y: spring(label.y, springProps),
rotation: spring(labelRotation, springProps),
...extractRGB(label.color, springProps),
},
}
})}
>
{interpolatedStyles =>
<g>
{interpolatedStyles.map(({ key, style, data }) => {
const { colorR, colorG, colorB } = style
const color = `rgb(${Math.round(colorR)},${Math.round(colorG)},${Math.round(
colorB
)})`

return (
<text
key={key}
transform={`translate(${style.x}, ${style.y}) rotate(${style.rotation})`}
alignmentBaseline="central"
textAnchor={data.textAnchor}
style={{ ...theme.sankey.label, fill: color }}
>
{data.id}
</text>
)
})}
</g>}
</TransitionMotion>
)
}

SankeyLabels.propTypes = {
nodes: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
x1: PropTypes.number.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
})
).isRequired,

width: PropTypes.number.isRequired,

labelPosition: PropTypes.oneOf(['inside', 'outside']).isRequired,
labelPadding: PropTypes.number.isRequired,
labelOrientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
getLabelTextColor: PropTypes.func.isRequired,

theme: PropTypes.shape({
sankey: PropTypes.shape({
label: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,

...motionPropTypes,
}

export default pure(SankeyLabels)
9 changes: 7 additions & 2 deletions src/components/charts/sankey/SankeyLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const SankeyLinks = ({

// links
linkOpacity,
linkContract,
getLinkColor,

// motion
Expand All @@ -34,7 +35,7 @@ const SankeyLinks = ({
key={`${link.source.id}.${link.target.id}`}
fill="none"
d={getLinkPath(link)}
strokeWidth={Math.max(1, link.width)}
strokeWidth={Math.max(1, link.width - linkContract * 2)}
stroke={getLinkColor(link)}
strokeOpacity={linkOpacity}
/>
Expand All @@ -55,7 +56,10 @@ const SankeyLinks = ({
key={`${link.source.id}.${link.target.id}`}
style={spring => ({
d: spring(getLinkPath(link), springConfig),
strokeWidth: spring(Math.max(1, link.width), springConfig),
strokeWidth: spring(
Math.max(1, link.width - linkContract * 2),
springConfig
),
stroke: spring(getLinkColor(link), springConfig),
strokeOpacity: spring(linkOpacity, springConfig),
})}
Expand All @@ -82,6 +86,7 @@ SankeyLinks.propTypes = {

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

Expand Down
Loading

0 comments on commit b90de33

Please sign in to comment.