0

I need to draw a sankey chart to show associations. I have scaled the nodes using d3.scaleLinear() to give meaning to small nodes as well but the links are not scaling as per nodes. I have tried link scaling as well but it is not working.

//node scale
var maxVal_node = d3.max(nodes.map(m => m.value));
var minVal_node = d3.min(nodes.map(m => m.value));
var nodeScale = d3.scaleLinear()
    .domain([minVal_node, maxVal_node])
    .range([10, 120]);

d3.sankey = function() {
  var sankey = {},
    nodeWidth = 24,
    nodePadding = 8,
    size = [1, 1],
    nodes = [],
    links = [];

  sankey.nodeWidth = function(_) {
    if (!arguments.length) return nodeWidth;
    nodeWidth = +_;
    return sankey;
  };

  sankey.nodePadding = function(_) {
    if (!arguments.length) return nodePadding;
    nodePadding = +_;
    return sankey;
  };

  sankey.nodes = function(_) {
    if (!arguments.length) return nodes;
    nodes = _;
    return sankey;
  };

  sankey.links = function(_) {
    if (!arguments.length) return links;
    links = _;
    return sankey;
  };

  sankey.size = function(_) {
    if (!arguments.length) return size;
    size = _;
    return sankey;
  };

  sankey.layout = function(iterations) {
    computeNodeLinks();
    computeNodeValues();
    computeNodeBreadths();
    computeNodeDepths(iterations);
    computeLinkDepths();
    return sankey;
  };

  sankey.relayout = function() {
    computeLinkDepths();
    return sankey;
  };

  sankey.link = function() {
    var curvature = .5;

    function link(d) {
      var x0 = d.source.x + d.source.dx,
        x1 = d.target.x,
        xi = d3.interpolateNumber(x0, x1),
        x2 = xi(curvature),
        x3 = xi(1 - curvature),
        y0 = d.source.y + d.sy + d.dy / 2,
        y1 = d.target.y + d.ty + d.dy / 2;
      return "M" + x0 + "," + y0 +
        "C" + x2 + "," + y0 +
        " " + x3 + "," + y1 +
        " " + x1 + "," + y1;
    }

    link.curvature = function(_) {
      if (!arguments.length) return curvature;
      curvature = +_;
      return link;
    };

    return link;
  };


  // Populate the sourceLinks and targetLinks for each node.
  // Also, if the source and target are not objects, assume they are indices.
  function computeNodeLinks() {
    nodes.forEach(function(node) {
      node.sourceLinks = [];
      node.targetLinks = [];
    });
    links.forEach(function(link) {
      var source = link.source,
        target = link.target;
      if (typeof source === "number") source = link.source = nodes[link.source];
      if (typeof target === "number") target = link.target = nodes[link.target];
      source.sourceLinks.push(link);
      target.targetLinks.push(link);
    });
  }

  // Compute the value (size) of each node by summing the associated links.
  function computeNodeValues() {
    nodes.forEach(function(node) {
      node.value = Math.max(
        d3.sum(node.sourceLinks, value),
        d3.sum(node.targetLinks, value)
      );
    });
  }

  // Iteratively assign the breadth (x-position) for each node.
  // Nodes are assigned the maximum breadth of incoming neighbors plus one;
  // nodes with no incoming links are assigned breadth zero, while
  // nodes with no outgoing links are assigned the maximum breadth.
  function computeNodeBreadths() {
    var remainingNodes = nodes,
      nextNodes,
      x = 0;

    while (remainingNodes.length) {
      nextNodes = [];
      remainingNodes.forEach(function(node) {
        node.x = x;
        node.dx = nodeWidth;
        node.sourceLinks.forEach(function(link) {
          if (nextNodes.indexOf(link.target) < 0) {
            nextNodes.push(link.target);
          }
        });
      });
      remainingNodes = nextNodes;
      ++x;
    }

    //
    moveSinksRight(x);
    scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
  }

  function moveSourcesRight() {
    nodes.forEach(function(node) {
      if (!node.targetLinks.length) {
        node.x = d3.min(node.sourceLinks, function(d) {
          return d.target.x;
        }) - 1;
      }
    });
  }

  function moveSinksRight(x) {
    nodes.forEach(function(node) {
      if (!node.sourceLinks.length) {
        node.x = x - 1;
      }
    });
  }

  function scaleNodeBreadths(kx) {
    nodes.forEach(function(node) {
      node.x *= kx;
    });
  }

  function computeNodeDepths(iterations) {
    var nodesByBreadth = d3.nest()
      .key(function(d) {
        return d.x;
      })
      .sortKeys(d3.ascending)
      .entries(nodes)
      .map(function(d) {
        return d.values;
      });

    //
    initializeNodeDepth();
    resolveCollisions();
    for (var alpha = 1; iterations > 0; --iterations) {
      relaxRightToLeft(alpha *= .99);
      resolveCollisions();
      relaxLeftToRight(alpha);
      resolveCollisions();
    }

    function initializeNodeDepth() {
      var ky = d3.min(nodesByBreadth, function(nodes) {
        return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
      });

      nodesByBreadth.forEach(function(nodes) {
        nodes.forEach(function(node, i) {
          node.y = i;
          node.dy = node.value * ky;
        });
      });

      links.forEach(function(link) {
        link.dy = link.value * ky;
      });
    }

    function relaxLeftToRight(alpha) {
      nodesByBreadth.forEach(function(nodes, breadth) {
        nodes.forEach(function(node) {
          if (node.targetLinks.length) {
            var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
            //node.y += (y - center(node)) * alpha;
          }
        });
      });

      function weightedSource(link) {
        return center(link.source) * link.value;
      }
    }

    function relaxRightToLeft(alpha) {
      nodesByBreadth.slice().reverse().forEach(function(nodes) {
        nodes.forEach(function(node) {
          if (node.sourceLinks.length) {
            var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
            //node.y += (y - center(node)) * alpha;
          }
        });
      });

      function weightedTarget(link) {
        return center(link.target) * link.value;
      }
    }

    function resolveCollisions() {
      nodesByBreadth.forEach(function(nodes) {
        var node,
          dy,
          y0 = 0,
          n = nodes.length,
          i;

        // Push any overlapping nodes down.
        nodes.sort(function(a, b) {
          return a.value > b.value ? -1 : a.value < b.value ? 1 : 0;
        });
        for (i = 0; i < n; ++i) {
          node = nodes[i];
          dy = y0 - node.y;
          if (dy > 0) node.y += dy;
          y0 = node.y + node.dy + nodePadding;
        }

        // If the bottommost node goes outside the bounds, push it back up.
        dy = y0 - nodePadding - size[1];
        if (dy > 0) {
          y0 = node.y -= dy;

          // Push any overlapping nodes back up.
          for (i = n - 2; i >= 0; --i) {
            node = nodes[i];
            dy = node.y + node.dy + nodePadding - y0;
            if (dy > 0) node.y -= dy;
            y0 = node.y;
          }
        }
      });
    }

    function ascendingDepth(a, b) {
      return a.y - b.y;
    }
  }

  function computeLinkDepths() {
    nodes.forEach(function(node) {
      node.sourceLinks.sort(ascendingTargetDepth);
      node.targetLinks.sort(ascendingSourceDepth);
    });
    nodes.forEach(function(node) {
      var sy = 0,
        ty = 0;
      node.sourceLinks.forEach(function(link) {
        link.sy = sy;
        sy += link.dy;
      });
      node.targetLinks.forEach(function(link) {
        link.ty = ty;
        ty += link.dy;
      });
    });

    function ascendingSourceDepth(a, b) {
      return a.source.y - b.source.y;
    }

    function ascendingTargetDepth(a, b) {
      return a.target.y - b.target.y;
    }
  }

  function center(node) {
    return node.y + node.dy / 2;
  }

  function value(link) {
    return link.value;
  }

  return sankey;
};


