Creating a chart with D3 and TypeScript – part 3

In the previous article, we managed to get data displayed on a time-based chart. Though interesting, this chart is a bit monolithic and quite hard to reuse. The chart class is far from respecting the single responsibility principle, as it fetches the data, renders these, manipulates axes, etc. This article will focus on refactoring the code and somehow improve the lookout of the chart.

Series style

As a first basic step, let’s change the color of the circles in the chart. To do that, simply add the .post class in your less file and set a fill color. I will go for red:

.post{
    fill:red;
}

That’s all there is to it. Your circle should now be of the color you chose. You can play with the fill and stroke properties as you like to style your svg controls and get them looking as nice as you like.

Refactoring the  horizontal axis

The next operation we will perform is extracting all the code related to the horizontal axis management. To do that, let’s create a new file in the src folder called xAxis.ts. In this file, let’s create a new class in the charting module:

/// <reference path="../typings/d3/d3.d.ts"/>
module charting{
    export class xAxis{

    }
}

To construct our axis, we will need a container and the width of the axis. The construction range (i.e. begin and end date) will be fixed arbitrarily and updated via another method. Let’s create a constructor accepting those two parameters:

constructor(container: D3.Selection, width: number) {
    this.init(container, width);
}

The constructor calls an init method that will initialize the different elements of the axis:

private init(container:D3.Selection, width: number) {
    this._scale = d3.time.scale();
    this._scale.range([0, width]);
    this._axis = d3.svg.axis()
        .scale(this._scale)
        .orient('bottom');

    this._group = container.append('g');

    var endDate = new Date(2015,0,0);
    var beginDate = new Date(2014,0,0);
    this.update(beginDate, endDate)
}

This init method requires some private fileds, namely _group, _scale and _axis:

private _scale: D3.Scale.TimeScale;
private _group: D3.Selection;
private _axis: D3.Svg.Axis;

Also, an update method is required, that will set the scale domain and draw the actual axis:

update(beginDate: Date, endDate: Date) {
    this._scale.domain([beginDate, endDate]);
    this._group.call(this._axis);
}

The chart itself will need to have access to the scale to be able to position data points according to that scale. Let’s expose the private _scale field through a public method:

scale() {
    return this._scale;
}

The charting.ts file can now be cleaned by removing the drawXAxis method, and calling the constructor of our new class in the init method:

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._xAxis = new xAxis(this._group, width);
    this._xAxis.translate(this._paddingLeft, (this._height - this._paddingBottom));

    this.draw(width, height);
}

This requires to create a private variable _xAxis for the charting class that will contain the reference to the instance of the axis.

The update method also has to be modified to account for the scale located in _xAxis:

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._xAxis.update(minDate, maxDate);
        var xScale = this._xAxis.scale();


        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);

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

Based on the same principle, the vertical axis can be refactored as well. That part is left to the reader as an exercise.

Refactoring data reading

The last part that I will focus on is refactoring the reading and updating of data. Currently, reading data is part of the charting class, which gives the class a responsibility it shoud not have. This also makes the chart tied to the data source, which is not what we want. Let’s revisit the update method so that it accepts an array of data:

update(data: Array<dataPoint>) {
    var minDate = d3.min(data, d => d.date);
    var maxDate = d3.max(data, d => d.date);
    var xScale = this._xAxis.update(minDate, maxDate);

    var minScore = d3.min(data, d => d.value);
    var maxScore = d3.max(data, d => d.value);
    var yScale = this._yAxis.update(minScore, maxScore);

    var dataSelection = this._dataGroup
        .selectAll('.post')
        .data(data);

    dataSelection.enter()
        .append('circle')
        .classed('post', true);

    dataSelection.attr({
        'r': 4,
        'cx': (d: dataPoint, i) => xScale(d.date),
        'cy': (d: dataPoint, i) => yScale(d.value),
        'transform': 'translate(' + this._paddingLeft + ',' + 0 + ')'
    });

    dataSelection.exit().remove();
}

