1

I've seen this problem several times across multiple SO posts, while it feels wrong to post something resembling a duplicate, this error seems to present itself for lots of different reasons and as such every question is slightly different from the last. So here we go with my problem:

Here is my code;

window.onload = function () {

    var width = 750,
        height = 750,
        counter = 0;

    var force = d3.layout.force()
        .size([width, height]);

    d3.csv("Data/BubbleData.csv", function (error, links) {

        var X = d3.scale.linear()
            .domain([0, width])
            .range([0, width]);

        var Y = d3.scale.linear()
            .domain([0, height])
            .range([height, 0]);

        var zoom = d3.behavior.zoom()
            .x(X)
            .y(Y)
            .scaleExtent([1, 10])
            .on("zoom", zoomed);

        //handle zooming
        function zoomed() {
            circles.attr("transform", transform);
        }

        function transform(d) {
            return "translate(" + X(d[0]) + "," + Y(d[1]) + ")";
        };

        var svg = d3.select("#GraphDiv")
                    .append("svg")
                        .attr("width", width)
                        .attr("height", height)
                        .append("g")
                        .call(d3.behavior.zoom().x(X).y(Y).scaleExtent([1, 10]).on("zoom", zoom))
                        .on("dblclick.zoom", null);


        var rect = svg.append("rect")
            .attr("width", width)
            .attr("height", height)
            .style("fill", "none")
            .style("pointer-events", "all");

        var color = d3.scale.category10();
        var edges = [],
            nodesByNames = {};

        //create the nodes
        links.forEach(function (link) {
            link.App_No = nodeByName(link.App_No);
            link.Server_No = nodeByName(link.Server_No);

            edges.push({ source: link.App_No, target: link.Server_No })
        });

        var nodes = d3.values(nodesByNames);

        //create container for all to go in
        var container = svg.append("g")
            .attr("class","container");

        //create the links
        var link = container.append("g")
                    .attr("class","LineGroup")
            .selectAll(".link")
                .data(edges)
            .enter().append("line")
            .attr("class", "link");

        //define mouse behaviour for nodes
        var drag = force.drag()
            .on("dragstart", function (d) { d3.select(this).select("circle").classed("fixed", d.fixed = true); });

        //create the nodes circles
        var node = container.append("g")
                .attr("class","circleGroup")
            .selectAll(".node")
                .data(nodes)
            .enter().append("g")
                .attr("class", "node")
            .call(drag);

        var circles = node.append("circle")
            .attr("r", 10)
            .attr("class", "circle")
            .attr("fill", function (d) { return color(d.group); })
            .on("dblclick", function (d) { d3.select(this).classed("fixed", d.fixed = false); });

        var labels = node.append("text")
            .attr("x", 12)
            .attr("y", ".35em")
            .text(function (d) { return d.Name; });

        force
            .nodes(nodes)
            .links(edges)
            .linkDistance(50)
            .charge(-100)
            .size([width, height])
            .on("tick", tick)
            .start();

        //draws the lines
        function tick() {
            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 + ")"; });

        }

        function nodeByName(name) {
            var groupNo;
            switch (name.substring(0, 1)) {
                case "A":
                    groupNo = 1;
                    break;
                case "S":
                    groupNo = 2;
                    break;
                default:
                    groupNo = 0;
            }
            return nodesByNames[name] || (nodesByNames[name] = { Name: name, group: groupNo });
        }
    });
};

And my data:

App_Name,App_No,Server_Name,Server_No
App_01,A1,Server_01,S1
App_01,A1,Server_02,S2
App_01,A1,Server_03,S3
App_01,A1,Server_04,S4
App_02,A2,Server_03,S3
App_02,A2,Server_04,S4
App_02,A2,Server_05,S5
App_03,A3,Server_05,S5
App_03,A3,Server_06,S6
App_03,A3,Server_07,S7

I am attempting to combine these two examples (here and here) in order to have a drag behaviour on the nodes while being able to semantically zoom. I've had to disable the zoom on dblclick behaviour(see code), but otherwise the click, drag, fix and un-fix behaviour all works perfectly, however I am getting a "cannot read property "on" of undefined" error that occurs inside the d3 library itself (debugger in chrome shows line number it breaks on (line 1321)).

Is my code correct? Am I applying the zoom behaviour in the right place (on the svg var)? and am I applying the transformation to the right group(in this case the circles group?)

