D3 force directed graph, different shape according to data and value given?
Asked Answered
T

4

13

I've made a force directed graph and I wanted to change shape of nodes for data which contains "entity":"company" so they would have rectangle shape, and other one without this part of data would be circles as they are now.

You can see my working example with only circle nodes here: http://jsfiddle.net/dzorz/uWtSk/

I've tried to add rectangles with if else statement in part of code where I append shape to node like this:

function(d)
    {
        if (d.entity == "company")
        {
            node.append("rect")
                .attr("class", function(d){ return "node type"+d.type})
                .attr("width", 100)
                .attr("height", 50)
                .call(force.drag);
        }
        else
        {
        node.append("circle")
            .attr("class", function(d){ return "node type"+d.type})
            .attr("r", function(d) { return radius(d.value) || 10 })
            //.style("fill", function(d) { return fill(d.type); })
            .call(force.drag);
        }
    }

But then I did not get any shape at all on any node.

What Is a proper way to set up this?

The whole code looks like this: script:

var data = {"nodes":[
                        {"name":"Action 4", "type":5, "slug": "", "value":265000},
                        {"name":"Action 5", "type":6, "slug": "", "value":23000},
                        {"name":"Action 3", "type":4, "slug": "", "value":115000},
                        {"name":"Yahoo", "type":1, "slug": "www.yahoo.com", "entity":"company"},
                        {"name":"Google", "type":1, "slug": "www.google.com", "entity":"company"},
                        {"name":"Action 1", "type":2, "slug": "",},
                        {"name":"Action 2", "type":3, "slug": "",},
                        {"name":"Bing", "type":1, "slug": "www.bing.com", "entity":"company"},
                        {"name":"Yandex", "type":1, "slug": "www.yandex.com)", "entity":"company"}
                    ], 
            "links":[
                        {"source":0,"target":3,"value":10},
                        {"source":4,"target":3,"value":1},
                        {"source":1,"target":7,"value":10},
                        {"source":2,"target":4,"value":10},
                        {"source":4,"target":7,"value":1},
                        {"source":4,"target":5,"value":10},
                        {"source":4,"target":6,"value":10},
                        {"source":8,"target":4,"value":1}
                        ]
               }    



    var w = 560,
        h = 500,
        radius = d3.scale.log().domain([0, 312000]).range(["10", "50"]);

    var vis = d3.select("body").append("svg:svg")
        .attr("width", w)
        .attr("height", h);

        vis.append("defs").append("marker")
        .attr("id", "arrowhead")
        .attr("refX", 17 + 3) /*must be smarter way to calculate shift*/
        .attr("refY", 2)
        .attr("markerWidth", 6)
        .attr("markerHeight", 4)
        .attr("orient", "auto")
        .append("path")
            .attr("d", "M 0,0 V 4 L6,2 Z"); //this is actual shape for arrowhead

    //d3.json(data, function(json) {
        var force = self.force = d3.layout.force()
            .nodes(data.nodes)
            .links(data.links)
            .distance(100)
            .charge(-1000)
            .size([w, h])
            .start();



        var link = vis.selectAll("line.link")
            .data(data.links)
            .enter().append("svg:line")
            .attr("class", function (d) { return "link" + d.value +""; })
            .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; })
            .attr("marker-end", function(d) {
                                                if (d.value == 1) {return "url(#arrowhead)"}
                                                else    { return " " }
                                            ;});


        function openLink() {
        return function(d) {
            var url = "";
            if(d.slug != "") {
                url = d.slug
            } //else if(d.type == 2) {
                //url = "clients/" + d.slug
            //} else if(d.type == 3) {
                //url = "agencies/" + d.slug
            //}
            window.open("//"+url)
        }
    }




        var node = vis.selectAll("g.node")
            .data(data.nodes)
          .enter().append("svg:g")
            .attr("class", "node")
            .call(force.drag);

        node.append("circle")
          .attr("class", function(d){ return "node type"+d.type})
            .attr("r", function(d) { return radius(d.value) || 10 })
          //.style("fill", function(d) { return fill(d.type); })
          .call(force.drag);

        node.append("svg:image")
            .attr("class", "circle")
            .attr("xlink:href", function(d){ return d.img_href})
            .attr("x", "-16px")
            .attr("y", "-16px")
            .attr("width", "32px")
            .attr("height", "32px")
            .on("click", openLink());

        node.append("svg:text")
            .attr("class", "nodetext")
            .attr("dx", 0)
            .attr("dy", ".35em")
            .attr("text-anchor", "middle")
            .text(function(d) { return d.name });

        force.on("tick", function() {
          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; });

          node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        });
    //});    

css:

