Skip to content

Instantly share code, notes, and snippets.

@Neilos
Last active Mar 10, 2020
Embed
What would you like to do?
Bi-directional hierarchical sankey diagram

This is a demonstration of a bi-directional hierarchical sankey diagram produced in javascript, html and css using d3. (Refresh page to generate new random data)

Sankey diagrams represent flows between nodes by varying the thickness of the connecting links.

This diagram was based off of Mike Bostock's sankey diagram, but additionally incorporates bi-directionality into the flow and caters for hierarchical relationships between nodes to allow drill down into the data.

All javascript code to generate the diagram markup is contained in the app.js file, but the underlying calculations are performed using a custom plugin: bihisankey.js.

'use strict';
var svg, tooltip, biHiSankey, path, defs, colorScale, highlightColorScale, isTransitioning;
var OPACITY = {
NODE_DEFAULT: 0.9,
NODE_FADED: 0.1,
NODE_HIGHLIGHT: 0.8,
LINK_DEFAULT: 0.6,
LINK_FADED: 0.05,
LINK_HIGHLIGHT: 0.9
},
TYPES = ["Asset", "Expense", "Revenue", "Equity", "Liability"],
TYPE_COLORS = ["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d"],
TYPE_HIGHLIGHT_COLORS = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494"],
LINK_COLOR = "#b3b3b3",
INFLOW_COLOR = "#2E86D1",
OUTFLOW_COLOR = "#D63028",
NODE_WIDTH = 36,
COLLAPSER = {
RADIUS: NODE_WIDTH / 2,
SPACING: 2
},
OUTER_MARGIN = 10,
MARGIN = {
TOP: 2 * (COLLAPSER.RADIUS + OUTER_MARGIN),
RIGHT: OUTER_MARGIN,
BOTTOM: OUTER_MARGIN,
LEFT: OUTER_MARGIN
},
TRANSITION_DURATION = 400,
HEIGHT = 500 - MARGIN.TOP - MARGIN.BOTTOM,
WIDTH = 960 - MARGIN.LEFT - MARGIN.RIGHT,
LAYOUT_INTERATIONS = 32,
REFRESH_INTERVAL = 7000;
var formatNumber = function (d) {
var numberFormat = d3.format(",.0f"); // zero decimal places
return "£" + numberFormat(d);
},
formatFlow = function (d) {
var flowFormat = d3.format(",.0f"); // zero decimal places with sign
return "£" + flowFormat(Math.abs(d)) + (d < 0 ? " CR" : " DR");
},
// Used when temporarily disabling user interractions to allow animations to complete
disableUserInterractions = function (time) {
isTransitioning = true;
setTimeout(function(){
isTransitioning = false;
}, time);
},
hideTooltip = function () {
return tooltip.transition()
.duration(TRANSITION_DURATION)
.style("opacity", 0);
},
showTooltip = function () {
return tooltip
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY + 15 + "px")
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", 1);
};
colorScale = d3.scale.ordinal().domain(TYPES).range(TYPE_COLORS),
highlightColorScale = d3.scale.ordinal().domain(TYPES).range(TYPE_HIGHLIGHT_COLORS),
svg = d3.select("#chart").append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM)
.append("g")
.attr("transform", "translate(" + MARGIN.LEFT + "," + MARGIN.TOP + ")");
svg.append("g").attr("id", "links");
svg.append("g").attr("id", "nodes");
svg.append("g").attr("id", "collapsers");
tooltip = d3.select("#chart").append("div").attr("id", "tooltip");
tooltip.style("opacity", 0)
.append("p")
.attr("class", "value");
biHiSankey = d3.biHiSankey();
// Set the biHiSankey diagram properties
biHiSankey
.nodeWidth(NODE_WIDTH)
.nodeSpacing(10)
.linkSpacing(4)
.arrowheadScaleFactor(0.5) // Specifies that 0.5 of the link's stroke WIDTH should be allowed for the marker at the end of the link.
.size([WIDTH, HEIGHT]);
path = biHiSankey.link().curvature(0.45);
defs = svg.append("defs");
defs.append("marker")
.style("fill", LINK_COLOR)
.attr("id", "arrowHead")
.attr("viewBox", "0 0 6 10")
.attr("refX", "1")
.attr("refY", "5")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", "1")
.attr("markerHeight", "1")
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 1 0 L 6 5 L 1 10 L 0 10 z");
defs.append("marker")
.style("fill", OUTFLOW_COLOR)
.attr("id", "arrowHeadInflow")
.attr("viewBox", "0 0 6 10")
.attr("refX", "1")
.attr("refY", "5")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", "1")
.attr("markerHeight", "1")
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 1 0 L 6 5 L 1 10 L 0 10 z");
defs.append("marker")
.style("fill", INFLOW_COLOR)
.attr("id", "arrowHeadOutlow")
.attr("viewBox", "0 0 6 10")
.attr("refX", "1")
.attr("refY", "5")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", "1")
.attr("markerHeight", "1")
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 1 0 L 6 5 L 1 10 L 0 10 z");
function update () {
var link, linkEnter, node, nodeEnter, collapser, collapserEnter;
function dragmove(node) {
node.x = Math.max(0, Math.min(WIDTH - node.width, d3.event.x));
node.y = Math.max(0, Math.min(HEIGHT - node.height, d3.event.y));
d3.select(this).attr("transform", "translate(" + node.x + "," + node.y + ")");
biHiSankey.relayout();
svg.selectAll(".node").selectAll("rect").attr("height", function (d) { return d.height; });
link.attr("d", path);
}
function containChildren(node) {
node.children.forEach(function (child) {
child.state = "contained";
child.parent = this;
child._parent = null;
containChildren(child);
}, node);
}
function expand(node) {
node.state = "expanded";
node.children.forEach(function (child) {
child.state = "collapsed";
child._parent = this;
child.parent = null;
containChildren(child);
}, node);
}
function collapse(node) {
node.state = "collapsed";
containChildren(node);
}
function restoreLinksAndNodes() {
link
.style("stroke", LINK_COLOR)
.style("marker-end", function () { return 'url(#arrowHead)'; })
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", OPACITY.LINK_DEFAULT);
node
.selectAll("rect")
.style("fill", function (d) {
d.color = colorScale(d.type.replace(/ .*/, ""));
return d.color;
})
.style("stroke", function (d) {
return d3.rgb(colorScale(d.type.replace(/ .*/, ""))).darker(0.1);
})
.style("fill-opacity", OPACITY.NODE_DEFAULT);
node.filter(function (n) { return n.state === "collapsed"; })
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", OPACITY.NODE_DEFAULT);
}
function showHideChildren(node) {
disableUserInterractions(2 * TRANSITION_DURATION);
hideTooltip();
if (node.state === "collapsed") { expand(node); }
else { collapse(node); }
biHiSankey.relayout();
update();
link.attr("d", path);
restoreLinksAndNodes();
}
function highlightConnected(g) {
link.filter(function (d) { return d.source === g; })
.style("marker-end", function () { return 'url(#arrowHeadInflow)'; })
.style("stroke", OUTFLOW_COLOR)
.style("opacity", OPACITY.LINK_DEFAULT);
link.filter(function (d) { return d.target === g; })
.style("marker-end", function () { return 'url(#arrowHeadOutlow)'; })
.style("stroke", INFLOW_COLOR)
.style("opacity", OPACITY.LINK_DEFAULT);
}
function fadeUnconnected(g) {
link.filter(function (d) { return d.source !== g && d.target !== g; })
.style("marker-end", function () { return 'url(#arrowHead)'; })
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", OPACITY.LINK_FADED);
node.filter(function (d) {
return (d.name === g.name) ? false : !biHiSankey.connected(d, g);
}).transition()
.duration(TRANSITION_DURATION)
.style("opacity", OPACITY.NODE_FADED);
}
link = svg.select("#links").selectAll("path.link")
.data(biHiSankey.visibleLinks(), function (d) { return d.id; });
link.transition()
.duration(TRANSITION_DURATION)
.style("stroke-WIDTH", function (d) { return Math.max(1, d.thickness); })
.attr("d", path)
.style("opacity", OPACITY.LINK_DEFAULT);
link.exit().remove();
linkEnter = link.enter().append("path")
.attr("class", "link")
.style("fill", "none");
linkEnter.on('mouseenter', function (d) {
if (!isTransitioning) {
showTooltip().select(".value").text(function () {
if (d.direction > 0) {
return d.source.name + " → " + d.target.name + "\n" + formatNumber(d.value);
}
return d.target.name + " ← " + d.source.name + "\n" + formatNumber(d.value);
});
d3.select(this)
.style("stroke", LINK_COLOR)
.transition()
.duration(TRANSITION_DURATION / 2)
.style("opacity", OPACITY.LINK_HIGHLIGHT);
}
});
linkEnter.on('mouseleave', function () {
if (!isTransitioning) {
hideTooltip();
d3.select(this)
.style("stroke", LINK_COLOR)
.transition()
.duration(TRANSITION_DURATION / 2)
.style("opacity", OPACITY.LINK_DEFAULT);
}
});
linkEnter.sort(function (a, b) { return b.thickness - a.thickness; })
.classed("leftToRight", function (d) {
return d.direction > 0;
})
.classed("rightToLeft", function (d) {
return d.direction < 0;
})
.style("marker-end", function () {
return 'url(#arrowHead)';
})
.style("stroke", LINK_COLOR)
.style("opacity", 0)
.transition()
.delay(TRANSITION_DURATION)
.duration(TRANSITION_DURATION)
.attr("d", path)
.style("stroke-WIDTH", function (d) { return Math.max(1, d.thickness); })
.style("opacity", OPACITY.LINK_DEFAULT);
node = svg.select("#nodes").selectAll(".node")
.data(biHiSankey.collapsedNodes(), function (d) { return d.id; });
node.transition()
.duration(TRANSITION_DURATION)
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
.style("opacity", OPACITY.NODE_DEFAULT)
.select("rect")
.style("fill", function (d) {
d.color = colorScale(d.type.replace(/ .*/, ""));
return d.color;
})
.style("stroke", function (d) { return d3.rgb(colorScale(d.type.replace(/ .*/, ""))).darker(0.1); })
.style("stroke-WIDTH", "1px")
.attr("height", function (d) { return d.height; })
.attr("width", biHiSankey.nodeWidth());
node.exit()
.transition()
.duration(TRANSITION_DURATION)
.attr("transform", function (d) {
var collapsedAncestor, endX, endY;
collapsedAncestor = d.ancestors.filter(function (a) {
return a.state === "collapsed";
})[0];
endX = collapsedAncestor ? collapsedAncestor.x : d.x;
endY = collapsedAncestor ? collapsedAncestor.y : d.y;
return "translate(" + endX + "," + endY + ")";
})
.remove();
nodeEnter = node.enter().append("g").attr("class", "node");
nodeEnter
.attr("transform", function (d) {
var startX = d._parent ? d._parent.x : d.x,
startY = d._parent ? d._parent.y : d.y;
return "translate(" + startX + "," + startY + ")";
})
.style("opacity", 1e-6)
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", OPACITY.NODE_DEFAULT)
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
nodeEnter.append("text");
nodeEnter.append("rect")
.style("fill", function (d) {
d.color = colorScale(d.type.replace(/ .*/, ""));
return d.color;
})
.style("stroke", function (d) {
return d3.rgb(colorScale(d.type.replace(/ .*/, ""))).darker(0.1);
})
.style("stroke-WIDTH", "1px")
.attr("height", function (d) { return d.height; })
.attr("width", biHiSankey.nodeWidth());
node.on("mouseenter", function (g) {
if (!isTransitioning) {
restoreLinksAndNodes();
highlightConnected(g);
fadeUnconnected(g);
d3.select(this).select("rect")
.style("fill", function (d) {
d.color = d.netFlow > 0 ? INFLOW_COLOR : OUTFLOW_COLOR;
return d.color;
})
.style("stroke", function (d) {
return d3.rgb(d.color).darker(0.1);
})
.style("fill-opacity", OPACITY.LINK_DEFAULT);
tooltip
.style("left", g.x + MARGIN.LEFT + "px")
.style("top", g.y + g.height + MARGIN.TOP + 15 + "px")
.transition()
.duration(TRANSITION_DURATION)
.style("opacity", 1).select(".value")
.text(function () {
var additionalInstructions = g.children.length ? "\n(Double click to expand)" : "";
return g.name + "\nNet flow: " + formatFlow(g.netFlow) + additionalInstructions;
});
}
});
node.on("mouseleave", function () {
if (!isTransitioning) {
hideTooltip();
restoreLinksAndNodes();
}
});
node.filter(function (d) { return d.children.length; })
.on("dblclick", showHideChildren);
// allow nodes to be dragged to new positions
node.call(d3.behavior.drag()
.origin(function (d) { return d; })
.on("dragstart", function () { this.parentNode.appendChild(this); })
.on("drag", dragmove));
// add in the text for the nodes
node.filter(function (d) { return d.value !== 0; })
.select("text")
.attr("x", -6)
.attr("y", function (d) { return d.height / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function (d) { return d.name; })
.filter(function (d) { return d.x < WIDTH / 2; })
.attr("x", 6 + biHiSankey.nodeWidth())
.attr("text-anchor", "start");
collapser = svg.select("#collapsers").selectAll(".collapser")
.data(biHiSankey.expandedNodes(), function (d) { return d.id; });
collapserEnter = collapser.enter().append("g").attr("class", "collapser");
collapserEnter.append("circle")
.attr("r", COLLAPSER.RADIUS)
.style("fill", function (d) {
d.color = colorScale(d.type.replace(/ .*/, ""));
return d.color;
});
collapserEnter
.style("opacity", OPACITY.NODE_DEFAULT)
.attr("transform", function (d) {
return "translate(" + (d.x + d.width / 2) + "," + (d.y + COLLAPSER.RADIUS) + ")";
});
collapserEnter.on("dblclick", showHideChildren);
collapser.select("circle")
.attr("r", COLLAPSER.RADIUS);
collapser.transition()
.delay(TRANSITION_DURATION)
.duration(TRANSITION_DURATION)
.attr("transform", function (d, i) {
return "translate("
+ (COLLAPSER.RADIUS + i * 2 * (COLLAPSER.RADIUS + COLLAPSER.SPACING))
+ ","
+ (-COLLAPSER.RADIUS - OUTER_MARGIN)
+ ")";
});
collapser.on("mouseenter", function (g) {
if (!isTransitioning) {
showTooltip().select(".value")
.text(function () {
return g.name + "\n(Double click to collapse)";
});
var highlightColor = highlightColorScale(g.type.replace(/ .*/, ""));
d3.select(this)
.style("opacity", OPACITY.NODE_HIGHLIGHT)
.select("circle")
.style("fill", highlightColor);
node.filter(function (d) {
return d.ancestors.indexOf(g) >= 0;
}).style("opacity", OPACITY.NODE_HIGHLIGHT)
.select("rect")
.style("fill", highlightColor);
}
});
collapser.on("mouseleave", function (g) {
if (!isTransitioning) {
hideTooltip();
d3.select(this)
.style("opacity", OPACITY.NODE_DEFAULT)
.select("circle")
.style("fill", function (d) { return d.color; });
node.filter(function (d) {
return d.ancestors.indexOf(g) >= 0;
}).style("opacity", OPACITY.NODE_DEFAULT)
.select("rect")
.style("fill", function (d) { return d.color; });
}
});
collapser.exit().remove();
}
var exampleNodes = [
{"type":"Asset","id":"a","parent":null,"name":"Assets"},
{"type":"Asset","id":1,"parent":"a","number":"101","name":"Cash"},
{"type":"Asset","id":2,"parent":"a","number":"120","name":"Accounts Receivable"},
{"type":"Asset","id":3,"parent":"a","number":"140","name":"Merchandise Inventory"},
{"type":"Asset","id":4,"parent":"a","number":"150","name":"Supplies"},
{"type":"Asset","id":5,"parent":"a","number":"160","name":"Prepaid Insurance"},
{"type":"Asset","id":6,"parent":"a","number":"170","name":"Land"},
{"type":"Asset","id":7,"parent":"a","number":"175","name":"Buildings"},
{"type":"Asset","id":8,"parent":"a","number":"178","name":"Acc. Depreciation Buildings"},
{"type":"Asset","id":9,"parent":"a","number":"180","name":"Equipment"},
{"type":"Asset","id":10,"parent":"a","number":"188","name":"Acc. Depreciation Equipment"},
{"type":"Liability","id":"l","parent":null,"number":"l","name":"Liabilities"},
{"type":"Liability","id":11,"parent":"l","number":"210","name":"Notes Payable"},
{"type":"Liability","id":12,"parent":"l","number":"215","name":"Accounts Payable"},
{"type":"Liability","id":13,"parent":"l","number":"220","name":"Wages Payable"},
{"type":"Liability","id":14,"parent":"l","number":"230","name":"Interest Payable"},
{"type":"Liability","id":15,"parent":"l","number":"240","name":"Unearned Revenues"},
{"type":"Liability","id":16,"parent":"l","number":"250","name":"Mortage Loan Payable"},
{"type":"Equity","id":"eq","parent":null,"number":"eq","name":"Equity"},
{"type":"Revenue","id":"r","parent":null,"number":"r","name":"Revenues"},
{"type":"Revenue","id":"or","parent":"r","number":"","name":"Operating Revenue"},
{"type":"Revenue","id":17,"parent":"or","number":"310","name":"Service Revenues"},
{"type":"Revenue","id":"nor","parent":"r","number":"","name":"Non-Operating Revenue"},
{"type":"Revenue","id":18,"parent":"nor","number":"810","name":"Interest Revenues"},
{"type":"Revenue","id":19,"parent":"nor","number":"910","name":"Asset Sale Gain"},
{"type":"Revenue","id":20,"parent":"nor","number":"960","name":"Asset Sale Loss"},
{"type":"Expense","id":"ex","parent":null,"number":"ex","name":"Expenses"},
{"type":"Expense","id":21,"parent":"ex","number":"500","name":"Salaries Expense"},
{"type":"Expense","id":22,"parent":"ex","number":"510","name":"Wages Expense"},
{"type":"Expense","id":23,"parent":"ex","number":"540","name":"Supplies Expense"},
{"type":"Expense","id":24,"parent":"ex","number":"560","name":"Rent Expense"},
{"type":"Expense","id":25,"parent":"ex","number":"570","name":"Utilities Expense"},
{"type":"Expense","id":26,"parent":"ex","number":"576","name":"Telephone Expense"},
{"type":"Expense","id":27,"parent":"ex","number":"610","name":"Advertising Expense"},
{"type":"Expense","id":28,"parent":"ex","number":"750","name":"Depreciation Expense"}
]
var exampleLinks = [
{"source":8, "target":28, "value":Math.floor(Math.random() * 100)},
{"source":17, "target":18, "value":Math.floor(Math.random() * 100)},
{"source":22, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":3, "target":13, "value":Math.floor(Math.random() * 100)},
{"source":24, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":5, "target":4, "value":Math.floor(Math.random() * 100)},
{"source":15, "target":5, "value":Math.floor(Math.random() * 100)},
{"source":18, "target":8, "value":Math.floor(Math.random() * 100)},
{"source":3, "target":20, "value":Math.floor(Math.random() * 100)},
{"source":17, "target":18, "value":Math.floor(Math.random() * 100)},
{"source":22, "target":5, "value":Math.floor(Math.random() * 100)},
{"source":4, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":26, "target":16, "value":Math.floor(Math.random() * 100)},
{"source":27, "target":6, "value":Math.floor(Math.random() * 100)},
{"source":23, "target":4, "value":Math.floor(Math.random() * 100)},
{"source":10, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":17, "target":16, "value":Math.floor(Math.random() * 100)},
{"source":5, "target":12, "value":Math.floor(Math.random() * 100)},
{"source":12, "target":16, "value":Math.floor(Math.random() * 100)},
{"source":19, "target":5, "value":Math.floor(Math.random() * 100)},
{"source":15, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":27, "target":2, "value":Math.floor(Math.random() * 100)},
{"source":26, "target":28, "value":Math.floor(Math.random() * 100)},
{"source":22, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":3, "target":18, "value":Math.floor(Math.random() * 100)},
{"source":18, "target":5, "value":Math.floor(Math.random() * 100)},
{"source":25, "target":28, "value":Math.floor(Math.random() * 100)},
{"source":12, "target":1, "value":Math.floor(Math.random() * 100)},
{"source":28, "target":21, "value":Math.floor(Math.random() * 100)},
{"source":9, "target":16, "value":Math.floor(Math.random() * 100)},
{"source":14, "target":23, "value":Math.floor(Math.random() * 100)},
{"source":6, "target":1, "value":Math.floor(Math.random() * 100)},
{"source":9, "target":15, "value":Math.floor(Math.random() * 100)},
{"source":16, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":22, "target":28, "value":Math.floor(Math.random() * 100)},
{"source":8, "target":21, "value":Math.floor(Math.random() * 100)},
{"source":22, "target":7, "value":Math.floor(Math.random() * 100)},
{"source":18, "target":10, "value":Math.floor(Math.random() * 100)},
{"source":"eq", "target":1, "value":Math.floor(Math.random() * 100)},
{"source":1, "target":21, "value":Math.floor(Math.random() * 100)},
{"source":1, "target":24, "value":Math.floor(Math.random() * 100)},
{"source":17, "target":1, "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)},
{"source":Math.ceil(Math.random() * 28), "target":Math.ceil(Math.random() * 28), "value":Math.floor(Math.random() * 100)}
]
biHiSankey
.nodes(exampleNodes)
.links(exampleLinks)
.initializeNodes(function (node) {
node.state = node.parent ? "contained" : "collapsed";
})
.layout(LAYOUT_INTERATIONS);
disableUserInterractions(2 * TRANSITION_DURATION);
update();
<!DOCTYPE html>
<html>
<head>
<title>bihisankey diagram</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<div id="chart"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="https://cdn.rawgit.com/Neilos/bihisankey/master/bihisankey.js"></script>
<script src="app.js"></script>
</body>
</html>
.node rect {
cursor: move;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 1px 1px 2px #fff;
font-size: 0.8em;
font-family: sans-serif;
}
#tooltip {
position: absolute;
pointer-events: none;
font-size: 0.7em;
font-family: sans-serif;
padding: 3px;
width: auto;
height: auto;
background-color: #F2F2F2;
-webkit-box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.4);
-mox-box-shadow: 0px 0px 0px 5px rgba(0, 0, 0, 0.4);
box-shadow: 0px 0px 5px rbga(0, 0, 0, 0.4);
pointer-events: none;
}
.value {
white-space: pre-line;
margin: 0;
}
@theredpea
Copy link

theredpea commented Feb 25, 2016

Hi Neil. I understand the "hierarchical". And I understand the "bi-directional". I don't understand this dataset; how are Assets and Revenues connected? I suspect you used the Assets/Liabilities/Revenues (General Ledger stuff) because it's good hierarchical data (Assets > Current Assets > Cash) , and then you connected various nodes** just to illustrate** the "network" aspect, not necessarily because it made sense. Am I right? Great work. Thanks.

@JamesJansson
Copy link

JamesJansson commented Jan 5, 2017

Fantastic. Quick question, what is your license for this? I want to use this in our platform to demonstrate user flow through a signup process.

@srehmanali
Copy link

srehmanali commented Jan 23, 2017

Hi Its awesome!!

I am new to d3 library can you guide me how can I change path color as per their origin color when it is hovered .

@aliacarmen
Copy link

aliacarmen commented Jun 20, 2017

Hi Neil,
This is one of my favourite examples in d3.
I have tried to expand the nodes double-clicking on them and it doesn't work as did previous times I played with it.

@bryanberger
Copy link

bryanberger commented Jan 5, 2018

+1 to @aliacarmen can't seem to get double clicking to work from the demo. Any thoughts @Neilos?

@emoruzzi
Copy link

emoruzzi commented Jan 7, 2018

@bryanberger
Copy link

bryanberger commented Jan 9, 2018

@emoruzzi you're a legend thank you!

@jacktang
Copy link

jacktang commented Jul 29, 2018

@emoruzzi Did you have plan to migrate the demo to d3 v4?

@Konard
Copy link

Konard commented May 10, 2019

@Neilos @aliacarmen @bryanberger double click for expanding grouped nodes do not work on WebKit browsers. Execution of this.parentNode.appendChild(this) breaks down click and double click events execution on WebKit browsers, but not at FireFox. That instruction is executed on dragstart event's handler at 410th line of app.js. So the solution, if you need to get it to work on WebKit browsers will be removing the whole 410th line from app.js. I assume that it is was added to put a dragged node on top (as there is no way to control z-index in svg for now), but I think this is not so critical.

For more info see:
https://stackoverflow.com/questions/37921993/chrome-doesnt-fire-click-event-for-svg-element-firefox-does
https://stackoverflow.com/questions/17786618/how-to-use-z-index-in-svg-elements

@Konard
Copy link

Konard commented May 10, 2019

@Neilos @JamesJansson At http://bl.ocks.org/Neilos/584b9a5d44d5fe00f779 page stated that there is no LICENCE for this example. So I`m kindly asking you to provide any. I have a project that uses this example, so if the license is not compatible I have to remove the usage of that example.

@Neilos
Copy link
Author

Neilos commented May 11, 2019

@Konard (@JamesJansson) The plugin that this example is demonstrating is no longer maintained

Screenshot 2019-05-11 at 14 16 43

Feel free to fork the plugin code and amend as you wish.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment