2

I was looking some great d3.js code examples when I saw something like:

var links = [
  {source: "test1", target: "test2"},
  {source: "test1", target: "test3"},
  {source: "test2", target: "test3"},
  {source: "test3", target: "test4"}
];

var nodes = {};

// Compute the distinct nodes from the links.
links.forEach(function(link) {
  link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
  link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
});

I didn't get immediately how the var nodes was being declared.

My first guess was to translate it into:

links.forEach(function(link) {
  if(link.source != nodes[link.source]){
    nodes[link.source] = {name: link.source};
  }
  if(link.target != nodes[link.target]){
    nodes[link.target] = {name: link.target};
  }
});

But the links are not drawn anymore.

What is the difference between the two methods?

What is the point of the initial syntax, is that just a shortcut or do it increase performances?

Is there a best practice to follow in such cases ?

Edit

So if I try to fully understand

link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
  1. As nodes[link.source] is an object link.source takes its reference. This is always happening.

  2. OR condition, I'm not sure to get that part.

    I guess if nodes[link.source] is defined link.source = nodes[link.source] return true we don't need go further.

    If it's not defined and return false, the OR clause force to go further...

  3. nodes[link.source] gets a value so thanks to the reference link.source is pointing to the same value.

    I guess at this stage link.source is not yet containing a reference to nodes[link.source], but its initial value. It will contain the new reference after the comma.

Am I wrong somewhere ? Point 2 seems strange to me.

3 Answers 3

2

The code used in the example is simply a syntactial shortcut. It is equivalent to

links.forEach(function(link) {
  if(nodes[link.source]) { // we already know about the source of this link
    link.source = nodes[link.source];
  } else { // we haven't seen this source before, create a new node for it
    nodes[link.source] = {name: link.source};
    link.source = nodes[link.source];
  }

  if(nodes[link.target]) { // we already know about the target of this link
    link.target = nodes[link.target];
  } else { // we haven't seen this target before, create a new node for it
    nodes[link.target] = {name: link.target};
    link.target = nodes[link.target];
  }
});

All of this is necessary because the nodes are only declared implicitly through the links -- that is, there's no list of nodes, it is only assembled by traversing the links and "harvesting" the sources and targets. This is what's stored in nodes -- it's a mapping from the ID of the node (from the links) to the object representing the node.

The code above simultaneously populates this map by creating new objects for unseen nodes (i.e. ones which are not present in the mapping) and inserting the respective mapping. The sources and targets of the links are then updated to point to those objects (which will later be operated on by the force layout to set their positions) instead of the ID of the node as referenced in the original data.

Edit: Your edited interpretations are correct. This is basically what's happening.

Sign up to request clarification or add additional context in comments.

Comments

0

my understanding to

link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});

is, if nodes[link.source] doesn't exist

then just do link.source = nodes[link.source] = {name: link.source};

Comments

0
link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});

Can be translated to:

if(!nodes[link.source]) {
    nodes[link.source] = {name: link.source};
}
link.source = nodes[link.source];

(Same thing with link.target).

So:

var links = [
  {source: "test1", target: "test2"},
  {source: "test1", target: "test3"},
  {source: "test2", target: "test3"},
  {source: "test3", target: "test4"}
];

would be transformed to:

links = [
  {source: { name: "test1" }, target: { name: "test2" }},
  {source: { name: "test1" }, target: { name: "test3" }},
  {source: { name: "test2" }, target: { name: "test3" }},
  {source: { name: "test3" }, target: { name: "test4" }}
];

and nodes will be equal to:

nodes = {
    "test1": { name: "test1" },
    "test2": { name: "test2" },
    "test3": { name: "test3" },
    "test4": { name: "test4" },
}

6 Comments

The outer check if(link.source != nodes[link.source]) { isn't actually necessary in this case (and not present in the original code).
It's implicit (link.source should be equal to nodes[link.source] in the end) and will prevent from setting link.source = nodes[link.source]; more than one time for duplicate values.
It's a map -- not sure how you would get duplicate values in there? And no, it's not implicit. The original code would make multiple assignments if there were duplicate values.
I'm talking about links where source is equal to test1 multiple times which will result in link.source = nodes[link.source]; called multiple times if I remove the outer check. The original was a shortcut code so it's obvious that expanding it can be in many different ways and may result in different code if optimization is needed (the result must stay exactly the same of course).
Ah, but link is inside a loop, so will point to a different object every time. This block of code won't be executed several times with link being the same object.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.