0

I have a directed network using the d3.layout.force . Adapting this answer, I have managed to get nodes and links to fade if connected (direction of connection doesn't matter).

What I am having trouble with is to be able to change the opacity of markers when the path they are on is having its opacity altered with a mouseover event.

This is the script including the isConnected function for determining what nodes are connected:

A live example is here.

<script>


  function bar() {
  console.log("click");
  force.stop();
  force.start();
}



  var links = [
  {source: "A", target: "D", type: "high"},
  {source: "A", target: "K", type: "high"},
  {source: "B", target: "G", type: "high"},
  {source: "C", target: "A", type: "low"},
  {source: "D", target: "K", type: "low"},
  {source: "E", target: "A", type: "low"},
  {source: "F", target: "B", type: "low"},
  {source: "K", target: "J", type: "low"},
  {source: "F", target: "A", type: "low"},
  {source: "F", target: "I", type: "low"},
  {source: "G", target: "H", type: "low"},
  {source: "E", target: "K", type: "high"},
  {source: "E", target: "G", type: "low"},
  {source: "E", target: "F", type: "high"},
  {source: "D", target: "E", type: "high"}  
];

var nodes = {};

// Compute the distinct nodes from the links.
links.forEach(function(link) {
  link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
  link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
});

var width = 960,
    height = 700;

var force = d3.layout.force()
    .nodes(d3.values(nodes))
    .links(links)
    .size([width, height])
    .linkDistance(105)
    .charge(-775)
    .on("tick", tick)
    .start();



force.on("start", function () {
    console.log("start");
});
force.on("end", function () {
    console.log("end");
});

R=18



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

// add defs-marker
svg.append('svg:defs')
    .append('svg:marker')
    .attr('id', 'end-arrow')
    .attr('viewBox', '0 0 10 10')
    .attr('refX', 2+R)
    .attr('refY', 5)
     .attr('markerWidth', 4)
     .attr('markerHeight', 4)
    .attr('orient', 'auto')
    .append('svg:path')
    .attr('d', 'M0,0 L0,10 L10,5 z');


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

var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter().append("g")
    .attr("class", "node")
    .call(force.drag);

node.append("circle")
    .attr("r", R)
    .on("mouseover", fade(.1))
    .on("mouseout", fade(1))
;

node.append("text")
    .attr("x", 0)
    .attr("dy", ".35em")
    .text(function(d) { return d.name; });




function tick() {
  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 + ")"; });
}



   var linkedByIndex = {};
    links.forEach(function(d) {
        linkedByIndex[d.source.index + "," + d.target.index] = 1;
    });

    function isConnected(a, b) {
        return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
    }

 function fade(opacity) {
        return function(d) {
            node.style("stroke-opacity", function(o) {
                thisOpacity = isConnected(d, o) ? 1 : opacity;
                this.setAttribute('fill-opacity', thisOpacity);
                return thisOpacity;
            });

            link.style("stroke-opacity", function(o) {
                return o.source === d || o.target === d ? 1 : opacity;
            });

           marker.style("opacity", function(o) {
                return o.source === d || o.target === d ? 1 : opacity;
            });
        };
 }


</script>

A tangentially related question would be how to shorten the path so that when the opacity of nodes and links fade, that the line going to the middle of each node is not noticeable.

Community
  • 1
  • 1
jalapic
  • 13,792
  • 8
  • 57
  • 87
  • You cannot directly style marker instances. For an explanation see the bottom half of my [answer](/35371604/4235784) to a similar problem. I'd suggest adding a copy of the marker's definition to the `` having reduced opacity. In your `fade()` function you are then able to switch the `marker-end` property to refer to the appropriate id. – altocumulus Feb 26 '16 at 17:25
  • @altocumulus thanks - could you repost the linnk - I'm getting a page not found. – jalapic Feb 26 '16 at 18:23
  • There you go: http://stackoverflow.com/a/35371604/4235784 – altocumulus Feb 26 '16 at 18:24
  • @altocumulus thanks - I'm making progress. I added a second marker def. However, when I write in the fade function to switch to the new marker `end-arrow-fade` it seems to be changing the `id` in the DOM but I'm not seeing any change in the marker ends. When the mouseover event ends it also doesn't return to `id='arrow-fade'` it remains on the mouseover one. example: http://blockbuilder.org/jalapic/14fcf6f266e877cb1c23 thanks for your help – jalapic Feb 26 '16 at 19:18
  • There is a rule `marker-end: url(#end-arrow);` in your CSS for class `.link` which overrides your settings. Removing that line will solve the issue. – altocumulus Feb 26 '16 at 19:46
  • @altocumulus oh my - i completely missed that ! Thanks for your help! - edit: except now it fades the markers but they don't reappear (go back to opacity 1) on ending the mouseover event. – jalapic Feb 26 '16 at 20:06
  • There is another glitch in your `fade()` function. When switching markers, you have to take the parameter `opacity` into account to ensure, that all markers will be reverted on mouseout. See this [update](http://blockbuilder.org/altocumulus/8b607978218e4a7513f3). – altocumulus Feb 26 '16 at 20:13
  • @altocumulus - ah that makes sense. If you want to post as an answer then I'll accept. – jalapic Feb 26 '16 at 20:16
  • I put this discussion into an answer. If you are still interested in the second part of your question concerning the visibility of lines underneath the circles, please consider posting that as another question as it is not directly related to this problem. – altocumulus Feb 26 '16 at 21:41

1 Answers1

2

Your approach is not feasible because of the way marker instances are rendered. Cannibalizing one of my own answers and quoting the SVG spec:

11.6.4 Details on how markers are rendered

[...]

The rendering effect of a marker is as if the contents of the referenced ‘marker’ element were deeply cloned into a separate non-exposed DOM tree for each instance of the marker. Because the cloned DOM tree is non-exposed, the SVG DOM does not show the cloned instance of the marker.

Only the original marker elements, i.e. the declaring <marker> elements, are stylable using CSS, whereas the cloned instances referenced via properties marker-start, marker-mid, or marker-end are not accessible and therefore not individually stylable.

CSS2 selectors can be applied to the original (i.e., referenced) elements because they are part of the formal document structure. CSS2 selectors cannot be applied to the (conceptually) cloned DOM tree because its contents are not part of the formal document structure.


To circumvent these constraints you could use two defining marker elements, the second being a cloned version of the first with reduced opacity.

// add defs-markers
svg.append('svg:defs').selectAll("marker")
    .data([{id:"end-arrow", opacity:1}, {id:"end-arrow-fade", opacity:0.1}])
  .enter().append('marker')
    .attr('id', function(d) { return d.id; })
    .attr('viewBox', '0 0 10 10')
    .attr('refX', 2+R)
    .attr('refY', 5)
    .attr('markerWidth', 4)
    .attr('markerHeight', 4)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M0,0 L0,10 L10,5 z')
    .style("opacity", function(d) { return d.opacity; });

Within your fade() function you are then able to switch the lines' marker-end properties to refer to the appropriate marker's id:

link.attr("marker-end", function(o) {
  return opacity === 1 || o.source === d || o.target === d
    ? 'url(#end-arrow)' : 'url(#end-arrow-fade)';
});          

Have a look at the following snippet for a working demo:

function bar() {
  console.log("click");
  force.stop();
  force.start();
}
  
var links = [
  {source: "A", target: "D", type: "high"},
  {source: "A", target: "K", type: "high"},
  {source: "B", target: "G", type: "high"},
  {source: "C", target: "A", type: "low"},
  {source: "D", target: "K", type: "low"},
  {source: "E", target: "A", type: "low"},
  {source: "F", target: "B", type: "low"},
  {source: "K", target: "J", type: "low"},
  {source: "F", target: "A", type: "low"},
  {source: "F", target: "I", type: "low"},
  {source: "G", target: "H", type: "low"},
  {source: "E", target: "K", type: "high"},
  {source: "E", target: "G", type: "low"},
  {source: "E", target: "F", type: "high"},
  {source: "D", target: "E", type: "high"}  
];

var nodes = {};

// Compute the distinct nodes from the links.
links.forEach(function(link) {
  link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
  link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
});

var width = 600,
    height = 600;

var force = d3.layout.force()
    .nodes(d3.values(nodes))
    .links(links)
    .size([width, height])
    .linkDistance(105)
    .charge(-775)
    .on("tick", tick)
    .start();

force.on("start", function () {
    console.log("start");
});
force.on("end", function () {
    console.log("end");
});

R=18
 
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

// add defs-markers
svg.append('svg:defs').selectAll("marker")
    .data([{id:"end-arrow", opacity:1}, {id:"end-arrow-fade", opacity:0.1}])
  .enter().append('marker')
    .attr('id', function(d) { return d.id; })
    .attr('viewBox', '0 0 10 10')
    .attr('refX', 2+R)
    .attr('refY', 5)
    .attr('markerWidth', 4)
    .attr('markerHeight', 4)
    .attr('orient', 'auto')
  .append('svg:path')
    .attr('d', 'M0,0 L0,10 L10,5 z')
    .style("opacity", function(d) { return d.opacity; });

var link = svg.selectAll(".link")
    .data(force.links())
    .enter()
    .append("line")
    .attr("class", "link")
    .attr('marker-end', 'url(#end-arrow)');  
 
var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter().append("g")
    .attr("class", "node")
    .call(force.drag);

node.append("circle")
    .attr("r", R)
    .on("mouseover", fade(.1))
    .on("mouseout", fade(1))
;

node.append("text")
    .attr("x", 0)
    .attr("dy", ".35em")
    .text(function(d) { return d.name; });
  
function tick() {
  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 + ")";
  });
}
  
var linkedByIndex = {};
links.forEach(function(d) {
  linkedByIndex[d.source.index + "," + d.target.index] = 1;
});

function isConnected(a, b) {
  return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index +   "," + a.index] || a.index == b.index;
}
  
function fade(opacity) {
  return function(d) {
    node.style("stroke-opacity", function(o) {
      thisOpacity = isConnected(d, o) ? 1 : opacity;
      this.setAttribute('fill-opacity', thisOpacity);
      return thisOpacity;
    });

    link.style("stroke-opacity", function(o) {
      return o.source === d || o.target === d ? 1 : opacity;
    });

    link.attr("marker-end", function(o) {
      return opacity === 1 || o.source === d || o.target === d
        ? 'url(#end-arrow)' : 'url(#end-arrow-fade)';
    });          
  };
 }
.node circle {
  fill: #DDD;
  stroke: #777;
  stroke-width: 2px;
}
.node text {
  font-family: sans-serif;
  text-anchor: middle;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
.link {
  stroke: #88A;
  stroke-width: 4px;
}
text {
  font: 18px sans-serif;
  pointer-events: none;
}
#end-arrow {
  fill: #88A;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
Community
  • 1
  • 1
altocumulus
  • 21,179
  • 13
  • 61
  • 84