/* ------------------- our code ------------------------ */

var units = "Widgets";

var linkData = [{
  "source": 7,
  "target": 6,
  "productIdx": "8350",
  "qty": 27200,
  "mv": 3054016,
  "matrixCost": 300,
  "sourceNodeName": "G",
  "targetNodeName": "L",
  "value": 27200
}, {
  "source": 7,
  "target": 11,
  "productIdx": "8350",
  "qty": 196599,
  "mv": 22074135.72,
  "matrixCost": 5000,
  "sourceNodeName": "G",
  "targetNodeName": "K",
  "value": 196599
}, {
  "source": 9,
  "target": 11,
  "productIdx": "8350",
  "qty": 113401,
  "mv": 12732664.28,
  "matrixCost": 5020,
  "sourceNodeName": "I",
  "targetNodeName": "K",
  "value": 113401
}, {
  "source": 2,
  "target": 10,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 100015,
  "sourceNodeName": "C",
  "targetNodeName": "J",
  "value": 0
}, {
  "source": 0,
  "target": 10,
  "productIdx": "8350",
  "qty": 2500000,
  "mv": 280700000,
  "matrixCost": 100033,
  "sourceNodeName": "A",
  "targetNodeName": "J",
  "value": 2500000
}, {
  "source": 9,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "I",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 9,
  "target": 3,
  "productIdx": "8350",
  "qty": 1309599,
  "mv": 147041775.72,
  "matrixCost": 240048,
  "sourceNodeName": "I",
  "targetNodeName": "D",
  "value": 1309599
}, {
  "source": 0,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "A",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 2,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "C",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 2,
  "target": 3,
  "productIdx": "8350",
  "qty": 3250408,
  "mv": 364955810.24,
  "matrixCost": 240048,
  "sourceNodeName": "C",
  "targetNodeName": "D",
  "value": 3250408
}, {
  "source": 8,
  "target": 1,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "H",
  "targetNodeName": "B",
  "value": 0
}, {
  "source": 4,
  "target": 1,
  "productIdx": "8350",
  "qty": 10,
  "mv": 1122.8,
  "matrixCost": 240048,
  "sourceNodeName": "E",
  "targetNodeName": "B",
  "value": 10
}]

var nodes = [{
  "nodeId": 0,
  "nodeName": "A",
  "entityCd": "16G"
}, {
  "nodeId": 1,
  "nodeName": "B",
  "entityCd": "161"
}, {
  "nodeId": 2,
  "nodeName": "C",
  "entityCd": "16G"
}, {
  "nodeId": 3,
  "nodeName": "D",
  "entityCd": "XX"
}, {
  "nodeId": 4,
  "nodeName": "E",
  "entityCd": "XX"
}, {
  "nodeId": 5,
  "nodeName": "F",
  "entityCd": "XX"
}, {
  "nodeId": 6,
  "nodeName": "L",
  "entityCd": "16G",
  "bucketCd": "LoantoCoverDublinPBCustShort",
  "bucketName": "Loan to Cover Dublin PB Cust Short",
  "bucketType": "U"
}, {
  "nodeId": 7,
  "nodeName": "G",
  "entityCd": "16G"
}, {
  "nodeId": 8,
  "nodeName": "H",
  "entityCd": "XX"
}, {
  "nodeId": 9,
  "nodeName": "I",
  "entityCd": "16G"
}, {
  "nodeId": 10,
  "nodeName": "J",
  "entityCd": "16G"
}, {
  "nodeId": 11,
  "nodeName": "K",
  "entityCd": "16G"
}]
var formatNumber = d3.format(",.0f"), // zero decimal places
  format = function(d) {
    return formatNumber(d)
  };

// append the svg object to the body of the page
var svg = d3.select("#" + "chart"),
  width = +svg.attr("width"),
  height = +svg.attr("height");


var sankey = d3.sankey()
  .nodeWidth(10)
  .nodePadding(10)
  .size([width - 1, height - 6]);

var path = sankey.link();

d3.selectAll('.tooltip-sankey').style("visibility", "hidden");
var tooltip = d3.select("body")
  .append("div")
  .attr("class", "tooltip-sankey")
  .style("position", "absolute")
  .style("z-index", "10")
  .style("visibility", "hidden");

// load the data

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

//node scale
var maxVal_node = d3.max(nodes.map(m => m.value));
var minVal_node = d3.min(nodes.map(m => m.value));
var nodeScale = d3.scaleLinear().domain([minVal_node, maxVal_node])
  .range([10, 120]);

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

  .on("mouseover", function(d) {
    var sourceId, targetId;
    for (var item in d.source.sourceLinks)
      if (d.value === d.source.sourceLinks[item].value) sourceId = item;
    for (var item in d.target.targetLinks)
      if (d.value === d.target.targetLinks[item].value) targetId = item;
    var source = d3.select("#inner-rect-" + d.source.id + "-" + sourceId);
    var target = d3.select("#inner-rect-" + d.target.id + "-" + targetId);
    source.style("stroke-opacity", 1).style("fill", "#1f77b4");
    target.style("stroke-opacity", 1).style("fill", "#1f77b4");

    //update text - one line
    var sourceText = d3.select("#text-" + d.source.id);
    sourceText.text(d.source.nodeName + ' : ' + d.value);
    var targetText = d3.select("#text-" + d.target.id);
    targetText.text(d.target.nodeName + ' : ' + d.value);

    tooltip.html("Source: <b>" + d.source.nodeName + "</b><br/>Target: <b>" + d.target.nodeName + "</b><br/>Identifier: <b>" + +"</b>" + "</b><br/>Qty: <b>" + format(d.value) + "</b>" + "</b><br/>Mv($): <b>" + format(d.mv) + "</b>");
    return tooltip.style("visibility", "visible");
  })
  .on("mousemove", function() {
    var tooltipWidth = parseInt(tooltip.style("width"));
    if (window.innerWidth - d3.event.pageX < tooltipWidth) return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX - tooltipWidth) + "px");
    return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px");
  })
  .on("mouseout", function(d) {
    var sourceId, targetId;
    for (var item in d.source.sourceLinks)
      if (d.value === d.source.sourceLinks[item].value) sourceId = item;
    for (var item in d.target.targetLinks)
      if (d.value === d.target.targetLinks[item].value) targetId = item;

    //update text - one line
    var sourceText = d3.select("#text-" + d.source.id);
    sourceText.text(d.source.nodeName + ' : ' + d.source.value);
    var targetText = d3.select("#text-" + d.target.id);
    targetText.text(d.target.nodeName + ' : ' + d.target.value);

    return tooltip.style("visibility", "hidden");
  });

