import React, { Component } from 'react';
import { json } from 'd3-request';
import { rgb } from 'd3-color';
import { interpolateHcl } from 'd3-interpolate';
import { scaleLinear, scaleOrdinal } from 'd3-scale';
import { arc, line, pie, curveMonotoneX } from 'd3-shape';
import { format } from 'd3-format';
import { min, max } from 'd3-array';
import { select } from 'd3-selection';
import { sankey as sankeyGraph, sankeyLinkHorizontal } from 'd3-sankey';
class Graph extends React.Component {
constructor(props) {
super(props);
this.createLineGraph = this.createLineGraph.bind(this);
this.createBarChart = this.createBarChart.bind(this);
this.createPieChart = this.createPieChart.bind(this);
this.createSankeyGraph = this.createSankeyGraph.bind(this);
// this.createRadialChart = this.createRadialChart.bind(this);
this.createTheGraphs = this.createTheGraphs.bind(this);
this.state = {
loading: false
};
}
getDimensions() {
const margin = {top: 20, right: 20, bottom: 20, left: 20},
padding = {top: 40, right: 40, bottom: 40, left: 40},
outerWidth = parseInt(this.props.size[0]),
outerHeight = parseInt(this.props.size[1]),
innerWidth = outerWidth - margin.left - margin.right,
innerHeight = outerHeight - margin.top - margin.bottom,
width = innerWidth - padding.left - padding.right,
height = innerHeight - padding.top - padding.botto,
radius = parseInt(min([innerWidth, innerHeight]) / 2),
donutHole = this.props.type === "DONUT" ? radius / 2 : 0,
color = scaleLinear()
.domain([1, this.props.data.length])
.interpolate(interpolateHcl)
.range([rgb("#AF2192"), rgb("#98D719")]);
// DON'T DO DATA MAPPING ON SANKEY GRAPH SINCE DATA STRUCTURE IS DIFFERENT
if (this.props.type !== "SANKEY") {
// HIGHEST VALUE OF ITEMS IN DATA ARRAY
const dataMax = max(this.props.data.map(item => item.value)),
dataSpread = (innerWidth / this.props.data.length),
// DEPEND SCALE OF ITEMS ON THE Y AXIS BASED ON HIGHEST VALUE
yScale = scaleLinear()
.domain([0, dataMax])
.range([0, innerHeight]),
// GENERATE THE LINE USING THE TOTAL SPACE AVAILABLE FROM THE SIZE PROP DIVIDED BY THE LENGTH OF THE DATA ARRAY
lineGen = line()
.x((d, i) => i * dataSpread)
.y(d => innerHeight - yScale(d))
// CURVEMONOTONEX GAVE THE BEST RESULTS
.curve(curveMonotoneX);
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color, dataMax, dataSpread, yScale, lineGen};
} else {
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color};
}
}
createSankeyGraph(data) {
const sankeyNode = this.node;
let graphData = this.props.data;
// console.log(graphData);
// console.log(graphData.links);
// console.log(graphData.nodes);
// console.log(dimensions.outerWidth, dimensions.outerHeight);
// GET DIMENSIONS IN A GLOBAL-VAR-LIKE WAY
this.getDimensions();
const formatNumber = format('.1f');
const formatted = function(d) {return formatNumber(d) + " Potential Guests"};
const color = scaleLinear()
.domain([1, 3])
.interpolate(interpolateHcl)
.range([rgb('#126382'), rgb('#417685')]);
var sankey = sankeyGraph()
.nodeWidth(15)
.nodePadding(10)
.extent([1, 1], [parseInt(dimensions.outerWidth) - 1, parseInt(dimensions.outerHeight) - 6]);
var SVG = select(sankeyNode)
.append('g')
.attr('transform', 'translate(' + dimensions.margin.left + ',' + dimensions.margin.top +')');
var link = SVG.append('g')
.attr('class', 'links')
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll('path')
var node = SVG.append('g')
.attr('class', 'nodes')
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll('g')
// json('https://api.myjson.com/bins/15xgsd', function(error, graphData){
sankey(graphData);
// console.log(graphData.nodes, graphData.links);
link = link
.data(graphData.links)
.enter()
.append('path')
.attr('d', sankeyLinkHorizontal())
.attr('stroke-width', function(d) { return Math.max(1, d.width); });
link.append('title')
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + formatted(d.value); });
node = node
.data(graphData.nodes)
.enter()
.append('g')
node.append('rect')
.attr('x', function(d) { return d.x0; })
.attr('y', function(d) { return d.y0; })
.attr('height', function(d) { return d.y1 - d.y0})
.attr('width', function(d) { return d.x1 - d.x0})
.attr("fill", function(d, i) { return color(i); })
.attr('stroke', 'black');
node.append('text')
.attr('x', function(d) {return d.x0 - 6})
.attr('y', function(d) {return (d.y1 + d.y0) / 2})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < dimensions.innerWidth / 2; })
.attr('x', function(d) { return d.x1 + 6; })
.attr('text-anchor', 'start');
node.append('title')
.text(d => d.name + "\n" + formatted(d.value));
// });
}
createTheGraphs() {
(this.props.type === "LINE") ? this.createLineGraph() : "";
(this.props.type === "BAR") ? this.createBarChart() : "";
(this.props.type === "PIE" || this.props.type === "DONUT") ? this.createPieChart() : "";
(this.props.type === "SANKEY") ? this.createSankeyGraph() : "";
(this.props.type === "RADIAL") ? this.createRadialChart() : "";
}
componentWillMount() {
this.setState({ loading: true });
}
componentDidMount() {
this.createTheGraphs();
}
componentDidUpdate() {
this.createTheGraphs();
}
render() {
return(
<div className="Graph">
<svg className='Graph_Container' ref={node => this.node = node}></svg>
<h2>{this.props.type} Placeholder</h2>
</div>
);
}
}
Graph.propTypes = {
};
export default Graph;
What's happening? Well, basically the console is outputting
Error: missing: Peter modules.js:54276:20
find http://localhost:3000/packages/modules.js:54276:20
computeNodeLinks/< http://localhost:3000/packages/modules.js:54353:62
forEach self-hosted:267:13
computeNodeLinks http://localhost:3000/packages/modules.js:54350:5
sankey http://localhost:3000/packages/modules.js:54292:5
createSankeyGraph http://localhost:3000/app/app.js:554:13
createSankeyGraph self-hosted:941:17
createTheGraphs http://localhost:3000/app/app.js:598:44
createTheGraphs self-hosted:941:17
componentDidMount http://localhost:3000/app/app.js:617:13
mountComponent/</< http://localhost:3000/packages/modules.js:17838:20
measureLifeCyclePerf http://localhost:3000/packages/modules.js:17649:12
mountComponent/< http://localhost:3000/packages/modules.js:17837:11
notifyAll http://localhost:3000/packages/modules.js:10464:9
close http://localhost:3000/packages/modules.js:20865:5
closeAll http://localhost:3000/packages/modules.js:11623:11
perform http://localhost:3000/packages/modules.js:11570:11
batchedMountComponentIntoNode http://localhost:3000/packages/modules.js:22897:3
perform http://localhost:3000/packages/modules.js:11557:13
batchedUpdates http://localhost:3000/packages/modules.js:20563:14
batchedUpdates http://localhost:3000/packages/modules.js:10225:10
_renderNewRootComponent http://localhost:3000/packages/modules.js:23090:5
_renderSubtreeIntoContainer http://localhost:3000/packages/modules.js:23172:21
render http://localhost:3000/packages/modules.js:23193:12
routes.js/< http://localhost:3000/app/app.js:1504:3
maybeReady http://localhost:3000/packages/meteor.js:821:6
loadingCompleted http://localhost:3000/packages/meteor.js:833:5
Which results in the graph not rendering the nodes it needs to base the lines (paths) on. The only HTML I get back is:
<svg class="Graph_Container">
<g transform="translate(20,20)">
<g class="links" fill="none" stroke="#000" stroke-opacity="0.2"></g>
<g class="nodes" font-family="sans-serif" font-size="10"></g>
</g>
</svg>
No nodes in the 'g.nodes' thus no links in the 'g.links'. The data structure this graph should be processing looks like:
<Graph type="SANKEY"
data={{
nodes: [
{name: "Peter"},
{name: "Test.com"},
{name: "Thing.com"},
{name: "AnotherName"}
], links: [
{source: "Peter", target: "Test.com", value: 50},
{source: "Peter", target: "Thing.com", value: 50},
{source: "Test.com", target: "AnotherName", value: 50},
{source: "Thing.com", target: "AnotherName", value: 50}
]
}}
size={[500, 500]} />
I don't know where to go from here. With this package I jumped from issue to issue altering the code line by line. The original issue looked like this.
In case links are specified using a source
and target
name (i.e. a string) instead of node indices, the solution is to specify a nodeId
mapping function:
d3.sankey()
.nodeId(d => d.name) // Needed to avoid "Error: Missing: myNode"
Of course one may have to adjust the function d => d.name
to correspond to the actual data.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With