Saturday, December 13, 2014

Getting your head around d3.js [part2]

Continued from part1.

On observing d3 code, and on observing the output of the program, you'll notice the most confusing parts are that of enter(), data(), datum() and exit(). It just doesn't seem to fit into the programming syntax you're familiar with from other languages. Not to worry, it's just a matter of understanding the purpose of the syntax, and you'll be flying along.

If you wanted to draw a few circles with JavaScript, you'd probably do something like this:
<html>
<body>
<script>
var svgHolder = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgHolder.setAttribute("height",500);
svgHolder.setAttribute("width",700);
document.body.appendChild(svgHolder);

for(var i = 0; i < 10; i++)
{

var circles = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circles.setAttribute("id", "c"+i)
circles.setAttribute("cx", (i+1)*50)
circles.setAttribute("cy", 100)
circles.setAttribute("r", (i+1)*10)
circles.setAttribute("stroke", "black")
circles.setAttribute("stroke-width", "3")
circles.setAttribute("fill", "red");
svgHolder.appendChild(circles);
}
</script>
</body>
</html>


To do the same thing in d3:

<html>
<head>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>
var cir = ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"];
var svgHolder = d3.select("body") //select the body tag
                .append("svg") //add an svg tag to body
                    .attr("width", 700) //specify the svg tag properties
                    .attr("height", 500);

svgHolder.selectAll("circle").data(cir).enter()
         .append("circle") //add an svg circle
                .attr("id", function(d) {return d;})
                .attr("cx", function(d,i) {return (i+1)*50;})
                .attr("cy", 110)
                .attr("r", function(d,i) {return (i+1)*10;})
                .attr("stroke", "black")
                .attr("stroke-width", "3")
                .attr("fill", "red");

</script>
</body>
</html>
 


I know what you're thinking:
"Hey, where did the for loop go? What does data() and enter() do? How did you manage to select all circles when none of them were yet created? What is d and i in those functions?"
We are just touching on the tip of the iceberg of the beautifully designed d3 syntax.


SelectAll
The cir array, basically acts as our specification of the loop limit. When you selectAll circle elements in the DOM (if they don't exist yet, d3 will create placeholders for them), you're also feeding each circle with data from the cir array using the data command.

What happens is, that for every circle that is created, d3 will assign the array values from cir to the placeholders one by one. So placeholder 1 will get the value "c1", placeholder 2 gets the value "c2" etc.


Enter and Data
The enter command tells d3 to take whatever commands come after enter, and attach them one by one to the placeholders. So the svg circles that get created with append("circle") will get attached to the placeholders that were created with selectAll("circle"). The number of placeholders to create is determined by the number of array elements in cir.
If you're dealing with only one element, you can use datum() instead of data().

It's that simple.


Anonymous functions and parameters d, i
Now, while specifying attributes for the circle with attr, you'd want to know something about which element of the array you're dealing with, right?
d3 allows you to use anonymous functions which accept zero to three parameters. If you specify the parameter as function(d), the value of d will be the array value of that particular placeholder.
Eg: For the first circle, d will be "c1" , for the second circle, d will be "c2" etc.

The second parameter is i as in function(d, i), which tells you whether the circle is the first one or the second one or third one etc. Similar to the i we use in for loops.

There's also a third parameter that you can get, which gives you data from a parent node, but let's not get into that for now.


Remove
In case you want to remove some of the circles, see how this works:
<html>
<head>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>

var cir = ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"];
var svgHolder = d3.select("body") //select the body tag
                .append("svg") //add an svg tag to body
                    .attr("width", 700) //specify the svg tag properties
                    .attr("height", 500);

svgHolder.selectAll("circle").data(cir).enter()
         .append("circle") //add an svg circle
                .attr("id", function(d) {return d;})
                .attr("cx", function(d,i) {return (i+1)*50;})
                .attr("cy", 110)
                .attr("r", function(d,i) {return (i+1)*10;})
                .attr("stroke", "black")
                .attr("stroke-width", "3")
                .attr("fill", "red");

var removethese = ["c1", "c2", "c3", "c4", "c5", "c9", "c10"];
svgHolder.selectAll("circle").data(removethese).remove();

</script>
</body>
</html> 

 
Interesting, isn't it?
You supply the array that you want to remove using data(), and d3 removes them from the DOM. But there's something wrong with how they got removed. Read on to find out.
btw, you could have also been specific, by typing  svgHolder.selectAll("circle").data(removethese).remove("circle");


Exit
The opposite of enter() though, is exit(). You can use it like this:

<html>
<head>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>

var cir = ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"];
var svgHolder = d3.select("body") //select the body tag
                .append("svg") //add an svg tag to body
                    .attr("width", 700) //specify the svg tag properties
                    .attr("height", 500);

svgHolder.selectAll("circle").data(cir).enter()
         .append("circle") //add an svg circle
                .attr("id", function(d) {return d;})
                .attr("cx", function(d,i) {return (i+1)*50;})
                .attr("cy", 110)
                .attr("r", function(d,i) {return (i+1)*10;})
                .attr("stroke", "black")
                .attr("stroke-width", "3")
                .attr("fill", "red");

var newcir = ["c1", "c2", "c3", "c4", "c5", "c9", "c10"];
svgHolder.selectAll("circle").data(newcir).exit().remove("circle");

</script>
</body>
</html>


Odd, isn't it? All you did is add an exit() between data() and remove(), and it worked exactly as the opposite of remove(). Instead of removing the values you supplied to data(), the values that were not supplied to data() got removed. d3 notices that the new array contains only 7 elements, and retains the first 7 placeholders, and the exit() function tells remove() to remove the remaining 3 placeholders.


A more accurate exit()
Noticed something else? When using just remove(), the first 7 circles got removed. When using it with exit(), the last 3 circles got removed. Both functions were not able to recognize that we had actually removed circles in-between. ie: circles "c6", "c7" and "c8".

To make d3 recognize which array value changed, you just have to supply an extra identity parameter to data().

<html>
<head>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>

var cir = ["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"];
var svgHolder = d3.select("body") //select the body tag
                .append("svg") //add an svg tag to body
                    .attr("width", 700) //specify the svg tag properties
                    .attr("height", 500);

svgHolder.selectAll("circle").data(cir, function(d) {return d;}).enter()
         .append("circle") //add an svg circle
                .attr("id", function(d) {return d;})
                .attr("cx", function(d,i) {return (i+1)*50;})
                .attr("cy", 110)
                .attr("r", function(d,i) {return (i+1)*10;})
                .attr("stroke", "black")
                .attr("stroke-width", "3")
                .attr("fill", "red");

var newcir = ["c1", "c2", "c3", "c4", "c5", "c9", "c10"];
svgHolder.selectAll("circle").data(newcir, function(d) {return d;}).exit().remove("circle");

</script>
</body>
</html>


...and viola! enter() and exit() recognize the placeholders by the second parameter we supplied to data().

To conclude
D3.js is a splendid library which allows dynamic creation, removal and modification of DOM elements. It's initially difficult to understand the enter(), exit() and data() syntax, which is why I thought I'd add my own tutorial on the internet to help newbies. Hope this was helpful, and do make use of plenty of anonymous functions and avoid using for loops, because data(), enter() and exit() offer far more powerful ways of dealing with data, than any for loop can. You'll realize it only when you get into more complex ways of representing information on a page, so it's better to start correctly, and use this syntax right from the beginning to make full use of the power of d3.

All the best!



Some people have emailed me asking if they could thank me for having given them knowledge. The best way to thank me is by contributing to Open Source. Being a sweetheart if you'd like to give a more personal thank you, then I don't really like the idea of monetary donations, but  maybe a wishlist wouldn't be that bad.


9 comments:

Jas Sohi said...

Great tutorial. I'm taking Udacity's Data Analyst Nanodegree and I'll definitely recommend it in the forums.

Cheers,

Jas

www.jassohi.com

Navin Ipe said...

@Jas: Balle balle!!! You brought a smile to my face on what was otherwise a rather disappointing day. Glad the tutorial helped and am both humbled and honoured that you feel it is worthy of mention :-)

Becky377 said...

Great info! I'm only be learning JavaScript for about a week and I found this really easy to understand. Thanks =)

Navin Ipe said...

:-) I've struggled a lot to learn d3.js even though there were quite a lot of tutorials around. Didn't want anyone else to go through the same trouble, so wrote this tutorial for all of you. You're most welcome, Becky. Glad you wrote in.

vijay pawar said...

nice

strictly momo said...

Thank you for the crystal clear tutorial!

Navin Ipe said...

"Crystal clear" is exactly the phrase that describes the objective of this tutorial. And you used the exact phrase. Amazing! Mission accomplished! :-)
Do share, so that more d3 learners can benefit from it. I hear from many people now-a-days, that they found d3 to be extremely tough to grasp. Even for seasoned programmers.

Abhishek Awanti said...

It is very useful tutorial.Not very difficult to understand and clear explanation.
I am looking forward for more tutorials on d3.js from you sir.
Thank you very much :)

Navin Ipe said...

Glad to hear that Abhi. I'm currently not working on D3, so unlikely I'd be writing tutorials on it. As developers, such contributions to open source are something all of us should be working on. http://nrecursions.blogspot.in/2014/02/contributing-to-open-source-community.html.
I hope to see you create tutorials that will be helpful to others. Right now for D3, the community is badly in need of good tutorials that explain zooming and scaling.