How To Build A Chart Component in Angular 2 and D3

Previously on Building Easy Stats with Angular 2, D3 and LoopBack I explained how to integrate Angular 2 with StrongLoop’s LoopBack Framework by using the Http Library and Observables. This time we will focus on creating an Area Chart Component in Angular 2 and D3 to represent our Customer Orders.

IMPORTANT: A revised version for this tutorial has been published and this version is deprecated. My recommendation for you is to follow the revised version that you will able to find IN HERE.

Project Description

In this tutorial we are going to create an Angular 2 Chart Component implementing the datasets we got in my last post with the help of the D3 Library.

I know in a near future there would be amazing NG2 Chart Components to get from NPM, but in this project I’m trying to achieve 2 things:

  • Show you how to create your own Angular 2 Components.
  • Show you how to create your D3 Charts.

This will help you in the future to deliver much more customized charts to your customers and/or software products.

Later on, in my next blog post we will be using Angular Material 2 to create a nice user interface to help us create customers, take orders and see how the charts are updated in real time.

Demo App

By the moment you finish this tutorial, you should be able to render an area chart built in Angular 2 and D3, even though we won’t have the user interface just jet, it is time for me to deploy a demo app:

Click Here For Demo APP

Articles Index

  • Part 1: Setting up the REST API.
  • Part 2: Creating Stats Endpoints.
  • Part 3: Setting up Angular 2 App.
  • Part 4: Build Chart Component

Install D3 and Moment

After you verified the app installed and ran correctly, kill the process and install d3.

1
2
$ npm install d3 moment --save
$ typings install d3 moment --save

Create Chart Component Files

1
2
3
4
5
+ charts
|- area-chart.css
|- area-chart.ts
|- area-chart-config.ts
|- index.ts

Create Area Chart Component

First we need to create an AreaChart Component so lets create the basic structure:

File: charts/area-chart.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import {Component} from 'angular2/core';

@Component({
selector: 'area-chart',
template: `<ng-content></ng-content>`,
styleUrls: ['app/components/charts/area-chart.css'],
directives: []
})

export class AreaChart {

private host; // D3 object referebcing host dom object
private svg; // SVG in which we will print our chart
private margin; // Space between the svg borders and the actual chart graphic
private width; // Component width
private height; // Component height
private xScale; // D3 scale in X
private yScale; // D3 scale in Y
private xAxis; // D3 X Axis
private yAxis; // D3 Y Axis
private htmlElement; // Host HTMLElement

/* Constructor, needed to get @Injectables */
constructor(): void {}

/* Will Update on every @Input change */
ngOnChanges(): void {}

/* Will setup the chart container */
private setup(): void {}

/* Will build the SVG Element */
private buildSVG(): void {}

/* Will draw the X Axis */
private drawXAxis(): void {}

/* Will draw the Y Axis */
private drawYAxis(): void {}

/* Will get the Maximum value in Y */
private getMaxY(): number {}

/* Will populate datasets into areas*/
private populate(): void {}
}

This is going to be our Component structure and for obvious it may seems I added some comments to explain every part of the component Class.

Create AreaChartConfig

Since we will require a configuration object to be set from the parent component and we decided to use TypeScript I decided to create the AreaChartConfig so we receive the specific needed configuration.

File: charts/area-chart.ts

1
2
3
4
export class AreaChartConfig { 
settings: { fill: string, interpolation: string };
dataset: Array<{ x: string, y: number }>
}

Great now we have set the rules of how the configuration should be passed to our AreaChart Component.

Add @Input config

Ok, we already set the rules of what the config structure should looks like, but now we need to say our AreaChart Component to receive this type of configuration, that can be done with the use of the @Input decorator.

File: charts/area-chart.ts

1
@Input() config: Array<AreaChartConfig>;

Of course if we are using Visual Studio Code, or any IDE with TypeScript support will complain of the missing dependency AreaChartConfig, so lets configure that.

Import AreaChart Component Dependencies

File: charts/area-chart.ts

1
2
3
import {AreaChartConfig} from './area-chart-config';
import * as D3 from 'd3/index';
import * as Moment from 'moment';

Add Methods Logic

Now We can add the logic of our AreaChart Component:
File: charts/area-chart.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import {Component, Input, ElementRef} from 'angular2/core';
import {AreaChartConfig} from './area-chart-config';
import * as D3 from 'd3/index';
import * as Moment from 'moment';

@Component({
selector: 'area-chart',
template: `<ng-content></ng-content>`,
styleUrls: ['app/components/charts/area-chart.css'],
directives: []
})

export class AreaChart {

@Input() config: Array<AreaChartConfig>;

private host;
private svg;
private margin;
private width;
private height;
private xScale;
private yScale;
private xAxis;
private yAxis;
private htmlElement: HTMLElement;
/**
* We request angular for the element reference
* and then we create a D3 Wrapper for our host element
**/

constructor(private element: ElementRef) {
this.htmlElement = this.element.nativeElement;
this.host = D3.select(this.element.nativeElement);
}
/**
* Everythime the @Input is updated, we rebuild the chart
**/

ngOnChanges(): void {
if (!this.config || this.config.length === 0) return;
this.setup();
this.buildSVG();
this.populate();
this.drawXAxis();
this.drawYAxis();
}
/**
* Basically we get the dom element size and build the container configs
* also we create the xScale and yScale ranges depending on calculations
**/

private setup(): void {
this.margin = { top: 20, right: 20, bottom: 40, left: 40 };
this.width = this.htmlElement.clientWidth - this.margin.left - this.margin.right;
this.height = this.width * 0.5 - this.margin.top - this.margin.bottom;
this.xScale = D3.time.scale().range([0, this.width]);
this.yScale = D3.scale.linear().range([this.height, 0]);
}
/**
* We can now build our SVG element using the configurations we created
**/

private buildSVG(): void {
this.host.html('');
this.svg = this.host.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
}
/**
* Method to create the X Axis, will use Month as tick date format
* Also assing some classes for CSS Stylimg
**/

private drawXAxis(): void {
this.xAxis = D3.svg.axis().scale(this.xScale)
.tickFormat(t => Moment(t).format('MMM').toUpperCase())
.tickPadding(15);
this.svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + this.height + ')')
.call(this.xAxis);
}
/**
* Method to create the Y Axis, will use numeric values as tick date format
* Also assing some classes for CSS Stylimg and rotating the axis vertically
**/

