Question

I am working with a d3 force based graph, whose node labels are in fact URLs, which when clicked, take the user to the target URL. For usability reasons, is there a way to underline the url? Better yet, can the underline appear and disappear, can the color of the text be changed, as the user hovers over a certain label? This would help users understand that the labels are clickable. Please help.

enter image description here

document.addEventListener('DOMContentLoaded', function () {
    drawVisual();
});

var QueuedORG = [];
var tempLIST = [];


function drawVisual()
{
    //alert(sessionStorage["queuedArray"]);
    /*var getArr = [];
    getArr = JSON.parse(localStorage.getItem('storeArray'));
    document.getElementById('myMSG').innerHTML = getArr[1].parentURL;*/

    QueuedORG.length = 0;
    tempLIST.length = 0;

    //var w = 1024, h = 768;

    var w=window.innerWidth
    || document.documentElement.clientWidth
    || document.body.clientWidth;

    var h=window.innerHeight
    || document.documentElement.clientHeight
    || document.body.clientHeight;
    //var w = 1024, h = 768;

    //var vis = d3.select("#tab_5_contents").append("svg:svg").attr("width", w).attr("height", h);
    var vis = d3.select("#forcedLayoutGraph").append("svg:svg").attr("width", w).attr("height", h);

            //get links from LocalStorage
            //QueuedORG = JSON.parse(sessionStorage.getItem("queuedArray"));
            //QueuedORG = JSON.parse(localStorage["queuedArray"]);

    QueuedORG.push({url: "http://understandblue.blogspot.com/", parentURL: "http://understandblue.blogspot.com", used:0});
    QueuedORG.push({url: "http://www.google.com", parentURL: "http://understandblue.blogspot.com/", used:0});
    QueuedORG.push({url: "http://paperfriendly.blogspot.com", parentURL: "http://understandblue.blogspot.com/", used:0});
    QueuedORG.push({url: "http://4pawsforever.org", parentURL: "http://understandblue.blogspot.com/", used:0});
    QueuedORG.push({url: "http://en.wikipedia.org", parentURL: "http://understandblue.blogspot.com/", used:0});

            var nodes = [];
            nodes.length = 0;

            var labelAnchors = [];
            labelAnchors.length = 0;
            var labelAnchorLinks = [];
            labelAnchorLinks.length = 0;
            var links = [];
            links.length = 0;

            for(var i = 0; i < QueuedORG.length; i++) 
            {
                var nodeExists = 0;

                //check to see if a node for the current url has already been created. If yes, do not create a new node
                for(var j = 0; j < nodes.length; j++)  
                {
                    if(QueuedORG[i].url == nodes[j].label)
                        nodeExists = 1;

                }

                if (nodeExists == 0)
                {
                    var urlLabel = QueuedORG[i].url;
                    //remove 'http://' part
                    /*urlLabel = urlLabel.split("http://")[1];
                    if(urlLabel.match("www"))
                    urlLabel = urlLabel.split("www.")[1];
                    var rest = urlLabel.split("\.")[1];
                    urlLabel = urlLabel.split("\.")[0];*/

                    var node = {
                        label : QueuedORG[i].url,
                        category : QueuedORG[i].category
                    };
                    nodes.push(node);
                    labelAnchors.push({
                        node : node
                    });
                    labelAnchors.push({
                        node : node
                    });
                }
            };

            /*for(var i=0;i<nodes.length; i++)
            {
                console.log("node i:"+i+nodes[i]+"\n");
                console.log("labelAnchor i:"+i+labelAnchors[i]+"\n");
            }*/

            //To create links for connecting nodes
            for(var i = 0; i < QueuedORG.length; i++) 
            {
                var srcIndx = 0, tgtIndx = 0;
                for(var j = 0; j < nodes.length; j++)
                {
                    if( QueuedORG[i].url == nodes[j].label ) //to find the node number for the current url
                    {
                        srcIndx = j;
                    }

                    if( QueuedORG[i].parentURL == nodes[j].label ) //to find the node number for the parent url
                    {
                        tgtIndx = j;
                    }
                }
                //console.log("src:"+srcIndx+" tgt:"+tgtIndx);

                //connecting the current url's node to the parent url's node
                links.push({
                    source : srcIndx,
                    target : tgtIndx,
                    weight : 1,
                });

                labelAnchorLinks.push({
                    source : srcIndx * 2,
                    target : srcIndx * 2 + 1,
                    weight : 1
                });
            };

            var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).charge(-10000).linkStrength(function(x) {
                return x.weight * 10                                            // charge is for inter-node repel, link distance is node-node distance 
            });
            force.linkDistance(function(d) {
                return d.weight * 100;
            });

            force.start();

            var force2 = d3.layout.force().nodes(labelAnchors).links(labelAnchorLinks).gravity(0).linkStrength(10).charge(-500).size([w, h]);   //charge is for inter-label repel, link distance is node-label distance
            force2.linkDistance(function(d) {
                return d.weight * 10;
            });

            force2.start();

            var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke", "#CCC");

            var colors = {"1": "black", "2": "blue", "3": "red"};           // 1=root node 2=blog nodes 3=.org nodes
            var shape = {"1": "diamond", "2": "cross", "3": "circle"};

            var node = vis.selectAll("g.node").data(force.nodes()).enter().append("path").attr("class", "node").call(force.drag);
        //node.append("circle").attr("r", 5).style("stroke", "#FFF").style("stroke-width", 3).attr("class", function(d) {return "node category"+d.category});

            node.attr("d", d3.svg.symbol().type(function(d) {return shape[d.category];})).style("stroke", "#FFF").style("fill", function(d){ return colors[d.category];}).on('click', function(d, i) {
                var win = window.open(d.node.label, '_blank);
                win.focus();
            });

            var anchorLink = vis.selectAll("line.anchorLink").data(labelAnchorLinks)//.enter().append("svg:line").attr("class", "anchorLink").style("stroke", "#999");

            var anchorNode = vis.selectAll("g.anchorNode").data(force2.nodes()).enter().append("svg:g").attr("class", "anchorNode").on('click', function(d, i){
                       var win = window.open(d.node.label, '_blank);
                       win.focus();         
            });

            anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
            anchorNode.append("svg:text").text(function(d, i) {
                return i % 2 == 0 ? "" : d.node.label
            }).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);

            var updateLink = function() {
                this.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;
                });
            }

            var updateNode = function() {
                this.attr("transform", function(d) {
                    return "translate(" + d.x + "," + d.y + ")";
                });

            }

            force.on("tick", function() {

                force2.start();

                node.call(updateNode);

                anchorNode.each(function(d, i) {
                    if(i % 2 == 0) {
                        d.x = d.node.x;
                        d.y = d.node.y;
                    } else {
                        var b = this.childNodes[1].getBBox();

                        var diffX = d.x - d.node.x;
                        var diffY = d.y - d.node.y;

                        var dist = Math.sqrt(diffX * diffX + diffY * diffY);

                        var shiftX = b.width * (diffX - dist) / (dist * 2);
                        shiftX = Math.max(-b.width, Math.min(0, shiftX));
                        var shiftY = 5;
                        this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
                    }
                });

                anchorNode.call(updateNode);

        link.call(updateLink);
        anchorLink.call(updateLink);

    });
}
Was it helpful?

