Showing posts with label D3. Show all posts
Showing posts with label D3. Show all posts

Sunday, September 22, 2013

D3 - My first bar chart

In this post, I'll use a simple bar chart to briefly introduce how to use D3, and the concept of the 3 states in data life cycle, enter, update and exit. Some helper function me may touch here, such as select, scale, axis and on, but the I'll not explain them in details.

Live Demo Here
var myData = [], // Bar chart's data source
    max = 10, // Y-axis maximum value
    margin = {top: 20, right: 20, bottom: 40, left: 40},
    width = 400, height = 300;

function randData(){ // Add random number to myData
    myData.push(Math.ceil(Math.random() * max));
}
function resetData(){ // Reset myData
    myData = [];
}

In the very beginning, I declare an array myData, which represents the data in the bar chart. Also there are some setting like, maximum value, margin of the chart, the dimension of the bar chart and 2 function to manipulate the data.
var y = d3.scale.linear() // Scale of y coordinate
    .domain([0, max])
    .range([height-margin.bottom, margin.top]);
var h = d3.scale.linear() // Scale of height
    .domain([0, max])
    .range([0, height-margin.top-margin.bottom]);

Then I define 2 scale object. Actually it is just a function to map a input domain data, into output range data. It is useful when you need to know a bar should locate to which x,y coordinate in the <svg> element, and how many pixel should be the high of the bar.
var svg = d3.select('#result').append('svg') // Create <svg> HTML Element
    .attr('width', width)
    .attr('height', height);
var xAxisLine = svg.append('g') // Create X-axis line
    .attr('class', 'axis')
        .append('line')
            .attr('x1', margin.left)
            .attr('y1', height - margin.bottom)
            .attr('x2', width - margin.right)
            .attr('y2', height - margin.bottom);
The above code show how I append <svg> and <line> element, then change their attributes. Actually the syntax is very similar to jQuery, I guess there is no difficulty for you to follow.
var yAxis = d3.svg.axis() // D3 axis object
    .scale(y)
    .orient("left");
var yAxisLine = svg.append("g") // Create Y-axis line
    .attr('class', 'axis')
    .attr("transform", "translate("+margin.left+",0)")
    .call(yAxis);
In previous code, you may found that I need 7 lines of code to create a single <line> element in order to represent the X-axis. So would it be very trouble to draw a axis with ticks and value labels? No, D3 provide a axis object to auto generate a axis.
function render(){
    var x = d3.scale.ordinal() // Create scale of X-axis
        .domain(myData.map(function(d, i){ return i; }))
        .rangeRoundBands([margin.left, width-margin.right], 0.1);
    var bar = svg.selectAll('.data') // Bind data
        .data(myData);
    bar.enter().append('rect') // Data enter
        .attr('class', 'data')
        .attr("width", x.rangeBand())
        .attr("height", h(0))
        .attr('x', function(d, i){ return x(i); })
        .attr('y', y(0));
    bar.transition() // Data update
        .duration(1000)
        .attr("height", h)
        .attr("width", x.rangeBand())
        .attr('x', function(d, i){ return x(i); })
        .attr('y', y);
    bar.exit().transition() // Data exit
        .duration(500)
        .attr("height", h(0))
        .attr('y', y(0))
        .remove();
}

Finally, we come to the main course of this post, use data-driven methodology to draw the bars. At the beginning of this function, I define a X-axis scale base on the number of data.
Then it comes to the data binding, in D3 each data element would bind to a DOM element. After data binding, we can define the actions when data enter, update and exit.
Data Binding (line 43,44) - so you can find I use function data for the selection result. This function allow 2 arguments, the first is an array of data, the second is optional, a function for identify the data element, default is a function return the array index of a data element.
Data Enter (line 45) - in D3 we always bind data to DOM element, and D3 would know which data is new / insert to the dataset. So we call enter to tell D3 we need to do action for those new data, and immediate call append to create a container for the data.
Data update (line 51) - since dataset may change time to time, so base on the dataset, the DOM element may need to update. Update would include both new data and existing data, so you can see I directly change attributes on the DOM element. The transition and duration call is mainly related to the animation for the bar shift when there is new entry data.
Data Exit (line 57) - it describes the data which is removed compare to the previous dataset. So what I need to do is just tell D3 to hide the bar for exiting data, and then remove them from DOM tree.
d3.select('input[type=button]').on('click', function(){
    randData();
    render();
});
d3.select('input[type=reset]').on('click', function(){
    resetData();
    render();
});

Lastly, I bind 2 onclick function for the button, so we can add and reset the data, all the stuff is just the same as jQuery.
To conclude, make use of the data binding, enter, update and exit allow us to render the data representation as batch. Actually, if you need to build a customize axis, you can also trend the ticks' values as data of the axis, and render it through its own life cycle.
The whole source code is as below:
<!DOCTYPE html>
<html>
<head>
    <style>
    svg {
        border: 1px solid black;
    }
    .axis line, .axis path{
        fill: none;
        stroke: black;
        shape-rendering: crispEdges;
    }

    rect.data {
        fill: steelblue;
    }
    </style>
