Skip to content

Commit

Permalink
Add basic Sankey diagram
Browse files Browse the repository at this point in the history
Closes #34.
  • Loading branch information
Balthazar Gronon authored and apercu committed Jan 19, 2017
1 parent 62234e7 commit ffa23ab
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 2 deletions.
110 changes: 110 additions & 0 deletions docs/sankey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Sankey

Note: This component is in alpha.

### Usage

```jsx
import {Sankey} from 'react-vis';

const nodes = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
const links = [
{source: 0, target: 1, value: 10},
{source: 0, target: 2, value: 20},
{source: 1, target: 2, value: 20}
];

<Sankey
nodes={nodes}
links={links}
width={200}
height={200}
/>
```

### Api

##### width (required, pixels)
##### height (required, pixels)
##### nodes (required)

An array of objects matching the following shape:

```
{
color: String,
opacity: Number,
key: String
}
```

All these fields are optional.

##### links (required)

An array of objects matching the following shape, where both `source` and `target`
are the indexes of the nodes they intent to represent, and `value` that would
match the height of the path link.

```
{
// required
source: Number,
target: Number,
value: Number,
// optional
color: String,
opacity: Number,
key: String
}
```

##### margin (pixels)

The margin that will be applied to each side of the Sankey.

Defaults to `20`.

##### nodeWidth (pixels)

Width of the nodes.

Defaults to `10`.

##### nodePadding (pixels)

Padding between each node.

Defaults to `10`.

##### align