EDIT: The error happens on line 1321 of the d3.js library. it says that "g" is null. could I just have a wrong d3 library for some reason?

JabbaWook
  • 677
  • 1
  • 8
  • 25
  • Make a fiddle or show the data you are using so we can see the problem ourselves – thatOneGuy Mar 15 '16 at 12:03
  • in your transform function how come youre using d[0] and d[1], you want to translate them via d.x and d.y ? I would look at this example to merge the two https://bl.ocks.org/mbostock/6123708 – thatOneGuy Mar 15 '16 at 12:04
  • @thisOneGuy The data im using is a csv file? i can copy the data into the question, but im sure the data plays no part in this problem. I've edited the OP tho to include it. and I used d[0] etc cause I was just following what the first example was doing? – JabbaWook Mar 15 '16 at 12:11
  • I just wanted a working version so I can see where you are going wrong. Much easier to debug that way. – thatOneGuy Mar 15 '16 at 12:21
  • @thisOneGuy did you find anything? – JabbaWook Mar 16 '16 at 09:26

2 Answers2

2

I used CSV to JSON converter to convert your data so I could create a fiddle. So here goes :

With some help from this answer : semantic zooming of force directed graph in d3

And this JSFiddle : http://jsfiddle.net/cSn6w/6/

I have solved the problem. Basically you want to drag the nodes and keep them in place, which you have solved right? And you want to zoom in but keep the nodes the same size ? (semantic zoom).

So when you zoom, you need to change the size of the nodes but to get the correct size you need to get the scale factor and to get the positions you need to know the translate factor. So create to variables like so :

var scaleFactor = 1;
var translation = [0,0];

These will get overwritten.

Now when zooming you want to update these like so :

var zoom = d3.behavior.zoom()
                .scaleExtent([0.1,10])
        //allow 10 times zoom in or out
                .on("zoom", zoomed);

function zoomed() {
   // console.log("zoom", d3.event.translate, d3.event.scale);
    scaleFactor = d3.event.scale;
    translation = d3.event.translate;

    tick(); //update positions
}

The zoomed function updates the scale and translate variables we declared earlier. And you call the tick to update the positions of the nodes and links and size of the nodes. Updated tick function :

//draws the lines
function tick() {
  link.attr("x1", function(d) {
      return translation[0] + scaleFactor * d.source.x;
    })
    .attr("y1", function(d) {
      return translation[1] + scaleFactor * d.source.y;
    })
    .attr("x2", function(d) {
      return translation[0] + scaleFactor * d.target.x;
    })
    .attr("y2", function(d) {
      return translation[1] + scaleFactor * d.target.y;
    });

  node.attr("cx", function(d) {
      //console.log(translation[0] + scaleFactor*d.x)
      return translation[0] + scaleFactor * d.x;
    })
    .attr("cy", function(d) {
      return translation[1] + scaleFactor * d.y;
    });

  node.attr("transform", function(d) {
    return "translate(" + (translation[0] + scaleFactor * d.x) + "," + (translation[1] + scaleFactor * d.y) + ")";
  });
}

What you have to understand here is when call this function it uses both the translate and scale variables to give the nodes the correct positions.

Now this would work okay but the drag wouldn't as you haven't implemented the scale and translate there. I have changed the drag function around :

var drag = force.drag()
  .origin(function(d) {
    return d;
  })
  .on("dragstart", dragstarted)
  .on("drag", dragged)
  .on("dragend", dragended);



function dragstarted(d) {
  console.log('start')
  d3.event.sourceEvent.stopPropagation();
  d3.select(this).classed("dragging", true);
}

function dragged(d) {
  console.log('dragged')
  d3.select(this).attr("cx", d.x = d3.event.x + translation[0]).attr("cy", d.y = d3.event.y + translation[1]);
}

function dragended(d) {
  console.log('dragended')
  d.fixed = true;
  d3.select(this).classed("dragging", false);
}

Notice in the dragged function I use the scale and translate to get the correct positions. Also the line d3.event.sourceEvent.stopPropagation(); stops flickering, if you take this out you can see what I mean.

Here is the updated fiddle : https://jsfiddle.net/reko91/rw0o9vxh/2/

Does that solve your problem ?

PS

Yours wasn't working as there was no values for d[0] and d[1].