.link10 { stroke: #ccc; stroke-width: 3px; stroke-dasharray: 3, 3; }
.link1 { stroke: #000; stroke-width: 3px;}
.nodetext { pointer-events: none; font: 10px sans-serif; }

.node.type1 {
  fill:brown;
}
.node.type2 {
  fill:#337147;
}
.node.type3 {
  fill:blue;
}
.node.type4 {
  fill:red;
}

.node.type5 {
    fill:#1BC9E0;
}

.node.type6 {
    fill:#E01B98;
}

image.circle {
    cursor:pointer;
}

You can edit my jsfiddle linked on beginning of post...

Tribadism answered 29/8, 2013 at 11:31 Comment(7)
You don't appear to be using the code to change shapes in your jsfiddle. Could you post one that demonstrates the problem please?Amundson
I've posted the problematic code with if else statement above (see first block of code)... In jsfiddle I've deleted this statement so in it you can see only circle shapesTribadism
It really does matter where you put that code and how you call it.Amundson
So to clear the misunderstanding, I replaced this: node.append("circle") .attr("class", function(d){ return "node type"+d.type}) .attr("r", function(d) { return radius(d.value) || 10 }) //.style("fill", function(d) { return fill(d.type); }) .call(force.drag); in code with that first block which you can see in the beggining of postTribadism
The code you've posted above defines a function. How are you calling that function?Amundson
sorry, but I'm nooby... I did not called function anyhow. Its the problem that I don't know how to correctly sort this outTribadism
Do you want something like in this question?Amundson
M
11

Solution here: http://jsfiddle.net/Bull/4btFx/1/

I got this to work by adding a class to each node, then using "selectAll" for each class to add the shapes. In the code below, I'm adding a class "node" and a class returned by my JSON (d.type) which is either "rect" or "ellipse".

  var node = container.append("g")
  .attr("class", "nodes")
  .selectAll(".node")
  .data(graph.nodes)
  .enter().append("g")
  .attr("class", function(d) {
     return d.type + " node";
  })
  .call(drag);

Then you can add the shape for all elements of each class:

  d3.selectAll(".rect").append("rect")
  .attr("width", window.nodeWidth)
  .attr("height", window.nodeHeight)
  .attr("class", function(d) { 
     return "color_" + d.class 
  });

  d3.selectAll(".ellipse").append("rect")
  .attr("rx", window.nodeWidth*0.5)
  .attr("ry", window.nodeHeight*0.5)
  .attr("width", window.nodeWidth)
  .attr("height", window.nodeHeight)
  .attr("class", function(d) { 
     return "color_" + d.class 
  });

In the above example, I used rectangles with radius to draw the ellipses since it centers them the same way as the rectangles. But it works with other shapes too. In the jsfiddle I linked, the centering is off, but the shapes are right.

Mnemonic answered 16/7, 2014 at 23:47 Comment(0)
S
4

I implemented this behavior using the filter method that I gleaned from Filtering in d3.js on bl.ocks.org.

initGraphNodeShapes() {
  let t = this;

  let graphNodeCircles =
    t.graphNodesEnter
      .filter(d => d.shape === "circle")
      .append("circle")
      .attr("r", 15)
      .attr("fill", "green");

  let graphNodeRects =
    t.graphNodesEnter
      .filter(d => d.shape === "rect")
      .append("rect")
      .attr("width", 20)
      .attr("height", 10)
      .attr("x", -10) // -1/2 * width
      .attr("y", -5) // -1/2 * height
      .attr("fill", "blue");

  return graphNodeCircles.merge(graphNodeRects);
}

I have this inside of initGraphNodeShapes call because my code is relatively large and refactored. The t.graphNodesEnter is a reference to the data selection after the data join enter() call elsewhere. Ping me if you need more context. Also, I use the d => ... version because I'm using ES6 which enables lambdas. If you're using pre-ES6, then you'll have to change it to the function(d)... form.

Scarlettscarp answered 11/11, 2016 at 19:56 Comment(0)
M
2

This is an older post, but I had the same trouble trying to get this concept working with D3 v5 in July of 2020. Here is my solution in case anyone else is trying to build a force-directed graph, I used both circle and rectangle elements to represent different types of nodes:

The approach was to create the elements, and then position them separately when invoking the force simulation (since a circle takes cx, cy, and r attributes, and the rect takes x, y, width and height). Much of this code follows the example in this blog post on medium: https://medium.com/ninjaconcept/interactive-dynamic-force-directed-graphs-with-d3-da720c6d7811

FYI I've declared 'svg' previously as the d3.select("some div with id or class"), along with a few helper functions not shown that read the data (setNodeSize, setNodeColor). I've used the D3.filter method to check for boolean field in the data - is the node initial or no?

Force simulation instance:

const simulation = d3.forceSimulation()
//the higher the strength (if negative), greater distance between nodes.
.force('charge', d3.forceManyBody().strength(-120)) 
//places the chart in the middle of the content area...if not it's top-left
.force('center', d3.forceCenter(width / 2, height / 2))

Create the circle nodes:

const nodeCircles = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.filter(d => d.initial)
.append('circle')
.attr('r', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)

Then create the rectangle nodes:

const nodeRectangles = svg.append('g')
.selectAll('rect')
.data(nodes)
.enter()
.filter(d => !d.initial)
.append('rect')
.attr('width', setNodeSize)
.attr('height', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)

And then when invoking the simulation:

simulation.nodes(nodes).on("tick", () => {
nodeCircles
    .attr("cx", node => node.x)
    .attr("cy", node => node.y)
nodeRectangles
    .attr('x', node => node.x)
    .attr('y', node => node.y)
    .attr('transform', 'translate(-10, -7)')

Of course there's more to it to add the lines/links, text-labels etc. Feel free to ping me for more code. The medium post listed above is very helpful!

Mide answered 14/7, 2020 at 15:21 Comment(0)
F
1

I am one step ahead of you :)

I resolved your problem with using "path" instead of "circle" or "rect", you can look my solution and maybe help me to fix problem which I have...

D3 force-directed graph: update node position

Fermin answered 29/8, 2013 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.