Skip to content

Commit

Permalink
feat(sankey): add support for vertical sankey
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte authored and Raphaël Benitte committed Mar 23, 2019
1 parent e0a56eb commit e299590
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 120 deletions.
11 changes: 5 additions & 6 deletions packages/sankey/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ declare module '@nivo/sankey' {
export type SankeySortFunction = (nodeA: SankeyDataNode, nodeB: SankeyDataNode) => number

export type SankeyProps = Partial<{
align: 'center' | 'justify' | 'left' | 'right'
align: 'center' | 'justify' | 'start' | 'end'
sort: 'auto' | 'input' | 'ascending' | 'descending' | SankeySortFunction

nodeOpacity: number
nodeHoverOpacity: number
nodeHoverOthersOpacity: number
nodeWidth: number
nodePaddingX: number
nodePaddingY: number
nodeThickness: number
nodeSpacing: number
nodeInnerPadding: number
nodeBorderWidth: number
nodeBorderColor: any

Expand Down Expand Up @@ -78,8 +79,6 @@ declare module '@nivo/sankey' {
theme: Theme

legends: LegendProps[]

sort: 'auto' | 'input' | 'ascending' | 'descending' | SankeySortFunction
}>

interface Dimensions {
Expand Down
1 change: 1 addition & 0 deletions packages/sankey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@nivo/core": "0.53.0",
"@nivo/legends": "0.53.0",
"d3-sankey": "^0.12.1",
"d3-shape": "^1.3.5",
"lodash": "^4.17.4",
"react-motion": "^0.5.2",
"recompose": "^0.30.0"
Expand Down
2 changes: 2 additions & 0 deletions packages/sankey/src/Sankey.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SankeyLabels from './SankeyLabels'
const Sankey = ({
nodes,
links,
layout,

margin,
width,
Expand Down Expand Up @@ -103,6 +104,7 @@ const Sankey = ({
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin} theme={theme}>
<SankeyLinks
links={links}
layout={layout}
linkContract={linkContract}
linkOpacity={linkOpacity}
linkHoverOpacity={linkHoverOpacity}
Expand Down
43 changes: 43 additions & 0 deletions packages/sankey/src/SankeyLinkGradient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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, { memo } from 'react'
import PropTypes from 'prop-types'

const SankeyLinkGradient = memo(({ id, layout, startColor, endColor }) => {
const gradientProps = {}
if (layout === 'horizontal') {
gradientProps.x1 = '0%'
gradientProps.x2 = '100%'
gradientProps.y1 = '0%'
gradientProps.y2 = '0%'
} else {
gradientProps.x1 = '0%'
gradientProps.x2 = '0%'
gradientProps.y1 = '0%'
gradientProps.y2 = '100%'
}

return (
<linearGradient id={id} spreadMethod="pad" {...gradientProps}>
<stop offset="0%" stopColor={startColor} />
<stop offset="100%" stopColor={endColor} />
</linearGradient>
)
})

SankeyLinkGradient.propTypes = {
id: PropTypes.string.isRequired,
layout: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
startColor: PropTypes.string.isRequired,
endColor: PropTypes.string.isRequired,
}

SankeyLinkGradient.displayName = 'SankeyLinkGradient'

export default SankeyLinkGradient
70 changes: 59 additions & 11 deletions packages/sankey/src/SankeyLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,63 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import pure from 'recompose/pure'
import { sankeyLinkHorizontal } from 'd3-sankey'
import { line, curveMonotoneX, curveMonotoneY } from 'd3-shape'
import { motionPropTypes, SmartMotion, blendModePropType } from '@nivo/core'
import SankeyLinksItem from './SankeyLinksItem'

const getLinkPath = sankeyLinkHorizontal()
const sankeyLinkHorizontal = () => {
const lineGenerator = line().curve(curveMonotoneX)

return (n, contract) => {
const thickness = Math.max(1, n.thickness - contract * 2)
console.log(n.thickness, contract)
const halfThickness = thickness / 2
const linkLength = n.target.x0 - n.source.x1
const padLength = linkLength * 0.12

const dots = [
[n.source.x1, n.pos0 - halfThickness],
[n.source.x1 + padLength, n.pos0 - halfThickness],
[n.target.x0 - padLength, n.pos1 - halfThickness],
[n.target.x0, n.pos1 - halfThickness],
[n.target.x0, n.pos1 + halfThickness],
[n.target.x0 - padLength, n.pos1 + halfThickness],
[n.source.x1 + padLength, n.pos0 + halfThickness],
[n.source.x1, n.pos0 + halfThickness],
[n.source.x1, n.pos0 - halfThickness],
]

return lineGenerator(dots) + 'Z'
}
}

const sankeyLinkVertical = () => {
const lineGenerator = line().curve(curveMonotoneY)

return n => {
const halfThickness = n.thickness / 2
const linkLength = n.target.y0 - n.source.y1
const padLength = linkLength * 0.12

const dots = [
[n.pos0 + halfThickness, n.source.y1],
[n.pos0 + halfThickness, n.source.y1 + padLength],
[n.pos1 + halfThickness, n.target.y0 - padLength],
[n.pos1 + halfThickness, n.target.y0],
[n.pos1 - halfThickness, n.target.y0],
[n.pos1 - halfThickness, n.target.y0 - padLength],
[n.pos0 - halfThickness, n.source.y1 + padLength],
[n.pos0 - halfThickness, n.source.y1],
[n.pos0 + halfThickness, n.source.y1],
]

return lineGenerator(dots) + 'Z'
}
}

const SankeyLinks = ({
links,
layout,

linkOpacity,
linkHoverOpacity,
Expand Down Expand Up @@ -47,18 +96,19 @@ const SankeyLinks = ({
return linkHoverOthersOpacity
}

const getLinkPath = layout === 'horizontal' ? sankeyLinkHorizontal() : sankeyLinkVertical()

if (animate !== true) {
return (
<g>
{links.map(link => (
<SankeyLinksItem
key={`${link.source.id}.${link.target.id}`}
link={link}
path={getLinkPath(link)}
width={Math.max(1, link.width - linkContract * 2)}
layout={layout}
path={getLinkPath(link, linkContract)}
color={link.color}
opacity={getOpacity(link)}
contract={linkContract}
blendMode={linkBlendMode}
enableGradient={enableLinkGradient}
showTooltip={showTooltip}
Expand All @@ -85,16 +135,15 @@ const SankeyLinks = ({
<SmartMotion
key={`${link.source.id}.${link.target.id}`}
style={spring => ({
path: spring(getLinkPath(link), springConfig),
width: spring(Math.max(1, link.width - linkContract * 2), springConfig),
path: spring(getLinkPath(link, linkContract), springConfig),
color: spring(link.color, springConfig),
opacity: spring(getOpacity(link), springConfig),
contract: spring(linkContract, springConfig),
})}
>
{style => (
<SankeyLinksItem
link={link}
layout={layout}
{...style}
blendMode={linkBlendMode}
enableGradient={enableLinkGradient}
Expand All @@ -114,6 +163,7 @@ const SankeyLinks = ({
}

SankeyLinks.propTypes = {
layout: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
links: PropTypes.arrayOf(
PropTypes.shape({
source: PropTypes.shape({
Expand All @@ -124,12 +174,11 @@ SankeyLinks.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
}).isRequired,
width: PropTypes.number.isRequired,
thickness: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
})
).isRequired,

// links
linkOpacity: PropTypes.number.isRequired,
linkHoverOpacity: PropTypes.number.isRequired,
linkHoverOthersOpacity: PropTypes.number.isRequired,
Expand All @@ -142,7 +191,6 @@ SankeyLinks.propTypes = {

...motionPropTypes,

// interactivity
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
setCurrentLink: PropTypes.func.isRequired,
Expand Down
71 changes: 30 additions & 41 deletions packages/sankey/src/SankeyLinksItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
*/
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import compose from 'recompose/compose'
import withPropsOnChange from 'recompose/withPropsOnChange'
import withHandlers from 'recompose/withHandlers'
import pure from 'recompose/pure'
import { compose, withPropsOnChange, withHandlers, pure } from 'recompose'
import { BasicTooltip, Chip, blendModePropType } from '@nivo/core'
import SankeyLinkGradient from './SankeyLinkGradient'

const tooltipStyles = {
container: {
Expand Down Expand Up @@ -56,49 +54,43 @@ TooltipContent.propTypes = {
}

const SankeyLinksItem = ({
link,
layout,
path,
width,
color,
opacity,
contract,
blendMode,
enableGradient,

// interactivity
handleMouseEnter,
handleMouseMove,
handleMouseLeave,
onClick,
}) => {
const linkId = `${link.source.id}.${link.target.id}`

link,
}) => (
<Fragment>
{enableGradient && (
<linearGradient
id={`${link.source.id}.${link.target.id}`}
gradientUnits="userSpaceOnUse"
x1={link.source.x}
x2={link.target.x}
>
{/*Use startColor & endColor if want to customize link color gradient*/}
<stop offset="0%" stopColor={link.startColor || link.source.color} />
<stop offset="100%" stopColor={link.endColor || link.target.color} />
</linearGradient>
)}
<path
fill="none"
d={path}
strokeWidth={Math.max(1, width - contract * 2)}
stroke={enableGradient ? `url(#${link.source.id}.${link.target.id})` : color}
strokeOpacity={opacity}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={onClick}
style={{ mixBlendMode: blendMode }}
/>
</Fragment>
)
return (
<Fragment>
{enableGradient && (
<SankeyLinkGradient
id={linkId}
layout={layout}
startColor={link.startColor || link.source.color}
endColor={link.endColor || link.target.color}
/>
)}
<path
fill={enableGradient ? `url(#${linkId})` : color}
d={path}
fillOpacity={opacity}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={onClick}
style={{ mixBlendMode: blendMode }}
/>
</Fragment>
)
}

SankeyLinksItem.propTypes = {
link: PropTypes.shape({
Expand All @@ -115,18 +107,15 @@ SankeyLinksItem.propTypes = {
color: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
}).isRequired,

layout: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
path: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
opacity: PropTypes.number.isRequired,
contract: PropTypes.number.isRequired,
blendMode: blendModePropType.isRequired,
enableGradient: PropTypes.bool.isRequired,

theme: PropTypes.object.isRequired,

// interactivity
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
setCurrent: PropTypes.func.isRequired,
Expand Down
42 changes: 22 additions & 20 deletions packages/sankey/src/SankeyNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,28 @@ const SankeyNodes = ({
if (!animate) {
return (
<Fragment>
{nodes.map(node => (
<SankeyNodesItem
key={node.id}
node={node}
x={node.x}
y={node.y}
width={node.width}
height={node.height}
color={node.color}
opacity={getOpacity(node)}
borderWidth={nodeBorderWidth}
borderColor={getNodeBorderColor(node)}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrent={setCurrentNode}
onClick={onClick}
tooltip={tooltip}
theme={theme}
/>
))}
{nodes.map(node => {
return (
<SankeyNodesItem
key={node.id}
node={node}
x={node.x}
y={node.y}
width={node.width}
height={node.height}
color={node.color}
opacity={getOpacity(node)}
borderWidth={nodeBorderWidth}
borderColor={getNodeBorderColor(node)}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
setCurrent={setCurrentNode}
onClick={onClick}
tooltip={tooltip}
theme={theme}
/>
)
})}
</Fragment>
)
}
Expand Down
Loading

0 comments on commit e299590

Please sign in to comment.