// add in the nodes
var node = svg.append("g").selectAll(".node")
  .data(nodes)
  .enter().append("g")
  .on("click", showConnections)
  .on("mouseover", function(d) {
    if (d.totalPledge == 0 || d.totalBorrow == 0) {
      tooltip.html("Name: <b>" + d.nodeName + "</b><br/>Total Qty: <b>" + format(d.value) + "</b>");
    } else {
      tooltip.html("Name: <b>" + d.nodeName + "</b><br/>Total Qty: <b>" + format(d.value) + "</b>");
    }
    return tooltip.style("visibility", "visible");
  })
  .on("mousemove", function() {
    var tooltipWidth = parseInt(tooltip.style("width"));
    if (window.innerWidth - d3.event.pageX < tooltipWidth) return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX - tooltipWidth) + "px");
    return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px");
  })
  .on("mouseout", function() {
    return tooltip.style("visibility", "hidden");
  })
  .attr("class", "node")
  .attr("transform", function(d) {
    return "translate(" + d.x + "," + d.y + ")";
  })
  .on("click", showConnections);

// add the rectangles for the nodes
node.append("rect")
  .attr("height", function(d) {
    return nodeScale(d.value);
  })
  .attr("width", sankey.nodeWidth())
  .style("fill", function() {
    return "#605f66"
  })
  .style("stroke", function(d) {
    return d3.rgb(d.color).darker(2);
  })
  .on("click", showConnections)


// add in the title for the nodes
node.append("text")
  .attr("x", -6)
  .attr("y", function(d) {
    return nodeScale(d.value) / 2
  })
  .attr("dy", ".15em")
  .attr("text-anchor", "end")
  //.style("font-size", "10px")
  .attr("transform", null)
  .attr("class", "nodeText")
  .text(function(d) {
    return d.nodeName;
  })
  .filter(function(d) {
    return d.x < width / 2;
  })
  .attr("x", 6 + sankey.nodeWidth())
  .attr("text-anchor", "start");

function showConnections(d) {
  if (!d3.select(this).classed("selected")) {
    d3.select(this).classed("selected", true);
    d3.select(this).style("stroke", "black")
      .style("stroke-opacity", 0.2);
    //If this link is connected to the node
    link.style("stroke-opacity", function(l) {
      if (l.source.nodeName == d.nodeName || l.target.nodeName == d.nodeName) {
        return 0.6;
      } else
        return 0.2;
    });
  } else {
    d3.select(this).classed("selected", false);
    d3.select(this).style("stroke", "black")
      .style("stroke-opacity", 0.2)
    link.style("stroke-opacity", function() {
      return 0.2;
    });
  }
}
.node rect {
  cursor: move;
}

.link {
  fill: none;
  stroke: #000;
  stroke-opacity: .2;
}

.link:hover {
  stroke-opacity: .5;
}

* {
  font: 11px sans-serif;
}

.linkLabel {
  z-index: 10;
}

svg line.links {
  stroke: #999999;
  stroke-opacity: 0.6;
  stroke-width: 1px;
}

.link-sankey {
  fill: none;
  stroke: #1f77b4;
  stroke-opacity: .2;
}

