Built with blockbuilder.org
Last active
August 21, 2018 18:52
-
-
Save davo/c5aeb1f68d3c813181777c4288022ee1 to your computer and use it in GitHub Desktop.
Network flow with happy path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Function that appends a path to selection that has sankey path data attached | |
// The path is formatted as dash array, and triangle paths to create arrows along the path | |
function pathArrows () { | |
var arrowLength = 10 | |
var gapLength = 50 | |
var arrowHeadSize = 4 | |
var path = null; | |
function appendArrows (selection) { | |
let totalDashArrayLength = arrowLength + gapLength | |
let arrows = selection | |
.append('path') | |
.attr('d', path) | |
.style('stroke-width', 1) | |
.style('stroke', 'black') | |
.style('stroke-dasharray', arrowLength + ',' + gapLength) | |
arrows.each(function (arrow) { | |
let thisPath = d3.select(this).node() | |
let parentG = d3.select(this.parentNode) | |
let pathLength = thisPath.getTotalLength() | |
let numberOfArrows = Math.ceil(pathLength / totalDashArrayLength) | |
// remove the last arrow head if it will overlap the target node | |
if ( | |
(numberOfArrows - 1) * totalDashArrayLength + | |
(arrowLength + (arrowHeadSize + 1)) > | |
pathLength | |
) { | |
numberOfArrows = numberOfArrows - 1 | |
} | |
let arrowHeadData = d3.range(numberOfArrows).map(function (d, i) { | |
let length = i * totalDashArrayLength + arrowLength | |
let point = thisPath.getPointAtLength(length) | |
let previousPoint = thisPath.getPointAtLength(length - 2) | |
let rotation = 0 | |
if (point.y == previousPoint.y) { | |
rotation = point.x < previousPoint.x ? 180 : 0 | |
} else if (point.x == previousPoint.x) { | |
rotation = point.y < previousPoint.y ? -90 : 90 | |
} else { | |
let adj = Math.abs(point.x - previousPoint.x) | |
let opp = Math.abs(point.y - previousPoint.y) | |
let angle = Math.atan(opp / adj) * (180 / Math.PI) | |
if (point.x < previousPoint.x) { | |
angle = angle + (90 - angle) * 2 | |
} | |
if (point.y < previousPoint.y) { | |
rotation = -angle | |
} else { | |
rotation = angle | |
} | |
} | |
return { x: point.x, y: point.y, rotation: rotation } | |
}) | |
let arrowHeads = parentG | |
.selectAll('.arrow-heads') | |
.data(arrowHeadData) | |
.enter() | |
.append('path') | |
.attr('d', function (d) { | |
return ( | |
'M' + | |
d.x + | |
',' + | |
(d.y - arrowHeadSize / 2) + | |
' ' + | |
'L' + | |
(d.x + arrowHeadSize) + | |
',' + | |
d.y + | |
' ' + | |
'L' + | |
d.x + | |
',' + | |
(d.y + arrowHeadSize / 2) | |
) | |
}) | |
.attr('class', 'arrow-head') | |
.attr('transform', function (d) { | |
return 'rotate(' + d.rotation + ',' + d.x + ',' + d.y + ')' | |
}) | |
.style('fill', 'black') | |
}) | |
} | |
appendArrows.arrowLength = function (value) { | |
if (!arguments.length) return arrowLength | |
arrowLength = value | |
return appendArrows | |
} | |
appendArrows.gapLength = function (value) { | |
if (!arguments.length) return gapLength | |
gapLength = value | |
return appendArrows | |
} | |
appendArrows.arrowHeadSize = function (value) { | |
if (!arguments.length) return arrowHeadSize | |
arrowHeadSize = value | |
return appendArrows | |
} | |
appendArrows.path = function(pathFunction) { | |
if (!arguments.length) { | |
return path | |
} | |
else{ | |
if (typeof pathFunction === "function") { | |
path = pathFunction; | |
return appendArrows | |
} | |
else { | |
path = function() { return pathFunction } | |
return appendArrows; | |
} | |
} | |
}; | |
return appendArrows; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
<!-- <script src="d3-path.arrows.js"></script> --> | |
<style> | |
body { | |
margin:50; | |
top:50; | |
right:50; | |
bottom:50; | |
left:50; | |
} | |
text { | |
fill: #fff; | |
text-anchor: middle; | |
} | |
circle { | |
stroke: white; | |
stroke-width: 5; | |
} | |
.arrow { | |
stroke-width: 2; | |
stroke: white; | |
fill: none; | |
} | |
.arrow-head { | |
fill: white; | |
} | |
.link { | |
fill: none; | |
} | |
</style> | |
</head> | |
<body> | |
<script> | |
let data = [ | |
{ | |
"source": "node1", | |
"target": "node2", | |
"value": 20, | |
"mainflow": true | |
}, | |
{ | |
"source": "node1", | |
"target": "node3", | |
"value": 8, | |
"mainflow": false | |
}, | |
{ | |
"source": "node1", | |
"target": "node4", | |
"value": 5, | |
"mainflow": false | |
}, | |
{ | |
"source": "node2", | |
"target": "node1", | |
"value": 9, | |
"mainflow": false | |
}, | |
{ | |
"source": "node2", | |
"target": "node3", | |
"value": 18, | |
"mainflow": true | |
}, | |
{ | |
"source": "node2", | |
"target": "node4", | |
"value": 5, | |
"mainflow": false | |
}, | |
{ | |
"source": "node3", | |
"target": "node1", | |
"value": 5, | |
"mainflow": false | |
}, | |
{ | |
"source": "node3", | |
"target": "node2", | |
"value": 3, | |
"mainflow": false | |
}, | |
{ | |
"source": "node3", | |
"target": "node4", | |
"value": 15, | |
"mainflow": true | |
}, | |
{ | |
"source": "node4", | |
"target": "node1", | |
"value": 5, | |
"mainflow": false | |
}, | |
{ | |
"source": "node4", | |
"target": "node2", | |
"value": 8, | |
"mainflow": false | |
}, | |
{ | |
"source": "node4", | |
"target": "node3", | |
"value": 5, | |
"mainflow": false | |
} | |
] | |
/*let data = [ | |
{"source":"node0","value":1686813,"target":"node1", "mainflow": true}, | |
{"source":"node2","value":1083523,"target":"node1", "mainflow": false}, | |
{"source":"node3","value":1285005,"target":"node1", "mainflow": false}, | |
{"source":"node4","value":1485331,"target":"node1", "mainflow": false}, | |
{"source":"node0","value":63398,"target":"node2", "mainflow": false}, | |
{"source":"node5","value":794704,"target":"node4", "mainflow": false}, | |
{"source":"node6","value":794704,"target":"node4", "mainflow": false}, | |
{"source":"node1","value":63398,"target":"node2", "mainflow": false}, | |
{"source":"node0","value":618423,"target":"node3", "mainflow": false}, | |
{"source":"node1","value":502228,"target":"node3", "mainflow": false}, | |
{"source":"node1","value":1166311,"target":"node4", "mainflow": false}, | |
{"source":"node0","value":1166311,"target":"node4", "mainflow": false}, | |
{"source":"node3","value":794704,"target":"node4", "mainflow": false}, | |
]*/ | |
let radians = 0.0174532925 | |
let width = 1000 | |
let height = 400 | |
let centre = height/2 | |
var arrowLength = 10 | |
var gapLength = 50 | |
var arrowHeadSize = 7 | |
let totalDashArrayLength = arrowLength + gapLength | |
let nestedData = d3.nest() | |
.key(function(d){ return d.source }) | |
.entries(data) | |
nestedData.forEach(function(d){ | |
d.total = d.values.reduce(function(sum, v){ return sum + v.value }, 0) | |
}) | |
let allNodes = [] | |
data.map(function(d){ | |
allNodes.push(d.target) | |
allNodes.push(d.source) | |
}) | |
let seriesNest = d3.nest() | |
.key(function(d){ return d }) | |
.entries(allNodes) | |
let series = seriesNest.map(function(d){ | |
return d.key | |
}) | |
let n = series.sort(d3.ascending) | |
let radius = d3.scaleSqrt() | |
.domain([0, d3.max(nestedData, function(d){ return d.total })]) | |
.range([0, 50]) | |
let strokeWidth = d3.scaleLinear() | |
.domain([0, d3.max(data, function(d){ return d.value })]) | |
.range([0, 50]) | |
let nodeCentreX = d3.scalePoint() | |
.padding(0.5) | |
.domain(series) | |
.range([0,width]) | |
let colour = d3.scaleOrdinal(d3.schemeDark2) | |
.domain(series) | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
var g = svg.append("g") | |
var links = g.selectAll("path") | |
.data(nestedData) | |
.enter() | |
.append("g") | |
.attr("transform", function(d) { | |
return "translate(" + nodeCentreX(d.key) + "," + centre + ")" | |
}) | |
links.selectAll("g") | |
.data(function(d){ return d.values }) | |
.enter() | |
.append("path") | |
.attr("class", "link") | |
.style("stroke", function(d) { return colour(d.target) }) | |
.style("stroke-width", function(d) { return strokeWidth(d.value) }) | |
.style("opacity", function(d) { return d.mainflow ? 1 : 0.5 }) | |
.attr("d", function(d){ return pathData(d.source, d.target, d.mainflow) }) | |
let arrows = links.selectAll("g") | |
.data(function(d){ return d.values }) | |
.enter() | |
.append("path") | |
.attr("class", "arrow") | |
.attr("d", function(d){ return pathData(d.source, d.target, d.mainflow) }) | |
.style('stroke-dasharray', arrowLength + ',' + gapLength) | |
.each(appendArrowHead) | |
var nodes = g.selectAll("circle") | |
.data(nestedData) | |
.enter() | |
.append("g") | |
.attr("transform", function(d) { | |
return "translate(" + nodeCentreX(d.key) + "," + centre + ")" | |
}) | |
nodes.append("circle") | |
.attr("cx", 0) | |
.attr("cy", 0) | |
.attr("r", function(d){ return radius(d.total) }) | |
.style("fill", function(d) { return colour(d.key) }) | |
nodes.append("text") | |
.text(function(d){ return d.key }) | |
.attr("dy", "0.35em") | |
function pathData(source, target, main) { | |
if (main) { | |
return "M0,0 L" + nodeCentreX.step() + ",0" | |
} | |
else { | |
let x1 = 0 | |
let x2 = nodeCentreX(target) - nodeCentreX(source) | |
let r1 = x2/2 | |
let r2 = x2/(3 + (nodeCentreX.step()/Math.abs(x2))) | |
let y = 0 | |
let sweep = 1 | |
return "M" + x1 + "," + y + " " | |
+ "A" + r1 + " " + r2 + " 0 0 " + sweep + " " + x2 + " " + y | |
} | |
} | |
function appendArrowHead(arrow) { | |
let thisPath = d3.select(this).node() | |
let parentG = d3.select(this.parentNode) | |
let pathLength = thisPath.getTotalLength() | |
let numberOfArrows = Math.ceil(pathLength / totalDashArrayLength) | |
// remove the last arrow head if it will overlap the target node | |
if ( | |
(numberOfArrows - 1) * totalDashArrayLength + | |
(arrowLength + (arrowHeadSize + 1)) > | |
pathLength | |
) { | |
numberOfArrows = numberOfArrows - 1 | |
} | |
let arrowHeadData = d3.range(numberOfArrows).map(function (d, i) { | |
let length = i * totalDashArrayLength + arrowLength | |
let point = thisPath.getPointAtLength(length) | |
let previousPoint = thisPath.getPointAtLength(length - 2) | |
let rotation = 0 | |
if (point.y == previousPoint.y) { | |
rotation = point.x < previousPoint.x ? 180 : 0 | |
} else if (point.x == previousPoint.x) { | |
rotation = point.y < previousPoint.y ? -90 : 90 | |
} else { | |
let adj = Math.abs(point.x - previousPoint.x) | |
let opp = Math.abs(point.y - previousPoint.y) | |
let angle = Math.atan(opp / adj) * (180 / Math.PI) | |
if (point.x < previousPoint.x) { | |
angle = angle + (90 - angle) * 2 | |
} | |
if (point.y < previousPoint.y) { | |
rotation = -angle | |
} else { | |
rotation = angle | |
} | |
} | |
return { x: point.x, y: point.y, rotation: rotation } | |
}) | |
let arrowHeads = parentG | |
.selectAll('.arrow-heads') | |
.data(arrowHeadData) | |
.enter() | |
.append('path') | |
.attr('d', function (d) { | |
return ( | |
'M' + | |
d.x + | |
',' + | |
(d.y - arrowHeadSize / 2) + | |
' ' + | |
'L' + | |
(d.x + arrowHeadSize) + | |
',' + | |
d.y + | |
' ' + | |
'L' + | |
d.x + | |
',' + | |
(d.y + arrowHeadSize / 2) | |
) | |
}) | |
.attr('class', 'arrow-head') | |
.attr('transform', function (d) { | |
return 'rotate(' + d.rotation + ',' + d.x + ',' + d.y + ')' | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment