0

I am trying to plot a network graph using networkD3 in R. I wanted to make some changes to the display so that the text labels (which appears when mouseover) can be easily read.

Please refer to the link here for an example. Note: Jump to the d3ForceNetwork plot.

As seen in the example, the labels are hard to read due to its colour and it often gets obstructed by the surrounding nodes. I have been messing around with the JS file and managed to change the text label color to black. However, having no knowledge of JS or CSS (I can't even tell the difference between the 2 actually), I have no idea how I can change the stack order such that the text labels will always be displayed above any other objects.

Can anyone advise me on how I can achieve the desired outcome?

Below is the full JS file:

HTMLWidgets.widget({

   name: "forceNetwork",

   type: "output",

   initialize: function(el, width, height) {

    d3.select(el).append("svg")
        .attr("width", width)
        .attr("height", height);

    return d3.layout.force();
  },

  resize: function(el, width, height, force) {

    d3.select(el).select("svg")
        .attr("width", width)
        .attr("height", height);

    force.size([width, height]).resume();
  },

  renderValue: function(el, x, force) {

  // Compute the node radius  using the javascript math expression specified
    function nodeSize(d) {
            if(options.nodesize){
                    return eval(options.radiusCalculation);

            }else{
                    return 6}

    }


    // alias options
    var options = x.options;

    // convert links and nodes data frames to d3 friendly format
    var links = HTMLWidgets.dataframeToD3(x.links);
    var nodes = HTMLWidgets.dataframeToD3(x.nodes);

    // get the width and height
    var width = el.offsetWidth;
    var height = el.offsetHeight;

    var color = eval(options.colourScale);

    // set this up even if zoom = F
    var zoom = d3.behavior.zoom();

    // create d3 force layout
    force
      .nodes(d3.values(nodes))
      .links(links)
      .size([width, height])
      .linkDistance(options.linkDistance)
      .charge(options.charge)
      .on("tick", tick)
      .start();

    // thanks http://plnkr.co/edit/cxLlvIlmo1Y6vJyPs6N9?p=preview
    //  http://stackoverflow.com/questions/22924253/adding-pan-zoom-to-d3js-force-directed
      var drag = force.drag()
        .on("dragstart", dragstart)
      // allow force drag to work with pan/zoom drag
      function dragstart(d) {
        d3.event.sourceEvent.preventDefault();
        d3.event.sourceEvent.stopPropagation();
      }

    // select the svg element and remove existing children
    var svg = d3.select(el).select("svg");
    svg.selectAll("*").remove();
    // add two g layers; the first will be zoom target if zoom = T
    //  fine to have two g layers even if zoom = F
    svg = svg
        .append("g").attr("class","zoom-layer")
        .append("g")

    // add zooming if requested
    if (options.zoom) {
      function redraw() {
        d3.select(el).select(".zoom-layer").attr("transform",
          "translate(" + d3.event.translate + ")"+
          " scale(" + d3.event.scale + ")");
      }
      zoom.on("zoom", redraw)

      d3.select(el).select("svg")
        .attr("pointer-events", "all")
        .call(zoom);

    } else {
      zoom.on("zoom", null);
    }

    // draw links
    var link = svg.selectAll(".link")
      .data(force.links())
      .enter().append("line")
      .attr("class", "link")
      .style("stroke", function(d) { return d.colour ; })
      //.style("stroke", options.linkColour)
      .style("opacity", options.opacity)
      .style("stroke-width", eval("(" + options.linkWidth + ")"))
      .on("mouseover", function(d) {
          d3.select(this)
            .style("opacity", 1);
      })
      .on("mouseout", function(d) {
          d3.select(this)
            .style("opacity", options.opacity);
      });

    // draw nodes
    var node = svg.selectAll(".node")
      .data(force.nodes())
      .enter().append("g")
      .attr("class", "node")
      .style("fill", function(d) { return color(d.group); })
      .style("opacity", options.opacity)
      .on("mouseover", mouseover)
      .on("mouseout", mouseout)
      .on("click", click)
      .call(force.drag);

    node.append("circle")
      .attr("r", function(d){return nodeSize(d);})
      .style("stroke", "#fff")
      .style("opacity", options.opacity)
      .style("stroke-width", "1.5px");

    node.append("svg:text")
      .attr("class", "nodetext")
      .attr("dx", 12)
      .attr("dy", ".35em")
      .text(function(d) { return d.name })
      .style("font", options.fontSize + "px " + options.fontFamily)
      .style("opacity", options.opacityNoHover)
      .style("pointer-events", "none");

    function tick() {
      node.attr("transform", function(d) {
        if(options.bounded){ // adds bounding box
            d.x = Math.max(nodeSize(d), Math.min(width - nodeSize(d), d.x));
            d.y = Math.max(nodeSize(d), Math.min(height - nodeSize(d), d.y));
        }

        return "translate(" + d.x + "," + d.y + ")"});

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

    function mouseover() {
      d3.select(this).select("circle").transition()
        .duration(750)
        .attr("r", function(d){return nodeSize(d)+5;});
      d3.select(this).select("text").transition()
        .duration(750)
        .attr("x", 13)
        .style("stroke-width", ".5px")
        .style("font", options.clickTextSize + "px ")
        .style('fill', 'black')
        .style('position','relative')
        .style("opacity", 1);
    }

    function mouseout() {
      d3.select(this).select("circle").transition()
        .duration(750)
        .attr("r", function(d){return nodeSize(d);});
      d3.select(this).select("text").transition()
        .duration(1250)
        .attr("x", 0)
        .style("font", options.fontSize + "px ") 
        .style("opacity", options.opacityNoHover);
    }

    function click(d) {
      return eval(options.clickAction)
    }

    // add legend option
    if(options.legend){
        var legendRectSize = 18;
        var legendSpacing = 4;
        var legend = svg.selectAll('.legend')
          .data(color.domain())
          .enter()
          .append('g')
          .attr('class', 'legend')
          .attr('transform', function(d, i) {
            var height = legendRectSize + legendSpacing;
            var offset =  height * color.domain().length / 2;
            var horz = legendRectSize;
            var vert = i * height+4;
            return 'translate(' + horz + ',' + vert + ')';
          });

        legend.append('rect')
          .attr('width', legendRectSize)
          .attr('height', legendRectSize)
          .style('fill', color)
          .style('stroke', color);

        legend.append('text')
          .attr('x', legendRectSize + legendSpacing)
          .attr('y', legendRectSize - legendSpacing)
          .style('fill', 'darkOrange')
          .text(function(d) { return d; });
    }

    // make font-family consistent across all elements
    d3.select(el).selectAll('text').style('font-family', options.fontFamily);
  },
});

I suspect I need to make some changes to the code over here:

function mouseover() {
  d3.select(this).select("circle").transition()
    .duration(750)
    .attr("r", function(d){return nodeSize(d)+5;});
  d3.select(this).select("text").transition()
    .duration(750)
    .attr("x", 13)
    .style("stroke-width", ".5px")
    .style("font", options.clickTextSize + "px ")
    .style('fill', 'black')
    .style("opacity", 1);
}
vestland
  • 55,229
  • 37
  • 187
  • 305
Seamus Lam
  • 155
  • 3
  • 10

1 Answers1

1

You need to resort the node groups holding the circles and text so the currently mouseover'ed one is the last in that group, and thus the last one drawn so it appears on top of the others. See the first answer here -->

Updating SVG Element Z-Index With D3

In your case, if your data doesn't have an id field you may have to use 'name' instead as below (adapted to use the mouseover function you've got):

function mouseover(d) {
    d3.selectAll("g.node").sort(function (a, b) { 
          if (a.name != d.name) return -1;               // a is not the hovered element, send "a" to the back
          else return 1;                             // a is the hovered element, bring "a" to the front (by making it last)
    });
    // your code continues

The pain might be that you have to do this edit for every d3 graph generated by this R script, unless you can edit the R code/package itself. (or you could suggest it to the package author as an enhancement.)

Community
  • 1
  • 1
mgraham
  • 6,147
  • 1
  • 21
  • 20
  • Hi, thx for the answer. However, as I am completely new to JS and CSS (this is the first time I ever looked at a JS file), I am not too sure where am I suppose to append the above code. Can you kindly be more specific on how the above solution can be incorporated into the JS file? I have attempted to append the code numerous times based on trial and error, but to no avail. – Seamus Lam Apr 28 '16 at 10:33
  • It should go at the start of your mouseover() function code you've pasted in your q, I've changed the answer slightly to mirror your example (now `function mouseover(d)` rather than `.on ("mouseover", function(d))`, that should work if you paste it in. Notice the mouseover function now has a (d) argument that yours doesn't - that's the data for the currently mouseover'ed node. – mgraham Apr 28 '16 at 11:45