.link-sankey:hover {
  stroke-opacity: .5 !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="550" height="400" id="chart"></svg>

Also the small nodes are kind of overlapping without any spaces in between.

Thanks in advance.

Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
Sam
  • 3
  • 2
  • Can someone take a look at fiddle and point out where to adjust the link height/stroke-widths? – Sam Oct 06 '20 at 12:46
  • It looks like you've given us almost all of your code, without widdling it down to a [mre]. In the future, please do that. We're all here as helpful volunteers, and if you make it easier for us to answer your question, you'll be much more likely to get useful answers – Ruben Helsloot Oct 06 '20 at 13:28

1 Answers1

0

It looks like you have been puzzling together different parts of d3 without looking carefully enough at the documentation for d3-sankey. It already has everything built in to scale nodes based on their value. d3.scaleLinear is meant for charts along axes, and sankey is a whole different kind of beast.

From the code you provided (before --- our code ---), I was able to find that every node has an attribute dx that denotes the width and dy that denotes the height. Using them fixed the issue outright.

EDIT: I've updated the answer by transforming the value of each link. By using Math.sqrt() and adding a baseWidth, I've given smaller quantities more power than larger ones. This means that the width of the link will no longer a direct read of its value, but since it's still more than proportional, that shouldn't matter. Note that now even the smallest nodes have some width.

d3.sankey = function() {
  var sankey = {},
    nodeWidth = 24,
    nodePadding = 8,
    size = [1, 1],
    nodes = [],
    links = [];

  sankey.nodeWidth = function(_) {
    if (!arguments.length) return nodeWidth;
    nodeWidth = +_;
    return sankey;
  };

  sankey.nodePadding = function(_) {
    if (!arguments.length) return nodePadding;
    nodePadding = +_;
    return sankey;
  };

  sankey.nodes = function(_) {
    if (!arguments.length) return nodes;
    nodes = _;
    return sankey;
  };

  sankey.links = function(_) {
    if (!arguments.length) return links;
    links = _;
    return sankey;
  };

  sankey.size = function(_) {
    if (!arguments.length) return size;
    size = _;
    return sankey;
  };

  sankey.layout = function(iterations) {
    computeNodeLinks();
    computeNodeValues();
    computeNodeBreadths();
    computeNodeDepths(iterations);
    computeLinkDepths();
    return sankey;
  };

  sankey.relayout = function() {
    computeLinkDepths();
    return sankey;
  };

  sankey.link = function() {
    var curvature = .5;

    function link(d) {
      var x0 = d.source.x + d.source.dx,
        x1 = d.target.x,
        xi = d3.interpolateNumber(x0, x1),
        x2 = xi(curvature),
        x3 = xi(1 - curvature),
        y0 = d.source.y + d.sy + d.dy / 2,
        y1 = d.target.y + d.ty + d.dy / 2;
      return "M" + x0 + "," + y0 +
        "C" + x2 + "," + y0 +
        " " + x3 + "," + y1 +
        " " + x1 + "," + y1;
    }

    link.curvature = function(_) {
      if (!arguments.length) return curvature;
      curvature = +_;
      return link;
    };

    return link;
  };


  // Populate the sourceLinks and targetLinks for each node.
  // Also, if the source and target are not objects, assume they are indices.
  function computeNodeLinks() {
    nodes.forEach(function(node) {
      node.sourceLinks = [];
      node.targetLinks = [];
    });
    links.forEach(function(link) {
      var source = link.source,
        target = link.target;
      if (typeof source === "number") source = link.source = nodes[link.source];
      if (typeof target === "number") target = link.target = nodes[link.target];
      source.sourceLinks.push(link);
      target.targetLinks.push(link);
    });
  }

  // Compute the value (size) of each node by summing the associated links.
  function computeNodeValues() {
    nodes.forEach(function(node) {
      node.value = Math.max(
        d3.sum(node.sourceLinks, value),
        d3.sum(node.targetLinks, value)
      );
    });
  }

  // Iteratively assign the breadth (x-position) for each node.
  // Nodes are assigned the maximum breadth of incoming neighbors plus one;
  // nodes with no incoming links are assigned breadth zero, while
  // nodes with no outgoing links are assigned the maximum breadth.
  function computeNodeBreadths() {
    var remainingNodes = nodes,
      nextNodes,
      x = 0;

    while (remainingNodes.length) {
      nextNodes = [];
      remainingNodes.forEach(function(node) {
        node.x = x;
        node.dx = nodeWidth;
        node.sourceLinks.forEach(function(link) {
          if (nextNodes.indexOf(link.target) < 0) {
            nextNodes.push(link.target);
          }
        });
      });
      remainingNodes = nextNodes;
      ++x;
    }

    //
    moveSinksRight(x);
    scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
  }

  function moveSourcesRight() {
    nodes.forEach(function(node) {
      if (!node.targetLinks.length) {
        node.x = d3.min(node.sourceLinks, function(d) {
          return d.target.x;
        }) - 1;
      }
    });
  }

  function moveSinksRight(x) {
    nodes.forEach(function(node) {
      if (!node.sourceLinks.length) {
        node.x = x - 1;
      }
    });
  }

  function scaleNodeBreadths(kx) {
    nodes.forEach(function(node) {
      node.x *= kx;
    });
  }

  function computeNodeDepths(iterations) {
    var nodesByBreadth = d3.nest()
      .key(function(d) {
        return d.x;
      })
      .sortKeys(d3.ascending)
      .entries(nodes)
      .map(function(d) {
        return d.values;
      });

    //
    initializeNodeDepth();
    resolveCollisions();
    for (var alpha = 1; iterations > 0; --iterations) {
      relaxRightToLeft(alpha *= .99);
      resolveCollisions();
      relaxLeftToRight(alpha);
      resolveCollisions();
    }

    function initializeNodeDepth() {
      var ky = d3.min(nodesByBreadth, function(nodes) {
        return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
      });

      nodesByBreadth.forEach(function(nodes) {
        nodes.forEach(function(node, i) {
          node.y = i;
          node.dy = node.value * ky;
        });
      });

      links.forEach(function(link) {
        link.dy = link.value * ky;
      });
    }

    function relaxLeftToRight(alpha) {
      nodesByBreadth.forEach(function(nodes, breadth) {
        nodes.forEach(function(node) {
          if (node.targetLinks.length) {
            var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
            //node.y += (y - center(node)) * alpha;
          }
        });
      });

      function weightedSource(link) {
        return center(link.source) * link.value;
      }
    }

    function relaxRightToLeft(alpha) {
      nodesByBreadth.slice().reverse().forEach(function(nodes) {
        nodes.forEach(function(node) {
          if (node.sourceLinks.length) {
            var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
            //node.y += (y - center(node)) * alpha;
          }
        });
      });

      function weightedTarget(link) {
        return center(link.target) * link.value;
      }
    }

    function resolveCollisions() {
      nodesByBreadth.forEach(function(nodes) {
        var node,
          dy,
          y0 = 0,
          n = nodes.length,
          i;

        // Push any overlapping nodes down.
        nodes.sort(function(a, b) {
          return a.value > b.value ? -1 : a.value < b.value ? 1 : 0;
        });
        for (i = 0; i < n; ++i) {
          node = nodes[i];
          dy = y0 - node.y;
          if (dy > 0) node.y += dy;
          y0 = node.y + node.dy + nodePadding;
        }

        // If the bottommost node goes outside the bounds, push it back up.
        dy = y0 - nodePadding - size[1];
        if (dy > 0) {
          y0 = node.y -= dy;

          // Push any overlapping nodes back up.
          for (i = n - 2; i >= 0; --i) {
            node = nodes[i];
            dy = node.y + node.dy + nodePadding - y0;
            if (dy > 0) node.y -= dy;
            y0 = node.y;
          }
        }
      });
    }

    function ascendingDepth(a, b) {
      return a.y - b.y;
    }
  }

  function computeLinkDepths() {
    nodes.forEach(function(node) {
      node.sourceLinks.sort(ascendingTargetDepth);
      node.targetLinks.sort(ascendingSourceDepth);
    });
    nodes.forEach(function(node) {
      var sy = 0,
        ty = 0;
      node.sourceLinks.forEach(function(link) {
        link.sy = sy;
        sy += link.dy;
      });
      node.targetLinks.forEach(function(link) {
        link.ty = ty;
        ty += link.dy;
      });
    });

    function ascendingSourceDepth(a, b) {
      return a.source.y - b.source.y;
    }

    function ascendingTargetDepth(a, b) {
      return a.target.y - b.target.y;
    }
  }

  function center(node) {
    return node.y + node.dy / 2;
  }

  function value(link) {
    return link.value;
  }

  return sankey;
};


/* ------------------- our code ------------------------ */

var units = "Widgets";

var linkData = [{
  "source": 7,
  "target": 6,
  "productIdx": "8350",
  "qty": 27200,
  "mv": 3054016,
  "matrixCost": 300,
  "sourceNodeName": "G",
  "targetNodeName": "L",
  "value": 27200
}, {
  "source": 7,
  "target": 11,
  "productIdx": "8350",
  "qty": 196599,
  "mv": 22074135.72,
  "matrixCost": 5000,
  "sourceNodeName": "G",
  "targetNodeName": "K",
  "value": 196599
}, {
  "source": 9,
  "target": 11,
  "productIdx": "8350",
  "qty": 113401,
  "mv": 12732664.28,
  "matrixCost": 5020,
  "sourceNodeName": "I",
  "targetNodeName": "K",
  "value": 113401
}, {
  "source": 2,
  "target": 10,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 100015,
  "sourceNodeName": "C",
  "targetNodeName": "J",
  "value": 0
}, {
  "source": 0,
  "target": 10,
  "productIdx": "8350",
  "qty": 2500000,
  "mv": 280700000,
  "matrixCost": 100033,
  "sourceNodeName": "A",
  "targetNodeName": "J",
  "value": 2500000
}, {
  "source": 9,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "I",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 9,
  "target": 3,
  "productIdx": "8350",
  "qty": 1309599,
  "mv": 147041775.72,
  "matrixCost": 240048,
  "sourceNodeName": "I",
  "targetNodeName": "D",
  "value": 1309599
}, {
  "source": 0,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "A",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 2,
  "target": 5,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "C",
  "targetNodeName": "F",
  "value": 0
}, {
  "source": 2,
  "target": 3,
  "productIdx": "8350",
  "qty": 3250408,
  "mv": 364955810.24,
  "matrixCost": 240048,
  "sourceNodeName": "C",
  "targetNodeName": "D",
  "value": 3250408
}, {
  "source": 8,
  "target": 1,
  "productIdx": "8350",
  "qty": 0,
  "mv": 0,
  "matrixCost": 240048,
  "sourceNodeName": "H",
  "targetNodeName": "B",
  "value": 0
}, {
  "source": 4,
  "target": 1,
  "productIdx": "8350",
  "qty": 10,
  "mv": 1122.8,
  "matrixCost": 240048,
  "sourceNodeName": "E",
  "targetNodeName": "B",
  "value": 10
}]

var nodes = [{
  "nodeId": 0,
  "nodeName": "A",
  "entityCd": "16G"
}, {
  "nodeId": 1,
  "nodeName": "B",
  "entityCd": "161"
}, {
  "nodeId": 2,
  "nodeName": "C",
  "entityCd": "16G"
}, {
  "nodeId": 3,
  "nodeName": "D",
  "entityCd": "XX"
}, {
  "nodeId": 4,
  "nodeName": "E",
  "entityCd": "XX"
}, {
  "nodeId": 5,
  "nodeName": "F",
  "entityCd": "XX"
}, {
  "nodeId": 6,
  "nodeName": "L",
  "entityCd": "16G",
  "bucketCd": "LoantoCoverDublinPBCustShort",
  "bucketName": "Loan to Cover Dublin PB Cust Short",
  "bucketType": "U"
}, {
  "nodeId": 7,
  "nodeName": "G",
  "entityCd": "16G"
}, {
  "nodeId": 8,
  "nodeName": "H",
  "entityCd": "XX"
}, {
  "nodeId": 9,
  "nodeName": "I",
  "entityCd": "16G"
}, {
  "nodeId": 10,
  "nodeName": "J",
  "entityCd": "16G"
}, {
  "nodeId": 11,
  "nodeName": "K",
  "entityCd": "16G"
}]
var formatNumber = d3.format(",.0f"), // zero decimal places
  format = function(d) {
    return formatNumber(d)
  };

// append the svg object to the body of the page
var svg = d3.select("#" + "chart"),
  width = +svg.attr("width"),
  height = +svg.attr("height");

var sankey = d3.sankey()
  .nodeWidth(10)
  .nodePadding(10)
  .size([width - 1, height - 6]);

var path = sankey.link();

d3.selectAll('.tooltip-sankey').style("visibility", "hidden");
var tooltip = d3.select("body")
  .append("div")
  .attr("class", "tooltip-sankey")
  .style("position", "absolute")
  .style("z-index", "10")
  .style("visibility", "hidden");

// Transform the data so even nodes with small links always have something to show for it.
var baseWeight = 50
linkData.forEach(function(link) {
  link.value = baseWeight + Math.sqrt(link.qty);
});


// load the data

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

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

  .on("mouseover", function(d) {
    var sourceId, targetId;
    for (var item in d.source.sourceLinks)
      if (d.value === d.source.sourceLinks[item].value) sourceId = item;
    for (var item in d.target.targetLinks)
      if (d.value === d.target.targetLinks[item].value) targetId = item;
    var source = d3.select("#inner-rect-" + d.source.id + "-" + sourceId);
    var target = d3.select("#inner-rect-" + d.target.id + "-" + targetId);
    source.style("stroke-opacity", 1).style("fill", "#1f77b4");
    target.style("stroke-opacity", 1).style("fill", "#1f77b4");

    //update text - one line
    var sourceText = d3.select("#text-" + d.source.id);
    sourceText.text(d.source.nodeName + ' : ' + d.value);
    var targetText = d3.select("#text-" + d.target.id);
    targetText.text(d.target.nodeName + ' : ' + d.value);

    tooltip.html("Source: <b>" + d.source.nodeName + "</b><br/>Target: <b>" + d.target.nodeName + "</b><br/>Identifier: <b>" + +"</b>" + "</b><br/>Qty: <b>" + format(d.value) + "</b>" + "</b><br/>Mv($): <b>" + format(d.mv) + "</b>");
    return tooltip.style("visibility", "visible");
  })
  .on("mousemove", function() {
    var tooltipWidth = parseInt(tooltip.style("width"));
    if (window.innerWidth - d3.event.pageX < tooltipWidth) return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX - tooltipWidth) + "px");
    return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px");
  })
  .on("mouseout", function(d) {
    var sourceId, targetId;
    for (var item in d.source.sourceLinks)
      if (d.value === d.source.sourceLinks[item].value) sourceId = item;
    for (var item in d.target.targetLinks)
      if (d.value === d.target.targetLinks[item].value) targetId = item;

    //update text - one line
    var sourceText = d3.select("#text-" + d.source.id);
    sourceText.text(d.source.nodeName + ' : ' + d.source.value);
    var targetText = d3.select("#text-" + d.target.id);
    targetText.text(d.target.nodeName + ' : ' + d.target.value);

    return tooltip.style("visibility", "hidden");
  });

