Angular 2 and D3: Build A Chart Component

Angular 2 and D3: Build A Chart Component

  • 2017-04-17
  • 3334

How To Build A Chart Component in Angular 2 and D3 (Revised Version)

Today I’m publishing a revised version of a previous article How To Build A Chart Component in Angular 2 and D3 focusing on creating an Area Chart Component in Angular 2 and D3 to represent our Customer Orders.

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.

For most of the cases, using libraries like the ng2d3 is completely fine and I actually believe that is the right choice, why? Well… Because now that Angular 2 Stable Release is out, I know the following days, weeks or even months a considerable amount of developers that are new to this technologies; will start searching for information and my ethic and duty is to advice you that now that literally everything is new, following this article may be confusing and unnecessary, unless you need to build a really custom chart.

In other words, this article is written for developers that either already understand Angular 2 Basics and for those developers that need a really custom design. This will help you in the future to deliver much more customized charts to your customers and/or software products.

Anyway, I’m not saying that this blog is not for you if you are new, actually in my next article I will connect this datasets with ng2d3 because I want you to achieve the fullstack solution form Part 1, Part 2 to ng2d3 stats.

So for now, if you are totally new to Angular 2, I recommend you to first see one of the following articles:

  • Angular 2: How Do Components Communicate
  • Angular 2: How To Build A Container Component
  • The Ultimate Guide For Native App Development

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, Moment and Type

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

$ cd easy-stats-app
$ npm install [email protected] [email protected] --save
$ npm install @[email protected] --save-dev

As you can see, I’m learning from my own mistakes and this time I’m setting the right versions, nowadays everything changes so fast that I need to define which versions work for this Article.

I’m unable to update to D3 V4 because the official examples remains in V3, even the @type/d3 version at the moment I publish this article is the 3.5.17. I may create a new revision when D3 updates the documentation, but also the D3 Type.

Again if you are new to TypeScript you need to know now that if you want to use JavaScript libraries like D3 then you will need to install the right type in order to avoid compilation issues.

Whenever you need to search for types, please be informed that there is an official site that allows you to find the official types:

Microsoft’s TypeSearch

At the moment I’m writing this article, the moment library already provides with the right type, therefore you don’t need to explicitly install it.

Setup D3 Type

Now you need to tell the TypeScript compiler that there is a type you want to use. For this you need to open the file easy-stats-app/src/tsconfig.json and add the following configuration:

{
  "compilerOptions": {
    ....
    "types": [
      "d3"
    ]
  }
}

Great, you’ve learned how to search and install types when using third party libraries. Now lets create the Chart Module.

Create Chart Module

Lets use the Angular CLI to generate our Chart Module:

$ ng generate module AreaChart
installing module
  create src/app/area-chart/area-chart.module.ts
installing component
  create src/app/area-chart/area-chart.component.css
  create src/app/area-chart/area-chart.component.html
  create src/app/area-chart/area-chart.component.spec.ts
  create src/app/area-chart/area-chart.component.ts

Create AreaChartConfig

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

File: area-charts/area-chart-config.d.ts

export interface 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.

Import AreaChart Component Dependencies

File: area-chart/area-chart.component.ts

import {
  Component,
  OnChanges,
  AfterViewInit,
  Input,
  ElementRef,
  ViewChild
} from [email protected]/core';
import { AreaChartConfig } from './area-chart-config';
import * as D3 from 'd3';
import * as Moment from 'moment';

Add @Input config

Ok, we already have set the rules of what the config structure should looks like and imported the dependencies, 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: area-chart/area-chart.component.ts

@Input() config: Array<AreaChartConfig>;

Add Methods Logic

Now we can add the logic for our AreaChart Component:
File: area-chart/area-chart.component.ts

import {
  Component,
  OnChanges,
  AfterViewInit,
  Input,
  ElementRef,
  ViewChild
} from [email protected]/core';
import { AreaChartConfig } from './area-chart-config';
import * as D3 from 'd3';
import * as Moment from 'moment';

@Component({
  selector: 'app-area-chart',
  templateUrl: './area-chart.component.html',
  styleUrls: ['./area-chart.component.css']
})
export class AreaChartComponent implements OnChanges, AfterViewInit {


  @Input() config: Array<AreaChartConfig>;
  @ViewChild('container') element: ElementRef;

  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() {}
  
  ngAfterViewInit() {
    this.htmlElement = this.element.nativeElement;
    this.host        = D3.select(this.htmlElement);
    this.setup();
  }
  /**
  * Everythime the @Input is updated, we rebuild the chart
  **/
  ngOnChanges(): void {
    if (!this.config || this.config.length === 0 || !this.host) 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: area-chart/area-chart.component.css

:host,
:host .container {
    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;
}

So… What does :host mean? Well, it is the way you can select the host element, lets say the component tag instance you set when you implement this component, e.g. <app-area-chart></app-area-chart> in this case you can format this component using encapsulated styling.

Update Area Chart View

File: area-chart/area-chart.component.html

<div #container class="container"><div>

If you want to know what the “#container” and @ViewChild means, I recommend you to read - Angular 2: How Do Components Communicate.

Update Stats Component

In Part 3 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.

import { Component, OnInit } from [email protected]/core';
import { Customer } from '../shared/sdk/models';
import { CustomerApi } from '../shared/sdk/services';
import { AreaChartConfig } from '../area-chart/area-chart-config';

@Component({
  selector: 'app-stat',
  templateUrl: './stat.component.html',
  styleUrls: ['./stat.component.css']
})
export class StatComponent implements OnInit {

  private customer: Customer = new Customer();
  private range: string = 'weekly';
  private areaChartConfig: Array<AreaChartConfig>;

  constructor(private customerApi: CustomerApi) { }

  ngOnInit() {
  }

  getStats() {
    this.customerApi.customerStatsWrapper(this.customer.id, this.range)
      .subscribe((stats: any) => {
        // We create a new AreaChartConfig object to set income by customer config
        let customerIncomeArea: AreaChartConfig = {
          settings: {
            fill: 'rgba(1, 67, 163, 1)',
            interpolation: 'monotone'
          }, 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 = {
          settings: {
            fill: 'rgba(195, 0, 47, 1)',
            interpolation: 'monotone'
          }, 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

<h1>Easy Stats</h1>
<label for="customerId">Customer Id:</label>
<input name="customerId" type="text" [(ngModel)]="customer.id" placeholder="customerId" />
<label for="range">Range:</label>
<input name="range" type="text" [(ngModel)]="range" placeholder="range" />
<button (click)="getStats()">Get Stats</button>
<app-area-chart [config]="areaChartConfig"></app-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…

Test

Thanks for reading.

Suggest

Curso de TypeScript - El lenguaje utilizado por Angular 2

Angular 2 Essentials

Angular 2 Master Class with Alejandro Rangel

Angular 2 Fundamentals for Web Developers

Learn and Understand AngularJS & Angular 2

Angular 2 From The Ground Up - Early Access