1

I'm trying to create a sankey chart using the d3 sankey plugin with dynamic shipping data. It works great most of the time except when I get a data set like this:

[{"DeparturePort":"CHARLESTON","ArrivalPort":"BREMERHAVEN","volume":5625.74},{"DeparturePort":"CHARLESTON","ArrivalPort":"ITAPOA","volume":2340},{"DeparturePort":"PT EVERGLADES","ArrivalPort":"PT AU PRINCE","volume":41.02},{"DeparturePort":"BREMERHAVEN","ArrivalPort":"CHARLESTON","volume":28}]

The key to my issue is the first and last entry in the data set. It seems that having opposite directions in the same sankey chart sends the javascript into an infinite loop and kills the browser. Any ideas on how to prevent this from happening?

Here's my chart code where raw would be the object above:

var data = raw;
                var units = "Volume";

                var margin = { top: 100, right: 0, bottom: 30, left: 0 },
                    width = $("#"+divID).width() - margin.left - margin.right,
                    height = divID == "enlargeChart" ? 800 - margin.top - margin.bottom : 600 - margin.top - margin.bottom;

                var formatNumber = d3.format(",.0f"),    // zero decimal places
                    format = function (d) { return ""; },
                    color = d3.scale.ordinal()
                    .range(["#0077c0", "#FF6600"]);

                // append the svg canvas to the page
                var svg = d3.select("#"+divID).append("svg")
                    .attr("width", width + margin.left + margin.right)
                    .attr("height", height + margin.top + margin.bottom)
                    .style("font-size", "12px")
                  .append("g")
                    .attr("transform",
                          "translate(" + margin.left + "," + margin.top + ")");

                // Set the sankey diagram properties
                var sankey = d3.sankey(width)
                    .nodeWidth(10)
                    .nodePadding(10)
                    .size([width, height]);

                var path = sankey.link();
                // load the data (using the timelyportfolio csv method)
                //d3.csv("sankey.csv", function (error, data) {

                    //set up graph in same style as original example but empty
                    graph = { "nodes": [], "links": [] };
                    var checklist = [];
                    data.forEach(function (d) {
                        if ($.inArray(d.DeparturePort, checklist) == -1) {
                            checklist.push(d.DeparturePort)
                            graph.nodes.push({ "name": d.DeparturePort });
                        }
                        if ($.inArray(d.ArrivalPort, checklist) == -1) {
                            checklist.push(d.ArrivalPort)
                            graph.nodes.push({ "name": d.ArrivalPort });
                        }
                        graph.links.push({
                            "source": d.DeparturePort,
                            "target": d.ArrivalPort,
                            "value": +d.volume
                        });
                    });

                    // return only the distinct / unique nodes
                    graph.nodes = d3.keys(d3.nest()
                      .key(function (d) { return d.name; })
                      .map(graph.nodes));

                    // loop through each link replacing the text with its index from node
                    graph.links.forEach(function (d, i) {
                        graph.links[i].source = graph.nodes.indexOf(graph.links[i].source);
                        graph.links[i].target = graph.nodes.indexOf(graph.links[i].target);
                    });

                    //now loop through each nodes to make nodes an array of objects
                    // rather than an array of strings
                    graph.nodes.forEach(function (d, i) {
                        graph.nodes[i] = { "name": d };
                    });


                    sankey
                      .nodes(graph.nodes)
                      .links(graph.links)
                      .layout(32);

                    // add in the links
                    var link = svg.append("g").selectAll(".link")
                        .data(graph.links)
                      .enter().append("path")
                        .attr("class", "link")
                        .attr("d", path)
                        .style("stroke-width", function (d) {  return Math.max(1, d.dy); })
                        .sort(function (a, b) { setTimeout(function () { return b.dy - a.dy; }, 10);});

                    // add the link titles
                    link.append("title")
                          .text(function (d) {
                            return d.source.name + " → " +
                                d.target.name + "\n" + d.value.toFixed(0) + " TEU";
                          });
                    $("#" + divID + " .loading").addClass("hide");


                    // add in the nodes
                    var node = svg.append("g").selectAll(".node")
                        .data(graph.nodes)
                      .enter().append("g")
                        .attr("class", "node")
                        .attr("transform", function (d) {
                                //setTimeout(function () {
                            return "translate(" + d.x + "," + d.y + ")";
                                //}, 10);
                        })
                      .call(d3.behavior.drag()
                        .origin(function (d) { return d; })
                        .on("dragstart", function () {
                            this.parentNode.appendChild(this);
                        })
                        .on("drag", dragmove));

                    // add the rectangles for the nodes
                    node.append("rect")
                        .attr("height", function (d) {  if (d.dy < 0) { d.dy = (d.dy * -1); } return d.dy; })
                        .attr("width", sankey.nodeWidth())
                        .style("fill", function (d) {
                            return d.color = color(d.name);
                        })
                        .style("stroke", function (d) {
                            return d3.rgb(d.color);
                        })
                      .append("title")
                        .text(function (d) {
                            return d.name + "\n" + format(d.value);
                        });
                    // add in the title for the nodes
                    node.append("text")
                        .attr("x", -6)
                        .attr("y", function (d) { return d.dy / 2; })
                        .attr("dy", ".35em")
                        .attr("text-anchor", "end")
                    .style("stroke", function (d) { return "#000000" })
                        .attr("transform", null)
                        .text(function (d) { return d.name; })
                      .filter(function (d) { return d.x < width / 2; })
                        .attr("x", 6 + sankey.nodeWidth())
                        .attr("text-anchor", "start");

                    // the function for moving the nodes
                    function dragmove(d) {
                        d3.select(this).attr("transform",
                            "translate(" + d.x + "," + (
                                    d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
                                ) + ")");
                        sankey.relayout();
                        link.attr("d", path);
                    }
            }, 0)
AOndracek
  • 13
  • 5
  • What do you expect the code to do in such case? Can you validate data before drawing the chart? – Gildor Sep 17 '15 at 23:19
  • Ideally they would add together but I'd be happy with just skipping the second one altogether just to avoid crashing the browser. – AOndracek Sep 18 '15 at 03:38

1 Answers1

3

It's an issue with the sankey.js script.

See this commit (on a fork of sankey.js) which fixed it: https://github.com/soxofaan/d3-plugin-captain-sankey/commit/0edba18918aac3e9afadffd4a169c47f88a98f81

while (remainingNodes.length) {

becomes:

while (remainingNodes.length && x < nodes.length) {

That should prevent the endless loop.