Built with blockbuilder.org
forked from tomshanley's block: Network flow with happy path
forked from tomshanley's block: Network flow with happy path
xxxxxxxxxx
<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":"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": true},
{"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": true},
]
let series = ["node0","node1","node2","node3","node4","node5","node6"]
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 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()
.range(["rgb(79,140,157)", "rgb(173,212,101)", "rgb(176,41,73)", "rgb(168,223,212)", "rgb(14,80,62)", "rgb(215,139,105)"])
.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/4
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 + ')'
})
}
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js