0

How would it be possible to place an icon in the middle of all edges of a force-directed graph?

This is how I imagine the graph to look like:

Illustration

I have read this post. However, this does not seem to work in my case or I have not understood the mechanics.

How could this icon be equipped with a click event listener? Would the icon element - when clicked - "know" which path it belongs to?

Many thanks.

I have annotated the 3 problematic code snippets with [?] HELP - THIS DOES NOT WORK in the code below. The code is massively shortened and will not run as is.

<!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> 
Community
  • 1
  • 1
pascal
  • 365
  • 1
  • 3
  • 16
  • I'm on the edge of my seat... I can't wait for the next exciting episode! Seriously, you need to ask a specific technical question and you need to demonstrate that you have made some effort. Post what you have tried so far and ask a question that demonstrates that you have actually bothered to engage your brain before asking for someone else to do your work for you. – Cool Blue Aug 24 '15 at 03:51
  • Ok, I am not sure I like the polemic response. You're right though. The reason I did not post the code was that my d3.js code is already >> 500 lines long and extracting the relevant bits is difficult without making mistakes. I will update the post though. – pascal Aug 24 '15 at 14:02
  • If you you go through the process of creating a minimal example of the feature you are trying to include then: a) that's exactly what I would have to do to help you and b) you'll probably figure it out for yourself. And even if you don't you can post that attempt and make a very nice question. Otherwise I'm afraid it's just another Oh lord won't you buy me a Mercedes Benz post... – Cool Blue Aug 24 '15 at 14:18
  • 1
    Your binding data to the icon before that data exists, `force.links()` returns an empty array at that point no? `var icon = svg.append("svg:g").selectAll("g").data(force.links()).enter().append("svg:g");` – Cool Blue Aug 24 '15 at 15:27

1 Answers1

1

As noted in my comment, you were creating a zero length selection because you were binding the data before it had been added to the force layout. Also, you need to select dynamically, not just once.

If you treat the icon selection the same as the others like this...

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

and then put something like this in your update function, after the new json is bound to the force layout...

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

And then in your tick function...

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

    });

  }

Then it would be close to being ok. I can't verify because you didn't provide a minimal working example.

Also, notice that the data bound to the icons (force.links()) is the same data that is bound to the paths so, if you put an event listener on it then you can use that information to help you find the link.

Cool Blue
  • 6,438
  • 6
  • 29
  • 68
  • Thank you @cool ! Based on your code I've managed to recreate a version of [this](http://bl.ocks.org/mbostock/4062045) where the icons are in the middle. Thank you. I have not managed do succeed with my own code yet (it is just too complex) - but I think I should be able to sort it out myself. – pascal Aug 24 '15 at 17:08
  • OK, good. D3 is HARD to understand sometimes - not because of d3 but because the functionality it delivers is COMPLICATED - so, I strongly advise doing what you did and following a feature - integration method. Now, do us all a favour and post a gist showing your adaptation and link a bl.ock of it to your question. If you don't know how I can advise... – Cool Blue Aug 24 '15 at 17:14
  • Will do. Thanks for your help. – pascal Aug 24 '15 at 23:51