强制导向图:如何在边缘中间添加图标?

时间:2015-08-21 14:59:49

标签: d3.js

如何在force-directed graph的所有边缘中间放置一个图标?

这就是我想象的图形:

Illustration

我看过this post。但是,这似乎不适用于我的情况,或者我还没有理解这些机制。

这个图标如何配备点击事件监听器?图标元素 - 点击时 - "知道"它属于哪条路径?

非常感谢。

我用[?] HELP注释了3个有问题的代码片段 - 这在以下代码中不起作用。代码大大缩短,不会按原样运行。



<!DOCTYPE html>

<meta charset="utf-8">

<!--

Structure:

    - #illustration_content (explicitly defined as div)
        - svg
            - g             (The g element is a container used to group objects.)
                - path      (for links)
                - text      (for labels)
                - circle    (for nodes)
                - marker    (for arrow heads)
                - icon      (for icons)

-->

<head>

<!-- EDITED OUT-->
    
</head>

<body>
    
<!-- EDITED OUT-->
    
    <script>

        // Declare the object that will contain the nodes later on
        var nodes = {};
        

        // <-- EDITED OUT-->


        // Set the size of the illustration (used multiple times --> hence defined here)
        var width = 800, // make sure this fits to the width of the illustration div
            height = 600;

        // <-- EDITED OUT-->

        var linkDistanceVariable = 50; // we will update this with increasing number of nodes

         // Create the force layout for d3
        var force = d3.layout.force()
            .size([width, height])
            .linkDistance(function() { return linkDistanceVariable; })
            .friction(0.7) //at each tick of the simulation, the particle velocity is scaled by the specified friction.
            .charge(-600) //A negative value results in node repulsion, while a positive value results in node attraction.
            .alpha(0.2) // cooling parameter:  If you do not stop the layout explicitly, it will still stop automatically after the layout's alpha decays below some threshold.
            .on("tick", tick); // this calls the tick function (which is in the separate JS) - the function is executed for each step of the animation

        // Append the d3 illustration to the illustration div container
        var svg = d3.select("#illustration_content").append("svg")
            .attr("width", width)
            .attr("height", height)
            .style("border", "1px solid black");

        // Append stuff
        var path = svg.append("g").selectAll("path"),
            text = svg.append("g").selectAll("text"),
            circle = svg.append("g").selectAll("circle"),
            marker = svg.append("defs").selectAll("marker");
        
        // [?] HELP - THIS DOES NOT WORK
        var icon = svg.append("svg:g").selectAll("g").data(force.links()).enter().append("svg:g");

        // Update function to update the visualisation with new data 
        function update(json){
            
            // <-- EDITED OUT (~100 LOC)-->

            // The idea here is to prevent the illustration from pulsing (i.e. updating when nothing has changed)
            // This introduces unnecessary movements
            // This will only update the screen if the new link array length differs from the previous one
            // This should be improved (also other things might change like values of properties)
            // Essentially, currently only a change in number of links will trigger an update (not a change in confidence etc.)
            if (links_previously.length !== links.length){

                console.info("Is the links array length different than previous one? -->" + (links_previously !== links));

                // Stop the layout
                force.stop();

                // Set the new linkDistance val
                linkDistanceVariable = 40 + 3 * Object.keys(nodes).length; // new

                // Start the layout again
                force.start();

                force
                    .nodes(d3.values(nodes))
                    .links(links)
                    .start();
            }

            // Compute the data join. This returns the update selection.
            marker = marker.data(["suit", "licensing", "resolved"]);

            // Remove any outgoing/old markers.
            marker.exit().remove();

            // Compute new attributes for entering and updating markers.
            marker.enter().append("marker")
                .attr("id", function(d) { return d; })
                .attr("viewBox", "0 -5 10 10")
                .attr("refX", 20) //this determines position of arrow head along path (x-axis)
                .attr("refY", 0) // this determines y-position of arrow head on path (0 = centered)
                .attr("markerWidth", 6) // width of arrow head?
                .attr("markerHeight", 6) // width of arrow head?
                .attr("orient", "auto")
                .append("path") // use ".append("line") for lines instead of arrows
                .attr("d", "M0,-5L10,0L0,5");


            // -------------------------------

            // Compute the data join. This returns the update selection.
            path = path.data(force.links());

            // Remove any outgoing/old paths.
            path.exit().remove();

            // Compute new attributes for entering and updating paths.
            path.enter().append("path")
                .attr("class", function(d) { return "link " + d.type; })
                .attr("marker-end", function(d) { return "url(#" + d.type + ")"; })
                .on("dblclick", pathMouseDoubleClick) // allow path to be clicked;

            // -------------------------------
            
            // [?] HELP - THIS DOES NOT WORK
            // Idea: https://stackoverflow.com/questions/14567809/how-to-add-an-image-to-an-svg-container-using-d3-js
            icon.append("image").attr("xlink:href","imgs/icon.png")
                .attr("x", -20)
                .attr("y", -2)
                .attr("width", 20).attr("height", 20)
                .attr("class", "type-icon");
            
            // -------------------------------

            // Compute the data join. This returns the update selection.
            circle = circle.data(force.nodes());

            // Add any incoming circles.
            circle.enter().append("circle");

            // Remove any outgoing/old circles.
            circle.exit().remove();

            // Compute new attributes for entering and updating circles.
            circle
                .attr("r", 10) // size of the circles
                .on("dblclick", nodeMouseDoubleClick) // allow nodes to be clicked
                .call(force.drag);

            // -------------------------------

            // Compute the data join. This returns the update selection.
            text = text.data(force.nodes());

            // Add any incoming texts.
            text.enter().append("text");

            // Remove any outgoing/old texts.
            text.exit().remove();

            // Compute new attributes for entering and updating texts.
            text
                .attr("x", 11) // Distance of the text from the nodes (x-axis)
                .attr("y", "0.5em") // Distance of the text from the nodes (y-axis)
                .text(function(d) { return d.name; });
        
            // <-- EDITED OUT-->

        } // end update function

        // ------------------------------------- loadNewData() --------------------------------------
        
        function loadNewData(){

            d3.json("data/dataFromPython.json", function(error, json){

                // <-- EDITED OUT-->

                update(json);
                
                // <-- EDITED OUT-->

            });


        };
        
        
        // ------------------------------ search functionality -----------------------------------
        
        // <-- EDITED OUT-->

        // -------------------------------------- Hide or show node labels ------------------------------
        
        // <-- EDITED OUT-->
        
        // ------------------------------------- Interval timer ----------------------------------------
        
        // Regularly update the data every x milliseconds (normal JS)
        setInterval(function () {

            loadNewData();
            console.info("Interval timer has just called loadNewData()");

        }, 3000);
        
        // ------------------------------------- Tick function ---------------------------------------- 
        
        // The tick function is executed for each tiny step of the animation
        // Use elliptical arc path segments to doubly-encode directionality
        function tick(){

            path.attr("d", linkArc);

            circle.attr("transform", transform);
            text.attr("transform", transform);

            // For icon in the middle of the path
            // does not work, taken from here: https://stackoverflow.com/questions/14582812/display-an-svg-image-at-the-middle-of-an-svg-path
            // [?] HELP - THIS DOES NOT WORK
            icon.attr("transform", function(d) {

                return "translate(" +((d.target.x+d.source.x)/2) + "," + ((d.target.y+d.source.y))/2 + ")";

            });

        }

        // ------------------------------------- Link arc and transform function ----------------------------------------
        
        function linkArc(d){

          var dx = d.target.x - d.source.x,
              dy = d.target.y - d.source.y,
              dr = (d.straight == 0)?Math.sqrt(dx * dx + dy * dy):0;
          return "M" + d.source.x + "," + d.source.y +
              "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
        }

        function transform(d){

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

    </script>   
        
</body> 
&#13;
&#13;
&#13;

1 个答案:

答案 0 :(得分:1)

正如我的评论中所述,您创建了一个零长度选择,因为您在将数据添加到强制布局之前绑定了数据。此外,您需要动态选择,而不只是一次。

如果您将图标选择与其他图标选择相同......

  // Append stuff
  var path = svg.append("g"),
    text = svg.append("g"),
    circle = svg.append("g"),
    marker = svg.append("defs"),
    icon = svg.append("svg:g");

然后在你的更新函数中添加这样的东西,在新的json绑定到强制布局之后......

function update(json) {
// ...

  force
    .nodes(d3.values(nodes))
    .links(links)
    .start();

// ...

  // Compute the data join. This returns the update selection.
  var markers = marker.selectAll("marker").data(["suit", "licensing", "resolved"]);
// ...

  // Compute the data join. This returns the update selection.
  var paths = path.selectAll("path").data(force.links());

// ...

  var icons =  icon.selectAll("g").data(force.links());
  icons.enter().append("svg:g")
    .append("image").attr("xlink:href", "imgs/icon.png")
    .attr("x", -20)
    .attr("y", -2)
    .attr("width", 20).attr("height", 20)
    .attr("class", "type-icon");
  icons.exit().remove();

// ...

// -------------------------------

  // Compute the data join. This returns the update selection.
  var circles = circle.selectAll("circle").data(force.nodes());
  // Add any incoming circles.
  circles.enter().append("circle")
  // Compute new attributes for entering circles.
    .attr("r", 10) // size of the circles
    .on("dblclick", nodeMouseDoubleClick) // allow nodes to be clicked
    .call(force.drag);

  // Remove any outgoing/old circles.
  circles.exit().remove();


// -------------------------------

  var texts = text.selectAll("text").data(force.nodes());

然后在你的滴答功能......

  function tick() {

    paths.attr("d", linkArc);

    circles.attr("transform", transform);
    texts.attr("transform", transform);

    // For icon in the middle of the path
    // does not work, taken from here: http://stackoverflow.com/questions/14582812/display-an-svg-image-at-the-middle-of-an-svg-path
    // [?] HELP - THIS DOES NOT WORK
    icons.attr("transform", function(d) {

      return "translate(" + ((d.target.x + d.source.x) / 2) + "," + ((d.target.y + d.source.y)) / 2 + ")";

    });

  }

然后就好了。 我无法验证,因为您没有提供最小的工作示例。

另外,请注意绑定到图标的数据(force.links())与绑定到路径的数据相同,因此,如果您在其上放置一个事件侦听器,那么您可以使用该信息来帮助您查找链接。