</head>
<body>
    <div id="result"></div>
    <input type="button" value="Add Data" />
    <input type="reset" value="Reset" />
    
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script src="main.js"></script>
</body>
</html>

var myData = [], // Bar chart's data source
    max = 10, // Y-axis maximum value
    margin = {top: 20, right: 20, bottom: 40, left: 40},
    width = 400, height = 300;

function randData(){ // Add random number to myData
    myData.push(Math.ceil(Math.random() * max));
}
function resetData(){ // Reset myData
    myData = [];
}

// Tha setting of the bar chart
var y = d3.scale.linear() // Scale of y coordinate
    .domain([0, max])
    .range([height-margin.bottom, margin.top]);
var h = d3.scale.linear() // Scale of height
    .domain([0, max])
    .range([0, height-margin.top-margin.bottom]);
var svg = d3.select('#result').append('svg') // Create <svg> HTML Element
    .attr('width', width)
    .attr('height', height);
var xAxisLine = svg.append('g') // Create X-axis line
    .attr('class', 'axis')
        .append('line')
            .attr('x1', margin.left)
            .attr('y1', height - margin.bottom)
            .attr('x2', width - margin.right)
            .attr('y2', height - margin.bottom);
var yAxis = d3.svg.axis() // D3 axis object
    .scale(y)
    .orient("left");
var yAxisLine = svg.append("g") // Create Y-axis line
    .attr('class', 'axis')
    .attr("transform", "translate("+margin.left+",0)")
    .call(yAxis);

// Main function to draw the bar chart's "bars"
function render(){
    var x = d3.scale.ordinal() // Create scale of X-axis
        .domain(myData.map(function(d, i){ return i; }))
        .rangeRoundBands([margin.left, width-margin.right], 0.1);
    var bar = svg.selectAll('.data') // Bind data
        .data(myData);
    bar.enter().append('rect') // Data enter
        .attr('class', 'data')
        .attr("width", x.rangeBand())
        .attr("height", h(0))
        .attr('x', function(d, i){ return x(i); })
        .attr('y', y(0));
    bar.transition() // Data update
        .duration(1000)
        .attr("height", h)
        .attr("width", x.rangeBand())
        .attr('x', function(d, i){ return x(i); })
        .attr('y', y);
    bar.exit().transition() // Data exit
        .duration(500)
        .attr("height", h(0))
        .attr('y', y(0))
        .remove();
}

// Event handler of buttons
d3.select('input[type=button]').on('click', function(){
    randData();
    render();
});
d3.select('input[type=reset]').on('click', function(){
    resetData();
    render();
});

Thursday, September 19, 2013

D3 - Build chart so easy

Today I would like to introduce an interesting library, D3. D3 means "Data Driven Document", which is a javascript data visualization library. With this library, you can very easy to repersent data in a more fancy way. Here are some offical examples.
D3 strong at make use of HTML5 inline SVG to render graphic, so it is not compatible with IE8. Eventhough there is such a limitation, I found that D3 is so easy to use, with great support for animation and interaction, and its key concept of data driven is really good at ploting graph.
The following is an example of a very simple bar chart. Note that since D3 is not a plugin but it is a library, I think use 40 lines code to build a bar chart is pretty good.

var myData = [{name:"A",value:3},
              {name:"B",value:5},
              {name:"C",value:2}];
var margin = {top: 20, right: 20, bottom: 40, left: 40};
var width = 400;
var height = 300;
var x = d3.scale.ordinal()
    .domain(myData.map(function(d){ return d.name; }))
    .rangeRoundBands([margin.left, width-margin.right], 0.1);
var y = d3.scale.linear()
    .domain([0, d3.max(myData, function(d){ return d.value; })])
    .range([height-margin.bottom, margin.top]);
var h = d3.scale.linear()
    .domain([0, d3.max(myData, function(d){ return d.value; })])
    .range([0, height-margin.top-margin.bottom]);
var xAxis = d3.svg.axis()
    .scale(x);
var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(5);
var svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height);
svg.append("g")
    .attr('class', 'axis')
    .attr("transform", "translate(0,"+(height-margin.bottom)+")")
    .call(xAxis);
svg.append("g")
    .attr('class', 'axis')
    .attr("transform", "translate("+margin.left+",0)")
    .call(yAxis);
var bar = svg.selectAll('.data').append('rect')
    .data(myData);
bar.enter().append('rect')
    .attr('class', 'data')
    .attr("width", x.rangeBand())
    .attr("height", function(d){ return h(d.value); })
    .attr('x', function(d){ return x(d.value); })
    .attr('y', function(d){ return y(d.value); });

Live Demo Here
In the next blog post, I'll dig in to the D3 and explian how to use.