Introduction
The charts are more meaningful when they have axes around them. The axes tell the quantity of a particular datum represented on the plane. This a how-to post to create the axes with scales around a D3 chart.
Using the code
D3 provides .json() method to load the JSON data from a URL. This method takes two parameters – first, a URL that returns a JSON data and second, an anonymous method that takes action on the data coming from the server (current directory in my case). This anonymous method is asynchronous by nature and and takes at most two parameter. The following lines loads the data from a JSON file from my current directory:
<code>var svgHeight = 500;
var svgWidth = 800;
var paddingH = 50;
var paddingV = 50;
var dataset = undefined;
var maxX, xScale, yScale, radiusScale, colorXScale, colorYScale;
var svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
d3.json('points.json', function ( error, data ) {
if(error == null || error == undefined) {
dataset = data;
renderTheGraph();
} else {
window.alert('Something wrong happened while loading the data from JSON file. Try again.');
}
});
</code>
The anonymous method, here, takes two arguments error and data. If data doesn’t load from the server then error will have a value in it and if loads successfully then it will be assigned to the data. After data has loaded it is assigned to a global variable datatset which can be further used to represent data on plane.
Here, my data is quite simple and represent the points with weights. Just assume that this data is generated by some another process. The data is –
<code>[
{
"x" : 5,
"y" : 20,
"weight" : 0.48
},
{
"x" : 34,
"y" : 1565,
"weight" : 0.9
},
{
"x" : 153,
"y" : 530,
"weight" : 0.7
},
{
"x" : 51,
"y" : 247,
"weight" : 0.5
},
{
"x" : 447,
"y" : 290,
"weight" : 0.74
},
{
"x" : 45,
"y" : 120,
"weight" : 0.63
},
{
"x" : 351,
"y" : 29,
"weight" : 0.95
},
{
"x" : 654,
"y" : 160,
"weight" : 0.31
},
{
"x" : 315,
"y" : 320,
"weight" : 0.17
},
{
"x" : 50,
"y" : 632,
"weight" : 0.57
},
{
"x" : 50,
"y" : 20,
"weight" : 0.64
},
{
"x" : 159,
"y" : 200,
"weight" : 0.83
},
{
"x" : 512,
"y" : 170,
"weight" : 0.19
},
{
"x" : 445,
"y" : 512,
"weight" : 0.77
}
]
</code>
Here the motive is to plot the dots on x-y plane based on the x and y values and give the size and color to each dot based on the weight respectively. For example first point with values –
<code> {
"x" : 5,
"y" : 20,
"weight" : 0.48
}
</code>
It will plot a dot on (5, 20) with intensity of 0.48. What I mean by intensity is that smaller the weight amount, lesser the radius and more the transparency of the dot. This all is handled with d3 scales.
Once the .json() method loads the data and it’s saved in the dataset variable, we call the renderChart() function. This method is below –
<code>function renderTheGraph() {
setScales();
var circles = svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle');
circles.attr('cx', function ( d ) {
console.log("The x - " + xScale(d.x));
return xScale(d.x);
})
.attr('cy', function ( d ) {
console.log("The y - " + d.y);
return yScale(d.y);
})
.attr('r', function ( d ) {
console.log("The radius - " + d.weight);
return radiusScale(d.weight);
})
.attr('fill', 'orange')
.attr('stroke', function (d) {
return 'rgba(' + (colorXScale(d.x) + colorYScale(d.y)) / 2+ "," + 0 + "," + 0 + ', '+ d.weight+')';
})
.attr('stroke-width' , function ( d ) {
console.log("The stroke-width - " + d.weight);
return radiusScale(d.weight) /2 ;
});
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function( d ) {
return "(" + d.x + "," + d.y + ")";
})
.attr('x', function ( d ) {
console.log("The x - " + xScale(d.x));
return xScale(d.x);
})
.attr('y', function ( d ) {
console.log("The y - " + d.y);
return yScale(d.y + (d.weight * 50));
})
.attr('text-anchor', 'middle')
.attr('font-size', 11);
renderAxes();
}
</code>
This method further does three sub tasks –
1. Setting the scales
2. Plotting the dots
3. Drawing the axes
<code>Setting the scales
Plotting the dots
Drawing the axes
</code>
2nd task could be deliberated to another function but laziness was all over me to do that. Before explaining the 2nd and 3rd sub-tasks we need to see how the scales are set according to the data coming from the server. The setScales function does that. Here is the code for that function –
<code>function setScales ( ) {
xScale = d3.scaleLinear()
.domain( [ 0, 50 + d3.max( dataset, function( d ) { return d.x }) ] )
.range( [ paddingH, svgWidth - paddingH ] );
yScale = d3.scaleLinear()
.domain( [ 0, 50 + d3.max( dataset, function( d ) { return d.y }) ] )
.range( [ svgHeight - (paddingV * 2), paddingV ] );
radiusScale = d3.scaleLinear()
.domain( [0, d3.max( dataset, function ( d ) { return d.weight })] )
.range( [ 5, 15] );
colorXScale = d3.scaleLinear()
.domain( [ 0, d3.max( dataset, function( d ) { return d.x }) ] )
.range( [ 100, 255 ] );
colorYScale = d3.scaleLinear()
.domain( [ 0, d3.max( dataset, function( d ) { return d.y }) ] )
.range( [ 100, 255 ] );
}
</code>
Note: The method scaleLinear() will work only with the version four of the D3 not the version 3. For version 3 use d3.scale.linear().
Before I explain what a scale is actually is we should get familiarize with the concept of the domain and range. Domain and Range are the concepts of the set theory. If you’ve studied the elementary mathematics then you probably remember (or maybe you already are a mathematics genius) that domain and range are both set of values. If you’re not too much into mathematics then think of them as array of values these values can be either primitive or object data types. In case of the d3, domain spans overs the data that comes from the server and we calculate the maximum and minimum from that data to create the domain. The range is the set of the values that we’ll map our incoming data to some other array of values. For example if our data has values from 10 to 1000 and we want to map it to 0 to 500 then 10 will be mapped to 0 and 1000 to 500; similarly the middle value of the data (domain), 500, will be mapped to somewhat little more that 250 (range).
But why actually would we need to scale our data to some range? in our case, for instance, if the value of x is 1200 and value of y is 750 then we’ll need a canvas of at least that much width and height (1200 × 750) so that point can be plotted at least on the very corner of the plane. And this much big canvas is too rigid because it’ll cross the boundaries of some devices (like mobiles and tablets). So, we need a solution so that big numbers could be represented inside a small SVG canvas. And the solution is to scaling down our big domain values to smaller range values. Same can be done for smaller values too (think about a data in which x and y coordinates are to be drawn from values in between 1 to 5!)
For calculating the the domain and range of the scale d3 provides the methods .domain() and .range() . Both of these methods take an array of length 2 (in case of linear scale). The first value of the array represents the lowest value while the second value represents the maximum value. So for both domain and range we give the maximum and minimum values. For domain we calculated the maximum values with d3.max() method which is self explanatory. Minimum in domain is taken as 0 (I assume no point in my data has -ve values). Range for the data is taken from the padding of the SVG canvas to the maximum (width and height) values of the canvas minus the padding. For y range I reversed the values so that my scatter plots will draw according to the charts we’ve been drawing since our childhood (Actually computer screens draw the things from upper left corner and that draws the y plots upside down, so in order to make them natural to us, the values are reversed in the range). You might be wondering that I’ve added 50 to the maximum of the data values in domain() method. I did it so that my highest valued dot doesn’t plot on the edge of the plane. Just remove that 50 + and see for yourself.
Once our x and y scales are defined we define our radius scale that is decided on the basis of the size of weight of a point. More the weight more the radius. But I scaled my weights from 5px to 15px radii because if some point weight 0 or near to 0 then it will be invisible on the plane. Next I defined the scale for the color values. You are smart you can figure out those color scales, I know.
Wooooooo! If you’ve digested all of that, congratulations!! Now we proceed to our second step of the chart drawing – plotting the dots on the plane (using scale).
Step – 2
Okay! Now that we know what scales and we can define our scales and use those scales to plot our points (circles in our scatterplot) on the x-y plane we plot our circles on the coordinates based on the x-y values define in the dataset. With following code we add some empty circle to the SVG canvas-
<code>var circles = svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle');
</code>
The method selectAll() selects all the attributes defined in the SVG canvas. Wait! What? We didn’t define any circle in the canvas then how did we select any circle tag? Yes you’re right there is no circle tag defined in the canvas that’s why this method, in our case returns the empty array. With data(dataset) we defined the data source that we will be working on. enter() is the method used to tell d3 that we are going to add the circle elements to the SVG canvas for the first time (no update or delete, we’ll look at them too… in later blog posts). The append method adds the circles to SVG canvas , though we don’t have any circles yet. We know it’s confusing that if there are no circles then how did we add the circles to the canvas? Actually d3 keeps in mind that we’ll add the circles to the canvas as we read the data from dataset and also set their attributes.
If we print the circles on console we see that –
<code>Object { _groups: Array[1], _parents: Array[1] }
</code>
So it’s actually an object with two fields which are arrays of length equals to 1.
Now, let’s define the attributes of the circles so that they start appearing on the SVG canvas. Here, we define the x-y coordinates with x and y scales, radius of the circles based on radius scale, fill orange color, create the boundary of each circle and set the width of each boundary. The attributes of the circle is define with the .attr() method.
Setting the x and y attributes of the circles –
<code>.attr('cx', function ( d ) {
console.log("The x - " + xScale(d.x));
return xScale(d.x);
})
.attr('cy', function ( d ) {
console.log("The y - " + d.y);
return yScale(d.y);
})
</code>
Here if we simply write the .attr('cx', 30 ) then all the circles will plot on the x = 30 vertical line, that is, all the circles will have the x value as 30. But instead of giving a fixed value we can define the x and y coordinates based on the data available in dataset. To do that, .attr() method takes an anonymous method as second parameter. This anonymous method get the values from the dataset one by one, first parameter to the anonymous method is the data element and second parameter is the index of that element in the dataset. As we don’t need index of the data element we simply don’t mention it in the parameter list of the anonymous method. Here function ( d ) {.....}‘s d parameter represent an object containing the keys “x”, “y” and “weight” and their respective values. So we simply takes the x values and pass them to xScale() and similarly y values are passed to yScale(). [Note: The scales we defined are functions which return the values according to limits set with domain() and range()] And eventually we set the “cx” and “cy” attributes of the circles to be plotted on the SVG canvas. You can use the console.log(....) to print the meaningful values in case you have some confusion about what is the intermediate state of a variable and/or function.
Similarly we define the various attributes using the .attr() method. Note the is an SVG element and all the attribute we set are the attributes defined on this SVG element. You can explore more of the attributes of this SVG element and use them to enhance the representation of the circles plotted on the canvas. The circle SVG element is explained here. To set the other attributes of the circles is a left to reader as a further study area.
The circles that we plotted on the canvas are pretty visible by now but we want to give meaning to the circles plotted on the canvas. So the values of each coordinate that each circle is plotted on, is written (actually drawn) around it with. The circle SVG element doesn’t itself have a way to draw the text on it [though we have “title” attribute which is different what we want to achieve]. To write the text inside an SVG canvas there is a svg element. So we arite the text in scatterplot with following lines –
<code>svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function( d ) {
return "(" + d.x + "," + d.y + ")";
})
.attr('x', function ( d ) {
console.log("The x - " + xScale(d.x));
return xScale(d.x);
})
.attr('y', function ( d ) {
console.log("The y - " + d.y);
return yScale(d.y + (d.weight * 50));
})
.attr('text-anchor', 'middle')
.attr('font-size', 11);
</code>
The has attributes “x” and “y” which are used to draw the text on a particular coordinates on the x-y plane. “text-anchor” shifts the text to the position so that the middle of the text lies over the x and y coordinates of the text set with attr() method. If you didn’t get the last line just remove the “text-anchor” attribute and see the difference. There are more attributes that can be set on the element.
So the step two, plotting the dots on x-y plane, is done! Yippie!! Now move on to draw the x and y axes on our SVG canvas.
Step – 3
The last step in order to draw a scatterplot with the axes using the scales is to complete the third sub task. We draw the axes on the SVG canvas with function renderAxes(). This function appends an svg group for each of the x and y axes. A group is a g SVG element that contains the inner elements that draw the lines or curves on the SVG canvas. We group them together in order to keep them separate and collectively process them with one property. Our axes are groups that further contain the groups of SVG elements. Once we are finished with the drawing the axes just inspect them in you browser’s developer tools. I’m using the firefox and opened the Inspector to see what actually are the DOM elements that draw the axes. It’s pretty interesting to see the structure of the elements. Our function to render the axes is –
<code>function renderAxes() {
svg.append('g')
.attr('transform', 'translate(' + 0 + ', '+ (svgHeight - paddingV * 2) + ')')
.call(d3.axisBottom(xScale)
.ticks(20));
svg.append("g")
.attr("transform", "translate( "+ paddingV+', '+ 0 + ')')
.call(d3.axisLeft(yScale)
.ticks(20))
}
</code>
First we append a group to SVG canvas for each of the axes. Then we transform that group to move the group to particular location in order to position them rightly (with translate() method of the SVG). One the groups have been transformed we draw our axes with .call(d3.axisBottom(xScale)) and .call(d3.axisLeft(yScale)) methods. This is the least requirement to draw the axes on the SVG canvas. We can further set the properties of an axis for example here we’ve set the ticks to 20. The d3.axisBottom() takes the xAxis (d3 scale) as parameter to draw horizontal axis and d3.axisLeft(yScale) takes yScale as parameter to draw the vertical axis.
One of the most crucial things in an SVG graph is defining the padding in graph. As we can see the padding is used everywhere, in scales and axes – that drawing everything. Tweaking the padding so as to draw everything precisely is itself a little pain. But the more you play with D3 the more you learn.
Finally our graph is ready and it looks like this-