Locate link labels and move the labels along with the links

52 Views Asked by At

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>
1

There are 1 best solutions below

8
On BEST ANSWER

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>