Community
  • 1
  • 1
thatOneGuy
  • 9,977
  • 7
  • 48
  • 90
  • Thank you so much for putting so much effort into this, i can see that your jsfiddle works (albeit with some strange behaviour when you move a node - all other nodes shoot of the screen) but for some reason when i implement the changes you have stated in your answer, it says that it cannot access the translation array in the tick function - giving the error "cannot read property 0 of undefined". Thanks again for taking so much time to help me out though – JabbaWook Mar 16 '16 at 13:51
  • I've just put in some console.logs and in my program `translation[][]` is undefined in the `zoomed` function? i feel like I'm missing something obvious here. – JabbaWook Mar 16 '16 at 14:07
  • Have you initialized it at the start ? var translation = [0,0]; ? And i know sorry only noticed those movements after i posted. Ill try fix when I have time but havent really at the moment – thatOneGuy Mar 16 '16 at 14:10
  • i have, i think it might be something to do with the fact that most of my code is wrapped in the `d3.csv` function, and you don't have to do that if you use `JSon` files. I have tried declaring the variables both in and outside the `d3.csv` function though and it hasn't made a difference. – JabbaWook Mar 16 '16 at 14:43
  • have you changed the variable for the data? You had links and I changed it to 'thisData' ? – thatOneGuy Mar 16 '16 at 14:54
  • no, your version works fine, but i'm using my csv file. <- using d3.csv(file,accessor)? – JabbaWook Mar 16 '16 at 15:03
  • interestingly in my console.logs scale is defined but translation is not :O whats going on! – JabbaWook Mar 16 '16 at 15:04
  • that's weird. Have you got more than one variable called tranlsation ? Perhaps rename and see what happens – thatOneGuy Mar 16 '16 at 15:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/106483/discussion-between-thisoneguy-and-jabbawook). – thatOneGuy Mar 16 '16 at 15:10
  • I knew i was missing something obvious! i had to call `d3.event.translate` - not `d3.event.translation`! Works great now, just have to figure out that bug with the nodes flying off the screen when i move one. Thank you so much for your help. – JabbaWook Mar 16 '16 at 15:11
  • so it was a typo on your side ? Haha typical :P When I have time ill try solve that problem. The problem is basically youre trying to pan the container you adjust the translate and when you drag the nodes you adjust the translate and so on – thatOneGuy Mar 16 '16 at 15:13
  • haha I know! sorry that was my bad! should have just copied and pasted your changed code, but i wanted to make the changes myself so i could understand what was going on. awesome thank you, i'll look into myself obviously :) – JabbaWook Mar 16 '16 at 15:17
  • 1
    if you just take out the `+translation[]` in the dragged function it works great :) – JabbaWook Mar 16 '16 at 15:22
-1

What about this line :

 .call(
     d3.behavior.zoom()
          .x(X)
          .y(Y)
          .scaleExtent([1, 10])
          .on("zoom", zoom)
  )
 .on("dblclick.zoom", null);

You may have wanted to place the dbliclick.zoom in the parenthesis.

Otherwise since the loading of your csv is performed by using a callback, it may be possible that your file error get swallowed.

If it's not the code i link use intermediary variables and debogguer tool to check if you have an undefined returned somewhere. Builder pattern is nice for syntax but not for debugging.

Walfrat
  • 5,363
  • 1
  • 16
  • 35
  • strangely when i put the dblclick inside with the zoom behaviour i get the same error but on line 492 of d3.js. This link here says that both methods are correct - http://stackoverflow.com/questions/11786023/how-to-disable-double-click-zoom-for-d3-behavior-zoom – JabbaWook Mar 15 '16 at 12:32
  • And if i comment it out completely then the original error comes back up. so I'm not sure it is caused by that – JabbaWook Mar 15 '16 at 12:33
  • what about change .on("dblclick.zoom", null); by .on("dblclick.zoom", function(){}); meaning binding a nooop function instead of null ? – Walfrat Mar 15 '16 at 15:14
  • nope that didnt make any different I'm afraid. I dont think it can be this line seeing as i get the same error even if i get rid of the dblclick line. – JabbaWook Mar 15 '16 at 16:21
  • then i guess you should go for the old debugging version : put a console.log at the start and the end of every function and callback and see what is happening – Walfrat Mar 16 '16 at 08:05