Solution

As Matt Walters said, you can change the text colour and add underlining with CSS, including adding hover effects.

However, I would highly recommend that you implement your links as actual <a> element hyperlinks, which are perfectly acceptable in SVG. I've tested it, and you can either wrap the text element in a link, or put the link inside the text element, but surrounding the actual text content. The only complication is that you have to use xlink:href for the url attribute, instead of simply href.

Here's a force-directed graph I created for someone else's debugging, adapted to turn all the text labels into links:
http://codepen.io/AmeliaBR/pen/AoFHg

Key code in the update function:

hypertext  = hypertext.data(force.nodes());
// hypertext refers to a selection of text elements, joined to the data

// Add labels for new nodes:
hypertext.enter().append("text") //add the text element
         //add any unchanging attributes of the <text>:
         .attr("text-anchor", "middle") 
    //add a link element within each text element
    .append("a") 
         //add unchanging attributes of the <a> link
         .attr("target", "_blank") //set the link to open in an unnamed tab
         .attr("xlink:show", "new"); 
          //show="new" is an XML way to tell the link to open in a new tab/window.
          //Either attribute *should* work on its own, but best use both to be safe.

// At this point, each new <text> element contains an <a> element, but the
// variable hypertext still refers to the text elements.

// Remove any outgoing/old text elements for non-existent nodes:
hypertext.exit().remove();

// Compute data-based attributes for entering and updating texts:
hypertext.attr("x", 8) //attributes of the <text> elements
      .attr("y", "0.3em")
  //Select the existing <a> element within each <text>
  .select("a")
      //Update the link attributes.  Note that the <a> element was never given
      //its own data, so it inherits the data from the parent <text> element.
      .attr("xlink:href", function (d) {
          return "http://example.com/" + d.name; //create url from data
      })
      //Set the text within the link, which in this case is the only text
      //within the text element.
      .text(function (d) {
          return d.name; //link text content
      });

Using the link element created all the necessary functionality, but it didn't add the default HTML link styles. For that, there's CSS:

text a {
  fill: navy;
}
text a:visited {
  fill:darkpurple;
}
text a:hover, text a:active {
  text-decoration: underline;
  fill:darkred;
}

OTHER TIPS

I think you should be able to do all of this in css. Thats part of the beauty of d3.js

Without seeing some code it'll be hard to say exactly what you need, but Ill bet you could do something like this for the underlining and something similar for the other effects you have.

Can you post some code, so we can help you further?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top