如何使 D3 中的标签和节点强制布局可点击以导航到 URL?

Posted

技术标签:

【中文标题】如何使 D3 中的标签和节点强制布局可点击以导航到 URL?【英文标题】:How to make labels and nodes in D3 forced layout clickable to navigate to a URL? 【发布时间】:2013-12-13 20:05:15 【问题描述】:

我正在使用 D3 使用基于力的布局,我想知道当我单击节点或标签时,是否有可能自动转到存储在该节点/标签中的 url?如果是,我怎样才能做到这一点?

这是图表的实际外观(标签表示 URL 本身)

我正在分享我用来生成 D3 图的 javascript 代码。在代码中,我目前使用的虚拟数据实际上将被 URL 替换。

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

function drawVisual()

    //document.getElementById('myMSG').innerhtml = "last name"+localStorage.lastname;
    /*var getArr = [];
    getArr = JSON.parse(localStorage.getItem('storeArray'));
    document.getElementById('myMSG').innerHTML = getArr[1].parentURL;*/


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

            var QueuedORG = [];
            //QueuedORG = JSON.parse(localStorage.getItem('storeArray'));
            QueuedORG.push(url: "Root", parentURL: "Root", used:0);
            QueuedORG.push(url: "a", parentURL: "Root", used:0);
            QueuedORG.push(url: "b", parentURL: "Root", used:0);
            QueuedORG.push(url: "c", parentURL: "Root", used:0);
            QueuedORG.push(url: "d", parentURL: "Root", used:0);
            QueuedORG.push(url: "e", parentURL: "a", used:0);
            QueuedORG.push(url: "f", parentURL: "a", used:0);
            QueuedORG.push(url: "g", parentURL: "a", used:0);
            QueuedORG.push(url: "h", parentURL: "a", used:0);
            QueuedORG.push(url: "p", parentURL: "b", used:0);
            QueuedORG.push(url: "q", parentURL: "b", used:0);
            QueuedORG.push(url: "r", parentURL: "b", used:0);
            QueuedORG.push(url: "x", parentURL: "c", used:0);
            QueuedORG.push(url: "y", parentURL: "x", used:0);
            QueuedORG.push(url: "y", parentURL: "c", used:0);
            QueuedORG.push(url: "x", parentURL: "a");
            QueuedORG.push(url: "y", parentURL: "b");


            var nodes = [];

            var labelAnchors = [];
            var labelAnchorLinks = [];
            var links = [];

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

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

    );

【问题讨论】:

【参考方案1】:

一般来说,您可以使用 D3.js 为 SVG 元素添加点击事件

.on('click', function(d, i) 
  window.location.href = d.url;
)

d 是数据对象,i 在集合中 d 的索引中。

只需将该点击处理程序添加到您的文本节点以及您的节点(路径)节点,就像这个小提琴http://jsfiddle.net/jNyrf/

【讨论】:

d.url 不起作用。有效的是 d.node.label,但感谢您引导我朝着正确的方向前进 :)【参考方案2】:

这里有两个选择。

您可以使用.on("click", ...) 处理程序将当前页面设置为目标。 您可以将a 元素与.attr("xlink:href", url) 一起使用,其中包含充当超链接的元素,以更传统的方式设置链接。

this question/answer 中的更多信息,尽管我相信您不必显式导入 xlink 命名空间,至少在最新版本的 D3 中不需要。

【讨论】:

您能否详细说明如何使用 .attr("xlink:href",url) 属性创建“a”元素?我应该把这行代码放在哪里? 我链接的问题有一个完整的例子。

以上是关于如何使 D3 中的标签和节点强制布局可点击以导航到 URL?的主要内容,如果未能解决你的问题,请参考以下文章

向 Hobbelt 的“Group/Bundle Nodes”D3 强制布局示例中的节点添加标签?

避免 D3 强制布局中节点和边之间的碰撞

如何使 D3.js 中的强制布局图响应屏幕/浏览器大小

d3强制定向布局中的神秘力量?

D3 强制布局应在单击节点时添加节点和链接

如何防止 d3.js 强制布局在恢复/重启时脉动/弹跳