block by davo c5aeb1f68d3c813181777c4288022ee1

Network flow with happy path

Full Screen

Built with blockbuilder.org

index.html

<!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 + ')'
        })
    }

d3-path-arrows.js

// 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;
  
}