// add in the nodes
var node = svg.append("g").selectAll(".node")
  .data(nodes)
  .enter().append("g")
  .on("click", showConnections)
  .on("mouseover", function(d) {
    if (d.totalPledge == 0 || d.totalBorrow == 0) {
      tooltip.html("Name: <b>" + d.nodeName + "</b><br/>Total Qty: <b>" + format(d.value) + "</b>");
    } else {
      tooltip.html("Name: <b>" + d.nodeName + "</b><br/>Total Qty: <b>" + format(d.value) + "</b>");
    }
    return tooltip.style("visibility", "visible");
  })
  .on("mousemove", function() {
    var tooltipWidth = parseInt(tooltip.style("width"));
    if (window.innerWidth - d3.event.pageX < tooltipWidth) return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX - tooltipWidth) + "px");
    return tooltip.style("top", (d3.event.pageY - 10) + "px").style("left", (d3.event.pageX + 10) + "px");
  })
  .on("mouseout", function() {
    return tooltip.style("visibility", "hidden");
  })
  .attr("class", "node")
  .attr("transform", function(d) {
    return "translate(" + d.x + "," + d.y + ")";
  })
  .on("click", showConnections);

// add the rectangles for the nodes
node.append("rect")
  .attr("height", function(d) { return d.dy; })
  .attr("width", function(d) { return d.dx; })
  .style("fill", "#605f66")
  .style("stroke", function(d) {
    return d3.rgb(d.color).darker(2);
  })
  .on("click", showConnections)


// add in the title for the nodes
node.append("text")
  .attr("x", -6)
  .attr("y", function(d) { return d.dy / 2; })
  .attr("dy", ".15em")
  .attr("text-anchor", "end")
  //.style("font-size", "10px")
  .attr("transform", null)
  .attr("class", "nodeText")
  .text(function(d) {
    return d.nodeName;
  })
  .filter(function(d) {
    return d.x < width / 2;
  })
  .attr("x", 6 + sankey.nodeWidth())
  .attr("text-anchor", "start");

function showConnections(d) {
  if (!d3.select(this).classed("selected")) {
    d3.select(this).classed("selected", true);
    d3.select(this).style("stroke", "black")
      .style("stroke-opacity", 0.2);
    //If this link is connected to the node
    link.style("stroke-opacity", function(l) {
      if (l.source.nodeName == d.nodeName || l.target.nodeName == d.nodeName) {
        return 0.6;
      } else
        return 0.2;
    });
  } else {
    d3.select(this).classed("selected", false);
    d3.select(this).style("stroke", "black")
      .style("stroke-opacity", 0.2)
    link.style("stroke-opacity", function() {
      return 0.2;
    });
  }
}
.node rect {
  cursor: move;
}

.link {
  fill: none;
  stroke: #000;
  stroke-opacity: .2;
}

.link:hover {
  stroke-opacity: .5;
}

* {
  font: 11px sans-serif;
}

.linkLabel {
  z-index: 10;
}

svg line.links {
  stroke: #999999;
  stroke-opacity: 0.6;
  stroke-width: 1px;
}

.link-sankey {
  fill: none;
  stroke: #1f77b4;
  stroke-opacity: .2;
}

.link-sankey:hover {
  stroke-opacity: .5 !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="550" height="400" id="chart"></svg>
Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • Thanks Ruben for taking a look and providing details. Will take care about minimum reproducible code. I am aware about dx and dy attribute and i actually implemented my first version the way you have mentioned here, which kind of sankey provides by default as you mentioned. But the issue was with the nodes at the bottom. We do not want to leave those out just bcz other nodes are relatively larger, hence i applied scaling for the nodes. In the process of applying node scaling, i found the links misaligned which is why i was looking for a solution for links alignment. – Sam Oct 06 '20 at 18:00
  • You can override the link value, as I've done in the edit to this answer, so it always gives even the smallest links at least some width, and thus the nodes have a bit more height. – Ruben Helsloot Oct 06 '20 at 19:57
  • Thanks, this works. I think i can reduce the height as well to further shorten the node height and can override link value to give smaller nodes more height. – Sam Oct 07 '20 at 12:38
  • Just a follow up thing - I think if a node X with outgoing link value is 30 , link value would become 80. But if a node Y has incoming 3 links of 0 each, total link value would be 150. So your X node will be smaller than Y which is misleading if we override link values with base weight. – Sam Oct 08 '20 at 03:23
  • Yes, that is why base weight should be tweaked to fit the situation. You have *huge* differences between quantities, so you'll always have something like that. But now, if one link has 2.5m, and another has 50 or 80, you'll never notice the difference. If you didn't have those extremely large values, you wouldn't need a large baseWeight either – Ruben Helsloot Oct 08 '20 at 06:19
  • So how should we decide the ideal base weight? I have dynamic data coming in and sometimes node A has a single link to B with 100 qty and H has links from C D E of 16 each. Now H will show 162 (54*3) while A will stay at 110. Ideally, H (48) should be less than A(100). Thanks. – Sam Oct 09 '20 at 19:30
  • It depends. Again, i the largest value is 2.5m, then 100 or 110 or 162 doesn't matter much. If the values are not that large, you could calculate the total value per node and divide that by the number of links -> lowest average link value becomes the `baseWeight`. I can't help you much further, because this is too subjective to give a good answer to I'm afraid – Ruben Helsloot Oct 09 '20 at 19:39