The alignment used for the sankey ([example](http://bl.ocks.org/vasturiano/b0b14f2e58fdeb0da61e62d51c649908)).
Can be `justify`, `center`, `left`, `right`.

Defaults to `justify`.

##### layout

The number of passes the sankey algorithm will do in order to arrange positioning.

Defaults to `50`.

##### hasVoronoi

Determine if the node selection will be done using a voronoi or not. Although less
precise, it can help providing a better interactive experience to the user.

Defaults to `false`.

##### onClick

Callback when clicking a node, or the voronoi assigned to this node, pass the node.

##### onHover

Callback when hovering a node, or the voronoi assigned to this node, pass the node.

##### onBlur

Callback when bluring a node, or the voronoi assigned to this node, pass the node.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"d3-color": "^1.0.2",
"d3-hierarchy": "^1.0.3",
"d3-interpolate": "^1.1.2",
"d3-sankey-align": "^0.1.0",
"d3-scale": "^1.0.4",
"d3-shape": "^1.0.4",
"d3-voronoi": "^1.1.1",
"deep-equal": "^1.0.1",
"global": "^4.3.1",
"react-motion": "^0.4.7"
Expand Down
11 changes: 11 additions & 0 deletions showcase/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import SimpleRadialChart from './radial-chart/simple-radial-chart';
import DonutChartExample from './radial-chart/donut-chart';
import CustomRadiusRadialChart from './radial-chart/custom-radius-radial-chart';

import BasicSankeyExample from './sankey/basic';

import VerticalDiscreteColorLegendExample from './legends/vertical-discrete-color';
import HorizontalDiscreteColorLegendExample from './legends/horizontal-discrete-color';
import SearchableDiscreteColorLegendExample from './legends/searchable-discrete-color';
Expand All @@ -80,6 +82,7 @@ const App = (
<li><a href="#treemaps">Treemaps</a></li>
<li><a href="#tables">Tables</a></li>
<li><a href="#legends">Legends</a></li>
<li><a href="#sankeys">Sankeys</a></li>
</nav>
</div>
</header>
Expand Down Expand Up @@ -286,6 +289,14 @@ const App = (
</section>
</article>

<article id="sankeys">
<h1>Sankeys</h1>
<section>
<h3>Basic</h3>
<BasicSankeyExample />
</section>
</article>

</main>
);

Expand Down
21 changes: 21 additions & 0 deletions showcase/sankey/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

import Sankey from 'sankey';

const nodes = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
const links = [
{source: 0, target: 1, value: 10},
{source: 0, target: 2, value: 20},
{source: 1, target: 2, value: 20}
];

export default function BasicSankeyExample() {
return (
<Sankey
nodes={nodes}
links={links}
width={200}
height={200}
/>
);
}
3 changes: 1 addition & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ export ContinuousColorLegend from 'legends/continuous-color-legend';
export ContinuousSizeLegend from 'legends/continuous-size-legend';

export Table from 'table';

export Treemap from 'treemap';

export RadialChart from 'radial-chart';
export Sankey from 'sankey';

export makeWidthFlexible from 'make-vis-flexible';

Expand Down
141 changes: 141 additions & 0 deletions src/sankey/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, {PropTypes, Component} from 'react';
import {voronoi} from 'd3-voronoi';
import {sankey} from 'd3-sankey-align';

import {DISCRETE_COLOR_RANGE} from 'theme';

const NOOP = f => f;

const DEFAULT_LINK_COLOR = DISCRETE_COLOR_RANGE[1];
const DEFAULT_LINK_OPACITY = 0.7;
const DEFAULT_NODE_COLOR = DISCRETE_COLOR_RANGE[0];
const DEFAULT_NODE_OPACITY = 1;

class Sankey extends Component {

static defaultProps = {
align: 'justify',
className: '',
hasVoronoi: false,
layout: 50,
margin: 20,
nodePadding: 10,
nodeWidth: 10,
onBlur: NOOP,
onClick: NOOP,
onHover: NOOP
}

static propTypes = {
align: PropTypes.oneOf(['justify', 'left', 'right', 'center']),
className: PropTypes.string,
hasVoronoi: PropTypes.bool,
height: PropTypes.number.isRequired,
layout: PropTypes.number,
links: PropTypes.arrayOf(PropTypes.shape({
source: PropTypes.number.isRequired,
target: PropTypes.number.isRequired
})).isRequired,
margin: PropTypes.number,
nodePadding: PropTypes.number,
nodes: PropTypes.arrayOf(PropTypes.object).isRequired,
nodeWidth: PropTypes.number,
onBlur: PropTypes.func,
onClick: PropTypes.func,
onHover: PropTypes.func,
width: PropTypes.number.isRequired
}

render() {

const {
align,
className,
hasVoronoi,
height,
layout,
links,
margin,
nodePadding,
nodes,
nodeWidth,
onBlur,
onClick,
onHover,
width
} = this.props;

const sankeyInstance = sankey()
.size([width, height])
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.nodes(nodes)
.links(links)
.align(align)
.layout(layout);

const nWidth = sankeyInstance.nodeWidth();
const path = sankeyInstance.link();

// Create a voronoi with each node center points
const voronoiInstance = hasVoronoi ?
voronoi()
.x(d => d.x + d.dx / 2)
.y(d => d.y + d.dy / 2)
.extent([[-margin, -margin], [width + margin, height + margin]]) :
null;

return (
<svg height={height + margin} width={width + margin} className={`rv-sankey ${className}`}>
<g transform={`translate(${margin / 2}, ${margin / 2})`}>

{links.map((link, i) => (
<path
d={path(link)}
className="rv-sankey__link"
opacity={Number.isFinite(link.opacity) ? link.opacity : DEFAULT_LINK_OPACITY}
stroke={link.color || DEFAULT_LINK_COLOR}
strokeWidth={Math.max(1, link.dy)}
fill="none"
key={link.id || link.key || `link-${i}`} />
))}

{nodes.map((node, i) => (
<g
transform={`translate(${node.x}, ${node.y})`}
className="rv-sankey__node"
opacity={Number.isFinite(node.opacity) ? node.opacity : DEFAULT_NODE_OPACITY}
key={node.id || node.key || `node-${i}`}>
<rect
onClick={() => onClick(node)}
onMouseOver={() => onHover(node)}
onMouseOut={() => onBlur(node)}
fill={node.color || DEFAULT_NODE_COLOR}
height={node.dy}
width={nWidth} />
</g>
))}

{hasVoronoi && (
<g className="rv-sankey__voronoi">
{voronoiInstance.polygons(nodes).map((d, i) => (
<path
d={`M${d.join('L')}Z`}
onClick={() => onClick(d.data)}
onMouseOver={() => onHover(d.data)}
onMouseOut={() => onBlur(d.data)}
fill="none"
style={{pointerEvents: 'all'}}
key={i} />
))}
</g>
)}

</g>
</svg>
);
}

}

export default Sankey;

0 comments on commit ffa23ab

Please sign in to comment.