The method now accepts a parameter of type Array, that has to be defined. Let’s create a new file, called dataPoint.d.ts, defining the interface for our dataPoints:

declare module charting {
    export interface dataPoint {
        date: Date;
        value: number;
    }
}

Our chart can now accept any array of data containing the value and date properties, which makes it more useful. Lets update our index.html to take that change into account, and read data from there:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Test charting</title>
    <link rel="stylesheet" href="../dist/charting.css">
</head>

<body>
    <div style="width:800px;height:600px;" id="test">

    </div>
    <div>
        Update
    </div>
    <a href="http://../bower_components/d3/d3.min.js">http://../bower_components/d3/d3.min.js</a>
    <a href="http://../dist/charting.js">http://../dist/charting.js</a>

        var chart = new charting.chart('#test');

        function update() {
            d3.json('http://api.reddit.com/', function (error, data) {
                var prepared = [];
                data.data.children.forEach(function (c) {
                    prepared.push({
                        value: c.data.score,
                        date: new Date(0).setSeconds(c.data.created)
                    });
                });
                chart.update(prepared);
            });
        }

</body>

</html>

The index page now shows an empty chart, and only displays values when clicking on the refresh button. This opens the door to making a chart with date navigation enabled, integration with other frameworks such as jQuery or AngularJS, and makes it more generic.

Conclusions and last words

Throughout this series of articles I (tried to) guide you on the basics of creating a chart using D3 and TypeScript. Core concepts of both worlds were covered, and by the end of this article, I hope you understand better how to create a viable  and maintainable charting framework based on those technologies. Let’s stress that there is no link  between those two, and the concepts for each are valid when using them without the other (lke using TypeScript for AngularJS applications or plain old ES5 D3 framework). I think, however, that using both in conjunction makes for a pretty solid ground to start building a framework, thanks to the power of D3 and the security and productivity offered by a typing system like TypeScript.

All code written for this example was created using Visual Studio Code as it offers a great experience to the user and offers an amazing experience with regards to TypeScript (i.e. autocompletion, code snippets, etc.).

I hoped you enjoyed creating that chart. If you have any comment/questions/suggestions, feel free to contact me.

If you wish to build on top of this chart, you can check my article about responding to events or making a chart responsive.

Advertisements
Leave a comment

7 Comments

  1. Thanks for the nice article. Just 1 question of what the difference between D3 and d3 (d3.min but D3.Selection)? I found d3.min but not D3.Selection in typings .d.ts. Thanks.

    Like

    Reply
    • Glad you liked the article (even though it is a bit old now).
      d3 stands for the library itself, and has the methods on it (like d3.min, d3.extent, d3.select, etc.). It represents the Javascript object you get from the library. D3 acts as a container for interfaces (D3.Selection, D3.Axis, etc.), which do not exists in the library.

      Depending on the version of d3 and types you use, you might see variations compared to the content of the article, especially now that d3 v4 is out.

      Like

      Reply
  2. prachi shah

     /  November 5, 2016

    nice artice but one question i am working with node.js and angular2. how can i take data from node js framework and create chart.?

    Like

    Reply
    • Thanks for your interest. If you want to use custom data for a chart, you just need to design your node.js API do that it returns data in a format understandable by your chart. You then simply need to call your API instead of reddit in the example, and then display your data.

      Like

      Reply
  3. JEEVAN SELVARAJ

     /  December 6, 2016

    in d3 we can import a csv or json file and pass a call back and use the data for a application how to import a csv file in typescript using angular 2

    Like

    Reply
    • I think the best way to do it would be to create a service, that would import d3.csv. I would create a promise, call d3.csv to parse the file, then resolve the promise if reading the file succeeds or reject it if it fails. I have not worked with observables yet, so I can’t offer suggestions there.
      Hope that helps!

      Like

      Reply
  1. Creating a chart with D3 and TypeScript – part 2 | Wandering in the community

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: