Search code examples
javascriptd3.jssvgd3-force-directed

How to change the opacity of a node?


Scenario:

On mouse hover on a particular node, I am trying to highlight that node and all its interconnected nodes by changing the opacity and all the other nodes should be blurred.

Expectation: On mouse hover on a node, Opacity of that node and all its interconnected nodes should be 1 whereas the other nodes should be blurred using lesser opacity.

Existing functionality: It does not work as expected when i hover on any node for the first time. But from the second time onward It is working properly.

Please refer the jsfiddle: jsfiddle

var nodeElements =  g.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(graph.nodes)
  .enter().append("circle")
  .attr("r", 60)

  .attr("stroke", "#fff")
  .attr('stroke-width', 21)
  .attr("id", function(d) { return d.id })
   //.attr("fill", function(d) {return color(d.id)}) 
     .attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
     .on('contextmenu', function(d){ 
        d3.event.preventDefault();
        menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
    })
      .on('mouseover', selectNode)
      .on('mouseout', releaseNode)
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));


function selectNode(selectedNode) {
  var neighbors = getNeighbors(selectedNode)
        nodeElements.transition().duration(500)
        .attr('opacity', function(node) {
        return setOpacity(node,neighbors, selectedNode);
   })
   .attr('r', function(node) {
        return getNodeRadius(node,neighbors);
   });
   nodeElements.attr('fill', function(node) {
        // send selectedNode to your getNodeColor
        return getNodeColor(node,neighbors,selectedNode);
    })
   textElements.style('font-size', function(node) {
    return getTextColor(node, neighbors)
  })
  textElements.attr('opacity', function(node) {
        return setOpacity(node,neighbors, selectedNode);
   })

  linkElements.style('stroke', function(link) {
    return getLinkColor(selectedNode, link)
  })
}

function releaseNode() {
nodeElements
.attr('r', 60)
.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
 .attr('opacity', 1);

linkElements.style('stroke', 'grey');
textElements.attr('opacity','1');
}



 function setOpacity(node, neighbors, selectedNode) {
        if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
            return 1;
        } else {
            return 0.3;
        }
    }

Solution

  • The solution is quite simple: don't use attr to set the opacity, use style instead:

    nodeElements.transition()
        .duration(500)
        .style('opacity', function(node) {
            return setOpacity(node,neighbors, selectedNode);
        })
    

    Here is the code with that change only: https://jsfiddle.net/uye24zjn/

    Explanation

    The explanation is a bit more complicated. Firstly, have in mind that you never set any previous opacity, be it using attr or style.

    When you transition the opacity D3 uses a getter to retrieve the original value, applying the respective method (attr or style). And here comes the problem: because you never set any opacity using the attr method, the previous value using attr as the getter is null. However, using style, the value is 1:

    const circle = d3.select("g")
      .append("circle")
      .attr("r", 50);
    
    console.log("The opacity value using 'attr' as a getter is: " + circle.attr("opacity"))
    console.log("The opacity value using 'style' as a getter is: " + circle.style("opacity"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <svg>
      <g transform="translate(150,75)"></g>
    </svg>
    <script src="https://d3js.org/d3.v4.min.js"></script>

    The consequence is that D3 will try to transition from null (zero) to 1, and you'll see the circles disappearing at the begin of the transition. As a demo, hover over the circle:

    const circle = d3.select("g")
      .append("circle")
      .attr("r", 50);
    
    circle.on("mouseover", function() {
      d3.select(this).transition()
        .duration(1000)
        .attr("opacity", function() {
          return 1
        });
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <svg>
      <g transform="translate(150,75)"></g>
    </svg>
    <script src="https://d3js.org/d3.v4.min.js"></script>

    The issue happens only on the first mouseover (just like your code), because after hovering over the circle the opacity is set as an attribute, and there will be no more null when attr is used as a getter.

    As a general rule, use attr to set attributes, and use style to set the styles. In other words: use style to set anything that you would do using a CSS file.