private drawYAxis(): void {
this.yAxis = D3.svg.axis().scale(this.yScale)
.orient('left')
.tickPadding(10);
this.svg.append('g')
.attr('class', 'y axis')
.call(this.yAxis)
.append('text')
.attr('transform', 'rotate(-90)');
}
/**
* Will return the maximum value in any dataset inserted, so we use
* it later for the maximum number in the Y Axis
**/

private getMaxY(): number {
let maxValuesOfAreas = [];
this.config.forEach(data => maxValuesOfAreas.push(Math.max.apply(Math, data.dataset.map(d => d.y))));
return Math.max(...maxValuesOfAreas);
}
/**
* Now we populate using our dataset, mapping the x and y values
* into the x and y domains, also we set the interpolation so we decide
* how the Area Chart is plotted.
**/

private populate(): void {
this.config.forEach((area: any) => {
this.xScale.domain(D3.extent(area.dataset, (d: any) => d.x));
this.yScale.domain([0, this.getMaxY()]);
this.svg.append('path')
.datum(area.dataset)
.attr('class', 'area')
.style('fill', area.settings.fill)
.attr('d', D3.svg.area()
.x((d: any) => this.xScale(d.x))
.y0(this.height)
.y1((d: any) => this.yScale(d.y))
.interpolate(area.settings.interpolation));
});
}
}

Add Basic Styling

File: charts/area-chart.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
:host {
width: 100%;
display:block;
}
:host .axis path,
:host .axis line {
fill: none;
stroke: rgba(0, 0, 0, 0.2);
color: rgba(0, 0, 0, 0.2);
shape-rendering: crispEdges;
}

:host .axis text {
font-size: 11px;
fill: rgba(0, 0, 0, 0.9);
}

:host .grid .tick {
stroke: rgba(0, 0, 0, 0.1);
opacity: 0.3;
}

:host .grid path {
stroke-width: 0;
}

:host .grid .tick {
stroke: rgba(0, 0, 0, 0.1);
opacity: 0.3;
}

:host .grid path {
stroke-width: 0;
}
:host .color-label{
display: inline;
}

Update Stats Component

In my last post I explained how to get the data and we actually proved by doing a console log of the result, but now is time to connect our Stats Component with our Area Chart Component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import {Component} from 'angular2/core';
import {StatsService} from './stats.service';
import {AreaChart, AreaChartConfig} from '../charts';

@Component({
selector: 'stats',
templateUrl: 'app/components/stats/stats.html',
styleUrls: ['app/components/stats/stats.css'],
providers: [StatsService],
directives: [AreaChart],
pipes: []
})

export class Stats {

private areaChartConfig: Array<AreaChartConfig>;

constructor(private statsService: StatsService) { this.getStats(); }

getStats() {
// For now we hardcode the customer Id until we build a user interface.
this.statsService.byCustomer(1, 'monthly').subscribe(stats => {
// Get JSON Object from Response
stats = stats.json();

// We create a new AreaChartConfig object to set income by customer config
let customerIncomeArea = new AreaChartConfig();
customerIncomeArea.settings = {
fill: 'rgba(1, 67, 163, 1)',
interpolation: 'monotone'
};
customerIncomeArea.dataset = stats.customerIncomeStats.map(data => {
return { x: new Date(data.date), y: data.count };
});

// We create a new AreaChartConfig object to set orders by customer config
let customerOrderArea = new AreaChartConfig();
customerOrderArea.settings = {
fill: 'rgba(195, 0, 47, 1)',
interpolation: 'monotone'
};
customerOrderArea.dataset = stats.customerOrderStats.map(data => {
return { x: new Date(data.date), y: data.count };
});

// to finish we append our AreaChartConfigs into an array of configs
this.areaChartConfig = new Array<AreaChartConfig>();
this.areaChartConfig.push(customerIncomeArea);
this.areaChartConfig.push(customerOrderArea);
});
}
}

Update Stats View

Now we pass a config object that contains a number of AreaChartConfigs

1
2
<h1>Angular 2 Chart Component</h1>
<area-chart *ngIf="areaChartConfig" [config]="areaChartConfig"></area-chart>

Test

If you start both, the front end/ back end servers again $ npm start and $ slc run you will be able to see the AreaChart component in the browser..

Also: Click Here For Demo APP

What is next?

In my next blog posts we are going to create a user interface using Angular Material 2 in order to make this project more interesting, so we can create new customers, take orders and see how the chart is updated.

If you like this series and want be aware of next releases and new packages, follow me on Twitter @johncasarrubias and if you feel it leave a comment here.

Thanks for reading.


Comments:

...