d3.jshyperlinklabelnodesd3-force-directed

Locate link labels and move the labels along with the links


I am developing a project on pattern representation through directed graphs. I have reached the point of loading it from a json, placing the node labels, the links between them and that both the links and the node labels move when moving the nodes. However, I am facing difficulties in placing the link labels ("type" in the json) in position (midpoint of the links) and linking to the movement of the links. Any idea of resolution and explanation? Thanks!!!

The actual code can be found at the link (https://jsfiddle.net/obordies25/wmLeganx/2/)

 <!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>[AF-PATTERN-EDITOR]</title>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <meta name="robots" content="noindex, nofollow">
  <meta name="googlebot" content="noindex, nofollow">
  <meta name="viewport" content="width=device-width, initial-scale=1">

<style  id="compiled-css" type="text/css">
.node {
    fill:#ccc;
    stroke: #fff;
    stroke-width: 2px;
}
.node-active {
    stroke: #555;
    stroke-width: 1.5px;
}
link-active {
    stroke: #555;
    stroke-opacity: 5;
}
line {
  stroke: rgb(212, 212, 212);
  stroke-width: 1px;
  shape-rendering: crispEdges;
}
.overlay {
    fill: none;
    pointer-events: all;
}
.link{
    stroke: #ccc;
    stroke-width: 2px;
    }
svg {
  box-sizing: border-box;
  border: 1px solid rgb(212, 212, 212);
}
</style>

  <body>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script  type="text/javascript">

var width = 960,
    height = 500,
    resolution = 150,
    r = 15;

var graph = {
    "nodes":  [
        {"task": "1", "label": "1", "social": "I", "id": 1, "x": 150, "y": 450},
        {"task": "2", "label": "2", "social": "G", "id": 2, "x": 300, "y": 150},
        {"task": "3", "label": "3", "social": "T", "id": 3, "x": 450, "y": 300}
        ],

    "links": [
        {"source": "1", "target": "2", "type": "N:1"},
        {"source": "2", "target": "3", "type": "1:N"},
        {"source": "1", "target": "3", "type": "1:1"}
        ]
}

/*d3.json("graph.json", function (error, graph) {
    if (error) throw error;
    update(graph.links, graph.nodes);
    })*/

var margin = {
    top: -5,
    right: -5,
    bottom: -5,
    left: -5
};

var colors = d3.scale.category20();

var svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height);

var force = d3.layout.force()
    .charge(-200)
    .linkDistance(50)
    .size([width + margin.left + margin.right, height + margin.top + margin.bottom])
    //.nodes(graph.nodes);

var drag = d3.behavior.drag()
    .origin(function(d) { return d; })
    .on('drag', dragged);

svg.selectAll('.vertical')
    .data(d3.range(1, width / resolution))
    .enter().append('line')
    .attr('class', 'vertical')
    .attr('x1', function(d) { return d * resolution; })
    .attr('y1', 0)
    .attr('x2', function(d) { return d * resolution; })
    .attr('y2', height);

svg.selectAll('.horizontal')
    .data(d3.range(1, height / resolution))
    .enter().append('line')
    .attr('class', 'horizontal')
    .attr('x1', 0)
    .attr('y1', function(d) { return d * resolution; })
    .attr('x2', width)
    .attr('y2', function(d) { return d * resolution; });

svg.append('defs').append('marker')
    .attr({'id':'arrowhead',
        'viewBox':'-0 -5 10 10',
        'refX':40,
        'refY':0,
        'orient':'auto',
        'markerWidth':5,
        'markerHeight':5,
        'xoverflow':'visible'})
    .append('svg:path')
    .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
    .attr('fill', '#777')
    .style('stroke','none');


var link = svg.selectAll("svg.link")
    .data(graph.links)
    .enter().append("line")
    .attr("class", "link")
    .attr('marker-end','url(#arrowhead)')

    .attr("data-source", function (d) {
          return d.source;
        })
    .attr("data-target", function (d) {
          return d.target;
        })
    .attr("x1", function (d) {
        for (let node of graph.nodes) {
            if (node.task === d.source)
                return node.x;
        }
    })
    .attr("x2", function (d) {
        for (let node of graph.nodes) {
            if (node.task === d.target)
                return node.x;
        }
    })
    .attr("y1", function (d) {
        for (let node of graph.nodes) {
            if (node.task === d.source)
                return node.y;
        }
    })
    .attr("y2", function (d) {
        for (let node of graph.nodes) {
            if (node.task === d.target)
                return node.y;
        }
    })
    .style("stroke-width", function(d) {
        return Math.sqrt(d.value);
});

