Creating a chart with D3 and TypeScript – part 2

In the previous article, we ended up with a nice axis drawn using d3, as a starting point to a fully working chart. We will continue to build on what we did previously.

Adding a second axis

After a horizontal axis, we will add a vertical axis to our chart. To do that, we will create a new method called drawYAxis that will handle the logic for this axis:

private drawYAxis() {
var scale = d3.scale.linear();

scale.domain([0, 1]);
scale.range([800, 0]);
var axis = d3.svg.axis()
.scale(scale)
.orient('left');
this._group.append('g')
.call(axis)
.attr({
'transform': 'translate(30 0)'
});
}

Similarly to the X axis, we create a scale with a domain and a range. However, the range is reversed, i.e. [800, 0]. This is a trick to be able to draw things the way we want: we want the our elements to move upward when their value is higher. The Y positioning in HTML goes top to bottom, 0 being the top of the page. By reversing the range of the axis, we manage to achieve the expected result.

Also, an additional parameter was added: .orient(‘left). Thanks to that option, D3 knows it has to draw a vertical axis with ticks on the left of the axis.

And finally, the group is translated 30px from the left of the page for the ticks to be visible.

Following the structure of this method, let’s arrange our first draw method to get some consistency and extract a drawXAxis method. Refactored code looks like this:

module charting {
export class chart {
private _group: D3.Selection;

constructor(container: any) {
this.init(container);
}

private init(container) {
this._group = d3.select(container).append('g');
this.draw();
}

draw() {
this.drawXAxis();
this.drawYAxis();
}

private drawXAxis() {
var scale = d3.scale.linear();
scale.domain([0, 1]);
scale.range([0, 600]);
var xAxis = d3.svg.axis()
.scale(scale)
.orient('bottom');
this._group.append('g').call(xAxis);

}
private drawYAxis() {
var scale = d3.scale.linear();
scale.domain([0, 1]);
scale.range([800, 0]);
var axis = d3.svg.axis()
.scale(scale)
.orient('left');
this._group.append('g')
.call(axis)
.attr({
'transform': 'translate(30 0)'
});

}
}
}

If everything went alright, this should produce the following result:

axes

Not amazing, but we’re getting there.

Positioning the X-axis

The main problem we are facing is the position of the X axis. This can be easily fixed using css transform property. We will extract the Y axis translation in a variable, as well as the height of the chart. The height and width will be retrieved from the containing element for more flexibility, and the containing svg will also be appended in the constructor, so that the user has less work to do:

module charting {
export class chart {
private _group: D3.Selection;
private _paddingLeft = 30;
private _paddingBottom = 30;
private _height: number;
constructor(container: any) {
this.init(container);
}

private init(container) {
var selection = d3.select(container);
var width = selection.node().clientWidth;
var height = selection.node().clientHeight;
this._height = height;
this._group = selection.append('svg')
.attr({
'width': width,
'height': height
})
.append('g');
this.draw(width, height);
}

draw(width: number, height: number) {
this.drawXAxis(width);
this.drawYAxis(height);
}

private drawXAxis(width) {
var scale = d3.scale.linear();
scale.domain([0, 1]);
scale.range([0, width - this._paddingLeft]);
var xAxis = d3.svg.axis()
.scale(scale)
.orient('bottom');
this._group.append('g')
.call(xAxis)
.attr({
'transform': 'translate(' + this._paddingLeft + ',' + (this._height - this._paddingBottom) + ')'
});

}
private drawYAxis(height: number) {
var paddingLeft = 30;
var scale = d3.scale.linear();
scale.domain([0, 1]);
scale.range([height - this._paddingBottom, 0]);
var axis = d3.svg.axis()
.scale(scale)
.orient('left');
this._group.append('g')
.call(axis)
.attr({
'transform': 'translate(' + this._paddingLeft + ', 0)'
});
}
}
}

Our html page has to be modified to take those changes into account, by setting the id on the div container and removing the svg.

That will place the axes correctly.

Styling the axes

Let’s put some styles on these axes. In the src folder, create a charting.less file. In this file, add the following content:

.tick{
font-size: 12px;
}
path.domain{
fill:none;
stroke: black;
}
body{
font-family: 'Segoe UI';
}

The .tick and .domain classes are generated automatically by D3. .tick handles the ticks while .domain handles the axis line. The result should now be nicer:

styled axes

One thing I like to do is to show the ticks marks on the axes, so let’s add that using our charting.less file:

.tick{
font-size: 12px;
line{
stroke: black;
}
}

In this case, less comes in handy, as it allows to put the line selctor directly into the .ticks selector, and will create the appropriate combination for us.

Let’s also update the X axis to make it a time-based axis and not number-based:

private drawXAxis(width) {
var scale = d3.time.scale();
this._xScale = scale;
var endDate = new Date(2015, 0, 0);
var beginDate = new Date(2014, 0, 0);
scale.domain([beginDate, endDate]);
scale.range([0, width - this._paddingLeft]);
var xAxis = d3.svg.axis()
.scale(scale)
.orient('bottom');
this._group.append('g')
.call(xAxis)
.attr({
'transform': 'translate(' + this._paddingLeft + ',' + (this._height - this._paddingBottom) + ')'
});
}

You’ll also notice that I kept a reference to the X scale, and did the same for the Y scale not shown, as those will be required to compute the positions of the elements in the chart. Our result now looks like:

time axis

Reading data

Now that the chart is waiting for data to display, let’s pull some from reddit. To do that, let’s create an update method and read data using D3. Let’s also update the axes according to the pulled data:

private update() {
d3.json('http://api.reddit.com/', (error, data: reddit.redditObject) => {
var children = data.data.children;

var minDate = new Date(new Date(0).setSeconds(d3.min(children, c => c.data.created)));
var maxDate = new Date(new Date(0).setSeconds(d3.max(children, c => c.data.created)));
this._xScale.domain([minDate, maxDate]);
this._xAxisGroup.call(this._xAxis);

var minScore = d3.min(children, c => c.data.score);
var maxScore = d3.max(children, c => c.data.score);
this._yScale.domain([minScore, maxScore]);
this._yAxisGroup.call(this._yAxis);
});
}

As you can see, reading json data from an url (http://api.reddit.com/) is very simple. I created an interface called reditObject to ease my work with the data (see the GitHub repository to grab it). After reading data, I use D3 to find the min and max dates, and min and max scores. I then assign these values to the scales, and update the axes using the call method.

To update our chart, let’s call that method in the constructor:

constructor(container: any) {
this.init(container);
this.update();
}

Drawing the data

The final step (before refactorign and styling) is to draw the data we just pulled. To do that, simply add following code after the y axis call from previous paragraph:

var dataGroup = this._group.append('g');
dataGroup.selectAll('.post')
.data(children)
.enter()
.append('circle')
.classed('post', true)
.attr({
'r': 2,
'cx': (d: reddit.redditChild, i)=>this._xScale(new Date(0).setSeconds(d.data.created)),
'cy': (d: reddit.redditChild, i) => this._yScale(d.data.score),
'transform':'translate(' + this._paddingLeft + ',' + 0 + ')'
});

This will create a new group that will contain all our circles representing the scores of the different posts. Then, data (children) are bound to a selection (dataGroup.selectAll('.post')). The enter method will then create an iteration on all new elements in the collection, and for each element, a circle with the class post (.classed('post', true)) is appended (.append('circle')). Each of these circles will have a radius of 2px ('r':2); the center of the circle will be on a position computed based on the values of the element and the scale, using a callback function.

If everything went correctly, you should get something like this:

reddit chart

Conclusion

This article built up on the embryo of chart created in the previous article. We saw how to create a second axis, structure the code to make it readable and maintainable. We also saw how to style our axes. Then we read some data from reddit, updated our axes according to those data, and finally displayed some data. In the next article, we will refactor the code and improve the layout of our chart.

Advertisements
%d bloggers like this: