Angular 2 Charts With RC5

tl;dr

Want to just see the code? Check out the GitHub repo
Prefer to just read a diff? We've got that, too


This is an update of a previous blog post

Well well well, here we are again and Angular 2 is now at release candidate 5! What does that mean for you? Let's go over that now.

What's New in Angular RC5?

While the changes between RC4 and RC5 are not as extensive as the changes outlined in our last blog post, there are still some quite important changes to be aware of.

Modules are finally here!

Angular 1 was built around the idea of modules. Modules are self-contained pieces that can combine different functionality into one loadable and reusable container. In Angular 1, modules were created the following way...

var myModule = angular.module('myModule', []);

Until RC5, Angular 2 had no explicit way of creating a module. Now, we have the fandangled @NgModule decorator.

Here's an example of how you'd create a module in Angular 2 RC5:

import { NgModule } from '@angular/core';

@NgModule({
  declarations: [],
  imports: [],
  providers: [],
  bootstrap: []
})
export class AppModule { }

It's all quite similar to creating a component in Angular 2. We have a decorator with metadata and an exported class directly underneath it. But, modules provide some nice features that save you dev time and help prevent future headaches.

Firstly, if you've tried to build anything in Angular 2 with any reusable directives or components, you're likely to have encountered the need to import a component, directive, or service into every file that needs it.

That means that if you had three component files (user-view.component.ts, user-list.component.ts, and user-create.component.ts, for example) and each had to use a service called user.service.ts, you'd have to import the user.service.ts in every one of those files and add it to the providers for each component.

If you made a new component that relied on that service, then guess what? You gotta import the service in the new file, too, and set the provider. Eww.

The end result is a lot of repeated code. Imagine each component had even more services and relied on additional components. You'd have to keep track of which dependencies each component had, updating the imports every time you wanted to use something new and removing imports if you no longer relied on it. That's a lot of overhead!

Well, modules save that headache. With modules, you can safely remove any service, directive, pipe, or component imports from your component files. You can even remove the components array, providers array, pipes array, and directives array. All those can now go in the module!

Here's where each piece goes in the module...

@NgModule({
  declarations: [/* Components, Pipes, and Directives go here */],
  imports: [/* Modules go here */],
  providers: [/* Services go here */],
  bootstrap: [/* The main component goes here */]
})

With all pipes, directives, components, and services now loaded in @NgModule, we can now use any of the loaded elements inside any component that we load into the module.

The end result is much less repeated code and much cleaner components.

Bootstrapping Modules

In RC4, we imported our bootstrap function, the app.component, and called bootstrap with the app.component as the argument. Since we now us @NgModule, our app bootstrapping has also changed.

We now import the compilation method (a.k.a. how Angular 2 turns all the cool code and markup into actual JS and HTML), the app module, and fire it all up.

Here's what that looks like...

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

What Else?

Angular 2 RC5 added several new features to the library. While we won't cover them here, you can read all about them on the Angular 2 release notes.

Let's See Some Angular 2 Charts!

Here's a complete demo using Angular 2 with ZingChart.

Check out the code on GitHub or follow along below.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>ZingChart - Angular 2 RC5</title>
    <meta charset="UTF-8">
    <script src="https://npmcdn.com/core-js/client/shim.min.js"></script>
    <script src="https://npmcdn.com/zone.js@0.6.12?main=browser"></script>
    <script src="https://npmcdn.com/reflect-metadata@0.1.3"></script>
    <script src="https://npmcdn.com/systemjs@0.19.27/dist/system.src.js"></script>
    <script src="https://cdn.zingchart.com/zingchart.min.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading...</my-app>
  </body>
</html>

Here we're loading all our dependencies for Angular 2 and SystemJS, the ZingChart library, and our SystemJS config file.

systemjs.config.js

/**
 * PLUNKER VERSION (based on systemjs.config.js in angular.io)
 * System configuration for Angular 2 samples
 * Adjust as necessary for your application needs.
 */
(function(global) {

  var ngVer = '@2.0.0-rc.5'; // lock in the angular package version; do not let it float to current!
  var routerVer = '@3.0.0-rc.1'; // lock router version
  var formsVer = '@0.3.0'; // lock forms version
  var routerDeprecatedVer = '@2.0.0-rc.2'; // temporarily until we update all the guides

  //map tells the System loader where to look for things
  var map = {
    'app':                        'app',

    '@angular':                   'https://npmcdn.com/@angular', // sufficient if we didn't pin the version
    '@angular/router':            'https://npmcdn.com/@angular/router' + routerVer,
    '@angular/forms':             'https://npmcdn.com/@angular/forms' + formsVer,
    '@angular/router-deprecated': 'https://npmcdn.com/@angular/router-deprecated' + routerDeprecatedVer,
    'angular2-in-memory-web-api': 'https://npmcdn.com/angular2-in-memory-web-api', // get latest
    'rxjs':                       'https://npmcdn.com/rxjs@5.0.0-beta.6',
    'ts':                         'https://npmcdn.com/plugin-typescript@4.0.10/lib/plugin.js',
    'typescript':                 'https://npmcdn.com/typescript@1.9.0-dev.20160409/lib/typescript.js',
 };

  //packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'main.ts',  defaultExtension: 'ts' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
  };

  var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'upgrade',
  ];

  // Add map entries for each angular package
  // only because we're pinning the version with `ngVer`.
  ngPackageNames.forEach(function(pkgName) {
    map['@angular/'+pkgName] = 'https://npmcdn.com/@angular/' + pkgName + ngVer;
  });

  // Add package entries for angular packages
  ngPackageNames.concat(['forms', 'router', 'router-deprecated']).forEach(function(pkgName) {

    // Bundled (~40 requests):
    packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };

    // Individual files (~300 requests):
    //packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' };
  });

  var config = {
    // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
    transpiler: 'ts',
    typescriptOptions: {
      tsconfig: true
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },
    map: map,
    packages: packages
  };

  System.config(config);

})(this);

In systemjs.config.js, we're setting up how we want SystemJS to load our modules and code. This is exactly the same as the systemjs.config.js example in the Angular 2 quickstart demo.

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

Our tsconfig.json file is the exact same as our tsconfig.json demo provided in the Angular 2 quickstart demo. It just contains the way we want our Typescript to be compiled to JavaScript.

app/chart.model.ts

export class Chart {
  id: String;
  data: Object;
  height: any;
  width: any;
  
  constructor (config: Object) {
    this.id = config.id;
    this.data = config.data;
    this.height = config.height || 400;
    this.width = config.width || '100%';
  }
}

Our first piece of app code, chart.model.ts is where we specify the data structure we want for our chart object. This structure exactly mirrors the structure that ZingChart accepts for rendering.

  • id specifies the ID of the element where we'll render the chart

  • data specifies the actual chart data we'll be rendering

  • height specifies the height, defaulting to 400px

  • width specifies the width, defaulting to 100%

That's all we need for our chart model! Nice and simple.

app/zingchart.component.ts

import { Component, NgZone, AfterViewInit, OnDestroy } from '@angular/core';
import { Chart } from './chart.model';

@Component({
  selector: 'zingchart',
  inputs: ['chart'],
  template: `<div id="{{chart.id}}"></div>`
})
export class ZingChart implements AfterViewInit, OnDestroy {
  chart: Chart
  
  constructor (private zone: NgZone) { }
  
  ngAfterViewInit () {
    this.zone.runOutsideAngular(() => zingchart.render(this.chart));
  }
  
  ngOnDestroy () {
    zingchart.exec(this.chart.id, 'destroy');
  }
}

Again, nothing has changed here. We import Component, NgZone, AfterViewInit, and OnDestroy from the Angular 2 core. We import the Chart from the chart model we made above. We then create a <zingchart> component that accepts a chart as data input and renders a <div> with the id we give it.

That component hooks into the ngAfterViewInit and ngOnDestroy events to do the actual rendering and destroying of the chart. We declare a chart on the component and a private NgZone.

Using the ngAfterViewInit event, we render the chart outside the component's zone. This bit might seem a little strange but there's a performance reason behind it. ZingChart uses setTimeout and setInterval to handle different parts of chart painting. Angular 2's zones hook into those functions by default.

Since ZingChart's use of setTimeout and setInterval is isolated to just the ZingChart library, we don't want Angular 2 to be snooping on them and assuming other UI components need to be updated. Calling zingchart.render outside the Angular zone prevents that problem.

With the ngOnDestroy event, we're listening to when Angular destroys the component. We hook into that event and destroy the chart just before it. Doing so ensures no memory leaks occur which is niiiiiiice.

That's all there is to the ZingChart component. The rest is just using it and setting up our app for Angular 2 RC5.

app/app.component.ts

import { Component } from '@angular/core';
import { Chart } from './chart.model';

@Component({
  selector: 'my-app',
  template: `<zingchart *ngFor="let chart of charts" [chart]="chart"></zingchart>`
})
export class AppComponent {
  charts: Chart[];
  
  constructor () {
    this.charts = [
      {
        id: 'chart-1',
        data: {
          type: 'bar',
          series: [
            {
              values: [1, 2, 3]
            }
          ]
        }
      }
    ]
  }
}

Aight, so what's going on here? As you can see from our git diff, all we did here was remove code. Well that's cool! Less code but the same functionality is a decent win.

First off, we removed the importing of the ZingChart component we created in app/zingchart.component.ts. We're gonna load that in our NgModule instead. DOPE.

Lastly, since we removed the ZingChart component import, we can safely remove the directives from the component since we'll load it in the module. Fresh all day.

Overall, what we're doing is:

  1. Importing the Component decorator from the Angular 2 core.

  2. Importing the Chart model we made in app/chart.model.ts

  3. Creating a component with the selector <my-app>

  4. Iterate over each chart object and make a <zingchart> component with the data

  5. We then export the AppComponent class which contains an array of Chart objects.

  6. We initialize the charts array with all the Chart objects we want rendered.

That's it! Not bad at all.

app/app.module.ts

Time to use that new @NgModule business we discussed earlier. It's nifty.

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { ZingChart } from './zingchart.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, ZingChart ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Looks pretty similar to everything else, right? Here's what's going on...

  1. We import the NgModule decorator from the Angular 2 core.

  2. We import the BrowserModule from the Angular 2 browser platform.

  3. We import the AppComponent from the app/app.component.ts file.

  4. We import the ZingChart component from the app/zingchart.component.ts file.

  5. We declare our module metadata.

We place BrowserModule inside imports since we're rendering in the browser. Since we use both the AppComponent and ZingChart components, we place them inside declarations. Finally, we want to start with the AppComponent so we place it inside bootstrap.

All that's left to do is export the AppModule. Tada! Our first module. Well done! Now, let's use that module.

app/main.ts

Like before, this is short and sweet. If you look at the git diff, you can all we made were some small substitutions.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
  1. We import platformBrowserDynamic from the Angular 2 platform browser dynamic.

  2. We import the AppModule we made in app/app.module.ts.

  3. We start up the platformBrowserDynamic process and bootstrap the AppModule.

That's all we need to do to start up our module. Pretty slick.

The Result

If you've been following along, all that's left to do is create a simple web server and visit the index.html file in the browser.

  1. python -m SimpleHTTPServer

  2. open http://localhost:8000

Tadaaa!!! A chart rendered with Angular 2 RC5! Party time!

What Now?

Now that we've covered how to make a Angular 2 charts, there are several places to go:

  • This tutorial is based off the Angular 2 quickstart demo from the Angular 2 docs. It's a great place to recap or get more technical explanations of what we did here.

  • The Angular 2: Tour of Heroes tutorial is a great way to start adding more functionality to your app.

As always, let us know how you use ZingChart and Angular 2 for your charting! Tweet at us at @ZingChart or shoot us an email at support@zingchart.com. We're always happy to chat.