var node = svg.selectAll("svg.node")
    .data(graph.nodes)
    .enter().append('g');

node.append("circle")
    .attr("class", "node")
    .attr('node-id', d => d.social)
    .attr('cx', function(d) { return d.x; })
    .attr('cy', function(d) { return d.y; })
    .attr('r', r * 2)
    .call(drag);//
    
node.append("text")
    .attr("class", "label")
    .attr('node-id', d => d.social)
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .attr('dx', function(d) { return d.x; })
    .attr('dy', function(d) { return d.y; })
    .text(function(d) { return d.social; });

force.on("tick", function () {
    link.attr("x1", function (d) { return d.source.x; })
        .attr("y1", function (d) { return d.source.y; })
        .attr("x2", function (d) { return d.target.x; })
        .attr("y2", function (d) { return d.target.y; });

    node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

});

function dragged(d) {
  var x = d3.event.x,
      y = d3.event.y,

      gridX = round(Math.max(r, Math.min(width - r, x)), resolution),
      gridY = round(Math.max(r, Math.min(height - r, y)), resolution);

      d3.select(this).attr('cx', d.x = gridX).attr('cy', d.y = gridY);
      d3.select(this).attr('dx', d.x = gridX).attr('dy', d.y = gridY);

      d3.selectAll(`[data-source='${d.task}']`).attr('x1', d.x).attr('y1', d.y);
      d3.selectAll(`[data-target='${d.task}']`).attr('x2', d.x).attr('y2', d.y);
      d3.select(`text[node-id='${d.social}']`).attr('dx', d.x).attr('dy', d.y)
}

function round(p, n) {
  return p % n < n / 2 ? p - (p % n) : p + n - (p % n);
}

</script>
</body>
<html>

Solution

  • Create link labels as <g> elements with a <circle> and a <text> under each one:

    const links = graph.links.map(l => {
      const source = graph.nodes.find(n => n.task === l.source);
      const target = graph.nodes.find(n => n.task === l.target);
      return {...l, source, target};
    });
            
    const linkLabel = svg.selectAll("g.link-label")
      .data(links)
      .enter()
      .append("g")
      .classed('link-label', true)
      .attr('transform', d => 
        `translate(${(d.source.x + d.target.x) / 2},${(d.source.y + d.target.y) / 2})`);
        
    linkLabel.append('circle')
      .attr('r', 20);
          
    linkLabel.append('text')
      .text(d => d.type)
      .attr('text-anchor', 'middle')
      .attr('alignment-baseline', 'middle')
      .style('stroke', 'none')
      .style('fill', 'black')
    
    

    See it's working in the snippet:

    var width = 960,
        height = 500,
        resolution = 150,
        r = 15;
    
    var graph = {
        "nodes":  [
            {"task": "1", "label": "1", "social": "I", "id": 1, "x": 150, "y": 450},
            {"task": "2", "label": "2", "social": "G", "id": 2, "x": 300, "y": 150},
            {"task": "3", "label": "3", "social": "T", "id": 3, "x": 450, "y": 300}
            ],
    
        "links": [
            {"source": "1", "target": "2", "type": "N:1"},
            {"source": "2", "target": "3", "type": "1:N"},
            {"source": "1", "target": "3", "type": "1:1"}
            ]
    }
    
    var margin = {
        top: -5,
        right: -5,
        bottom: -5,
        left: -5
    };
    
    var colors = d3.scale.category20();
    
    var svg = d3.select('body').append('svg')
        .attr('width', width)
        .attr('height', height);
    
    var force = d3.layout.force()
        .charge(-200)
        .linkDistance(50)
        .size([width + margin.left + margin.right, height + margin.top + margin.bottom])
    
    var drag = d3.behavior.drag()
        .origin(function(d) { return d; })
        .on('drag', dragged);
    
    svg.selectAll('.vertical')
        .data(d3.range(1, width / resolution))
        .enter().append('line')
        .attr('class', 'vertical')
        .attr('x1', function(d) { return d * resolution; })
        .attr('y1', 0)
        .attr('x2', function(d) { return d * resolution; })
        .attr('y2', height);
    
    svg.selectAll('.horizontal')
        .data(d3.range(1, height / resolution))
        .enter().append('line')
        .attr('class', 'horizontal')
        .attr('x1', 0)
        .attr('y1', function(d) { return d * resolution; })
        .attr('x2', width)
        .attr('y2', function(d) { return d * resolution; });
    
    svg.append('defs').append('marker')
        .attr({'id':'arrowhead',
            'viewBox':'-0 -5 10 10',
            'refX':40,
            'refY':0,
            'orient':'auto',
            'markerWidth':5,
            'markerHeight':5,
            'xoverflow':'visible'})
        .append('svg:path')
        .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
        .attr('fill', '#777')
        .style('stroke','none');
    
    
    var link = svg.selectAll("line.link")
        .data(graph.links)
        .enter().append("line")
        .attr("class", "link")
        .attr('marker-end','url(#arrowhead)')
    
        .attr("data-source", function (d) {
              return d.source;
            })
        .attr("data-target", function (d) {
              return d.target;
            })
        .attr("x1", function (d) {
            for (let node of graph.nodes) {
                if (node.task === d.source)
                    return node.x;
            }
        })
        .attr("x2", function (d) {
            for (let node of graph.nodes) {
                if (node.task === d.target)
                    return node.x;
            }
        })
        .attr("y1", function (d) {
            for (let node of graph.nodes) {
                if (node.task === d.source)
                    return node.y;
            }
        })
        .attr("y2", function (d) {
            for (let node of graph.nodes) {
                if (node.task === d.target)
                    return node.y;
            }
        })
        .style("stroke-width", function(d) {
            return Math.sqrt(d.value);
            });   
        
        const links = graph.links.map(l => {
        const source = graph.nodes.find(n => n.task === l.source);
        const target = graph.nodes.find(n => n.task === l.target);
        return {...l, source, target};
        });
            
        const linkLabel = svg.selectAll("g.link-label")
        .data(links)
        .enter()
        .append("g")
        .attr("class", "link-label")
        .attr('transform', d => `translate(${(d.source.x + d.target.x) / 2},${(d.source.y + d.target.y) / 2})`);
        
        linkLabel.append('circle')
            .attr('r', 20);
          
        linkLabel.append('text')
            .text(d => d.type)
          .classed('label', true)
          .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .style('stroke', 'none')
        .style('fill', 'black')
          
     console.log('L: ', graph.links);   
    
    
    var node = svg.selectAll("svg.node")
        .data(graph.nodes)
        .enter().append('g');
    
    node.append("circle")
        .attr("class", "node")
        .attr('node-id', d => d.social)
        .attr('cx', function(d) { return d.x; })
        .attr('cy', function(d) { return d.y; })
        .attr('r', r * 2)
        .call(drag);//
        
    node.append("text")
        .attr("class", "label")
        .attr('node-id', d => d.social)
        .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .attr('dx', function(d) { return d.x; })
        .attr('dy', function(d) { return d.y; })
        .text(function(d) { return d.social; });
    
    force.on("tick", function () {
        link.attr("x1", function (d) { return d.source.x; })
            .attr("y1", function (d) { return d.source.y; })
            .attr("x2", function (d) { return d.target.x; })
            .attr("y2", function (d) { return d.target.y; });
            
       linkLabel.attr('transform', d => `translate(${(d.source.x + d.target.x) / 2},${(d.source.y + d.target.y) / 2})`);
    
        node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
    
    });
    
    function dragged(d) {
      var x = d3.event.x,
          y = d3.event.y,
    
          gridX = round(Math.max(r, Math.min(width - r, x)), resolution),
          gridY = round(Math.max(r, Math.min(height - r, y)), resolution);
    
          d3.select(this).attr('cx', d.x = gridX).attr('cy', d.y = gridY);
          d3.select(this).attr('dx', d.x = gridX).attr('dy', d.y = gridY);
    
          d3.selectAll(`[data-source='${d.task}']`).attr('x1', d.x).attr('y1', d.y);
          d3.selectAll(`[data-target='${d.task}']`).attr('x2', d.x).attr('y2', d.y);
          d3.select(`text[node-id='${d.social}']`).attr('dx', d.x).attr('dy', d.y)
          
          linkLabel.attr('transform', d => `translate(${(d.source.x + d.target.x) / 2},${(d.source.y + d.target.y) / 2})`);
    }
    
    function round(p, n) {
      return p % n < n / 2 ? p - (p % n) : p + n - (p % n);
    }
    .node, .link-label {
        fill:#ccc;
        stroke: #fff;
        stroke-width: 2px;
    }
    .node-active {
        stroke: #555;
        stroke-width: 1.5px;
    }
    link-active {
        stroke: #555;
        stroke-opacity: 5;
    }
    line {
      stroke: rgb(212, 212, 212);
      stroke-width: 1px;
      shape-rendering: crispEdges;
    }
    .overlay {
        fill: none;
        pointer-events: all;
    }
    .link{
        stroke: #ccc;
        stroke-width: 2px;
        }
    svg {
      box-sizing: border-box;
      border: 1px solid rgb(212, 212, 212);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>