Thursday, May 15, 2014

D3.js: An introduction

D3.js is a framework for data driven documents. You can use it to create data visualizations. If you require some charting component for your java application you can easily create components based on this library or a derivative of it. In this post i will highlight some of its features via a html example and in the next post will give you an example of how you can change the renderer of prime faces component to use nvd3 component( A set of charting components built using  d3.js).

D3 has a syntax similar to jquery that means you can easily select, append, remove existing elements, chain method calls etc. I will first present the source code of the example and then explain it step by step.

<html>
<head>
<style>
.chart {
fill:steelblue;
}

.bar {
fill:steelblue;
}
.chart text {
fill: black;
font: 10px sans-serif;
text-anchor: middle;
}
.axis text {
font: 10px sans-serif;
}

.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}



div.tooltip {
position: absolute;
text-align: center;
width: 80px;
height: 40px;
padding: 8px;
font: 10px sans-serif;
background: #ddd;
border: solid 1px #aaa;
border-radius: 8px;
pointer-events: none;
}
</style>
<script src="d3/d3.js" charset="utf-8"></script>

</head>
<body>

<svg class="chart"></svg>

</body>
<script>
//Data to be used
var data = [{key:'Mathematics',value:80}, {key:'English',value:70},{key:'Physics',value:90},{key:'Biology',value:60},{key:'History',value:80},{key:'Chemistry',value:50},{key:'Geology',value:50}];

//specify margins
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//calculate each barwidth dynamically
var barWidth=width/data.length;
//Since we do not have integer coordinates and only string labels the x axis scale is ordinal
//here we have specified only range and domain will be defined later
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .4);
//Since cordinates start at top so we want the higher values to have lower y
var y = d3.scale.linear()
.range([height, 0]);
//Define an x axis, specify the scale to use and orientation
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
//Define a Y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// Selecting an element to create the initial g element under svg and translate the element to (x,y) of margins

var chart=d3.select(".chart").attr("width",width+margin.left + margin.right).attr("height",height+ margin.top + margin.bottom).append("g").attr("transform", "translate("+margin.left+","+margin.top+")");
//Now we specify the domains here x domain is special because we use map function to map each key and hence construct a domain for x axis
x.domain(data.map(function(d) { return d.key; }));
//y domain is simply from 0 to the maximum value of data, here we used data with a function to calculate the maximum of associative array
y.domain([0, d3.max(data, function(d) { return d.value;})]);

//Now Use enter selection to Append rectangle, notice y is given the value of hieght so that the bars are at base, add showtop function to //show tooltips
chart.selectAll(".bar").data(data).enter().append("rect").attr("class","bar").attr("x",function(d,i){return x(d.key);}).
attr("y",height).attr("width",x.rangeBand()).attr("height",0).on("mousemove",showTip);
//append the tooltip element and set its opacity to invisible value

var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 1e-6);
//Call x and yaxis functions and append them to the chart div element
chart.append("g").attr("class","x axis").attr("transform", "translate(0,"+height+")").call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
//This is where we set the actual values of the y positions and give the bars the transition effect
chart.selectAll(".bar").transition().duration("1000").attr("y",function(d,i){return y(d.value);}).attr("height",function(d,i){ return height-y(d.value);});
//give a olive transition effect on mouseover
chart.selectAll(".bar").on("mouseover",function (){d3.select(this).transition().duration("1000").style("fill","olive");mouseoverTip();
});

function showTip(d) {
div.text("Scored "+d.value+" in "+ d.key)
.style("left", (d3.event.pageX - 34) + "px")
.style("top", (d3.event.pageY - 12) + "px");
}
//onmouseout transition
chart.selectAll(".bar").on("mouseout",function (){
d3.select(this).transition().duration("1000").style("fill","steelblue");
mouseoutTip();
}
);



function mouseoutTip() {
div.transition()
.duration(500)
.style("opacity", 1e-6);
}
function mouseoverTip() {
div.transition()
.duration(500)
.style("opacity", 1);
}

//Append label to y axis

chart.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Subject Scores");



</script>
</html>

I have added explanation comments to the source code. Now I will explain the details of each section.



  • D3 Selections(Enter/Update/Exit):  In D3 you generally work with selections which binds your data to elements or element. You can think of them as joins. Also the data that is joined is available to all the functions in the join statement, this is important because you can then perform functions on the data.

    • Enter Selection : In case of enter selection when data is bound to elements all the non existing elements for which is there is not data are created. So let’s say you have initially no elements and want to create them, then you do a Enter Selection.  In this example enter selection is used to append rectangle elements and since no existing elements with class .bar exist, new ones will be created.
    • Update Selection: This is used to update the elements  with new  attribute values; The thing to note is that the elements must exist, only then will they be updated with new data values.
    • Exit Selection: This is the opposite of enter selection, here all the elements for which there is no data are removed.

  • Dynamic chart scaling: You generally define a scaling function for representing data on graph by dynamically mapping data values to their respective positions in the graph. The scaling function can be linear, ordinal etc. In the above mentioned example we do not have x coordinates but only String labels, so to map the labels to the corresponding positions, we use ordinal scale. The domain function accepts an array of values, So to generate the array of values from the data that we use javascript’s map function. The range function in case of x coordinates is rangeroundbands which divides the entire range into n bands(1. It gives exact integer values 2. Here n is the number of values in the domain 3. It accepts second argument which specifies spacing between elements). For y scale we use a linear scale because we want our values to scale linearly.
  • Coordinate System: If you are aware about element positioning, you will know that the top left corner represents the origin i.e x=0,y=0 position. So for us to draw the bars at the base, we give higher values of y in range to lower values in the domain, this is so because we have to subtract these values from height. So y becomes higher for lower values but height of the bar becomes lower.
  • Transitions: In this example we use two different types of transitions. 1) Position transition: If you followed the example you would have noticed that initial y position of the rect svg element is set to height, this is done because we want the bars to transition from base to their respective heights. Also transition requires that the elements must exist before it can occur, this is why it is done in the second step. 2) Style transition: If you hover your mouse over the bars, you would notice that the bar’s colour  changes to olive smoothly, this is done by changing the fill value of rect elements and using the transition function.
  • Events: The selected elements can easily be bound to events using the on() function. In this example transition and tooltip functions are bound to events such as mouseover, mouseout, mousemove etc.

The screenshot of the graph is given below. In the next post i will cover an example of how to extend a primefaces component to use nvd3 library.


 


d3example